汇编程序由4种类型的组件组成:指令(instruction)、伪指令(directive)、标号(label)及注释(comment)

AT&T和Intel语法

AT&T语法会在每个寄存器前面加上%,每个常量前加上$,并且源操作数在目的地操作数前

mov $0x1, %eax

mov eax, 0x1

x86指令的机器级结构

x86指令由可选前缀(prefix)、操作码(opcode)及零个或多个操作数(operand)组成。注意除了操作码外,剩余部分都是可选的

image-20241127141753463

寄存器

通用寄存器

image-20241127141826710

其他寄存器:

rip:指令寄存器

rflag:标志寄存器,用于一些条件,判断等标志位

csdsssesfsgs段寄存器:x86-64目前已废止内存分段

常见指令

指令 描述
数据传输
mov dst,src 将src赋给dst
xchg dst1,dst2 互换dst1和dst2
push src 将src压栈,并递减rsp
pop dst 出栈赋给dst,并递增rsp
算术
add dst, src dst +=src
sub dst, src dst –= src
inc dst dst += 1
dec dst dst –= 1
neg dst dst = –dst
cmp src1, src2 根据src1−src2设置状态标志位
逻辑/按位
and dst, src dst &= src
or dst, src dst |= src
xor dst, src dst ˆ= src
not dst dst = ~dst
test src1, src2 根据src1 & src2设置状态标志位
无条件分支
jmp addr 跳转到地址
call addr 压入返回地址到栈上,然后调用函数地址
ret 从栈上弹出返回地址,然后跳转到该地址
syscall 进入内核执行系统调用
跳转分支(基于状态标志位)jcc addr仅在条件cc成立时才跳转到该地址,否则进入jncc相反条件,在条件cc不成立时跳转
je addr / jz addr 如果设置ZF零标志位则跳转(如当上一个cmp中的操作数相同时)
ja addr 上一次比较中,如果dst大于src则跳转(无符号)
jb addr 上一次比较中,如果dst小于src则跳转(无符号)
jg addr 上一次比较中,如果dst大于src则跳转(有符号)
jl addr 上一次比较中,如果dst小于src则跳转(有符号)
jge addr 上一次比较中,如果dst大于等于src则跳转(有符号)
jle addr 上一次比较中,如果dst小于等于src则跳转(有符号)
js addr 上一次比较中,如果结果为负则跳转,符号位置1
杂项
lea dst, src 将内存地址加载到dst中,(dst=&src,其中src必须在内存)
nop 空指令,不执行操作(用作代码填充)

函数调用栈

在compiler explorer看,简单的函数调用栈,调用前edi保存第一个参数

call指令把下一条指令地址压栈,这里编译器优化并没有为foo开辟新栈,pop恢复栈帧,ret返回mov eax,0这条指令地址

栈的弹出后原本数据依然存在,只是复制该值并更新了rsp,所以可以通过此来获取一些数据?

左边:指令地址
中间:机器码

右边:指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void foo(int i)
{}
1129: f3 0f 1e fa endbr64
112d: 55 push %rbp
112e: 48 89 e5 mov %rsp,%rbp
1131: 89 7d fc mov %edi,-0x4(%rbp)
1134: 90 nop
1135: 5d pop %rbp
1136: c3 ret

0000000000001137 <main>:
int main()
{
1137: f3 0f 1e fa endbr64
113b: 55 push %rbp
113c: 48 89 e5 mov %rsp,%rbp
113f: 48 83 ec 10 sub $0x10,%rsp
int i = 0;
1143: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
foo(1);
114a: bf 01 00 00 00 mov $0x1,%edi
114f: e8 d5 ff ff ff call 1129 <foo>
1154: b8 00 00 00 00 mov $0x0,%eax
1159: c9 leave
115a: c3 ret
1
2
3
4
5
0x7fffffffde68: 0x00    0x00    0x00    0x00    0x01    0x00    0x00    0x00
0x7fffffffde70: 0x90 0xde 0xff 0xff 0xff 0x7f 0x00 0x00
0x7fffffffde78: 0x54 0x51 0x55 0x55 0x55 0x55 0x00 0x00
0x7fffffffde80: 0x70 0xdf 0xff 0xff 0xff 0x7f 0x00 0x00
0x7fffffffde88: 0xb8 0xdf 0xff 0xff 0x00 0x00 0x00 0x00

最后总结一下:

当call指令执行时,caller会根据需要保存调用方寄存器eax、ecx和edx,然后从最后一个参数开始压栈,最后保存下一条指令的地址;4字节以内的返回值存储在eax,如果超过4字节,the caller passes an “extra” first argument to the callee,这里像是x=foo(a,b,c)和foo(&x,a,b,c)的区别,第二个x是指向返回值的地址

当被调用foo获得程序控制权,首先设置堆栈帧,执行两条指令push ebp move ebp,esp,然后为临时变量分配空间(上面的程序没有临时变量需要存储,所以直接用main的堆栈);最后如果使用ebx,esi,edi则callee保存这些内容;不论esp如何push和pop,始终可以通过ebp+8指向第一个参数