通过汇编一个简单的C程序,分析汇编代码理解计算机是如何工作的
1. C语言的代码非常简单
1 int g(int x) 2 { 3 return x + 4; 4 } 5 6 int f(int x) 7 { 8 return g(x); 9 } 10 11 int main(void) 12 { 13 return f(6) + 2; 14 }
2. 直接利用实验楼的Linux环境编译。因为环境是64位的,所以加上了-m32参数指定生成32位程序。
gcc main.c -m32
可以看到生成了一个a.out文件。用objdump看一下格式,果然是32位的
3. 用gcc生成汇编代码,直接加上参数-S就可以得到汇编代码,-o可以指定生成文件的文件名
gcc –S –o main.s main.c -m32
4. 整理一下main.s文件,只保留汇编指令
1 g: 2 pushl %ebp 3 movl %esp, %ebp 4 movl 8(%ebp), %eax 5 addl $4, %eax 6 popl %ebp 7 ret 8 f: 9 pushl %ebp 10 movl %esp, %ebp 11 subl $4, %esp 12 movl 8(%ebp), %eax 13 movl %eax, (%esp) 14 call g 15 leave 16 ret 17 main: 18 pushl %ebp 19 movl %esp, %ebp 20 subl $4, %esp 21 movl $6, (%esp) 22 call f 23 addl $2, %eax 24 leave 25 ret
5. 分析代码的运行。纸上谈兵不如实际运行,直接用gdb来单步调试刚才编译的a.out
gdb ./a.out
看一下main函数的前3条汇编
和main.s里面的一样。
但是因为main并不是程序真正的入口(linux真正的入口是_start),为了观察main函数的运行,首先在main函数第一条汇编的地址处加断点
然后打开disassemble-next-line开关,每次停下的时候就会自动显示当前的汇编指令。
set disassemble-next-line on
运行程序,就会停在断点处
察看一下当前的寄存器值
重点关注esp = 0xffffd48c,ebp=0,eip=0x804840b,后续步骤也只列出这几个寄存器。
然后单步运行一条汇编指令
esp = 0xffffd488,ebp=0,eip=0x804840c
看到因为上一条指令将ebp压栈,esp向下增长了4字节,eip跳到下一条指令。看一下esp指向的内容
确实是ebp的值。
继续单步
esp = 0xffffd488,ebp=0xffffd488,eip=0x804840e,(%esp)=0
esp = 0xffffd484,ebp=0xffffd488,eip=0x8048411,(%esp)=0
这两步将调用f的参数压栈
esp = 0xffffd484,ebp=0xffffd488,eip=0x8048418,(%esp)=6
接下来不能再用ni了,ni(next instruction)的作用类似于高级语言调试的step over,继续ni会直接把f的调用运行完。这里要用si(step instruction),即高级语言的step into。
esp = 0xffffd480,ebp=0xffffd488,eip=0x80483f8,(%esp)=0x0804841d
可以看出call指令把下一条指令的地址压入栈内
esp = 0xffffd47c,ebp=0xffffd488,eip=0x80483f9,(%esp)=0xffffd488
esp = 0xffffd47c,ebp=0xffffd47c,eip=0x80483fb,(%esp)=0xffffd488
这两步保存并移动栈顶,从而保护上一个栈帧
esp = 0xffffd478,ebp=0xffffd47c,eip=0x8048404,eax=6,(%esp)=6
这三步取出了在上一个栈帧(main)中保存的函数入参6,并存在eax中,接下来把该值压栈,作为下一个调用的参数。
esp = 0xffffd474,ebp=0xffffd47c,eip=0x80483ed,(%esp)=0x08048409
esp = 0xffffd470,ebp=0xffffd470,eip=0x80483f0,(%esp)=0xffffd47c
进入g函数,保存返回地址、保护f的栈帧,都是套路
esp = 0xffffd470,ebp=0xffffd470,eip=0x80483f3,eax=6,(%esp)=0xffffd47c
从前一个栈帧中取出入参,并存入eax。接下来进行g函数中的运算a+4,结果仍然在eax中
esp = 0xffffd470,ebp=0xffffd470,eip=0x80483f6,eax=10,(%esp)=0xffffd47c
下面开始一层一层返回,先恢复f的栈顶
esp = 0xffffd474,ebp=0xffffd47c,eip=0x80483f7,eax=10,(%esp)=0x08048409
再恢复eip返回f
esp = 0xffffd478,ebp=0xffffd47c,eip=0x8048409,eax=10,(%esp)=6
继续恢复main的栈顶
esp = 0xffffd480,ebp=0xffffd488,eip=0x804840a,eax=10,(%esp)=0x0804841d
再恢复eip返回main
esp = 0xffffd484,ebp=0xffffd488,eip=0x804841d,eax=10,(%esp)=6
接下来进行main中的计算
esp = 0xffffd484,ebp=0xffffd488,eip=0x8048420,eax=12,(%esp)=6
最后退出main
6. 总结
从这个小程序的运行过程可以看出函数调用、堆栈保护的机制。和c语言对比一下,可以看出编译器替我们做了很多工作。
另外也练习了GDB的使用,以前没有用GDB调试过程序,都是套着IDE用的。
顺便给个彩蛋,这是编译器-O2优化后的汇编代码 _(:з」∠)_