zoukankan      html  css  js  c++  java
  • 记一次x87 FPU寄存器栈溢出

    工作中遇到项目贴的一个JIRA ticket,说是地图渲染的道路有异常色块:

     

    接着花了大半天时间在VS各种窗口中奋战,踩了无数坑后,最终结论是x87 FPU寄存器栈溢出引起的,很可能是MSVC编译器bug。

    (即使不是编译器的bug,调试过程也颇为值得记录一下)

    虽然最终定位到起因就一行汇编,但是为了找到这一行非有悬梁刺股的决心不可。

    1、复现

    尽管是稳定复现的bug,但是必须链接Jenkins自动编译的库才能复现,我本地编译的库不管怎么同步CMake里面那些flag,什么/O2, /Ob2, /MP啦,都不行。

    不得已只能加个/Zi把符号表弄进去,至少还能设个断点嘛。

    复现规律还有另一个特点:只在PC模拟器复现,真机设备上不复现。(这一点很重要)

    种种迹象表明,这bug绝非善类。

    2、NAN

    直觉告诉我是坐标异常引起的,于是把问题瓦片对应道路的坐标都打印了一下:

     

    可整整有三千多行,还好我眼尖一下子发现了那个-1.#IND00,嗯,这是一个NAN(not a number),看来就是他了。其他还有好几处,每一处应该就对应着一段色块。

    稳妥起见,再对比了一下本地正常运行的打印结果,确实没有那些NAN。

    3、拦截

    写了段代码用断点拦截出现NAN的时机,很简单,一个for循环就可以。

    唯一需要注意的是判断x是否NAN需要用x!=x,而不能傻傻地写x==NAN。这又是IEEE 754的一个dark corner。

    之后就是漫长的定位范围缩小再缩小……

    4、条件断点

    最终利用VS的条件断点找到了最小复现单元:

    MiterJoint_calculate()执行之后,输出的joint.p6.y变成了NAN,其他一切正常。joint是一个局部变量,像是内存的堆栈被写坏了。

    另外,这个是一个无任何全局数据依赖的函数,奇怪的是,当我在其他地方独立地用同样的输入调用该函数时,却复现不了了。

    5、内存断点

    跟踪进入函数体,发现joint.p6被优化掉了(不出所料)。

    还好有内存断点这样的终极武器:

    果然没有让我失望:

    6、汇编

    (奈何最终还是要走到这一步...)

    Alt+8切到汇编:

    看到这些f开头的指令,突然明白为什么我自己的编的库不能复现了:指令集和Jenkins自动构建的不一样,忘了加/arch:IA32,默认用了SSE2而不是x87.

    这些f开头的指令属于Intel x87 FPU指令集,在现今SIMD横行的年代已经属于老古董,它的80位long double一般人也用不到。

    fstp的意思是将一个浮点数从寄存器栈(是的,x87设计了一个大小仅为8的浮点数寄存器栈!)弹出并存进内存。

    关于x87寄存器的说明参见:

    Simply FPU Chap.1 (masmforum.com)

    在fstp处设断点,观察floating point寄存器内容:

    寄存器栈顶ST0确实是NAN。

    接下来又重跑了一次,找到了产生这个NAN产生的罪魁祸首:

    00FA7192  fld         st(0)  

    这条fld指令(FLD — Load Floating Point Value (felixcloutier.com))是将st(0)寄存器的内容读取,并压入寄存器栈。因为st(0)本身就是栈顶,因此期望结果是st(0)重复了两次。

    这条指令之前的寄存器:

    执行之后却是这样的:

    ST1及之后的没有问题,确实因为压栈被偏移了一个单元。

    ST0却出现了NAN!

    7、stack overflow

    经过漫长的搜索之后,终于找到一位老哥遇到类似的问题,而且踩的坑比我惨多了:

    Everything Old is New Again, and a Compiler Bug | Random ASCII – tech blog of Bruce Dawson (wordpress.com)

    里面指出了寄存器栈溢出这一现象。

    为了验证发生了栈溢出,除了利用文中提到的修改CTRL寄存器为027E之外,还有两个更直接的办法:

    1)STAT寄存器

    STAT是一个16位的状态寄存器,每条指令执行后相关的一些标志位会被设置。

    注意到FLD文档中提到:

    可以通过STAT中的C1标志位查看是否发生了栈溢出(从低位起第9位):

    0x036D == 0000_0011_0110_1101(b)
    

    确实是1。

    2)TAGS寄存器

    TAGS也是一个16位寄存器,直接显示8个浮点数寄存器的状态,每个用两比特表示,具体含义可以参见以上文档。

    执行指令前的TAGS:

    0x0002 == 0000_0000_0000_0010b
    

    其中7个浮点数寄存器状态是00(正常值),另外一个是10(NAN),总之8个寄存器没有空的,栈已满。接下来的fld指令自然就溢出了。

    多年以来遇到过各种内存栈溢出,FPU寄存器栈溢出还是第一次遇见!

    推测极有可能是MSVC生成汇编代码的bug。

    回顾最初的复现现象,一切都已经豁然开朗:

    为什么必须是PC才能复现?-因为只有PC上才有x87这种老古董

    为什么必须开O2才能复现?-优化过猛容易翻车

    为什么不能独立复现?-FPU寄存器栈是有历史的,不卡到那个点就不会溢出

    和大多数难查的bug一样,修复异常简单:将CMakeLists.txt中的/arch:IA32删掉。

  • 相关阅读:
    收藏夹
    获取某个元素在页面上的偏移量
    React多行文本溢出处理(仅针对纯文本)
    react
    CDN初学搭建(ats)
    linux查看cpu、内存、版本信息
    MySQL5.6版本性能调优my.cnf详解
    How to install cacti on centos 6
    win10安装.net3.5 报错解决
    CentOS6.5安装Cacti统计图乱码解决
  • 原文地址:https://www.cnblogs.com/xrst/p/14443275.html
Copyright © 2011-2022 走看看