C&Golang函数调用过程详解(二)

上篇文章聊到在main中执行了调用sum函数的call指令。

这时CPU跳到sum开始执行如下命令:

0x0000000000400526 <+0>:push   %rbp          0x0000000000400527 <+1>:mov   %rsp,%rbp 0x000000000040052a <+4>:mov   %edi,-0x14(%rbp)  0x000000000040052d <+7>:mov   %esi,-0x18(%rbp)  0x0000000000400530 <+10>:mov   -0x14(%rbp),%edx0x0000000000400533 <+13>:mov   -0x18(%rbp),%eax0x0000000000400536 <+16>:add   %edx,%eax0x0000000000400538 <+18>:mov   %eax,-0x4(%rbp)0x000000000040053b <+21>:mov   -0x4(%rbp),%eax0x000000000040053e <+24>:pop   %rbp0x000000000040053f <+25>:retq

sum前两条指令与main的一样。

0x0000000000400526 <+0>:push   %rbp            # sum函数序言,保存调用者的rbp0x0000000000400527 <+1>:mov   %rsp,%rbp   # sum函数序言,调整rbp寄存器指向自己的栈帧起始位置

它们都是在保存调用者的rbp然后设置新值来指向当前函数栈帧起始地址,这时sum保存了main的rbp的值(0x7fffffffe510),并将rbp的值修改为sum自己的栈帧的起始位置(0x7fffffffe4e0)。

通过上述指令可以看到,sum的函数序言并没有像main的序言一样,通过调整rsp的值,给sum的局部变量和临时变量预留栈空间。

这是不是说明sum没有使用栈来存储局部变量呢?

从后文的分析中可以看到,sum局部变量s还是存在栈上的,没有预留也可以使用的原因之前也提到过,栈上的内存不需要在应用层代码中进行分配,操作系统已经分配好了,直接使用就可以了。

main之所以还要调整rsp的值来预留局部变量和临时变量使用的栈空间,是因为main还需要使用call调用sum,而call会自动将rsp的值减去8,然后将函数的返回地址存到rsp所指的栈内存位置,如果main不调整rsp的值,则call保存函数返回地址的值时就会覆盖main的局部变量或临时变量的值,而sum中没有任何指令会自动使用rsp来保存数据到栈上,所以不需要调整rsp的值。

看下紧接着执行的四条指令。

0x000000000040052a <+4>:mov   %edi,-0x14(%rbp)  # 把第1个参数a放入临时变量0x000000000040052d <+7>:mov   %esi,-0x18(%rbp)  # 把第2个参数b放入临时变量0x0000000000400530 <+10>:mov   -0x14(%rbp),%edx # 从临时变量中读取第1个到edx寄存器0x0000000000400533 <+13>:mov   -0x18(%rbp),%eax # 从临时变量中读取第2个到eax寄存器

上述指令通过rbp加偏移量的方式将main传递给sum的两个参数保存在当前栈帧的合适位置,然后又取出来放入寄存器,看着有点儿多此一举,这是因为在编译时未给gcc指定优化级别,而gcc编译程序时,默认不做任何优化,所以看起来比较啰嗦。

再来看紧接着的三条指令。

0x0000000000400536 <+16>:add   %edx,%eax            # 执行a + b并把结果保存到eax寄存器0x0000000000400538 <+18>:mov   %eax,-0x4(%rbp)  # 把加法结果赋值给变量s0x000000000040053b <+21>:mov   -0x4(%rbp),%eax  # 读取s变量的值到eax寄存器

上述第一条指令负责执行加法运算并将并将结果存入eax中,第二条指令将eax中的值存入局部变量s所在的内存,第三条指令将局部变量s的值读取到eax中,可以看到,局部变量s被编译器安排到了rbp  -0x4这个地址对应的内存中。

到这里,sum主要功能已运行完毕,来看下当前栈和寄存器的状态图:

C&Golang函数调用过程详解(二)

需要说明的是,sum的两个参数和返回值都是int,在内存中只占4个字节,而图中每个栈内存单元都是8个字节且按8字节地址边界进行了对齐,所以才是上图这个样子。

接下来继续执行pop %rbp这个指令,它包含以下两个操作:

  1. 将当前rsp所指的栈内存中的值放到rbp,如此rbp就恢复到未执行sum的第一条指令时的值,也就是重新指向了main栈帧的起始位置。

  2. 将rsp的值加8,如此rsp就指向了包含0x40055e这个值的栈内存,而这个栈单元中的值是当初main调用sum时call放入的,放入的这个值就是紧跟在call后面的下一条指令的值。

状态图如下:

C&Golang函数调用过程详解(二)

继续执行retq指令,上述指令将rsp指向的栈单元中0x40055e取出存入rip,同时将rsp的值加8,这样一来,rip的值就变成main调用sum的call指令的下一条指令,于是就返回到main中继续执行。

此时eax中的值是3,也就是sum执行后返回的值,来看下状态图:

C&Golang函数调用过程详解(二)

继续执行下面这条指令:

mov   %eax,-0x4(%rbp)  # 把sum函数的返回值赋给变量n

上述指令将eax中的值(3)放入rbp  -0x4所指的内存中,这里也是main的局部变量n所在位置,所以此指令的含义就是将sum返回值赋值给局部变量n,此时状态图如下:

C&Golang函数调用过程详解(二)

再往后的指令如下:

0x0000000000400561 <+33>:mov   -0x4(%rbp),%eax0x0000000000400564 <+36>:mov   %eax,%esi0x0000000000400566 <+38>:mov   $0x400604,%edi0x000000000040056b <+43>:mov   $0x0,%eax0x0000000000400570 <+48>:callq 0x400400 <printf@plt>0x0000000000400575 <+53>:mov   $0x0,%eax

上述指令首先为printf准备参数,然后调用printf,具体过程和调用sum的过程相似,让CPU直接执行到main倒数第二条leaveq指令处,此时栈和寄存器状态如下:

C&Golang函数调用过程详解(二)

leaveq指令的上一条mov $0x0, %eax指令作用是将main返回值0放到寄存器eax,等main返回后调用main可拿到这个值。

执行leaveq指令相当于执行如下两天指令:

mov %rbp, %rsppop %rbp

leaveq指令首先将rbp的值复制给rsp,如此rsp就指向rbp所指的栈单元,之后leaveq指令将该栈单元的值pop给rbp,如此rsp和rbp就恢复成刚进入main时的状态。如下:

C&Golang函数调用过程详解(二)

此时main就只剩下retq指令了,之前分析sum时介绍过,此指令执行完后会完全返回到调用main的函数中继续执行。

到此,关于C的函数调用过程就介绍完毕了,下文接着聊一下Go函数调用过程。

好啦,到这里本文就结束了,喜欢的话就来个三连击吧。 

扫码关注公众号,获取更多优质内容。

C&Golang函数调用过程详解(二)  

上一篇:VB.NET版机房收费系统—DataGridView应用


下一篇:CSAPP第4章家庭作业参考答案