首页
社区
课程
招聘
gcc -O2编译,gdb单步执行怪怪的
2024-5-22 19:12 4974

gcc -O2编译,gdb单步执行怪怪的

2024-5-22 19:12
4974

代码(test.c):

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int foo = rand();

    printf("foo is %d\n", foo++);
    return foo;
}

编译:

gcc test.c -O2 -g -Wall

调试:

gdb a.out

(gdb) b main
(gdb) r
(gdb) layout src
(gdb) p foo
(gdb) layout asm

此时rip指向main()函数的第一条指令,打印foo变量,显示"<optimized out>"。

(gdb) layout src
(gdb) n
(gdb) p foo
(gdb) layout asm

此时rip指向main()函数的第二条指令,打印foo变量,仍然显示"<optimized out>"。

(gdb) layout src
(gdb) n
(gdb) p foo
(gdb) layout asm

此时rip指向main()函数的第三条指令,打印foo变量,仍然显示"<optimized out>"。

可以看到:C代码与汇编代码界面吻合,一边到达printf()函数,一边到达为printf()传参的指令,但是奇怪的是,既然"foo=rand()"执行完了,为什么看不到foo的值?

(gdb) layout src
(gdb) n
(gdb) p foo
(gdb) layout asm

此时rip指向main()函数的第四条指令,打印foo变量,仍然显示"<optimized out>"。

可以看到:C代码界面的显示很奇怪,没到下一行,反而回到上一行了,不过从汇编代码看,此时确实是在备份rand()的返回值(rax->rbx)

(gdb) layout src
(gdb) n
(gdb) p foo
(gdb) layout asm

此时rip指向main()函数的第五条指令(地址:0x40047d),打印foo变量,显示为rand()返回值。

为什么到达第五条指令,才能显示foo的值?

objdump --dwarf=info a.out

这是因为,gdb是根据.debug_info节区的指引,获取foo的值,而"location list"表示:更详细指引信息,参照.debug_loc节区。

objdump --dwarf=loc a.out

.debug_loc节区内容表示,执行到[0x40047d, 0x400481)指令时,rax寄存器的值,即为foo的值,执行到[0x400481, 0x400489)指令时,rbx寄存器的值,即为foo的值。

这一定与代码的逻辑吻合:

一方面,其实执行到0x400476时,即rand()一执行完,就可以通过"p $rax",知道foo的值了,但是gcc生成的起始地址是0x40047d,那"p foo"就只能晚一点才能显示出来;

另一方面,[0x400476, 0x40047f)区间的指令,都未修改rax寄存器,但是执行到0x400481处时,eax寄存器就被异或为0了,所以从0x400481开始,需要从rbx寄存器,获取foo的值(0x40047b处的指令,已经将eax备份到ebx了)。


为什么gcc -O0,不会显示"<optimized out>"?

如果利用栈传递参数和保存局部变量,每个函数的参数和局部变量,都占有独立的内存,在刚进入函数的时候就确定了,并且位置很容易推导。所以,"<optimized out>"的根本原因是寄存器传参,这是因为每个cpu只有少量的寄存器,要不停的复用,即使在同一个函数的不同的代码区间,对应的含义也不同。然而寄存器传参是一种约定,比如linux(决定用哪种约定)+ x86(决定有哪些寄存器)环境:

64位用户态函数、系统调用传参:rdi, rsi, rdx, rcx, r8, r9
64位内核态函数传参:rdi, rsi, rdx, r10, r8, r9
32位系统调用传参:ebx, ecx, edx, esi, edi, ebp
32位用户态、内核态函数传参:栈

这跟编译的优化级别无关,即使-O0,也要遵守这个约定(调用一个带参数的函数,gcc -O0编译,然后反汇编看一下就可以验证)。

那么,-O0跟-O2之间,是什么区别导致这种差别的呢?

objdump -S a.out

可以看出:跟-O2编译的代码相比,多了很多读写栈桢的内存操作,个人觉得这可能就是"两面迎合",既遵守约定,又专门为gdb往栈里写一份。

objdump --dwarf=info a.out

foo地址 = DW_OP_fbreg - 20,并且,DW_OP_fbreg = DW_AT_frame_base = DW_OP_call_frame_cfa。

# 查看.eh_fram节区
readelf -Wwf a.out

"pc=000000000040055d..000000000040058c",与main()函数指令地址区间对应,最终计算出:foo地址 = rbp-4,这与反汇编代码中,将rand()返回值压入"-0x4(%rbp)"吻合,并且由于gcc的事先准备,gdb可以始终可以从main()函数的栈帧中读到foo的值。



所以,遇到"<optimized out>",通常可以尝试调整一下断点位置,也就是gdb-black-magics这篇文章,介绍的第一个魔法。

dwarf文档:https://snapshots.sourceware.org/dwarfstd/dwarf-spec/2024-05-15_18-28_1715797681/dwarf6-20240515-1827.pdf


[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

最后于 2024-5-23 15:15 被jmpcall编辑 ,原因:
收藏
免费 2
打赏
分享
最新回复 (1)
雪    币: 143
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_hgrbqfun 2024-5-26 18:54
2
0
要调试最好是不开优化,要开优化只推荐汇编调试,源码是对不上的,很多变量或者逻辑都会被优化。
游客
登录 | 注册 方可回帖
返回