汇编call指令详解_GO汇编
AT&T格式汇编
go汇编对⽐
在go汇编中,寄存器的名字没有位数之分,⽐如AX寄存器没有EAX和RAX之类的名字,指令中⼀律使⽤AX,所以如果指令中有操作数寄存器或是指令需要访问内存,则操作码都要带上后缀:B(8位),W(16位),D(32位),Q(64位),例如:
MOVQ BP, SP
两个虚拟寄存器FP,主要⽤来引⽤函数参数和SB,保留程序地址空间的起始地址。
函数定义,这⾥以go runtime中的gogo
// func gogo(buf *gobuf)
// restore state from Gobuf; longjmp
TEXT runtime·gogo(SB), NOSPLIT, $16-8
......
TEXT runtime·gogo(SB):指明在代码区定义了⼀个名字叫gogo的全局函数(符号),该函数属于runtime包。
NOSPLIT:指⽰编译器不要在这个函数中插⼊检查栈是否溢出的代码。
$16-8:数字16说明此函数的栈帧⼤⼩为16字节,8说明此函数的参数和返回值⼀共需要占⽤8字节⼤⼩的内存。因为这⾥的gogo函数没有返回值,只有⼀个指针参数,对于AMD64平台来说,指针⼤⼩是8个字节。go语⾔中函数调⽤的参数和函数返回值都是放在栈上的,⽽且这部分栈内存是由调⽤者⽽⾮被调⽤函数负责预留,所以在函数定义时需要说明到底要在调⽤者的栈帧中预留多少的空间。
这⾥举个例⼦:
#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: %dn", n); //在屏幕输出 n 的值
return 0
}
汇编代码查看:
前三条指令称为函数序⾔,基本上每个函数都以函数序⾔开始,其主要作⽤是保存调⽤者的rbp寄存器以及为当前函数分配栈空间。上图第⼀⾏代码中的=>表⽰这是CPU将要执⾏的下⼀条指令,也就是rip寄存器当前的值。当前的状态是前⼀条指令已经执⾏完毕,这⼀条指令还未开始执⾏,使⽤如下命令查看⼀下rbp,rsp,rip寄存器的值:
汇编指令有多少个i r rbp rsp rip
rip指向的是Main函数的第⼀条指令,rsp指向当前函数调⽤栈的栈顶,rbp应该是指向当前函数调⽤栈的栈底,这⾥没有指向我们关注的函数的栈和指令。
现在开始执⾏第⼀条指令
0x0000000000401140 <+0>: push %rbp # 保存调⽤者的rbp寄存器的值
这条指令把栈基址寄存器rbp的值临时保存在main函数的栈帧⾥,因为main函数需要使⽤这个寄存器
来存放⾃⼰的栈基地址,⽽调⽤者在调⽤main函数之前也把他的栈基地址保存在了rbp寄存器中,所以Main函数需要把这个寄存器的值先保存起来,等main执⾏完返回时再把这个寄存器恢复原样,如果不恢复原样,main函数返回后调⽤者使⽤rbp寄存器时就会有问题,因为在执⾏调⽤者的代码时rbp本应指向调⽤函数的栈,但现在却指向了main函数的栈。
在这条指令之前,代码还在使⽤调⽤者的栈帧,执⾏完这条指令之后,开始使⽤main函数的栈帧,⽬前main函数的栈帧⾥⾯只保存有调⽤者的rbp这⼀个值,执⾏完成以后,rsp指向了main函数的栈帧的起始位置,rip指向了main函数的第⼆条指令。
接着执⾏第⼆条指令
# 调整rbp寄存器,使其指向main函数栈帧的起始位置
0x0000000000401141 <+1>: mov %rsp,%rbp
这条指令把rsp寄存器的值拷贝到rbp寄存器,让rbp指向main函数栈帧的起始位置。
接着是第三条指令
# 调整rsp寄存器的值,为局部和临时变量预留栈空间
0x0000000000401144 <+4>: sub $0x20,%rsp
这条指令把rsp寄存器的值减去32,使其指向了栈空间中⼀个更低的位置,这⼀步看似只是简单修改了rsp寄存器的值,其实质是给Main函数的局部变量和临时变量预留了32字节的栈空间,为什么说是预留⽽不是分配,因为栈的分配操作是由操作系统⾃动完成的,程序启动时操作系统就会给函数分配⼀块内存⽤做函数调⽤栈,程序到底使⽤了多少栈内存由栈顶寄存器rsp决定。
该指令执⾏完成后,从rsp所指位置到rbp所指的这段栈内存就构成了Main函数的完整栈帧,其⼤⼩为40字节,(8字节⽤于保存调⽤者的rbp,另外32字节⽤于保存main函数局部变量和临时变量)。
接下来4条指令
0x0000000000401148 <+8>: mov %edi,-0x14(%rbp)
0x000000000040114b <+11>: mov %rsi,-0x20(%rbp)
0x000000000040114f <+15>: mov $0x2,%esi
0x0000000000401154 <+20>: mov $0x1,%edi
前两条指令负责把main函数的两个参数保存到栈帧⾥⾯,可以看到,这⾥使⽤rbp加偏移量的⽅式来访问内存。这⾥之所以要保存main函数的两个参数,是因为调⽤者在调⽤Main函数时使⽤edi和rsi两个寄存器来给main函数分别传递argc和argv两个参数,⽽main函数⼜需要⽤这两个寄存器给sum函数传递参数,为了不覆盖argc和argv,这⾥需要先把这两个参数保存在栈⾥⾯。然后再把传递给sum函数的两个参数1和2放到这两个寄存器中。
后⾯两条指令再给sum函数准备参数,我们可以看到,第⼀个参数放在了edi寄存器,第⼆个参数放在了esi寄存器,这其实是⼀个约定,⼤家约定好,调⽤函数时,调⽤者负责把第⼀个参数放在rdi寄存器,第⼆个参数放在rsi寄存器。被调⽤函数去这两个寄存器取参数。这⾥传给sum的两个参数⽤的是edi和esi⽽不是rdi和rsi,这是因为C语⾔中int是32位的,⽽rdi和rsi都是64位的,edi和esi可以当作rdi和rsi的⼀部分来使⽤。
参数准备好之后,开始调⽤call执⾏sum函数
# 调⽤sum函数
0x0000000000401159 <+25>: callq 0x401126
call指令有点特殊,刚开始执⾏它的时候,rip指向的是call的下⼀条指令,也就是说rip寄存器的值是0x
40115e这个地址,但在call这个指令执⾏过程中,call指令会把当前rip值(0x40115e)⼊栈,然后把rip的值修改为call指令后⾯的操作数,这⾥是0x401126,也就是sum 函数第⼀条指令的地址,这样cpu就会跳转到sum函数去执⾏。
可以看到sum函数的前两条指令和main函数⼀模⼀样,
# sum函数序⾔,保存调⽤者的rbp
0x0000000000401126 <+0>: push %rbp
# sum函数序⾔,调整rbp寄存器指向⾃⼰的栈帧起始位置
0x0000000000401127 <+1>: mov %rsp,%rbp
都是在保存调⽤者的rbp然后设置新值使其指向当前函数栈帧的起始位置。这⾥sum函数保存了main函数的rbp寄存器的地址,并使rbp寄存器指向了⾃⼰栈帧起始位置。
可以看到,sum函数序⾔并未向main函数⼀样通过调整rsp寄存器的值来给sum函数预留⽤于局部变量和临时变量的栈空间,那这是不是说明sum函数没有使⽤栈空间来保存局部变量呢?其实不是,从后⾯的分析可以看到,sum函数的局部变量s还是保存在栈空间的。没有预留为什么也可以使⽤呢,原因前⾯就说过,栈上的内存不需要在应⽤层代码中分配。操作系统已经分配好了,直接⽤就⾏了。main函数之所以需要调整rsp寄存器的值,是因为它需要使⽤call指令来调⽤sum函数,⽽call指令会⾃动把rsp寄存器的值减8,然后把函数返回值地址保存到rsp所指的栈内存的位置,如果Main函数不调整rsp寄存器的值,则call变量的返回值会覆盖局部变量或临时变量的值,⽽sum函数中没有任何指令会⾃动使⽤rsp寄存器来保存数据到栈上,因此不⽤调整rsp寄存器的值。
紧接着的4条指令
# 把第1个参数a放⼊临时变量
0x000000000040112a <+4>: mov %edi,-0x14(%rbp)
# 把第2个参数b放⼊临时变量
0x000000000040112d <+7>: mov %esi,-0x18(%rbp)
# 从临时变量中读取第1个到edx寄存器
0x0000000000401130 <+10>: mov -0x14(%rbp),%edx
# 从临时变量中读取第2个到eax寄存器
0x0000000000401133 <+13>: mov -0x18(%rbp),%eax
通过rbp寄存器加偏移的⽅式,把main函数传递的参数保存到当前栈帧的合适位置,然后⼜取出来到寄存器,这⾥有点多此⼀举,因为我们编译的时候未给编译器指定优化级别,gcc编译程序时默认不做任何优化。
紧接着的⼏条指令
# 执⾏a + b并把结果保存到eax寄存器
0x0000000000401136 <+16>: add %edx,%eax
# 把加法结果赋值给变量s
0x0000000000401138 <+18>: mov %eax,-0x4(%rbp)
# 读取s变量的值到eax寄存器
0x000000000040113b <+21>: mov -0x4(%rbp),%eax
可以看到局部变量s被安排在了rbp - 0x4这个内存地址
接下来
0x000000000040113e <+24>: pop %rbp
该指令包含两个操作,
把当前rsp寄存器的值放⼊rb寄存器中,这样rbp就恢复到了还没有执⾏sum函数时第⼀条指令的值,也就是重新指向了main函数的栈帧起始地址。
把rsp寄存器的值加8,这样rsp就指向了包含0x40115e这个值的栈内存,⽽这个栈单元中的值是当初main函数调⽤sum时call指令放⼊的,放⼊的这个值就是紧跟在call指令后⾯的下⼀条指令的地址。
继续retq指令
0x000000000040113f <+25>: retq
该指令把rsp寄存器指向栈单元的0x400115e取出给rip寄存器,同时rsp加8,这样rip寄存器的值就变成main函数调⽤sum的call指令的下⼀条指令,于是就返回到main函数中继续执⾏。注意此时eax寄存器中的值是3,也就是sum函数的返回值。
继续执⾏Main函数中的
# 把sum函数的返回值赋给变量n
0x000000000040115e <+30>: mov %eax,-0x4(%rbp)
该指令把eax寄存器中的值3放⼊rbp-0x4所指的内存,这⾥是变量n所在的位置,所以这条语句其实就是把sum函数的返回值赋值给n。
后⾯⼏条指令
0x0000000000401161 <+33>: mov -0x4(%rbp),%eax
0x0000000000401164 <+36>: mov %eax,%esi
0x0000000000401166 <+38>: mov $0x402010,%edi
0x000000000040116b <+43>: mov $0x0,%eax
0x0000000000401170 <+48>: callq 0x401030
0x0000000000401175 <+53>: mov $0x0,%eax
就是调⽤printf函数,调⽤过程和sum函数⼀样。
leaveq上⾯⼀条指令mov $0x0, %eax,作⽤在于把Main函数的返回值0放到eax寄存器中,等Main返回后,调⽤Main函数的函数可以拿到这个返回值。
0x000000000040117a <+58>: leaveq
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论