汇编/机器指令

汇编是指令集的助记符，由编译器 + 风格 + 指令集架构 (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 和字符串常量也是声明数组的两种方法

Copy
.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 表示的地址；

Copy
;合法寻址的例子：加[]表示存储的是地址，引用地址指向内存内容
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

Copy
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 出来；调用方可以认为，除这三个之外，其他寄存器值没有被修改过。

Copy
;保存现场
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)

Copy
.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 指定输出格式

Copy
gcc -c test.c // 先用 gcc 编译成二进制文件
objdump -d test.o // 默认输出 AT&T 格式
objdump -M intel -d test.o // 可指定输出 intel 格式

也可选择输出特定风格的汇编编译器，再生成汇编文件查看

Copy
gcc -S test.c // 默认输出 AT&T 格式
gcc -S -masm=intel test.c // 输出 intel 的语法格式