C程序运行堆栈分析

最近在上孟宁老师的《Linux内核分析》,本文是该课程的实验做业,经过分析汇编代码来理解C程序在计算机中是如何工做的。分析的实验代码以下:
code程序员

右边为经过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, 栈指针寄存器)都指向栈底。这里使用的内存堆栈模型为更常见的由下至上,而非课程视频中由上之下的结构。同时须要注意的是,右边的数字并不是内存的实际地址,这里只是将内存作了简单的编号。
empty.pngspa

18 pushl %ebp,将ebp寄存器中的值压栈,同时esp向上移一个单位
18.png指针

19 movl %esp, %ebp 将esp中的至赋值给ebp。此时,esp和ebp都指向1
19.pngcode

20 subl $4, %esp 这条指令的直接做用是将esp中的值减去4,而后把结果存回esp中。这里须要说明两点:视频

  1. 这里的4指的是4个字节,也就是内存中真实地址移动4个单位(至关于本文模型中的1个单位)ip

  2. 由于栈是向低地址扩展的数据结构。对应本文内存模型就是,1的地址比0要小4个单位,2的地址比1要小4个单位,以此类推。这也是为何这里用了减法指令内存

因此这条指令的执行结果就是将esp指向2
20.pngget

21 movl $52, (%esp) 这条指令的含义是将52这个数传入esp指向的内存地址中,也就是内存2
21.png

22 call f call是一个宏指令,其对应的两个指令为pushl %eipmovl f, %eip。上面说过eip的指表明下一条指令的位置,这里也就是第23行代码(记做EIP23)。pushl %eip就是将EIP23压栈,而后经过movl f, %eip将f函数的地址(EIP8)传入eip,使得下一条指令从f函数开始,从而实现C函数的调用。
22.png

9 pushl %ebp 将ebp的值入栈,也就是将EBP 1放入内存4中,同时esp上移一个单位
9.png

10 movl %esp, %ebp 将esp的值传入ebp中,此时esp 和 ebp同时指向内存4
10.png

11 subl $4, %esp 将esp上移一个单位,指向内存5
11.png

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)
13.png

14 call g 一样的,call至关于pushl %eipmovl g, %eip,此时eip指向第15条指令(记做EIP15)
14.png

2 pushl %ebp 将ebp的值入栈
2.png

3 movl %esp, %ebp 将esp的值传入ebp,执行后ebp和esp都指向内存7
3.png

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
6.png
7 ret ret也是一个宏指令,实际执行的效果为popl %eip,就是将栈顶的数据传入eip,同时esp下移一个单位,此时eip指向第15行指令
7.png
15 leave leave指令对应movl %ebp, %esppopl %ebp,先将ebp的值传入esp,执行后ebp和esp都指向内存4,而后将内存4的数据弹出并传入ebp中。因此执行leave执行后ebp指向内存1,esp指向内存3
15.png
16 ret 也就是popl %eip,执行后eip指向第23行代码,esp指向内存2
16.png
23 addl $33, %eax 将33累加到eax中,结果为116(83+33)
24 leavemovl %ebp, %esppopl %ebp,执行后ebp和esp均指向内存0。至此,改程序的堆栈又从新变为空栈
empty
25 ret 该程序执行结束,经过popl %eip将eip指向上个程序的指令

总结:经过分析能够看出,C语言实际上是对汇编语言作了一层抽象,以方便程序员编写和阅读代码。计算机在执行程序时,也只能循序渐进的逐条执行,这中间其实多了不少看似繁琐的过程。好比每次进入一个函数,都要先保存ebp指针。同时系统分配给每一个程序的栈空间是有限的,若是调用的函数过多,则会致使栈溢出,引起程序异常。

相关文章
相关标签/搜索