函数调用的栈帧分析

张建帮 原创作品转载请注明出处 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

最近开始学习孟宁老师在网易云课堂上开设的《Linux内核分析》课程,虽然才只上了一堂课,但是感觉收获颇丰,特撰文以记。

第一次课以C语言为例讲述了具体的函数调用过程,虽然只有短短的不到30行代码,但却包含了函数调用的精髓,让我对整个计算机系统有了更深入的理解。

仿照孟宁老师的代码,我的代码如下:

int g(int x, int y)
{
      return x + y;
}

int f(int x)
{
      return g(x, 1);
}

int main(void)
{
      return f(8) + 1;
}

唯一的区别就在于,函数g由以前的1个参数变为了2个参数,目的是为了观察参数传递中的入栈顺序。事实证明,在参数传递过程中,是逆序压栈的,即最后一个参数第一个压栈,倒数第二个参数第二个压栈…第一个参数最后压栈。

使用 gcc –S –o main.s main.c -m32 命令将代码编译成汇编码后,再将所有以“.”开头的行去掉,并添加上我的个人注解,最后得到的代码如下:

g:
    pushl   %ebp          ;保存函数f的栈底
    movl    %esp, %ebp    ;设置当前函数g的栈底

    ;开始运算,运算的结果存放在寄存器%eax中
    movl    12(%ebp), %eax  
    movl    8(%ebp), %edx   
    addl    %edx, %eax      

    popl    %ebp      ;恢复函数f的栈底
    ret    ;相当于popl %eip,即开始执行f中的leave指令

f:
    pushl   %ebp    ;保存函数main的栈底
    movl    %esp, %ebp    ;设置当前函数f的栈底

    ;由于要调用函数g,因此要先为参数分配空间
    ;先将最后一个参数入栈,注意!!是最后
    ;一个参数值先入栈,然后是倒数第二个,
    ;然后是倒数第三个...一直到第一个
    subl    $8, %esp    
    movl    $1, 4(%esp)
    movl    8(%ebp), %eax
    movl    %eax, (%esp)

    ;参数准备好之后,开始调用函数g
    call    g    ;相当于push %eip,即将下一条指令
                 ;leave的地址入栈

    leave  ;执行此条指令之前,esp指向本函数中的
           ;最后一个参数,此指令相当于顺序执行下面
           ;2条指令:movl %ebp,%esp + popl %ebp
    ret    ;相当于popl %eip,开始执行main中的 addl指令

main:
    pushl   %ebp    ;保存main函数调用者的栈底
    movl    %esp, %ebp    ;设置main函数的栈底

    ;由于要调用函数f,因此要先为参数分配空间
    ;先将最后一个参数入栈,注意!!是最后
    ;一个参数值先入栈,然后是倒数第二个,
    ;然后是倒数第三个...一直到第一个
    subl    $4, %esp    
    movl    $8, (%esp)

    call    f    ;相当于push %eip,即将下一条指令
                 ;addl的地址入栈

    addl    $1, %eax    ;f函数的运算结果保存在%eax中,然后
                        ;将其加1,保存在%eax中,得到main函数的
                        ;的运算结果

    leave  ;执行此条指令之前,esp指向本函数中的
           ;最后一个参数,此指令相当于顺序执行下面
           ;2条指令:movl %ebp,%esp + popl %ebp

    ret    ;相当于popl %eip,开始执行main调用者的下一条指令

看不懂吗,没关系!如果是基本的汇编指令不懂,可以单击本文标题下面的链接,由网易云课堂的孟宁老师为你倾情奉上详解;如果是逻辑关系过于繁琐,那也没关系,可以先看看下面这幅图片:

函数栈帧结构

这幅图片给出了基本的函数栈帧的结构,每个函数的栈帧都是由图中这样的结构组成:

  1. 调用该函数的函数的栈底地址,函数栈的开始,该函数的栈底指向这块内存
  2. 该函数的局部变量
  3. 该函数调用其他函数时需要传递的参数,如上图所示,为逆序入栈
  4. 该函数调用完其他函数后,下一条要执行的指令的地址,即被调用函数的返回地址

接下来就进入被调用函数的函数栈帧了,其结构与上面相同。而该函数在调用完成后,会释放掉它的栈帧空间,至于具体的分配与释放的细节,都在上面汇编代码的注解中,在孟宁老师的网易云课堂中《linux内核分析》也有详细的讲解。计算机中的这些函数调用来调用去,内存中的栈帧精准不停分配地释放,最后便达到了正确运行的目的。

明白了这整个结构后,再来看上面的汇编代码,相信大家应该没什么问题了。(友情提示:读汇编代码时最好是从main开始读,笔者一开始是其他地方开始读的,结果是丈二和尚摸不着头脑…)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值