从汇编层面看函数调用的实现原理

作者:爱写程序的阿波张

公众号:go语言核心编程技术

本文是《go调度器源代码情景分析》系列 第一章 预备知识的第6小节。

 

前面几节我们介绍了CPU寄存器、内存、汇编指令以及栈等基础知识,为了达到融会贯通加深理解的目的,这一节我们来综合运用一下前面所学的这些知识,看看函数的执行和调用过程。

本节我们需要重点关注的问题有:

  • CPU是如何从调用者跳转到被调用函数执行的?
  • 参数是如何从调用者传递给被调用函数的?
  • 函数局部变量所占内存是怎么在栈上分配的?
  • 返回值是如何从被调用函数返回给调用者的?
  • 函数执行完成之后又需要做哪些清理工作?

解决了这些问题,我们对计算机执行程序的原理就会有一个大致的了解,这对于我们理解goroutine的调度有非常重要的作用。

相对于go来说,C语言更接近于硬件,编译后的汇编代码也更加简单直观,更容易让我们掌握函数调用的基本原理,所以我们首先来看C语言的函数调用在汇编指令层面是如何实现的,然后在此基础上分析go语言的函数调用过程。

 

C语言函数调用过程

我们用一个简单的例子程序来开始分析。

#include <stdio.h>

// 对参数 a 和 b 求和
int sum(int a, int b)
{
        int s = a + b;

        return s;
}

// main函数:程序入口
int main(int argc, char *argv[])
{
        int n = sum(1, 2); // 调用sum函数对求和

        printf("n: %d\n", n);  //在屏幕输出 n 的值

        return 0;
}

 

 

用gcc编译这个程序得到可执行程序call,然后使用gdb调试。在gdb中我们通过disass main反汇编main函数找到main的第一条指令所在的地址为0x0000000000400540,然后使用b *0x0000000000400540在该地址下一个断点并运行程序:

bobo@ubuntu:~/study/c$ gdb ./call
(gdb) disass main
Dump of assembler code for function main:
  0x0000000000400540 <+0>:push   %rbp
  0x0000000000400541 <+1>:mov   %rsp,%rbp
  0x0000000000400544 <+4>:sub   $0x20,%rsp
  0x0000000000400548 <+8>:mov   %edi,-0x14(%rbp)
  0x000000000040054b <+11>:mov   %rsi,-0x20(%rbp)
  0x000000000040054f <+15>:mov   $0x2,%esi
  0x0000000000400554 <+20>:mov   $0x1,%edi
  0x0000000000400559 <+25>:callq 0x400526 <sum>
  0x000000000040055e <+30>:mov   %eax,-0x4(%rbp)
  0x0000000000400561 <+33>:mov   -0x4(%rbp),%eax
  0x0000000000400564 <+36>:mov   %eax,%esi
  0x0000000000400566 <+38>:mov   $0x400604,%edi
  0x000000000040056b <+43>:mov   $0x0,%eax
  0x0000000000400570 <+48>:callq 0x400400 <printf@plt>
  0x0000000000400575 <+53>:mov   $0x0,%eax
  0x000000000040057a <+58>:leaveq
  0x000000000040057b <+59>:retq  
End of assembler dump.
(gdb) b *0x0000000000400540
Breakpoint 1 at 0x400540
(gdb) r
Starting program: /home/bobo/study/c/call
Breakpoint 1, 0x0000000000400540 in main ()
 

程序停在了我们下的断点处,也就是main函数的第一条指令的位置。再次反汇编一下将要执行的main函数,我们先来看其最前面的3条指令:

(gdb) disass
Dump of assembler code for function main:
=> 0x0000000000400540 <+0>:push   %rbp
  0x0000000000400541 <+1>:mov   %rsp,%rbp
  0x0000000000400544 <+4>:sub   $0x20,%rsp
  ......
 

这3条指令我们一般称之为函数序言,基本上每个函数都以函数序言开始,其主要作用在于保存调用者的rbp寄存器以及为当前函数分配栈空间,后面我们会详细介绍这3条指令,我们先来说明一下gdb输出的反汇编代码的格式,gdb反汇编出来的代码主要分为3个部分:

  • 指令地址
  • 指令相对于当前函数起始地址以字节为单位的偏移
  • 指令

比如第一行代码 0x0000000000400540 <+0>: push   %rbp,表示main函数的第一条指令push   %rbp在内存中的地址为0x0000000000400540,偏移为0(因为它是main函数的第一条指令)。这行代码各组成部分如下图所示:

 

 

这里需要说明一点,gdb反汇编输出的结果中的指令地址和偏移只是gdb为了让我们更容易阅读代码而附加上去的,保存在内存中以及被CPU执行的代码只有上图指令部分。

注意,上面反汇编结果中的第一行代码的最左边还有一个 => 符号,它表示这条指令是CPU将要执行的下一条指令,也就是rip寄存器目前的值为0x0000000000400540,当前的状态是前一条指令已经执行完毕,这一条指令还未开始执行,使用i r rbp rsp rip察看一下rbp、rsp和rip这3个寄存器的值:

(gdb) i r rbp rsp
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值