C程序运行堆栈分析

最近在上孟宁老师的《Linux内核分析》,本文是该课程的实验作业,通过分析汇编代码来理解C程序在计算机中是如何工作的。分析的实验代码如下:
《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, 栈指针寄存器)都指向栈底。这里使用的内存堆栈模型为更常见的由下至上,而非课程视频中由上之下的结构。同时需要注意的是,右边的数字并非内存的实际地址,这里只是将内存做了简单的编号。
《C程序运行堆栈分析》

18 pushl %ebp,将ebp寄存器中的值压栈,同时esp向上移一个单位
《C程序运行堆栈分析》

19 movl %esp, %ebp 将esp中的至赋值给ebp。此时,esp和ebp都指向1
《C程序运行堆栈分析》

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

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

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

所以这条指令的执行结果就是将esp指向2
《C程序运行堆栈分析》

21 movl $52, (%esp) 这条指令的含义是将52这个数传入esp指向的内存地址中,也就是内存2
《C程序运行堆栈分析》

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

9 pushl %ebp 将ebp的值入栈,也就是将EBP 1放入内存4中,同时esp上移一个单位
《C程序运行堆栈分析》

10 movl %esp, %ebp 将esp的值传入ebp中,此时esp 和 ebp同时指向内存4
《C程序运行堆栈分析》

11 subl $4, %esp 将esp上移一个单位,指向内存5
《C程序运行堆栈分析》

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)
《C程序运行堆栈分析》

14 call g 同样的,call相当于pushl %eipmovl g, %eip,此时eip指向第15条指令(记作EIP15)
《C程序运行堆栈分析》

2 pushl %ebp 将ebp的值入栈
《C程序运行堆栈分析》

3 movl %esp, %ebp 将esp的值传入ebp,执行后ebp和esp都指向内存7
《C程序运行堆栈分析》

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

总结:通过分析可以看出,C语言其实是对汇编语言做了一层抽象,以方便程序员编写和阅读代码。计算机在执行程序时,也只能按部就班的逐条执行,这中间其实多了很多看似繁琐的过程。比如每次进入一个函数,都要先保存ebp指针。同时系统分配给每个程序的栈空间是有限的,如果调用的函数过多,则会导致栈溢出,引发程序异常。

    原文作者:食用淡水鱼
    原文地址: https://segmentfault.com/a/1190000004531921
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞