在 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 的真实值。