最近在上孟宁老师的《Linux内核分析》,本文是该课程的实验作业,通过分析汇编代码来理解C程序在计算机中是如何工作的。分析的实验代码如下:
右边为通过gcc -S main.c -o main.s -m32
命令转成的x86汇编代码,下文分析以右边代码为准
C代码
int g(int x) {
return x + 31;
}
int f(int x) {
return g(x);
}
int main(void) {
return f(52) + 33;
}
x86汇编代码
g:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %eax
addl $31, %eax
popl %ebp
ret
f:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
movl 8(%ebp), %eax
movl %eax, (%esp)
call g
leave
ret
main:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
movl $52, (%esp)
call f
addl $33, %eax
leave
ret
由于C程序的入口为main函数,所以这段代码的起始点为第17行,eip(Extended Instruction Pointer, 指令寄存器)指向第18行(eip指向下一条指令)。程序在启动时,系统会为程序分配一个堆栈空间,此时程序的堆栈为空,ebp(Extended Base Pointer, 栈基指针寄存器)和esp(Extended Stack Pointer, 栈指针寄存器)都指向栈底。这里使用的内存堆栈模型为更常见的由下至上,而非课程视频中由上之下的结构。同时需要注意的是,右边的数字并非内存的实际地址,这里只是将内存做了简单的编号。
18 pushl %ebp
,将ebp寄存器中的值压栈,同时esp向上移一个单位
19 movl %esp, %ebp
将esp中的至赋值给ebp。此时,esp和ebp都指向1
20 subl $4, %esp
这条指令的直接作用是将esp中的值减去4,然后把结果存回esp中。这里需要说明两点:
这里的4指的是4个字节,也就是内存中真实地址移动4个单位(相当于本文模型中的1个单位)
因为栈是向低地址扩展的数据结构。对应本文内存模型就是,1的地址比0要小4个单位,2的地址比1要小4个单位,以此类推。这也是为什么这里用了减法指令
所以这条指令的执行结果就是将esp指向2
21 movl $52, (%esp)
这条指令的含义是将52这个数传入esp指向的内存地址中,也就是内存2
22 call f
call是一个宏指令,其对应的两个指令为pushl %eip
和movl f, %eip
。上面说过eip的指代表下一条指令的位置,这里也就是第23行代码(记作EIP23)。pushl %eip
就是将EIP23压栈,然后通过movl f, %eip
将f函数的地址(EIP8)传入eip,使得下一条指令从f函数开始,从而实现C函数的调用。
9 pushl %ebp
将ebp的值入栈,也就是将EBP 1放入内存4中,同时esp上移一个单位
10 movl %esp, %ebp
将esp的值传入ebp中,此时esp 和 ebp同时指向内存4
11 subl $4, %esp
将esp上移一个单位,指向内存5
12 movl 8(%ebp), %eax
8(%ebp) = (8 + %ebp)
也就是ebp指针下移两个单位,指向内存2,然后将内存2中的值(也就是52)传入eax(Extended Accumulator X,累加寄存器)。这条指令执行完后堆栈中并无变化,只是将52这个数传给了eax
13 movl %eax, (%esp)
将%eax中的数值传入%esp指向的内存位置(内存5)
14 call g
同样的,call相当于pushl %eip
和movl g, %eip
,此时eip指向第15条指令(记作EIP15)
2 pushl %ebp
将ebp的值入栈
3 movl %esp, %ebp
将esp的值传入ebp,执行后ebp和esp都指向内存7
4 movl 8(%ebp), %eax
将内存5中的数据(也就是52)传入eax。此时堆栈不变化
5 addl $31, %eax
将eax中的数据加上31,并把结果存入eax,所以此时eax中的值为83(52+31)
6 popl %ebp
将栈顶的数据弹出,并传入ebp,所以执行后ebp指向内存4。同时esp下移一个单位,指向内存6
7 ret
ret也是一个宏指令,实际执行的效果为popl %eip
,就是将栈顶的数据传入eip,同时esp下移一个单位,此时eip指向第15行指令
15 leave
leave指令对应movl %ebp, %esp
和popl %ebp
,先将ebp的值传入esp,执行后ebp和esp都指向内存4,然后将内存4的数据弹出并传入ebp中。所以执行leave执行后ebp指向内存1,esp指向内存3
16 ret
也就是popl %eip
,执行后eip指向第23行代码,esp指向内存2
23 addl $33, %eax
将33累加到eax中,结果为116(83+33)
24 leave
即movl %ebp, %esp
和popl %ebp
,执行后ebp和esp均指向内存0。至此,改程序的堆栈又重新变为空栈
25 ret
该程序执行结束,通过popl %eip
将eip指向上个程序的指令
总结:通过分析可以看出,C语言其实是对汇编语言做了一层抽象,以方便程序员编写和阅读代码。计算机在执行程序时,也只能按部就班的逐条执行,这中间其实多了很多看似繁琐的过程。比如每次进入一个函数,都要先保存ebp指针。同时系统分配给每个程序的栈空间是有限的,如果调用的函数过多,则会导致栈溢出,引发程序异常。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。