本周的实验内容是:汇编一个C语言程序代码,反汇编并分析其汇编指令执行过程。
在实验楼的Linux环境中用命令行创建文件,main.c截图如下图所示:
将.c文件反汇编成.s文件,使用-m32选项让它生成32位汇编指令
gcc -S -o main.s main.c -m32
编译阶段 | 命令 | 截断后的产物 |
---|---|---|
C源程序 | ||
预处理 | gcc -E | 替换了宏的C源程序(没有了#define,#include…), 删除了注释 |
编译 | gcc -S | 汇编源程序 |
汇编 | gcc -c | 目标文件,二进制文件, 允许有不在此文件中的外部变量、函数 |
链接 | gcc | 可执行程序,一般由多个目标文件或库链接而成, 二进制文件,所有变量、函数都必须找得到 |
反汇编源文件生成的汇编文件如下图所示:
代码中有许多以.
开头的代码行,属于链接时候的辅助信息,在实际中不会执行,把它删除,得到下列的代码就是纯汇编代码了:
g: pushl %ebp movl %esp, %ebp movl 8(%ebp), %eax addl $23, %eax popl %ebp ret f: pushl %ebp movl %esp, %ebp pushl 8(%ebp) call g addl $4, %esp leave ret main: pushl %ebp movl %esp, %ebp pushl $23 call f addl $4, %esp addl $1, %eax leave ret
函数执行
每次call一个函数,函数总是先把当前的栈底指针压入堆栈,然后把栈底指针移动到当前的栈顶,这样子做,相当于在旧的栈上新起了一个栈。然后在新栈上执行函数。
结束函数执行的时候,如果有堆栈变化,我们在写单片机汇编的时候,我们的习惯是一个函数有多少push就写多少pop,但是,由于我们新引进了一个寄存器,我们可以用movl %ebp, %esp
来瞬间恢复堆栈。当然,如果没有堆栈的变化,我们当然可以优化编译器把这句话去了。
这时候,马上就要ret飞回调用它的函数了,在此之前,我们还需要恢复栈底指针,否则回去的日子就难过了。于是popl %ebp
。然后如果可以的话,我们会用leave
来代替刚刚的两行代码。
函数调用
call f:这是调用f(23)函数。先把23压栈,然后调用了f函数。等到ret后,返回了现在的call的下一行汇编代码。这时候,esp和ebp是一个值,所以这以后如果压栈的时候,会覆盖了栈底指针,把esp往栈顶上移动1个单位也就是4个字节,这时候就完美解决了调用后的问题,才是真正调用完成了。
总结:
-
每次都是各种取指针执行,在程序中各种跳转。
-
函数执行前要
enter
,函数执行后要leave
(如果没有改变esp就可以省去把ebp赋值给esp的步骤了),ret
-
函数取值可以靠ebp很方便做到
-
函数调用结束后要记住恢复堆栈指针(esp)
- x86体系结构栈地址是向下增长的(地址不断减小),由于是我们使用的32位的x86汇编指令,这里的堆栈是从高向低入栈的,和51单片机的堆栈不同。