函数调用¶
相关汇编¶
- intel格式:
optcode destination, source
; 将寄存器rsp的值存储到寄存器rbp中
mov rbp, rsp
; 将四个字节的4存储到地址为rbp-4的栈上
mov DWORD PTR [rbp-4], 4
; 将rsp的值减去16
sub rsp, 16
-
传递数据的时候说明数据的大小:会在数据前面说明数据的大小
-
AT&T格式:
optcode source, destination
- 传递数据的时候说明数据的大小:后缀
- 存储的地方:前缀(%q:寄存器;$立即数;()内存)
常见汇编指令
相关寄存器¶
8086¶
-
通用寄存器:均可用来存放地址和数据
-
指针和变量寄存器:用来存放,某一段内地址偏移量,用来形成操作数地址,主要用来再堆栈操作或者变址操作中使用
-
段寄存器:由于存储器空间是分段的,所以这些段寄存器则是每个段的首地址
-
指令指针:IP用来存放将要执行的下一条指令在现在代码段的偏移量,将这个偏移量+段寄存器中存放的基地址,就找到了下一条指令的地址
-
标志位寄存器:用来存放计算结果的特征,这些标志位常常被用作接下来程序运行的条件
-
8086处理器内部有8个16位的通用寄存器,也就是CPU内部的数据单元,分别是:AX、BX、CX、DX、SP、BP、SI、DI
-
CS、DS、SS、ES这四个寄存器都是16位寄存器,用来存储进程的地址空间信息:
-
CS是代码段寄存器(Code Segment Register),通过它可以找到代码在内存中的位置
-
DS是数据段寄存器(Data Segment Register),通过它可以找到数据在内存中的位置
-
SS是栈寄存器(Stack Register),栈是程序运行过程所需要的一种数据结构,主要用于记录函数调用的关系
-
ES是一个附加段寄存器(Extra Segment Register),当发现段寄存器不够用的时候,你可以考虑使用ES段寄存器。
CS和DS中都存放着一个段的起始地址,代码段的偏移值存放在IP寄存器中,而数据段的偏移值放在通用寄存器中;由于8086架构中总线地址是20位的,而段寄存器和IP寄存器以及通用寄存器都是16位的,所以为了得到20位的地址,先将段寄存器中起始地址左移4位,然后再加上偏移量,就得到了20位的地址;也正是由于偏移量是16位的,所以每个段最大的大小是64K的
对于20位的地址总线来说,能访问到的内存大小最多也就只有2^20=1MB(如果计算得到某个要访问的地址是1MB+X,那么最后访问的是地址X,因为地址线只能发送低20位的)
x86¶
x86-64¶
-
多于6个的参数,依然还是通过入栈实现传递
-
为了效率尽量使用少于6个参数的函数
-
传递比较大的参数,尽量使用指针,因为寄存器只有64位
-
x86-64向下兼容:对于每个寄存器,我们还可以只使用它的一部分,并使用另一个新的名字
函数调用¶
- Caller Function(调用者)
- Callee Function(被调用者)
在子函数调用时,需要切换上下文使得当前调用栈进入到一个新的执行中:
- 父函数将调用参数从后向前压栈:由函数调用者完成(上文中的Caller逻辑)
- 将返回地址压栈保存:call指令完成
- 跳转到子函数起始地址执行:call指令完成
- 子函数将父函数栈帧起始地址(%rpb)压栈:由函数被调用者完成(上文中的Callee逻辑)
- 将%rbp的值设置为当前%rsp的值,即将%rbp指向子函数栈帧的起始地址:由函数被调用者完成(上文中的Callee逻辑),完成函数上下文的切换
eg¶
func():
push rbp
mov rbp, rsp
nop
pop rbp
ret
my_func():
push rbp
mov rbp, rsp
call func()
nop
pop rbp
ret
- 开始的两句就是由编译器默认生成的切换上下文语句(函数my_func中也存在这个语句是因为它最终也会被其他函数s调用)
当my-func函数调用func函数时:
- 首先,执行call指令,保存返回地址,并跳转至func函数起始地址(这里没有压栈调用参数是因为func入参为空)
- 随后,在func函数中,使用push rbp和mov rbp,rsp保存上下文,随后开始执行func函数中的逻辑
- 由于没有代码,且没有返回值,此次为nop指令
- 最后,恢复上下文,并返回
- 函数返回时,我们只需要得到函数的返回值(保存在%rax中),之后就需要将栈的结构恢复到函数调用之前的状态,并跳转到父函数的返回地址处继续执行即可
- 首先执行pop rbp指令,直接将调用栈地址恢复至调用函数之前的状态
- 通过ret指令跳转至返回地址处并执行