汇编基础
汇编程序由4种类型的组件组成:指令(instruction)、伪指令(directive)、标号(label)及注释(comment)
AT&T和Intel语法
AT&T语法会在每个寄存器前面加上%,每个常量前加上$,并且源操作数在目的地操作数前
mov $0x1, %eax
mov eax, 0x1
x86指令的机器级结构
x86指令由可选前缀(prefix)、操作码(opcode)及零个或多个操作数(operand)组成。注意除了操作码外,剩余部分都是可选的
寄存器
通用寄存器
其他寄存器:
rip:指令寄存器
rflag:标志寄存器,用于一些条件,判断等标志位
cs
、ds
、ss
、es
、fs
及gs
段寄存器: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 | void foo(int i) |
1 | 0x7fffffffde68: 0x00 0x00 0x00 0x00 0x01 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指向第一个参数