汇编
程序编码
在编译时指定'-Og'选项让GCC产生符合原始程序结构的机器代码
机器级代码
对C语言隐藏, 但对汇编代码可见的:
- 程序计数器
- 整数寄存器文件
- 条件码寄存器
- 向量寄存器
输出c源码的机器表示
gcc -Og -S mstore.c
机器代码与反汇编的特性:
x86-64的指令长度从1-15字节不等
指令设计的格式, 从某个给定位置, 能将字节唯一解码成机器指令
_哈夫曼编码_
反汇编无需访问源代码
反汇编与gcc的命名规则有些许差别
_比如movq的q在反汇编中会被省略_
关于格式的注解
.file "mstore.c" .text .globl mulstore .type mulstore, @functionmulstore:.LFB0: .cfi_startproc pushq %rbx .cfi_def_cfa_offset 16 .cfi_offset 3, -16 movq %rdx, %rbx call mult2 movq %rax, (%rbx) popq %rbx .cfi_def_cfa_offset 8 ret .cfi_endproc.LFE0: .size mulstore, .-mulstore .ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-36)" .section .note.GNU-stack,"",@progbits
以上是gcc完整生成的.s文件
所有. 开头的是伪指令, 可以忽略
ATT汇编代码格式
.file "mstore.c" .text .globl mulstore .type mulstore, @functionmulstore:.LFB0: .cfi_startproc pushq %rbx .cfi_def_cfa_offset 16 .cfi_offset 3, -16 movq %rdx, %rbx call mult2 movq %rax, (%rbx) popq %rbx .cfi_def_cfa_offset 8 ret .cfi_endproc.LFE0: .size mulstore, .-mulstore .ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-36)" .section .note.GNU-stack,"",@progbits
数据格式
- 8位: 字节(b)
- 16位: 字(w)
- 32位: 双字(l)
- 64位: 四字(q)
访问信息
x86-64的CPU包含一组16个存储64位的通用目的寄存器
63 31 15 8 0%rax %eax %ax %ah %al 返回值%rbx %ebx %bx %bh %bl 被调用者保存%rcx %ecx %cx %ch %cl 第4个参数%rdx %edx %dx %dh %dl 第3个参数%rsi %esi %si %sil 第2个参数%rdi %edi %di %dil 第1个参数%rbp %ebp %bp %bpl 被调用者保存%rsp %esp %sp %spl 栈指针%r8 %r8d %r8w %r8b 第5个参数%r9 %r9d %r9w %r9b 第6个参数%r10 %r10d %r10w %r10b 调用者保存%r11 %r11d %r11w %r11b 调用者保存%r12 %r12d %r12w %r12b 被调用者保存%r13 %r13d %r13w %r13b 被调用者保存%r14 %r14d %r14w %r14b 被调用者保存%r15 %r15d %r15w %r15b 被调用者保存
每个寄存器都可以作为8位、16位、32位、64位来访问
%rax %rbx %rcx %rdx 是通用目的64位寄存器,用于存储操作数和计算结果
%rsi %rdi 是源索引寄存器和目的索引寄存器,通常用于字符串和数据传输
%rbp %rsp 是基址指针寄存器和栈指针寄存器,用于处理函数调用和栈操作
%r8 %r9 %r10 %r11 %r12 %r13 %r14 %r15 是额外的通用目的寄存器,用于扩展操作数和寄存器的数量
16位操作可以访问2位字节
32位操作可以访问4位字节...
操作数指示符
立即数: 代表常数
$后面接c语言表示法的整数
寄存器: 表示寄存器里的内容
$r_a$用来表示寄存器a 用$R[r_a]$表示里面的内容
内存引用: 指定内存地址里的内容 M[地址]
数据传送指令
指令 | 效果 | 描述 |
---|---|---|
MOVE S,D | D←S | 传送 |
movb | 传送字节 | |
movw | 传送字 | |
movl | 传送双字 | |
movq | 传送四字 | |
movabsq I,R | R←I | 传送绝对的四字 |
零扩展:用于将较窄的整数类型(如8位或16位整数)扩展为较宽的整数类型(如32位或64位整数),原始数据的低位(等于或小于原始数据位数的位)保持不变,而高位被填充为零
符号扩展:用于将较窄的整数类型(如8位或16位整数)扩展为较宽的整数类型(如32位或64位整数),原始数据的低位(等于或小于原始数据位数的位)保持不变,而高位被填充为原始数据的符号位
指令 | 效果 | 描述 |
---|---|---|
MOVEZ S,R | R←零扩展(S) | 以零扩展进行传送 |
movzbw | 将做了零扩展的字节传送到字 | |
movzbl | 将做了零扩展的字节传送到双字 | |
movzwl | 将做了零扩展的字传送到双字 | |
movzbq | 将做了零扩展的字节传送到四字 | |
movzwq | 将做了零扩展的字传送到四字 | |
MOVS S,R | R←符号扩展(S) | 以符号扩展进行传送 |
movsbw | 将做了符号扩展的字节传送到字 | |
movsbl | 将做了符号扩展的字节传送到双字 | |
movswl | 将做了符号扩展的字传送到双字 | |
movsbq | 将做了符号扩展的字节传送到四字 | |
movswq | 将做了符号扩展的字传送到四字 | |
movslq | 将做了符号扩展的整形传送到四字 | |
cltq | %rax←符号扩展(%eax) | 把%eax符号扩展到%rax |
压入栈和弹出栈数据
. 将四字压入栈pushq S . 将四字弹出栈popq D
%rsp 是栈指针 %rax是返回值
算术和逻辑操作
指令 | 效果 | 描述 |
---|---|---|
leaq S,D | D←&S | 加载有效地址 |
INC D | D←D+1 | 加1 |
DEC D | D←D-1 | 减1 |
NEG D | D←-D | 取反 |
NOT D | D←~D | 取反 |
ADD S,D | D←D+S | 加 |
SUB S,D | D←D-S | 减 |
IMUL S,D | D←D*S | 乘 |
XOR S,D | D←D^S | 异或 |
OR S,D | D←D | 或 |
AND S,D | D←D&S | 与 |
SAL S,D | D←D<<S | 左移 |
SHL S,D | D←D<<S | 左移 = SAL |
SAR S,D | $D←D>>_AS$ | 算术右移 |
SHR S,D | $D←D>>_LS$ | 逻辑右移 |
加载有效地址
. x= y+x*4leaq (%rdi,%rsi,4), %rax
一元和二元操作
. 从%edi中减去%esisubl %esi, %edi
移位操作
. 将x左移四位salq $4, %rax
特殊的算术操作
指令 | 效果 | 描述 |
---|---|---|
imulq S | R[%rdx]: R[%rax]←S * R[%rax] | 有符号全乘法 |
mulq S | R[%rdx]: R[%rax]←S * R[%rax] | 无符号全乘法 |
clto | R[%rdx]: R[%rax]← 符号扩展(R[%rax]) | 转换为八字 |
idivq S | R[%rdx]←R[%rdx]: R[%rax] mod/÷ S | 有符号除法 |
divq S | R[%rdx]←R[%rdx]: R[%rax] mod/÷ S | 无符号除法 |
控制
条件码
cmp 指令在被执行时,会首先比较两个变量的大小,并根据比较结果,动态调整 CPU 上 FLAGS 寄存器中的相应位
test 指令的执行方式与 cmp 类似,只不过它会对传入的两个操作数做隐式的“与”操作,而非减法操作
标志位名称 | 位 | 全称 | 什么情况下置位(即变更为值1) |
---|---|---|---|
CF | 0 | Carry | 指令执行引起了进位或借位 |
PF | 2 | Parity | 指令执行结果的最低有效字节中值为1的位个数为偶数 |
ZF | 6 | Zero | 指令执行结果为0 |
SF | 7 | Sign | 指令执行结果的最高有效位为1 |
OF | 11 | Overtlow | 当操作致被当做有符号数时,指令的执行产生了溢出 |
指令 | 基于 | 描述 |
---|---|---|
CMP S2,S1 | S1-S2 | 比较 |
cmpb | 比较 byte | |
cmpw | 比较 word | |
cmpl | 比较 double word | |
TEST S2,S1 | S1&S2 | 测试 |
testb | 测试 byte | |
testw | 测试 word | |
testl | 测试 double word |
这些指令不修改寄存器的值,只设置条件码
cmp DWORD PTR [rbp-4], 1 jne .L2 mov eax, 101 jmp .L3.L2: mov eax, 10.L3: pop rbp ret
对应于
if (num == 1) { return 101;}return 10;
循环、选择等操作,都是通过 cmp + jmp 来实现的
读取条件码
指令 | 别名 | 效果 | 设置条件 |
---|---|---|---|
sete D | setz | D←ZF | 相等或为0 |
setne D | setnz | D←!ZF | 不相等或非0 |
sets D | D←SF | 负数 | |
setns D | D←!SF | 非负数 | |
setg D | setnle | D←~(SF ^ OF) & ~ZF | 有符号大于 |
setge D | setnl | D←~(SF ^ OF) | 有符号大于或等于 |
setl D | setnge | D←SF ^ OF | 有符号小于 |
setle D | setng | `D←(SF ^ OF) | ZF` |
seta D | setnbe | D←~CF & ~ZF | 无符号大于 |
setae D | setnb | D←~CF | 无符号大于或等于 |
setb D | setnae | D←CF | 无符号小于 |
setbe D | setna | `D←CF | ZF` |
跳转指令
指令 | 别名 | 跳转条件 | 描述 |
---|---|---|---|
jmp LABEL | 1 | 直接跳转 | |
jmp *Operand | 1 | 间接跳转 | |
je LABEL | jz | ZF | 相等或为0 |
jne LABEL | jnz | ZF | 不相等或不为0 |
js LABEL | SF | 负数 | |
jns LABEL | ~SF | 非负数 | |
jg LABEL | jnle | ~(SF ^ OF) & ~ZF | 有符号大于 |
jge LABEL | jnl | ~(SF ^ OF) | 有符号大于或等于 |
jl LABEL | jnge | SF ^ OF | 有符号小于 |
jle LABEL | jng | (SF ^ OF) | ZF |
ja LABEL | jnbe | ~CF & ~ZF | 无符号大于 |
jae LABEL | jnb | ~CF | 无符号大于或等于 |
jb LABEL | jnae | CF | 无符号小于 |
jbe LABEL | jna | `CF | ZF` |
用条件控制实现分支控制
cmpq %rsi, %rdi jg .L4 movq %rdi, %rax subq %rsi, %rax ret.L4: leaq (%rdi,%rsi), %rax ret
对应的c代码:
if (x > y){ return x+y;}else{ return x-y;}
用条件传送实现条件分支
分支预测
指令 | 别名 | 传送条件 | 描述 |
---|---|---|---|
cmov S,R | cmovz | ZF | 相等或为0 |
cmovne S,R | cmovnz | ~ZF | 不相等或非0 |
cmovs S,R | SF | 负数 | |
cmovns S,R | ~SF | 非负数 | |
cmovg S,R | cmovnle | ~(SF ^ OF) & ~ZF | 有符号大于 |
cmovge S,R | cmovnl | ~(SF ^ OF) | 有符号大于或等于 |
cmovl S,R | cmovnge | SF ^ OF | 有符号小于 |
cmovle S,R | cmovng | `(SF ^ OF) | ZF` |
cmova S,R | cmovnbe | ~CF & ~ZF | 无符号大于 |
cmovae S,R | cmovnb | ~CF | 无符号大于或等于 |
cmovb S,R | cmovnae | CF | 无符号小于 |
cmovbe S,R | cmovna | `CF | ZF` |
循环
do-while
while
- guarded-do
for
switch语句
跳转表:一种用空间换时间的条件匹配策略,这种优化手段通过将每个case标签生成一个唯一的标号,然后创建一个跳转表,其中每个条目对应一个case标签,再通过 jmp 指令,对输入值进行计算,以计算出跳转表的实际索引,然后跳转过去
jmp qword ptr [8*rdi +.LJTIO_0].LJT10_0: .quad .LBBO 4 ...
函数调用
- 传递控制
- 传递数据
- 分配和释放内存
运行时栈
转移控制
保存当前程序地址,将程序计数器设置为新过程地址 返回时读取保存的地址,继续执行
在 x86-64 架构下,其会通过 call 指令将当前的程序地址压入栈中,并跳转到指定的函数地址,被调用的函数通过 ret 指令返回结果
int foo(int n) { return n +1;}int main() { foo(1); return 1;}
foo(int): push rbp mov rbp, rsp mov DWORD PTR [rbp-4], edi mov eax, DWORD PTR [rbp-4] add eax, 1 pop rbp retmain: push rbp mov rbp, rsp mov edi, 1 call foo(int) mov eax, 1 pop rbp ret
参数传递
返回值传递
- 函数调用产生整数类型的返回值,且小于等于 64 位时,通过寄存器 rax 进行传递
- 大于 64 位,小于等于 128 位时,则使用寄存器 rax 与 rdx 分别存储返回值的低 64 位与高 64 位
- 对于浮点数类型的返回值,默认使用 xmm0 与 xmm1 寄存器进行存储。而当返回值过大时,则会选择性使用 ymm 与 zmm 来替代 xmm 寄存器
栈上的局部存储
寄存器中的局部存储空间
- 被调用者保存寄存器
- 调用者保存寄存器
递归过程
尾递归优化
在一定条件下,编译器可以直接利用跳转指令取代函数调用指令。尾递归调用的一个重要条件是:递归调用语句必须作为函数返回前的最后一条语句
编译器会使用跳转指令(如je、jne、jle等)来替换函数调用时所使用的 call 指令,这样就
int f(int i, int sum) { if (i == 0) { return sum; } return (i - 1, sum * i);}
f(int, int): mov eax, esi test edi, edi je .L1 imul eax, edi.L1: ret
数组的分配和访问
基本原则
T A[N]
指针运算
&D [ i ] [ j ] = X
D
L(Ci+j)
定长数组
变长数组
异质的数据结构
都是对地址进行偏移得到的
- 结构
- 联合
- 数据对齐
在机器级程序中将控制与数据结合起来
理解指针
在计算机科学中,指针(Pointer)是编程语言中的一个对象,利用地址,它的值直接指向(points to)存在电脑存储器中另一个地方的值
对于这样的一条语句
int *p = &n;
其会通过 lea 指令找到 n 的地址,然后通过 mov 指令将 n 的值赋给 p
lea rax, [rbp-12]mov QWORD PTR [rbp-8], rax
GDB调试器
UNIX及UNIX-like下的调试工具
内存越界引用和缓冲区溢出
对抗缓冲区溢出攻击
- 栈随机化
- 栈破坏检测
- 限制可执行代码区域
变长帧
浮点代码
%ymm0 ~ %ymm15
浮点传送和转换操作
指令 | 源 | 目的 | 描述 |
---|---|---|---|
vmovss | $M_{32}$ | X | 传送单精度数 |
vmovss | X | $M_{32}$ | 传送单精度数 |
vmovsd | $M_{64}$ | X | 传送双精度数 |
vmovsd | X | $M_{64}$ | 传送双精度数 |
vmovaps | X | X | 传送对齐的封装好的单精度数 |
vmovapd | X | X | 传送对齐的封装好的双精度数 |
指令 | 源 | 目的 | 描述 |
---|---|---|---|
vcvttss2si | $X/M_{32}$ | $R_{32}$ | 用截断的方法把单精度数转换成整数 |
vevttsd2si | $X/M_{64}$ | $R_{32}$ | 用截断的方法把双精度数转换成整数 |
vcvttss2siq | $X/M_{32}$ | $R_{64}$ | 用截断的方法把单精度数转换成四字整数 |
vcvttsd2siq | $X/M_{64}$ | $R_{64}$ | 用截断的方法把双精度数转换成四字整数 |
指令 | 源1 | 源2 | 目的 | 描述 |
---|---|---|---|---|
vcvtsi2ss | $M_{32}/R_{32}$ | X | X | 把整数转换成单精度数 |
vcvtsi2sd | $M_{32}/R_{32}$ | X | X | 把整数转换成双精度数 |
vcvtsi2ssq | $M_{64}/R_{64}$ | X | X | 把四字整数转换成单精度数 |
vcvtsi2sdq | $M_{64}/R_{64}$ | X | X | 把四字整数转换成双精度数 |
过程中的浮点代码
使用XMM寄存器来传递浮点参数
浮点运算操作
单精度 | 双精度 | 效果 | 描述 |
---|---|---|---|
vaddss | vaddsd | D←S2+S1 | 浮点数加 |
vsubss | vsubsd | D←S2-S1 | 浮点数减 |
vmulss | vmulsd | D←S2XS1 | 浮点数乘 |
vdivss | vdivsd | D←S2/S1 | 浮点数除 |
vmaxss | vmaxsd | D←max(S2,S1) | 浮点数最大值 |
vminss | vminsd | D←min(S2,S1) | 浮点数最小值 |
sgrtss | sqrtsd | $D←\sqrt{S1}$ | 浮点数平方根 |
定义和使用浮点常数
浮点操作不能把立即数作为操作数
编译器必须为所有浮点常量初始化存储空间
在浮点代码中使用位级操作
单精度 | 双精度 | 效果 | 描述 |
---|---|---|---|
vxorps | vorpd | D←S2·S1 | 位级异或(EXCLUSIVE-OR) |
vandps | andpd | D←S2&S1 | 位级与(AND) |