汇编是指令集的助记符,由编译器 + 风格 + 指令集架构 (ISA) 的组合决定语言特性,没统一标准:
编译器:
1)MASM:仅支持 Windows 平台,唯一完美支持按需编译的编译器,不支持输出 bin 格式。
2)NASM:跨平台,支持多种输出格式 (bin/coff/omf/elf/…)
风格:
1)Intel 风格
2)AT&T 风格
主流指令集架构:
1)x86-64/x64/amd64/Intel64
2)ARM64/AArch64
3)RISC-V
4)MIPS
x86 汇编#
以 intel 风格的汇编器为例
内存和寻址模式#
.DATA
声明静态数据区#
数据类型修饰原语:
)DB
: Byte, 1 Byte
)DW
: Word, 2 Bytes
)DD
: Double Word, 4 Bytes
)汇编中只有一维数组,只有没有二维和多维数组。一维数组其实就 是内存中的一块连续区域。另外,DUP
和字符串常量也是声明数组的两种方法
.DATA
var DB 64 ; 声明一字节大小的变量 var,并将其初始化为 64
var2 DB ? ; 声明一字节大小的未初始化变量 var2,其初值未定义,直到在程序中明确赋值
DB 10 ; 声明一字节大小的常量,值为 10,The Byte's location is var2 + 1.由于没标签,这个值并不能通过标签访问
X DW ? ; 声明一字大小(16位)的未初始化变量 X,初值未定义,直到程序中明确赋值。
Y DD 30000 ; 声明一个 4-byte 值, referred to as location Y, initialized to 30000.
Z DD 1, 2, 3 ; 声明数组Z,每个元素4字节大小,初始化为 1, 2, 3.Z标签表示数据存储的起始位置 Z+下标*4 是元素地址
bytes DB 10 DUP(?) ; 声明数组bytes,每个元素1字节大小;10 DUP(?)表示声明 10 个未初始化的字节
arr DD 100 DUP(0) ; 声明数组arr,每个元素4字节大小;100 DUP(0)表示声明 100 个初始化为0的元素
str DB 'hello',0 ; 声明 6 bytes starting at the address str, 初始化为 hello and the null (0) byte.
内存寻址 Addressing Memory#
MOV
将在内存和寄存器间移动数据 (默认移动 32 位数据),接受两个参数:第一个参数是目的地,第二个是源。源和目的其实都是地址;[寄存器] 表示引用寄存器的值作为地址;[var] 表示引用符号 var 表示的地址;
;合法寻址的例子:加[]表示存储的是地址,引用地址指向内存内容
mov eax, [ebx] ; Move the 4 bytes in memory at the address contained in EBX into EAX
mov [var], ebx ; Move the contents of EBX into the 4 bytes at memory address var. (Note, var is a 32-bit constant).
mov eax, [esi-4] ; Move 4 bytes at memory address ESI + (-4) into EAX
mov [esi+eax], cl ; Move the contents of CL into the byte at address ESI+EAX
mov edx, [esi+4*ebx] ; Move the 4 bytes of data at address ESI+4*EBX into EDX
;非法寻址的例子:
mov eax, [ebx-ecx] ; 只能对寄存器的值相加,不能相减
mov [eax+esi+edi], ebx ; 最多只能有 2 个寄存器参与地址计算
数据类型 (大小) 原语(Size Directives)#
修饰指针类型:就是表示引动数据的位数
)BYTE PTR
- 1 Byte
)WORD PTR
- 2 Bytes
)DWORD PTR
- 4 Bytes
mov BYTE PTR [ebx], 2 ; Move 2 into the single byte at the address stored in EBX.
mov WORD PTR [ebx], 2 ; Move the 16-bit integer representation of 2 into the 2 bytes starting at the address in EBX.
mov DWORD PTR [ebx], 2 ; Move the 32-bit integer representation of 2 into the 4 bytes starting at the address in EBX.
指令#
分类 | 指令 | 描述 | 例子 |
---|---|---|---|
数据移动 | mov | 将源操作数的值复制到目标操作数 | mov ax, bx ; 将 bx 寄存器的值复制到 ax 寄存器 |
push | 将数据压入栈中 | push ax ; 将 ax 寄存器的值压入栈 | |
pop | 将栈顶数据弹出到目标操作数 | pop bx ; 将栈顶的值弹出并存储到 bx 寄存器 | |
lea | 加载有效地址,将地址存储到目标寄存器 | lea ax, [bx + 4] ; 将 [bx + 4] 的有效地址存入 ax | |
算术 / 逻辑运算 | add | 执行加法操作 | add ax, bx ; 将 bx 加到 ax 寄存器中 |
sub | 执行减法操作 | sub ax, bx ; 从 ax 中减去 bx 的值 | |
inc | 对操作数加 1 | inc ax ; 将 ax 寄存器的值增加 1 | |
dec | 对操作数减 1 | dec bx ; 将 bx 寄存器的值减少 1 | |
imul | 有符号乘法 | imul ax, bx ; ax = ax * bx ,有符号乘法 | |
idiv | 有符号除法 | idiv bx ; 将 ax 除以 bx ,结果存储在 ax 和 dx | |
and | 按位与操作 | and ax, bx ; 将 ax 和 bx 按位与操作结果存回 ax | |
or | 按位或操作 | or ax, bx ; 将 ax 和 bx 按位或操作结果存回 ax | |
xor | 按位异或操作 | xor ax, bx ; 将 ax 和 bx 按位异或操作结果存回 ax | |
not | 按位取反操作 | not ax ; 将 ax 寄存器的所有位取反 | |
neg | 求负操作 | neg ax ; 将 ax 寄存器的值取反(即加上相反数) | |
shl | 左移操作 | shl ax, 1 ; 将 ax 左移 1 位,移出的位被丢弃 | |
shr | 右移操作 | shr bx, 1 ; 将 bx 右移 1 位,移出的位被丢弃 | |
控制流 | jmp | 无条件跳转 | jmp label ; 跳转到 label 标签处 |
je / jz | 如相等则跳转(je :jump if equal,jz :jump if zero) | je label ; 如果零标志设置(表示相等),跳转到 label | |
jne | 如果不相等则跳转(jump if not equal) | jne label ; 如果零标志未设置(表示不相等),跳转到 label | |
jg | 如果大于则跳转(jump if greater) | jg label ; 如果大于跳转到 label | |
jl | 如果小于则跳转(jump if less) | jl label ; 如果小于跳转到 label | |
cmp | 比较两个操作数(通过设置标志寄存器) | cmp ax, bx ; 比较 ax 和 bx 的值(设置标志寄存器) | |
call | 调用过程,跳转到子程序并将返回地址压入栈 | call subroutine ; 调用 subroutine 子程序 | |
ret | 从过程返回,弹出返回地址并跳转回调用点 | ret ; 从当前过程返回到调用点 |
Calling Convention#
子过程(函数)调用需遵守一套共同的协议,规定如何调用及如何从过程返回。例如,给定一组 calling convention rules,程序员无需查看子函数的定义就可以确定如何将参数传给它。进一步地 ,给定一组 calling convention rules,高级语言编译器只要遵循这些 rules,就可以使 得汇编函数和高级语言函数互相调用。
C Language Calling Convention#
Calling conventions 有多种。C 语言调用约定使用最广泛。遵循这个约定,可以使汇编代码安全地被 C/C++ 调用 ,也可以从汇编代码调用 C 函数库。
1)强烈依赖硬件栈的支持 (hardwared-supported stack)
2)基于 push
, pop
, call
, ret
指令
3)子过程参数通过栈传递: 寄存器保存在栈上,子过程用到的局部变量也放在栈上
大部分处理器上实现的大部分高级过程式语言,都使用与此相似的调用惯例。调用惯例分为两部分。第一部分用于 调用方(caller),第二部分用于被调 用方(callee)。需要强调的是,错误地使用这些规则将导致栈被破坏,程序 很快出错;因此在你自己的子过程中实现 calling convention 时需要格外仔细。
调用方规则 Caller Rules#
调用方需保存现场再调用子过程:
1)调用方保存的寄存器caller-saved registers:EAX, ECX, EDX; 这几个寄存器可能会被被 callee 修改,保存它们在调用结束后恢复栈的状态。
2)将传给子过程的参数 push onto stack:参数按逆序 push 入栈(最后一个参数先入)。由于栈是向下生长的,第一个参数 会被存储在最低地址(这个特性使得变长参数列表成为可能)。
3)使用 call 指令,调用子过程 (函数):call 会自动将返回地址 push onto stack,然后开始执行子过程代码。子过程返回后(call 执行结束后),被调用方会将返回值放到 EAX 寄存器,调用方可从中读取。为恢复机器状态,调用方需要做:
a.从栈上删除传递的参数
b.恢复由调用方保存的寄存器(EAX
, ECX
, EDX
)—— 从栈上 pop 出来;调用方可以认为,除这三个之外,其他寄存器值没有被修改过。
;保存现场
push eax
push ecx
push edx
push [var] ; Push last parameter first
push 216 ; Push the second parameter
push eax ; Push first parameter last
call _myFunc ; Call the function (assume C naming);call 会自动将返回地址push onto stack
;恢复现场。
add esp, 24 ;清理栈空间,恢复栈指针,删除传递的 6 个参数,栈指针向高地址移动24
mov result, eax ; 读取返回值到 result 变量
被调用方规则 Callee Rules#
1)将原栈帧基址寄存器 EBP
的值入栈,然后 copy ESP
to EBP
;作为子过程栈帧的新基址
2)在栈上为局部变量分配空间;栈自顶向下生长,故随着变量的分配,栈顶指针不断减小
3)调用方保存的寄存器callee-saved
—— 将他们压入栈。包括 EBX
, EDI
, ESI
; 这几个寄存器是被调用方负责保存和恢复
)执行子过程的代码,** 将返回值保存在 EAX
;** 当子过程返回后:
a. 恢复应由被调用方保存的寄存器(EDI
, ESI
) —— 从栈上 pop 出来
b. 释放局部变量
c. 恢复调用方 base pointer EBP
—— 从栈上 pop 出来
d. 最后,执行 ret
,返回给调用方 (caller)
.486 ;告诉汇编器使用什么什么指令集架构编译
.MODEL FLAT ;扁平内存模型
.CODE ;标记汇编文件中代码段的开始
PUBLIC _myFunc ;外部可见/可链接/非本文件私有的函数
_myFunc PROC
;1)
push ebp
mov ebp, esp
;2)
sub esp, 4 ; 为一个局部变量分配空间(4 字节)
;3)
push edi ; 保存寄存器值,EDI 会被修改
push esi ; 保存寄存器值,ESI 会被修改
;)子程序主体
mov eax, [ebp+8] ; 将第一个参数的值移入 EAX
mov esi, [ebp+12] ; 将第二个参数的值移入 ESI
mov edi, [ebp+16] ; 将第三个参数的值移入 EDI
mov [ebp-4], edi ; 将 EDI 存入局部变量
add [ebp-4], esi ; 将 ESI 加入局部变量
add eax, [ebp-4] ; 将局部变量的内容加到 EAX 中,作为最终结果
;)a
pop esi ; 恢复寄存器 ESI 的值
pop edi ; 恢复寄存器 EDI 的值
;)b
mov esp, ebp ; 释放局部变量
;)c
pop ebp ; 恢复调用方的基指针值
;)d
ret ;将栈中的返回地址弹出,跳转到返回地址继续执行调用方的代码
_myFunc ENDP
END ;标记程序的结束
riscV 汇编#
arm 汇编#
Intel/AT&T 语法风格差异#
AT&T syntax | Intel syntax |
---|---|
insn source, destination | insn destination, source |
内存操作#
AT&T 内存寻址使用的是 ()
Intel 内存寻址使用的是 []
AT&T syntax | Intel syntax |
---|---|
movl -12(%rbp), %eax | mov eax, DWORD PTR -12[rbp] |
寻址#
AT&T 语法:disp (base, index, scale)
Intel 语法:[base + index*scale + disp]
最终地址为 base + disp + index * scale
AT&T syntax | Intel syntax |
---|---|
movl -12(%rbp), %eax | mov eax, DWORD PTR -12[rbp] |
leaq 0(,%rax,4), %rdx | lea rdx, 0[0+rax*8] |
以特定汇编格式输出#
objdump 反汇编:linux 下 objdump 默认反汇编出 AT&T 格式,添加 -M 指定输出格式
gcc -c test.c // 先用 gcc 编译成二进制文件
objdump -d test.o // 默认输出 AT&T 格式
objdump -M intel -d test.o // 可指定输出 intel 格式
也可选择输出特定风格的汇编编译器,再生成汇编文件查看
gcc -S test.c // 默认输出 AT&T 格式
gcc -S -masm=intel test.c // 输出 intel 的语法格式