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),%edx
0x0000000000400533 <+13>:mov -0x18(%rbp),%eax
0x0000000000400536 <+16>:add %edx,%eax
0x0000000000400538 <+18>:mov %eax,-0x4(%rbp)
0x000000000040053b <+21>:mov -0x4(%rbp),%eax
0x000000000040053e <+24>:pop %rbp
0x000000000040053f <+25>:retq
sum前两条指令与main的一样。
0x0000000000400526 <+0>:push %rbp # sum函数序言,保存调用者的rbp
0x0000000000400527 <+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) # 把加法结果赋值给变量s
0x000000000040053b <+21>:mov -0x4(%rbp),%eax # 读取s变量的值到eax寄存器
上述第一条指令负责执行加法运算并将并将结果存入eax中,第二条指令将eax中的值存入局部变量s所在的内存,第三条指令将局部变量s的值读取到eax中,可以看到,局部变量s被编译器安排到了rbp -0x4这个地址对应的内存中。
到这里,sum主要功能已运行完毕,来看下当前栈和寄存器的状态图:
需要说明的是,sum的两个参数和返回值都是int,在内存中只占4个字节,而图中每个栈内存单元都是8个字节且按8字节地址边界进行了对齐,所以才是上图这个样子。
接下来继续执行pop %rbp这个指令,它包含以下两个操作:
-
将当前rsp所指的栈内存中的值放到rbp,如此rbp就恢复到未执行sum的第一条指令时的值,也就是重新指向了main栈帧的起始位置。
-
将rsp的值加8,如此rsp就指向了包含0x40055e这个值的栈内存,而这个栈单元中的值是当初main调用sum时call放入的,放入的这个值就是紧跟在call后面的下一条指令的值。
状态图如下:
继续执行retq指令,上述指令将rsp指向的栈单元中0x40055e取出存入rip,同时将rsp的值加8,这样一来,rip的值就变成main调用sum的call指令的下一条指令,于是就返回到main中继续执行。
此时eax中的值是3,也就是sum执行后返回的值,来看下状态图:
继续执行下面这条指令:
mov %eax,-0x4(%rbp) # 把sum函数的返回值赋给变量n
上述指令将eax中的值(3)放入rbp -0x4所指的内存中,这里也是main的局部变量n所在位置,所以此指令的含义就是将sum返回值赋值给局部变量n,此时状态图如下:
再往后的指令如下:
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
上述指令首先为printf准备参数,然后调用printf,具体过程和调用sum的过程相似,让CPU直接执行到main倒数第二条leaveq指令处,此时栈和寄存器状态如下:
leaveq指令的上一条mov $0x0, %eax指令作用是将main返回值0放到寄存器eax,等main返回后调用main可拿到这个值。
执行leaveq指令相当于执行如下两天指令:
mov %rbp, %rsp
pop %rbp
leaveq指令首先将rbp的值复制给rsp,如此rsp就指向rbp所指的栈单元,之后leaveq指令将该栈单元的值pop给rbp,如此rsp和rbp就恢复成刚进入main时的状态。如下:
此时main就只剩下retq指令了,之前分析sum时介绍过,此指令执行完后会完全返回到调用main的函数中继续执行。
到此,关于C的函数调用过程就介绍完毕了,下文接着聊一下Go函数调用过程。
好啦,到这里本文就结束了,喜欢的话就来个三连击吧。
扫码关注公众号,获取更多优质内容。
标签:sum,mov,函数调用,rbp,Golang,eax,详解,指令,main 来源: https://blog.csdn.net/luyaran/article/details/120671104