- 计算机在执行汇编代码时,只会顺序执行
- 通过call,jmp,ret这种指令来完成跳转
- 汇编代码执行流不像高级语言程序一样流程明确
- 汇编指令代码会经常跳转导致可读性差
例一:将下列代码转换成汇编指令:
int i=0;
while(i<100)
i++;
i=0;
翻译后的结果如下:
mov rax 0;//int i=0;
label1:
inc rax; //a++
cmp rax 100;
jge label2://if a>=100,break;
jmp label1:
label2:
mov rax 0;
jge 通过eflag寄存器中的标志位来判断
eflag标志通过之前的cmp来设置
例二:
int a,b;//存放在rax和rbx中
两种swap方式
第一种:
int c=a;
a=b;
b=c;
相当于:
mov rax rax;int c=a;
mov rax rbx;a=b;
mov rbx rcx;b=c;
第二种:
a=a+b;
b=a-b;
a=a-b;
相当于:
add rax rbx; a=a+b
mov rcx rax;
sub rcx rbx;
mov rbx rcx; b=a-b
sub rax rbx; a=a-b
基础汇编指令:
POP:
- POP指令的作用是弹栈,将栈顶的数据弹出到寄存器,然后栈顶指针向下移动一个单位
- 如pop rax指令,作用相当于mov rax [rsp]; add rsp 8
PUSH:
- PUSH指令的作用是压栈,将栈顶指针向上移动一个单位的距离,然后将一个寄存器的值存放在栈顶
- 如push rax指令,作用效果相当于sub rsp 8;mov [rsp] rax
JMP:
- 立即跳转,不涉及函数调用,用于循环,ifelse这种场合
- 如jmp 1234h指令,作用相当于mov rip 1234h
CALL:
- 函数调用,需要保存返回地址(返回地址保存在栈中)
- 如call 1234h指令,效果就是:push rip;mov rip 1234h
LEAVE
- mov rsp,rbp
- pop rbp
RET:
- 用于函数返回
- 等效于POP RIP
函数调用流程
举个例子
main调用func_b,func_b调用func_a
代码如下
1.从main函数开始,分析栈帧变化void func_a() { //do sth return; } void func_b() { func_a(); int c=1; return; } int main() { func_b(); int a=2; return 0; }
- 当运行到call func_b时main函数的栈帧
- rbp指向栈底,rsp指向栈顶
- 栈帧存放了一些main的局部变量
- main函数需要调用func_b,main只需要call func_b
- 即push rip;mov rip func_b
2.跳转到func_b后
- func_b直接执行主逻辑吗?并不,被调用函数还需要维护被调用函数自己的栈帧
被调函数维护自己栈帧的步骤
- push rbp;将调用函数的栈底指针保存
- mov rbp rsp;将栈底指针指向当前的栈顶
- sub rsp xxx;开辟被调用函数的栈帧,此时上一步的rbp就指向栈帧的底部
所谓栈帧的维护就是维护rbp和rsp两个指针
rsp永远指向栈顶
rbp用来定位局部变量
3.func_b要调用func_a,其调用流程和main函数调用func_b基本一致
不同处在于返回地址,rbp和rsp指向的地址,以及开辟的栈空间不同
调用完毕后,函数返回
leave指令
作用是维护栈帧,通常是出现在函数的结尾,与ret连用
其实际作用为:
- mov rsp rbp;
- pop rbp
即
将栈顶指针指向栈帧的底部,然后在栈中弹出新的栈底指针
在一个函数执行结束返回时,会执行leave;ret
等效于
- mov rsp rbp;
- pop rbp;
- pop eip
总结
- 调用函数
只需要将rip压栈,即push rip,然后rip赋值为被调用函数的起始地址,这一操作被隐性内置在call指令当中
- 被调用函数
push rbp
mov rbp rsp
sub rsp 0xxx
即保存调用函数的rbp指针,将自己的rbp指针指向栈顶,然后开辟栈空间给自己用,此时rbp就变成被调用函数的栈底
- 函数返回
leave;ret
等效于
mov rsp rbp
pop rbp
pop rip
即恢复栈帧,返回调用函数的返回地址