作者:爱写程序的阿波张
公众号: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