在 GCC -O3
优化级别下,很多局部变量是会被优化掉的,此时只能通过人工分析反汇编代码来获取所需信息,而这么做的前提是保存下来的寄存器中的值是准确的。绝大部分情况下 coredump 是由于 segment fault 或 assert 触发的,segment fault 情况下 Kernel 保存下来的 registers 信息是准确的,GDB 中直接用 info registers
就可以看到。然而若是由 assert 触发,由于 assert 会进行多层函数调用后最终执行 raise()
,错误现场的寄存器信息是不准确的,这时候就需要一些其他手段来解决此问题。下面用一个具体例子来说明此问题。
测试程序代码:
1 | volatile int final = 0; |
运行此程序肯定会发生 assert failed,我们用 gdb 来看下调用栈:
1 | Program terminated with signal SIGABRT, Aborted. |
切换到 fun()
的栈帧:
1 | gef> f 4 |
可以看到 a
与 b
都被优化掉了,到底是哪个值触发了 assert 就不能直接确定了。当然并不是就彻底没办法知道了,来看下 fun()
函数的反汇编:
1 | gef> disassemble |
在 -O3
优化下 fun()
直接被内联到 main()
里面了,不过这不影响基本分析,重点关注 <+16>
~ <+32>
这几行,这就对应 fun()
的前几行逻辑,if (b > 0)
是通过 test
+ jg
来实现的,b
的值此时就是 %esi
寄存器中的值。看下 gdb 分析出来的当前栈帧的寄存器值:
1 | gef> info registers |
是不是其中 %rsi
的值就是我们需要的 b
了呢?非也!注意到 <+68>
行,在调用 __assert_fail()
前 %esi
又被重新赋值用于传递参数了,且由于 %esi
属于 caller save 的寄存器,在 __assert_fail()
内有可能会被再次改写。因此 使用 GDB 分析 coredump 文件不同栈帧的 register 信息时,只有为数不多的几个 callee save 寄存器的值是可靠的,其他的都是不可靠的。 那如何才能得到可靠的寄存器值呢?一般来说只有靠我们自己保存了,一个简单思路是只要在调用 __assert_fail()
前把所有寄存器的值保存到一个全局数组中就可以了。
在 assert()
前添加如下一段内联汇编代码即可实现此目的:
1 | __asm__ __volatile__("movq $0, %%r15;\n\t" |
再来看下此时的反汇编代码:
1 | gef> disassemble |
<+59>
~ <+180>
行就是我们新加的逻辑,可以看到这段代码紧接在 <+32>
行之后,理论上分析的确是可以保存准确的寄存器信息。来看下实际效果:
1 | p registers_data |
registers_data[4]
与 final
的值完全相同,而从源代码和反汇编 <+26>
行可以看到,final
中保存的就是 b
的真实值。