X86结构中,cs寄存器和rip寄存器共同控制着CPU要执行的下一条指令(当前在不同的模式中控制方式不同,如:实地址2模式和保护模式,长模式等),一般会按照指令在内存中存储的顺序,依次执行。如果想要在普通程序(除去系统调用和中断)执行中跳转到某一条指令,就需要使用JMP
、CALL
、RET
及其变种指令。
利用上述相关指令进行搭配,就可以实现在一块汇编代码(a)执行中,跳转到另一块汇编代码(b),执行完(b)后返回到(a)的下一条指令继续执行;从而可以实现函数的调用和返回。
通过分析c语言编译->反汇编后汇编代码,可以看出c语言的调用方式,注意不同的编译环境调用机制可能不一样,实验环境是gcc 4.4.7
int add(int a,int b){
int c = a+b;
return c;
}
void main(){
int i = 0;
int j = 1;
add(i,j);
}
栈的生长方向是由高地址到低地址,即压栈时(pop),rsp寄存器会减小;出栈时(push),rsp寄存器会增大。
leaveq等效于
movq %rbp, %rsp; popq %rbp;这条指令的主要为了对应函数入口处的push %rbp;mov %rsp,%rbp。
000000000000009a : ;add函数机器码和对应的汇编代码(64位)
; 地址 机器码 汇编代码
9a: 55 push %rbp
9b: 48 89 e5 mov %rsp,%rbp
9e: 89 7d ec mov %edi,-0x14(%rbp)
a1: 89 75 e8 mov %esi,-0x18(%rbp)
a4: 8b 45 e8 mov -0x18(%rbp),%eax
a7: 8b 55 ec mov -0x14(%rbp),%edx
aa: 8d 04 02 lea (%rdx,%rax,1),%eax
ad: 89 45 fc mov %eax,-0x4(%rbp)
b0: 8b 45 fc mov -0x4(%rbp),%eax
b3: c9 leaveq
b4: c3 retq
00000000000000b5 : ;main函数机器码和对应的汇编代码(64位)
; 地址 机器码 汇编代码
b5: 55 push %rbp
b6: 48 89 e5 mov %rsp,%rbp
b9: 48 83 ec 10 sub $0x10,%rsp
bd: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%rbp)
c4: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%rbp)
cb: 8b 55 fc mov -0x4(%rbp),%edx
ce: 8b 45 f8 mov -0x8(%rbp),%eax
d1: 89 d6 mov %edx,%esi
d3: 89 c7 mov %eax,%edi
d5: 48 b8 9a 00 00 00 00 mov $0x9a,%rax
dc: 00 00 00
df: ff d0 callq *%rax
e1: 89 45 f8 mov %eax,-0x8(%rbp)
e4: c9 leaveq
e5: c3 retq
分析上述反汇编后的汇编代码。
main函数和add函数的入口部分代码和出口部分代码都是:
;函数入口
push %rbp ;保存rbp的值
mov %rsp,%rbp ;将当前栈顶数值(a)->rbp寄存器
;中间部分,不会改变rbp寄存器的值
;函数出口
;以下2条汇编代码等同于leaveq
mov %rbp,%rsp ;将rbp寄存器的数值(a)->rsp,现在栈顶的数就是入口保存的rbp值
popq %rbp ;将栈顶值恢复到rbp寄存器
可以得出所有c语言函数的入口和出口部分都会保存和恢复rbp的值,并且在保存rbp旧值到当前栈顶后,会将当前栈顶赋值给rbp;阅读函数中间部分汇编代码,发现不会改变rbp的值了,这样的话不管函数中间部分如何改变rsp的值(可能该函数会使用push或pop指令,或者使用call指令进行了压栈等等可以改变rsp寄存器的值的指令),最终都可以在函数出口时,将栈环境恢复到入口未执行时的栈环境(栈环境指:rsp和rbp寄存器的值,还有栈中的值)。
add函数中间部分:
mov %edi,-0x14(%rbp) ;保存第一个参数的值
mov %esi,-0x18(%rbp) ;保存第二个参数的值
mov -0x18(%rbp),%eax
mov -0x14(%rbp),%edx
lea (%rdx,%rax,1),%eax
mov %eax,-0x4(%rbp) ;保存局部变量c的值
mov -0x4(%rbp),%eax ;保存返回值到eax
首先rbp在入口部分结束时,被赋予了新值(当前栈顶),其次因为栈时由高地址向低地址扩展;所以上述代码等于先将edi和esi的值保存到内存栈扩展方向的某2个位置上(rbp-0x14,rbp-0x18),然后将内存中这两个值读出来进行运算,完成后保存到内存栈扩展方向的另一个位置上(rbp-0x4);通过对比add函数的c语言代码,可以发现内存(rbp-0x14,rbp-0x18)位置最终保存该函数的2个输入参数,内存(rbp-0x4)位置,最终保存局部变量c;由此得出,寄存器edi,esi分别为add传入了2个参数,寄存器eax保存了返回参数。
观察main函数中间部分,来验证上述假设:
sub $0x10,%rsp ;rsp=rsp-0x10 使得在callq调用时,将下一条指令地址保存到(rsp-0x10)位置
movl $0x0,-0x8(%rbp) ;保存第一个局部变量i的值0
movl $0x1,-0x4(%rbp) ;保存第二个局部变量j的值1
mov -0x4(%rbp),%edx
mov -0x8(%rbp),%eax
mov %edx,%esi ;将j的值保存到esi中(第二个参数)
mov %eax,%edi ;将i的值保存到edi中(第一个参数)
mov $0x9a,%rax ;将add函数的入口地址保存到rax中
callq *%rax ;调用add函数(将下一条指令地址压栈,然后跳转到0x9a处执行,即跳到add函数执行第一条指令)
mov %eax,-0x8(%rbp) ;将返回值赋值给i,(rbp-0x8)位置代表变量i
当改变add函数的输入参数为一个float变量和一个int变量时:
int add(float a,int b){
int c = a+b;
return c;
}
查看反汇编后的汇编代码:
0000000000000027 :
27: 55 push %rbp
28: 48 89 e5 mov %rsp,%rbp
2b: f3 0f 11 45 ec movss %xmm0,-0x14(%rbp)
30: 89 7d e8 mov %edi,-0x18(%rbp)
33: f3 0f 2a 45 e8 cvtsi2ssl -0x18(%rbp),%xmm0
38: f3 0f 58 45 ec addss -0x14(%rbp),%xmm0
3d: f3 0f 2c c0 cvttss2si %xmm0,%eax
41: 89 45 fc mov %eax,-0x4(%rbp)
44: 8b 45 fc mov -0x4(%rbp),%eax
47: c9 leaveq
48: c3 retq
可以发现函数会从xmm0寄存器获取输入的float参数。
add函数和mian函数返回时,都使用了下述指令:
retq
该指令时是64位时,ret指令的变种。因为执行到该指令时,被调用函数的出口部分已经执行完毕,栈恢复到调用者刚进入时的环境,被调用者执行ret指令将会弹出当前rsp指向的栈顶的指令地址,然后跳转到该指令执行。所以在a函数调用b函数时,要确保跳转到b入口处时,当前rsp指向的栈顶保存了a函数在b函数返回后想要指行的指令地址。返回值保存到了寄存器eax中。
如果将调用者mian函数callq指令和被调用者add函数的retq指令联系起来,如下:
00000000000000b5 : ;main函数机器码和对应的汇编代码(64位)
; 地址 机器码 汇编代码
b9: 48 83 ec 10 sub $0x10,%rsp ;将rsp栈指针赋予新值newRsp,相当于初始化了一个空栈newStack
;...........
d5: 48 b8 9a 00 00 00 00 mov $0x9a,%rax
dc: 00 00 00
df: ff d0 callq *%rax ;将mian函数下一条指令地址(0xel)压到栈newStack,然后跳转到(0x9a)执行
e1: 89 45 f8 mov %eax,-0x8(%rbp) ;add函数retq执行后,跳回到该指令继续执行main函数。
;...........
e4: c9 leaveq ;mian函数执行完成后,恢复到main函数入口时的栈环境,同时销毁了newStack(rsp = rbp)
;...........
000000000000009a : ;add函数机器码和对应的汇编代码(64位)
; 地址 机器码 汇编代码
9a: 55 push %rbp ;将mian函数的rbp保存到新栈newStack
9b: 48 89 e5 mov %rsp,%rbp ;让rbp指向新栈newStack栈顶
;........................
b3: c9 leaveq ;add业务代码执行完成,恢复到入口时的新栈newStack环境,此时新栈newStack中只保存了(0xel),即main下一条指令地址
b4: c3 retq ;弹出0xel,并跳转到(0xel)执行,此时rsp=newRsp,恢复栈环境到mian函数未执行Call时。
当执行一个c语言函数X时,都会在执行到函数X的第一条指令前,初始化好一个栈,(rsp减去一个栈单位的地址)是当前函数X的栈基址—减去一个栈单位,是因为在函数X退出时(ret)会额外在栈中弹出一个栈单位的数作为返回地址,因为ret指令也是在当前函数X中执行,因此函数X的栈基地址应该为函数X执行第一条指令前的(rsp减去一个栈单位的地址)。
栈底部保存着函数的返回地址(即函数X执行完后,要返回到哪个地址处执行指令)和函数X入口时的rbp值,紧接着保存函数X会使用到的局部变量和函数参数。函数X如果有输入参数,则按照从左向右的顺序依次将通用寄存器RDI、RSI、RDX、RCX、R8和R9中的参数值保存到栈中;同时,寄存器XMM0~XMM7用来获取浮点参数,而RAX寄存器则用于保存函数的返回值;因为函数的所有参数还有局部变量都会被保存到该函数的栈中,在参数保存到栈中后,对于这些寄存器就可以随意使用,因为对于变量和参数的赋值都会直接通过栈进行。
当函数X执行结束返回后,会使当前的rsp指向函数 X的栈基址。
AMD64 Architecture Programmer’s Manual, Volume 1: Application Programming
一个64位操作系统的设计与实现 第 2 章 环境搭建及基础知识
自制操作系统GitHub地址