最近在上孟宁老师的《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, 栈指针寄存器)都指向栈底。这里使用的内存堆栈模型为更常见的由下至上,而非课程视频中由上之下的结构。同时须要注意的是,右边的数字并不是内存的实际地址,这里只是将内存作了简单的编号。spa
18 pushl %ebp
,将ebp寄存器中的值压栈,同时esp向上移一个单位指针
19 movl %esp, %ebp
将esp中的至赋值给ebp。此时,esp和ebp都指向1code
20 subl $4, %esp
这条指令的直接做用是将esp中的值减去4,而后把结果存回esp中。这里须要说明两点:视频
这里的4指的是4个字节,也就是内存中真实地址移动4个单位(至关于本文模型中的1个单位)ip
由于栈是向低地址扩展的数据结构。对应本文内存模型就是,1的地址比0要小4个单位,2的地址比1要小4个单位,以此类推。这也是为何这里用了减法指令内存
因此这条指令的执行结果就是将esp指向2get
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指针。同时系统分配给每一个程序的栈空间是有限的,若是调用的函数过多,则会致使栈溢出,引起程序异常。