汇编语言
汇编语言
配置安装
| |
gdb
| |
常用gdb调试
| |
hello world
查看系统调用号
| |
| |
x86-64 基础
寄存器
| 分类 | 英文全称 | 16 位 | 32 位 | 64 位 |
|---|---|---|---|---|
| 通用寄存器:累加结果数据 | Accumulator | ax,高8位ah,低8位al | eax | rax |
| 通用寄存器:数据段数据指针 | Base | bx | ebx | rbx |
| 通用寄存器:字符串和循环计数器 | Counter | cx | ecx | rcx |
| 通用寄存器:I/O指针 | Data | dx | edx | rdx |
| 指针寄存器:栈指针,指向当前栈顶 | Stack Pointer | sp | esp | rsp |
| 指针寄存器:基址指针,通常用于访问栈帧中的局部变量 | Base Pointer | bp | ebp | rbp |
| 变地址寄存器:源变址寄存器 | Source Index | si | esi | rsi |
| 变地址寄存器:目的变址寄存器 | Destination Index | di | edi | rdi |
| 控制寄存器:指令指针,指向下一条要执行的指令 | Instruction Pointer | ip | eip | rip |
| 控制寄存器:标志寄存器,存储 CPU 的状态标志 | Flag | flag | eflag | rflag |
| 段寄存器:代码段寄存器 | Code Segment | cs | cs | cs |
| 段寄存器:数据段寄存器 | Data Segment | ds | ds | ds |
| 段寄存器:栈段寄存器 | Stack Segment | ss | ss | ss |
| 段寄存器:额外的寄存器 | Extra Segment | es | es | es |
基本汇编语法
基础指令
MOV(数据传送指令)
| |
用途:最常用的数据传送指令,可以在寄存器之间、寄存器与内存之间传送数据,也可以传送立即数到寄存器/内存。
示例:
| |
ADD(加法指令)
| |
用途:执行加法运算,支持寄存器、内存、立即数操作。
示例:
| |
标志位影响:会影响CF、OF、ZF、SF、AF、PF标志。
SUB(减法指令)
| |
用途:执行减法运算。
示例:
| |
标志位影响:会影响CF、OF、ZF、SF、AF、PF标志。
INC/DEC(自增/自减指令)
| |
用途:对操作数进行加1或减1操作,比add/sub更高效。
示例:
| |
标志位影响:不影响CF标志,其他标志同add/sub。
MUL/IMUL(乘法指令)
| |
用途:执行乘法运算,128位结果中,高64位在rdx,低64位在rax。
示例:
| |
DIV/IDIV(除法指令)
| |
用途:执行除法运算,被除数是rdx:rax(128位),除数是src。
注意:除法前必须先将rdx清零(xor rdx, rdx),否则会得到错误结果。
示例:
| |
LEA(load Effective Address 加载有效地址指令)
| |
用途:计算内存地址并加载到寄存器,相当于C语言中的&取地址操作,也常用于数学计算(不影响标志位)。
示例:
| |
逻辑运算指令
逻辑运算指令都是按位操作,会影响标志寄存器中的ZF、SF、PF、OF、CF位。
AND(按位与)
| |
用途:清零某些位,保留特定位(掩码操作)
示例:
| |
标志位影响:CF=0,OF=0,根据结果设置ZF/SF/PF
OR(按位或)
| |
用途:设置某些位为1
示例:
| |
标志位影响:CF=0,OF=0,根据结果设置ZF/SF/PF
XOR(按位异或)
| |
用途:翻转某些位、清零寄存器、简单加密
示例:
| |
标志位影响:CF=0,OF=0,根据结果设置ZF/SF/PF
NOT(按位取反)
| |
用途:按位翻转所有位,0变1,1变0
示例:
| |
标志位影响:不影响标志位
TEST(位测试)
| |
用途:检测某些位是否为1,相当于不修改操作数的AND指令
示例:
| |
标志位影响:CF=0,OF=0,根据结果设置ZF/SF/PF
移位指令
移位指令也是按位操作指令,常用于快速乘除和位操作。
SHL/SAL(Shift Left/Shift Airthmetic Left)逻辑左移/算术左移
| |
用途:左移一位相当于乘以2,常用于快速乘法运算。
示例:
| |
标志位影响:CF=移出的最高位,OF=移位1位时最高位变化则置1,其他标志根据结果设置。
SHR/SAR(Shift Right/Shift Airthmetic Right)逻辑右移/算术右移
| |
用途:右移一位相当于除以2,shr用于无符号数,sar用于有符号数。
示例:
| |
标志位影响:CF=移出的最低位,OF=移位1位时最高位变化则置1,其他标志根据结果设置。
ROL/ROR(Rotate Left/Right)循环左移/循环右移
| |
用途:循环移位,不丢失位信息,常用于加密、校验等场景。
示例:
| |
RCL/RCR(Rotate through Carry Left/Right)带进位循环左移/右移
| |
用途:循环移位并保留进位标志,常用于加密、校验等场景。
示例:
| |
Directive Instruction
定义常量
| |
定义内存
| |
基础数据类型
NASM中常用的数据类型定义:
| 指令 | 说明 | 大小 | 对应C语言类型 |
|---|---|---|---|
db | 定义字节(Define Byte) | 1字节(8位) | char |
dw | 定义字(Define Word) | 2字节(16位) | short |
dd | 定义双字(Define Doubleword) | 4字节(32位) | int, float |
dq | 定义四字(Define Quadword) | 8字节(64位) | long, double, 指针 |
寻址方式
x86-64模式下使用虚拟地址,采用平坦内存模型,段寄存器通常固定为基地址0,直接使用偏移地址访问。
x86-64架构提供了多种灵活的内存寻址方式,用于访问内存中的数据:
| 寻址方式 | 示例 | 说明 |
|---|---|---|
| 立即寻址 | mov rax, 10 | 直接使用常量值,不访问内存 |
| 寄存器寻址 | mov rax, rbx | 直接操作寄存器中的值,不访问内存 |
| 直接寻址 | mov rax, [ARR] | 直接使用内存地址访问数据 |
| 寄存器间接寻址 | mov rax, [rsi] | 使用寄存器中存储的地址访问内存 |
| 基址+偏移寻址 | mov rax, [rbp - 8] | 基址寄存器加上固定偏移量 |
| 比例变址寻址 | mov rax, [ARR + rbx * 8] | 基址 + 变址寄存器 * 比例因子(1/2/4/8) |
| 基址+变址+偏移寻址 | mov rax, [ARR + rbx * 8 + 16] | 基址 + 变址*比例 + 固定偏移 |
比例因子说明:
- 访问字节数据:比例因子为1
- 访问字(2字节):比例因子为2
- 访问双字(4字节):比例因子为4
- 访问四字(8字节):比例因子为8
控制流
比较指令
| Bit | Label | Description |
|---|---|---|
| 0 | CF | Carry Flag(进位标志):运算结果的最高有效位有进位(加法)或借位(减法)时,进位标志置1 |
| 2 | PF | Parity Flag(奇偶标志):运算结果的最低8位中1的个数是偶数置1 |
| 4 | AF | Auxiliary Carry flag(辅助进位标志位):第3位向第4位发生了进位,那么AF标志位置1 |
| 6 | ZF | Zero Flag:结果为0,置1 |
| 7 | SF | Sign Flag:结果为负数(最高位为1),置1 |
| 8 | TF | Trap Flag:陷阱标志位 ,用于调试,置 1 时单步执行。 |
| 9 | IF | Interrupt enable Flag:是否响应中断 |
| 10 | DF | Direction Flag(方向标志位)控制字符串操作的方向(0:递增,1:递减) |
| 11 | OF | Overflow Flag(溢出标志位) |
| 12-13 | IOPL | I/O privilege level:控制 I/O 指令的执行权限 |
| 14 | NT | Nested task |
| 16 | RF | Resume Flag 用于调试,控制是否忽略断点 |
| 17 | VM | Virtual-8086 mode:置 1 时进入虚拟 8086 模式 |
| 18 | AC | Alignment check / Access Control:置 1 时启用对齐检查 |
| 19 | VIF | Virtual Interrupt Flag:虚拟模式下的中断标志 |
| 20 | VIP | Virtual Interrupt Pending:虚拟模式下的中断挂起状态。 |
| 21 | ID | ID Flag :支持 CPUID 指令的标志 |
| |
对于无符号数字计算,存在以下场景: ZF(Zero Flag), CF(Carry Flag)
- a=b => ZF=1, CF=0
- a>b => ZF=0, CF=0
- a<b => ZF=0, CF=1
对于有符号数字计算,存在以下场景: ZF(Zero Flag), OF(Overflow Flag), SF(Sign Flag)
- a=b => ZF=1
- a>b => ZF=0, OF = SF
- a<b => ZF=0, OF != SF
跳转指令
| 指令 | 描述 | 条件 |
|---|---|---|
| jmp Label | 无条件跳转到指定标签 | |
| jmp *Operand | 跳转到指定地址 | |
| je / jz | Jump equal/zero | ZF=1 |
| jne / jnz | Jump not equal/nonzero | ZF=0 |
| js | Jump negative | SF=1 |
| jns | Jump nonnegative | SF=0 |
| jg / jnle | Jump (>) greater (signed) | ZF=0 and SF=OF |
| jge / jnl | Jump (>=) greater or equal (signed) | SF=OF |
| jl / jnge | Jump (<) less (signed) | SF!=OF |
| jle / jng | Jump (<=) less or equal (signed) | ZF=1 or SF!=OF |
| ja / jnbe | Jump (>) above (unsigned) | CF=0 and ZF=0 |
| jae / jnb | Jump (>=) above or equal (unsigned) | CF=0 |
| jb / jnae | Jump (<) below (unsigned) | CF=1 |
| jbe / jna | Jump (<=) below or equal (unsigned) | CF=1 or ZF=1 |
| |
循环指令
| |
宏定义
NASM的宏类似于C语言的#define,可以封装重复代码,简化编写,提高代码复用性。
无参数宏
| |
示例:退出程序宏
| |
带参数宏
| |
示例:打印字符串宏
| |
宏内部局部标签
宏内部使用%%前缀定义局部标签,避免多次展开宏时出现标签重复定义错误。
示例:结果检查宏
| |
结构体定义与内存对齐
NASM提供了struc和istruc关键字用于定义自定义数据结构,类似于C语言的struct。
| |
NASM会自动生成两个常量:
结构体名_size:结构体的总大小(字节)结构体名.字段名:字段在结构体中的偏移量
内存对齐
内存对齐是为了提高CPU访问内存的效率,未对齐的内存访问会导致性能下降甚至触发异常。
x86-64平台默认对齐规则:
| 数据类型 | 大小 | 对齐要求 |
|---|---|---|
| byte/char | 1字节 | 1字节对齐 |
| word/short | 2字节 | 2字节对齐 |
| dword/int/float | 4字节 | 4字节对齐 |
| qword/long/double/指针 | 8字节 | 8字节对齐 |
alignb前面要有标签。
| |
填充的字节不会被结构体使用,仅用于保证后续字段的对齐位置。
结构体数组操作
| |
栈操作指令(push/pop)
栈是x86-64架构中核心的动态内存区域,采用从高地址向低地址生长的存储方式,由rsp(栈指针寄存器)始终指向当前栈顶位置。
push和pop是栈的基础操作指令:
push <操作数>:将操作数写入栈顶位置,同时rsp自动减8(64位模式下默认操作8字节),栈顶向低地址方向移动pop <目标位置>:将栈顶的值读取到目标寄存器或内存地址,同时rsp自动加8,栈顶向高地址方向移动
栈操作遵循「先进后出」原则,push和pop的调用顺序必须严格对应,否则会导致栈结构失衡引发程序崩溃。
常见应用场景:
- 保存/恢复寄存器上下文:函数调用前保存寄存器值,调用完成后恢复
- 传递函数参数:寄存器不足时,通过栈传递额外参数
- 存储局部变量:函数内部的临时变量通常分配在栈空间
- 保存返回地址:
call指令会自动将返回地址压入栈,ret指令自动弹出返回地址完成跳转
函数调用基础
函数调用通过call和ret指令实现,本质是修改指令指针rip并保存返回地址到栈中:
| |
函数调用过程中栈的变化:
- 调用者将参数按照约定传递(寄存器或栈)
call指令将返回地址压入栈- 进入函数后通常会设置栈帧(
push rbp; mov rbp, rsp) - 函数执行完成后恢复栈帧,通过
ret返回
AT&T
GCC 生成的 .s 默认是 AT&T 语法,而项目里的手写 .asm 使用的是 NASM/Intel 语法。主要区别如下:
- 操作数顺序相反:AT&T 是
source, destination - 立即数前面加
$ - 寄存器前面加
% - 内存操作数写成
disp(base, index, scale)
| |
| AT&T 指令 | 描述 | Intel |
|---|---|---|
| movq | 64位 | mov qword |
| movl | 32位 | mov dword |
| movw | 16位 | mov word |
| movb | 8位 | mov byte |
数据类型
| 命令 | 数据类型 | nasm |
|---|---|---|
.ascii | 字符串 | db |
.asciz | 以 \0 结尾的字符串 | db 0 |
.byte | 字节 | db |
.double | 双精度浮点 | dq |
.float | 单精度浮点 | dd |
.int | 32位整数 | dd |
.long | 32位整数和(.int 相同) | dd |
.octa | 16字节整数 | |
.quad | 8字节整数 | dq |
.short | 16位整数 | dw |
.single | 单精度浮点 | dd |
伪指令
节定义 .section;段 segment = section + ... + section
代码段 .text, 数据段 .data, bss 段 .bss
BSS Block Started Symbol; resb
| 命令 | 描述 |
|---|---|
.comm | 通用缓存区域 |
.lcomm | 本地缓存区域 (只本文件可用的区域) |
寻址方式
Intel:
[base_address + index * size + offset]
AT&T:
offset(base_address, index, size)
base_address 和 index 是寄存器,offset 是立即数位移,size 只能是 1/2/4/8。
| |
gcc 汇编
CFI(Call Frame Information) 是调用栈帧信息。.cfi_* 指令主要服务于调试器和异常回溯。
-fno-asynchronous-unwind-tables:去掉大部分.cfi_*信息,便于阅读汇编。
PIC(Position Independent Code) 是位置无关代码。32 位资料里常能看到 __x86.get_pc_thunk.ax 这种辅助代码;x86-64 下编译器通常直接用 rip 相对寻址,例如 leaq .LC0(%rip), %rax。
-fno-pic/-fno-pie:减少位置无关代码带来的额外跳转和重定位。
-masm=intel:让 GCC 输出 Intel 风格汇编;默认是 AT&T 风格。
常见标记:
| 标记 | 英文 | 含义 |
|---|---|---|
LC0 | local constant | 本地常量 |
LFB0 | local function beginning | 函数开始 |
LFE0 | local function ending | 函数结束 |
LBB0 | local block beginning | 代码块开始 |
LBE0 | local block ending | 代码块结束 |
L | local labels | 本地标记 |
局部变量一般存储在栈中。
常用工具:
| |
内联汇编
1. 直接操作全局变量
| |
2. 输入 / 输出约束
| |
格式可以记成:
| |
当模板字符串里显式写寄存器名时,需要用 %%eax、%%edx 这种写法转义。
3. 位置占位符 %0/%1/%2
| |
%0对应第 1 个输出操作数%1、%2对应输入操作数
4. 复用已有输出操作数
| |
- “0”(result) 表示这个输入操作数也是 result
- 0 指向第 0 个操作数,也就是输出 %0
- 因此这个输入和输出必须绑定到同一个寄存器或同一个位置
这种写法常用于“读旧值,再写回同一位置”。
5. 命名占位符
用命名形式代替 %0/%1/%2,可读性更好:
| |
6. clobber 列表
明确告诉编译器该汇编块会改写哪些寄存器:
| |
如果漏掉 clobber,编译器可能错误地假设某些寄存器值没有变。
7. 内存约束和 early-clobber
展示 m 约束,也说明了 x86 指令一般不允许两个操作数同时都是内存:
| |
展示 & 修饰符:
| |
& 表示输出寄存器会在所有输入读取完之前就被写坏,编译器不能把它和输入操作数复用到同一个寄存器。
常用约束
| |
| 约束 | 含义 |
|---|---|
a | 使用 rax/eax 及其子寄存器 |
b | 使用 rbx/ebx 及其子寄存器 |
c | 使用 rcx/ecx 及其子寄存器 |
d | 使用 rdx/edx 及其子寄存器 |
S | 使用 rsi/esi 及其子寄存器 |
D | 使用 rdi/edi 及其子寄存器 |
r | 使用任意通用寄存器 |
m | 使用变量的内存位置 |
i | 使用立即数 |
输出修饰符
| 修饰符 | 含义 |
|---|---|
= | 只写 |
+ | 可读可写 |
& | early-clobber,防止和输入复用 |
直接发起 syscall
Linux x86-64 的系统调用寄存器约定:
| |
rax=1:系统调用号writerdi=1:标准输出rsi=str:字符串地址rdx=len:长度syscall会破坏rcx和r11
函数调用约定
Linux x86-64 的 System V ABI:
- 整数和指针参数依次放在
rdi,rsi,rdx,rcx,r8,r9 - 浮点参数依次放在
xmm0到xmm7 - 整数和指针返回值放在
rax - 浮点返回值放在
xmm0 - 调用者保存:
rax,rcx,rdx,rsi,rdi,r8-r11 - 被调用者保存:
rbx,rbp,r12-r15 - 调用
call之前,rsp需要按 16 字节对齐
Linux x86-64 的 syscall 约定和 C 函数调用不同:
rax放系统调用号- 参数依次放在
rdi,rsi,rdx,r10,r8,r9 syscall会破坏rcx和r11
C 调用汇编
| |
| |
汇编调用 C
| |
| |
32 位补充
最常见的 32 位约定是 cdecl 和 fastcall。它们保留在这里,仅用于对照。
cdecl
- 参数从右向左入栈
- 返回值放在
eax eax,ecx,edx由调用者保存ebx,esi,edi,ebp由被调用者保存- 一般由调用者清理参数栈空间
fastcall
- 常见实现里前两个整数参数通过
ecx,edx传递 - 其余参数从右向左入栈
- 返回值通常仍然放在
eax - 具体细节随编译器和平台实现而变
参考阅读
Intel 64 and IA-32 Architectures Software Developer Manuals
x86-64 Machine-Level Programming
在 C 中使用汇编语言(使用 GNU 编译器集合 (GCC))