X86汇编
常见寄存器(64位)
Ref: zhuanlan.zhihu.com/p/502718676
通用寄存器(16个)
rax, rbx, rcx, rdx, rsi, rdi, rbp, rsp, r8~r15
- rsi,rdi, rdx, rcx //用来参数传递(linux)
- rsp, rbp //指向栈顶与栈底
标志寄存器
rflags 标识指令执行的状态
指令寄存器
rip 存放下一条指令的内存地址
控制寄存器
控制CPU的工作状态,例如保护模式,分页等
模型特定寄存器
这是一组寄存器,常常与一些新功能有关,例如VMX
指令
- push :
esp--
+mov data *%esp
以下为对称设计 - call :
push ip
+jump
- enter :
push ebp
+mov %esp $ebp
- leave :
mov %ebp %esp
+pop %ebp
//与enter 搭配使用 - ret :
pop ip
+jump
//与call搭配使用
lea 与 mov 区别
mov %eax, %ebx # 把eax拷贝到ebx
mov (%eax), %ebx # 把地址为eax的数据拷贝到ebx寄存器
mov 0x1(%eax), %ebx # 把地址为eax+1的数据拷贝到ebx寄存器
lea (%eax), %ebx # 把eax拷贝到ebx 与 mov %eax, %ebx 相同
lea 0x1(%eax), %ebx # 把eax+1拷贝到ebx(只是地址,不是数据)
lea %eax, %ebx # 错误写法。
函数调用栈
C代码
int fun(int a, int b)
{
int c=0;
c = a + b;
return c;
}
int main()
{
int a =1;
int result = fun(a,2);
return 0;
}
反汇编代码(64位)
00000000000005fa <_Z3funii>:
5fa: 55 push %rbp //压栈,用于后期还原,会修改esp
5fb: 48 89 e5 mov %rsp,%rbp //修改rbp, 两者指向同一个位置。
5fe: 89 7d ec mov %edi,-0x14(%rbp) //传入参数1,写到内存
601: 89 75 e8 mov %esi,-0x18(%rbp) //传入参数2,写到内存
604: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) //内存上创建临时变量c
60b: 8b 55 ec mov -0x14(%rbp),%edx //从内存中读取参数1到寄存器
60e: 8b 45 e8 mov -0x18(%rbp),%eax //从内存中读取参数2到寄存器
611: 01 d0 add %edx,%eax //执行加法计算
613: 89 45 fc mov %eax,-0x4(%rbp) //结果从寄存器写入内存
616: 8b 45 fc mov -0x4(%rbp),%eax
619: 5d pop %rbp //将5fa中压入栈的地址还原
61a: c3 retq //pop ip,进而修改函数的执行顺序。
000000000000061b <main>:
61b: 55 push %rbp
61c: 48 89 e5 mov %rsp,%rbp
61f: 48 83 ec 10 sub $0x10,%rsp
623: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%rbp)
62a: 8b 45 f8 mov -0x8(%rbp),%eax
62d: be 02 00 00 00 mov $0x2,%esi //待传参数放到寄存器1
632: 89 c7 mov %eax,%edi //待传参数放到寄存器2
634: e8 c1 ff ff ff callq 5fa <_Z3funii> //jump + push ip
639: 89 45 fc mov %eax,-0x4(%rbp) //将返回值从寄存器拷贝到内存
63c: b8 00 00 00 00 mov $0x0,%eax
641: c9 leaveq
642: c3 retq
643: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
64a: 00 00 00
64d: 0f 1f 00 nopl (%rax)
栈变化图解(右边表示指向此地址的寄存器)
#原始状态
data1 | esp
data2 | ebp
#after call
old ip | esp
data1 |
data2 | ebp
#at the begining of function, after push ebp, move esp ebp
old ebp | esp&ebp
old ip |
data1 |
data2 |
#at the end of function, after mov rbp rsp , pop rbp
old ip | esp
data1 |
data2 | ebp
#after ret
data1 | esp
data2 | ebp
栈缩略图
返回地址(old IP)
------前后栈分割线----------
old EBP(当前EBP寄存器指向此位置)
函数参数
局部变量
返回地址(old IP)
------前后栈分割线----------
old EBP(当前EBP寄存器指向此位置)
......
函数的前后缀
push %rbp # 把rbp指针压栈
mov %rsp,%rbp # 此时rbp,与rsp相同,都指向了一个
mov %rbp, %rsp
pop %rbp
ret # 就是pop %rip,还原到原来的位置。
Tips:
- 有两套存储系统,分别是寄存器以及内存,其中cpu只能在寄存器上完成计算,内存的话分为代码区与数据区。ip寄存器与代码区打交道,ebp以及esp等与数据区打交道。
- esp指向frame的最后一个数据,而非空数据。此值会随着push,pop修改,也可以手动改变。
- mov 与movl,在这里一样,mov会被自动翻译成(movl)
- 汇编不能原操作地址与目的地址都是内存。
- EBP就是暂存ESP的内容,EBP寄存器所指向地址内容为上一个EBP。可以通过加编译优化选项,将ebp优化掉。
- 32位参数传递与64位不同,32通过push到内存栈的方式,而64位通过寄存器的方式。
- 32位程序中
__x86.get_pc_thunk.ax
,是因为无法直接读取eip寄存器,会通过借助call,将eip寄存器内容push到内存中,然后mov 到ax,进而实现全局变量的访问。 - 程序在运行的时候,不断根据ip寄存器,取址执行,但是jmp,call,ret之类的指令会修改ip寄存器,改变程序执行的方向。
Inline assemble in C language
在C里面嵌入asm代码。
#include <stdio.h>
int main()
{
int src = 1;
int dst;
asm ("mov %1, %0\n\t"
"add $1, %0"
: "=r" (dst)
: "r" (src));
printf("%d\n", dst);
return 0;
}
格式一:
asm asm-qualifiers ( AssemblerTemplate // %0,%1,%2分别代表了后面的第一个,第二个,第三个;AT&T格式默认从输入输出顺序为从左到右;常量需要加$符号
: OutputOperands // 输出必须要加+,r/m分别代表寄存器,也可同时指定,让编译器去决定。
: InputOperands // 输入不需要加=
: Clobbers
: GotoLabels)
格式二:就是不使用序号,使用符号,这里的d_c,e_c指定是c代码里的变量。
asm("mov %[e], %[d]"
: [d] "=rm" (d_c)
: [e] "rm" (*e_c))
如果要对寄存器进行操作,需要加两个百分号, 例如:
%%EAX
clobber的作用如下:
内联代码并非存粹的,不需要任何修改的汇编代码,gcc asm会进行相关的编译(载入c局部变量等),这时某些指令会修改额外的寄存器,或者汇编代码里面直接使用了某些寄存器,gcc并不知道,这里必须告诉gcc,我的asm代码中这个寄存器会被cloberring(修改),你在转汇编代码的时候,不能使用这些寄存器。这里以clobber是否添加rax为例,对比了两个汇编的区别。
int fun2()
{
int input =1;
int output;
asm("mov $2, %[dst]\n\t"
"add %[src], %[dst]"
:[dst] "=rm" (output)
:[src] "rm" (input)
);
return 0;
}
int fun3()
{
int input =1;
int output;
asm("mov $2, %[dst]\n\t"
"add %[src], %[dst]"
:[dst] "=rm" (output)
:[src] "rm" (input)
: "rax");
return 0;
}
对应的汇编代码如下。
0000000000001193 <fun2>:
1193: f3 0f 1e fa endbr64
1197: 55 push %rbp
1198: 48 89 e5 mov %rsp,%rbp
119b: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%rbp)
11a2: b8 02 00 00 00 mov $0x2,%eax
11a7: 03 45 f8 add -0x8(%rbp),%eax
11aa: 89 45 fc mov %eax,-0x4(%rbp)
11ad: b8 00 00 00 00 mov $0x0,%eax
11b2: 5d pop %rbp
11b3: c3 retq
00000000000011b4 <fun3>:
11b4: f3 0f 1e fa endbr64
11b8: 55 push %rbp
11b9: 48 89 e5 mov %rsp,%rbp
11bc: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%rbp)
11c3: ba 02 00 00 00 mov $0x2,%edx
11c8: 03 55 f8 add -0x8(%rbp),%edx
11cb: 89 55 fc mov %edx,-0x4(%rbp)
11ce: b8 00 00 00 00 mov $0x0,%eax
11d3: 5d pop %rbp
11d4: c3 retq
两个特别的参数:
cc
:修改了flag寄存器。memory
:告诉编译器,我的代码会对其他内存进行读写,这里加入一个barrier,让其他值flush到相关的寄存器。
PAUSE指令
spin-lock,如果一直在循环里,会让branch predictor 以为一直会执行循环的指令,但是如果某个时刻开始满足条件,就要flush整个pipeline,这会让锁的时间超过期望时间,加入PAUSE之后,会延缓内存的读取,这样整个pipeline不会填充满推测的指令了。
- tpause(timed pause)指令是对pause的优化,pause会消耗140个时钟周期,tpause会程序进入C0.1(弱睡眠)的状态,进而节省电量。
- Ref: stackoverflow.com/questions/1…