zoukankan      html  css  js  c++  java
  • 一段C语言和汇编的对应分析,揭示函数调用的本质

    最近网易云课堂开放了一节叫 Linux内核分析 的课程。一直对操作系统和计算机本质很感兴趣,于是进去看了下,才第一堂课,老师就要求学生写一篇关于课时1的博客作为作业。对于这种新颖的作业形式,笔 者相当惊讶。好吧,作为任务,还是完成一下吧,刚好需要消化一下。本文将会按照要求,将一段C语言代码编译成汇编,并给予分析和自己的思考。

    首先对会涉及到的一些CPU寄存器和汇编的基础知识罗列一下:

    • 16位、32位、64位的CPU寄存器名称有所不同,比如指令地址寄存器 ip ,在16位中叫 ip ,32位中叫 eip ,64位叫 rip
    • 32位的汇编指令通常以 l 结尾,比如 movl 相当于 mov 的含义
    • ebp : 堆栈基地址 寄存器,这个寄存器保存的是当前执行绪的 栈底地址
    • esp : 堆栈栈顶 寄存器,这个寄存器保存的是当前执行绪的 栈顶地址
    • eip : 指令地址 寄存器,这个寄存器保存的是指令所在的地址,CPU会不断的根据 eip 所指向的指令去内存取指令并执行,并自行累加取下一条指令逐条执行。 eip 无法直接赋值, call 、 ret 、 jmp 等指令可以起到修改 eip 的作用
    • % 用于直接寻址寄存器, $ 用于表示立即数。 movl $8, %eax 表示把立即数存到 eax 中
    • () 用于内存间接寻址,比如 movl $10, (%esp) 表示将立即数保存到 esp 所指向的内存地址中
    • 8(%ebp) 表示先找到 ebp 所指向的地址值 +8 后得到的地址
    • 栈地址值是向下增长的,即栈顶从高地址向低地址移动

    准备工作

    准备一段C代码:

    int g(int x)
    {
      return x+5;
    }
    int f(int x)
    {
      return g(x);
    }
    int main(void)
    {
      return f(10)+1;
    }
    
    

    使用 实验楼 环境

    编译成汇编代码

    使用如下命令编译上面的c代码

    gcc -S -o main.s main.c -m32
    

    去掉不重要的部分后,得到:

    汇编代码结果为:

    g:
      pushl   %ebp
      movl	%esp, %ebp
      movl	8(%ebp), %eax
      addl	$5, %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	$10, (%esp)
      call	f
      addl	$1, %eax
      leave
      ret
    
    

    分析

    具体的逐步分析,这里就省了,老师课上讲的很详细了,这里主要是要进行思考和归纳。

    首先,我们看到3个C函数对应生成了3个部分的汇编代码,分别用函数名作为标号隔开了

    int g(int x) -> g:
    int f(int x) -> f:
    int main(void) -> main:
    
    

    我们知道程序是从 main 函数开始执行的,那么当程序被加载并运行时,上面的汇编代码会被加载到内存的某一个区域。而且,CPU中的很多寄存器都会初始化,当然其中最重要的是 eip ,因为 eip 是指向下一条将要执行的命令所在的内存地址,所以此时的 eip 应该指向 main 标号下的 pushl %ebp :

    main:
    eip ->  pushl %ebp
    

    程序开始执行...

    我们捆绑着看,首先先看这两条:

    pushl   %ebp
    movl    %esp, %ebp
    

    再观察一下整个代码,有没有发现不仅仅是 main 函数,函数 f 和 g 的开头也是这两个指令。分析一下,不难得出,这两条指令是指 将当前栈基地址压栈后,重新将基地址定位到栈顶 ,这个含义其实是保存好当前的基地址,重新开始一个新的栈。由于函数可以调函数, 这里的当前基地址,实际上是上一个函数的栈基地址 。例如,在 f 函数中的这两句指令,实际上保存的是 main 函数的栈基地址。

    接着来分析两句:

    subl    $4, %esp
    movl    $10, (%esp)
    

    对照C代码不难发现,这是 参数进栈 ,将立即数,保存到栈顶(esp所指向的内存地址是栈顶)。而在 f 函数中也可以发现类似的语句:

    subl    $4, %esp
    movl    8(%ebp), %eax
    movl    %eax, (%esp)
    

    所以,我们可以得出结论是,在调用函数前需要把参数逐个压栈,而压栈的顺序根据笔者的测试是从右向左的。

    接着调用 call 指令,跳转到 f 函数,我们知道 call 指令等同于下面的伪代码:

    pushl %eip+1
    movl %eip f
    

    即把 call 指令的后一条指令进栈后,将 eip 赋值为目标函数的第一个指令地址。这样做显而易见:当所调用的函数结束后,需要返回当前函数继续执行,所以必须要保存下一条指令,否则回来的时候就找不到了。

    来到 f 函数,首先是保存main函数的栈基地址,然后需要调用 g 函数,于是需要参数先进栈:

    subl    $4, %esp
    movl    8(%ebp), %eax
    movl    %eax, (%esp)
    

    这里重点思考一下, f 函数是如何获得main函数传递过来的参数的,我们看到

    movl    8(%ebp), %eax
    

    为什么参数是从 8(%ebp) 中获得的呢?我们知道 8(%ebp) 表示的是以ebp为基准向栈底回溯8个字节得到,为什么是8个字节呢?

    回想一下,在 main 函数中完成了参数进栈后做了两件事情:

    1. 由于 call f 指令的作用, call f 下一条指令的地址被压栈了,这占用率个字节
    2. 进入 f 函数后,立即将 main 函数的栈基地址进栈了,而且将 ebp 靠向了栈顶 esp ,这又占用了个字节

    于是通过 8(%ebp) 可以找到前一个函数的第一个整型参数的值。

    一张图告诉你怎么回事:

    看过了进入函数,调用函数的过程,再看一下函数是如何退出的。观察 main 和 f 不难发现,退出函数使用的是如下指令

    leave
    ret
    

    leave 指令相当于如下指令:

    movl    %ebp, %esp
    popl    %ebp
    
    • 第一条语句是将 esp 重置到 ebp ,可以理解为清空当前函数所使用的栈
    • 第二条语句是将栈顶值赋值给 ebp ,并弹出,栈顶值是什么呢?通过上面的分析不难发现,此时的栈顶值实际上是前一个函数的栈基地址,所以第二条语句的意思就是把 ebp 恢复到前一个函数的栈基地址

    接着 ret 就是相当于,恢复指令指向:

    popl %eip
    

    为什么g函数没有leave呢?因为g函数内部没有任何的变量声明和函数调用栈一直都是空的,所以编译器优化了指令

    总结

    最后,通过这个例子,总结一下函数调用的过程:

    进入函数:

    1. 当前栈基地址压栈(当前栈基地址实际上是前一个函数的栈基地址)

    调用其他函数:

    1. 参数从右到左进栈
    2. 下一条指令地址进栈

    退出函数:

    1. 栈顶 esp 归位,回到本函数的 ebp
    2. 基地址回退到上一个函数的基地址
    3. eip 退回到上一个函数即将要执行的那条语句的地址上
  • 相关阅读:
    【网易官方】极客战记(codecombat)攻略-游戏开发1-漫游者危险
    【网易官方】极客战记(codecombat)攻略-游戏开发1-军事训练
    【网易官方】极客战记(codecombat)攻略-游戏开发1-取舍
    【网易官方】极客战记(codecombat)攻略-游戏开发1-碾碎它
    【网易官方】极客战记(codecombat)攻略-游戏开发1-斩首老鼠
    课程报名 | 5G时代的视频云服务关键技术与实践
    干货 | 京东云账号安全管理最佳实践
    这大概是今年介绍云原生最清晰明了的文章!
    “大促”背后的技术 | 当我们说促销的时候,我们在谈什么?
    技术沙龙 | 从高并发架构到企业级区块链探索零售创新
  • 原文地址:https://www.cnblogs.com/www886/p/4367952.html
Copyright © 2011-2022 走看看