1. 汇编基础
汇编语言是一种低级的、面向机器的、使用助记符标记的编程语言。汇编语言是汇编格式指令、伪指令与其他符号组成的集合体。
汇编语言中的指令和机器指令是一一对应的,相当于机器码的“助记符”;
伪指令没有对应的机器码,主要是为程序提供“定义数据”、“设置程序起始点”等信息,由汇编器负责解析和执行,不会产生机器码,仅在汇编阶段有效。
汇编语言对应于不同的处理器架构,也就是说不同的处理器架构下汇编语言不同,这里主要使用两种:
- IA-32:Intel的32位架构,又名X86;
- X64:最先由AMD公司研发的64位指令集架构,Intel也采用了类似的设计,称为Intel 64或EM64T。它是 IA-32 的 64 位扩展。AMD64和Intel 64通常是指同一种 64 位指令集架构。
还有一种的处理器结构是IA-64,Intel研发的专用于其Itanium系列的处理器,它与IA-32、X86-64有着本质的区别,所有不要混淆,这里不再多说。
2. 寄存器
寄存器是计算机中央处理器(CPU)内部的高速存储器,主要用于暂存指令、数据、地址等信息。
2.1 X86CPU(32位)
X86主要寄存器分类如下:
- 8个通用寄存器:EAX、EBX、ECX、EDX、EBP、ESP、ESI、EDI
- 6个段寄存器:CS、DS、SS、ES、FS、GS
- 1个标志寄存器:EFLAGS
- 1个指令指针寄存器:EIP
- 5个控制寄存器:CR0、CR1、CR2、CR3、CR4
- 4个系统地址寄存器:GDTR、IDTR、LDTR、TR
- 8个调试寄存器:DR0~DR7
编写shellcode用到的最多的是通用寄存器,其次是段寄存器和标志寄存器,其他的极少数用到。通用寄存器可以单独使用,也可以按高低字节拆分成16位或者8位使用:
| 4-Byte register | Bytes 0-1(16位) | Bytes 1(高8位) | Bytes 0(低8位) | Name |
| eax | ax | ah | al | 累加器 |
| ebx | bx | bh | bl | 基址寄存器 |
| ecx | cx | ch | cl | 计数器 |
| edx | dx | dh | dl | 数据寄存器 |
| ebp | bp | — | — | 基址指针 |
| esp | sp | — | — | 栈指针 |
| esi | si | — | — | 源索引 |
| edi | di | — | — | 目标索引 |
EFLAGS标志寄存器是按位取用,每一位都表示一个标志:

状态标志
- CF:进位标志,第0位,当无符号整数运算产生进位(加法溢出)或借位(减法下溢)时设置;
- PF:奇偶校验标志,第2位;如果运算结果的低八位中包含偶数个
1,则设置此标志; - AF:辅助进位标志,第4位,用于特殊的BCD运算;
- ZF:零标志,第6位,如果运算结果为零,则设置此标志;
- SF:符号标志,第7位,反映运算结果最高有效位的状态;
- OF:溢出标志,第11位,当有符号整数运算发生溢出时设置;
控制标志
- DF:方向标志,第10位,控制字符串操作指令的方向;
系统标志
- TF:陷阱标志,第8位,用于单步调试中断模式;
- IF:中断标志,第9位;控制 CPU 是否响应外部可屏蔽中断;
- IOPL:I/O特权级别标志,第12-13位,定义当前任务访问 I/O 端口所需的最小特权级别;
- NT:嵌套任务标志,第14位,用于管理任务切换的执行流程;
- RF:恢复标志,第16位,控制调试异常的响应;
- VM:虚拟8086模式标志,第17位,用于设置实模式与保护模式;
- AC:对齐检查标志,第18位,设置该位表示用户态下对内存引用进行对齐检查;
- VIF:虚拟中断标志,第19位,为IF标志的虚拟映象;
- VIP:虚拟中断挂起标志,第20位,设置表示有一个中断被挂起;
- ID:ID标志,第21位,通过修改该位的值可以测试是否支持
CPUID指令;
Intel架构中的段寄存器是根据计算机的内存分段机制设置,用于保存16位的段选择符。分段机制请参考其他资料,X86一共6个段寄存器,且都是16位的,却可以32位管理内存分段:
| 寄存器 | 名称 | 功能 |
| CS | 代码段 | 指向存储当前正在执行的指令(程序代码)的内存区域 |
| DS | 数据段 | 指向存储全局变量和静态数据的内存区域 |
| SS | 堆栈段 | 指向存储程序堆栈(用于函数调用、局部变量)的内存区域 |
| ES | 附加段 | 通常用于辅助数据操作或字符串操作的目的地址 |
| FS | 附加段 | 通常用于用户空间和内核空间中实现线程本地存储(TLS) |
| GS | 附加段 | 常被保留用于操作系统内核的内部数据访问 |
EIP是32位指令指针寄存器,指向下一条要执行的指令在内存中的地址,不能直接被指令访问。
2.2 X64CPU(64位)
x64(AMD64或Intel 64)架构中,寄存器都是向后兼容的,寄存器的数量和类型相比32位x86架构有了显著扩展。X64运行在IA-32e模式下,其中包含一个兼容模式,允许16位和32位程序在不重新编译的情况下运行,但是32位CPU却不能运行64位的程序。
X64主要寄存器分类集合:
- 16个通用寄存器:RAX、RBX、RCX、RDX、RBP、RSP、RSI、RDI、R8~R15
- 6个段寄存器:CS、DS、SS、ES、FS、GS
- 1个标志寄存器:RFLAGS
- 1个指令指针寄存器:RIP
- 6个控制寄存器:CR0、CR1、CR2、CR3、CR4、CR8
- 4个系统地址寄存器:GDTR、IDTR、LDTR、TR
- 8个调试寄存器:DR0~DR7
X86-64架构下,通用寄存器的数量由原32位的8个扩展到16个,并且由32位扩展到64位,同X86架构一样,可以单独使用,也可以拆分为32位、16位、低8位,但没有高8位。
| 8-Byte register | Bytes 0-3(32位) | Bytes 0-1(16位) | Bytes 0(低8位) | Name |
| rax | eax | ax | al | 返回值;累加器 |
| rbx | ebx | bx | bl | 基址 |
| rcx | ecx | cx | cl | 计数器 |
| rdx | edx | dx | dl | 数据 |
| rbp | ebp | bp | bpl | 帧指针 |
| rsp | esp | sp | spl | 栈指针 |
| rsi | esi | si | sil | 源索引 |
| rdi | edi | di | dil | 目标索引 |
| r8 | r8d | r8w | r8b | 通用 |
| r9 | r9d | r9w | r9b | 通用 |
| r10 | r10d | r10w | r10b | 通用 |
| r11 | r11d | r11w | r11b | 通用 |
| r12 | r12d | r12w | r12b | 通用 |
| r13 | r13d | r13w | r13b | 通用 |
| r14 | r14d | r14w | r14b | 通用 |
| r15 | r15d | r15w | r15b | 通用 |
64位架构下的标志寄存器由32位扩展为64位,并改名为RFLAGS。虽然寄存器大小为64位,但高32位保留不使用,低32位与EFLAGS相同。有两个标志位的值有差别:
- X64模式下,不使用虚拟8086模式,处理器会忽略设置;
- X64模式下,处理器不会设置NT标志位,否则会产生GP错误;
X86-64模式下,基本放弃了分段管理机制,所有的段寄存器(CS、DS、ES、SS)都默认指向一个基地址为0的“平坦”内存空间,唯独 FS 和 GS 寄存器被保留下来,供软件和操作系统用于特殊目的。
FS寄存器主要用于管理用户态的线程数据(TLS),而GS则主要用于管理内核态的CPU特定数据。
RIP是64位的指令指针寄存器,作用和32位的EIP一致,两者在寻址方式方面有些差别。
3. 指令集
X86指令集是一种复杂指令集(CISC)架构,这就意味着指令的长度是可变的,寻址方式更加灵活。这里只列举基本的、常用的指令:
3.1 数据传送指令
| 指令 | 功能 |
| mov dest,src | 将源操作数复制到目标操作数 |
| lea reg,src | 将源操作数的有效地址加载到目标寄存器 |
| push src | 压栈 |
| pop dest | 出栈 |
| xchg | 交换两个操作数的值 |
| movzx dest,src | 零扩展传送指令 |
| movsx dest,src | 符号扩展传送指令 |
3.2 算术指令
| 指令 | 功能 |
| add dest,src | dest = dest + src |
| sub dest,src | dest = dest – src |
| inc op | op++ |
| dec op | op– |
| mul dest,src | 无符号乘法 |
| div dest,src | 无符号除法 |
3.3 逻辑操作指令
| 指令 | 功能 |
| and dest,src | 按位与,常用于清零特定位 |
| or dest,src | 按位或,常用于设置特定位 |
| xor dest,src | 按位异或,常用于寄存器清零 |
| not op | 按位取反 |
| shl dest,src | 按位左移 |
| shr dest,src | 按位右移 |
| test op1,op2 | 执行and运算,常用于检查寄存器是否为0 |
| cmp op1,op2 | 执行sub运算,用于比较大小 |
| ror op1,op2 | 循环右移指令 |
3.4 控制流指令
| 指令 | 功能 |
| jmp label | 无条件跳转到标号或指令处 |
| jz/je label | 当 ZF 标志被设置(结果为零或相等)时跳转 |
| jnz/jne label | 当 ZF 标志未被设置(结果非零或不相等)时跳转 |
| jg/jl label | 无符号比较结果,当大于或小于时跳转 |
| call dest | 调用子程序,将返回地址压栈 |
| ret | 从子程序中返回,从堆栈中弹出返回地址并跳转 |
3.5 其他指令
| 指令 | 功能 |
| nop | 空操作,常用于代码对齐或调试 |
| int n | 软中断指令,Linux系统下用于系统调用 |
| syscall/sysenter | 快速的系统调用,替换传统的int n |
| lodsb | 将内存中的一个字节数据复制到al寄存器 |
| call dest | 调用子程序,将返回地址压栈 |
| ret | 从子程序中返回,从堆栈中弹出返回地址并跳转 |
X64指令集是X86指令集的超集,大多数核心指令名称是相同的,在操作数类型、寻址能力、堆栈操作、调用约定等方面存在着差异,这里不再多说了。
4. 寻址方式
X86寻址方式指的是访问数据的方式,确定操作数的位置。想要明白寻址方式,首先要了解什么是有效地址,根据有效地址的元素可以组合成多种寻址方式。
有效地址(Effective Address):简称EA,用于指定内存中操作数的位置,从而执行读写操作。
有效地址(EA)的组成部分:
- Base(基址):一个通用寄存器(如 EAX, EBX, EBP, ESP 等)的值;
- Index(索引):另一个通用寄存器(如 ESI, EDI, ECX 等)的值;
- Scale(比例因子):只能是 1, 2, 4, 或 8,用于将索引值乘以数据类型的大小;
- Displacement(偏移量/位移):一个 8 位或 32 位的常量值,直接包含在指令中;
常用的寻址方式主要分为立即数寻址、寄存器寻址、内存寻址三种:
- 立即数寻址:操作数是立即数(常量),直接包含在指令中,示例:mov eax,1234h;
- 寄存器寻址:操作数在寄存器中,高速的寻址方式,不经过内存,示例:mov eax,ebx;
- 直接寻址:操作数在内存中,指令中包含一个固定的32位有效地址,示例:mov eax,[0x1234];
- 间接寻址:操作数在内存中,指令中包含一个寄存器,寄存器中的内容就是内存地址,示例:mov eax,[ebx]:ebx寄存器中存放了内存地址,将内存地址处的数据传送到eax寄存器中;
- 基址寻址:操作数位于内存中,有效地址=基址寄存器+1个可选的偏移量,常用于访问结构体成员或堆栈上的局部变量,示例:mov eax,[ebp+8]表示访问ebp指向的内存地址偏移8字节处的数据,然后传送到eax寄存器中;
- 变址寻址:操作数位于内存中,有效地址=变址寄存器+1个可选的偏移量,常用于访问数组成员,示例:mov eax,[esi+8]表示将esi中存放的内存地址偏移8字节处的数据传送给eax寄存器;
- 基址变址寻址:操作数位于内存中,有效地址=基址寄存器+变址寄存器x比例因子+1个可选的偏移量,常用于访问二维数组或更加复杂的数据结构,示例mov eax,[ebx+esi*4+8]表示将ebx+esi*4+8内存地址处的数据传送到eax寄存器;
X64架构体系保留了X86所有的传统寻址模式,还引进了新的寻址方式:RIP相对寻址。主要用来访问RIP寄存器指向的下一条指令±2GB范围内的数据,简化了位置无关代码(PIC)的实现,非常灵活:
- RIP相对寻址:有效地址=RIP的值+32位带符号的偏移量,示例:mov eax,[rip+data_offset]表示加载相对于当前指令的数据;
X64 还使用 REX 前缀来启用 64 位数据操作和访问新增的寄存器(R8-R15)。
5. 伪指令
汇编语言中的伪指令不是真正的机器指令,本身不产生机器码,主要用于提供给汇编器信息,辅助汇编器更好的完成汇编工作,以下是常用的使用场景:
- 定义数据,包括变量、常量等数据;
- 宏定义;
- 分配内存空间,通常情况下定义数据的同时分配内存;
- 程序控制流程
汇编语言对应不同的处理器架构,也依赖于特定的汇编器,也就是说不同的汇编器使用的语法不同,伪指令也会有所不同,一般情况下Linux系统使用的是nasm汇编器,而WIndows平台则是masm汇编器。这里总结的伪指令以nasm语法为标准。
当然,数据定义和高级语言一样,也要有数据类型,汇编里的数据类型表明了操作数的大小,常用的数据类型如下:
| 数据类型 | 操作数大小(位) |
| byte | 8 |
| word | 16 |
| dword | 32 |
| qword | 64 |
| tword | 80 |
| oword | 128 |
| yword | 256 |
常用伪指令数据定义如下:
//1.db、dw、dd、dq指令通用的语法,变量名是可选的,只是更方便引用该内存地址
name db 10 //定义一个名为name的字节变量,初始值为10
names db 10,20,30,40 //定义一个名为names的字节数组,包含4个字节
msg db "Hello, World!",0 //定义一个以null结尾的字符串
hex_val db 0x55 //定义十六进制的字节变量,值为0x55
ascii_a db 'A' //定义一个字符'A'的字节变量
bss_val db ? //预留一个字节的空间,不进行初始化
db 0x55 //当前数据段位置分配一个字节的内存空间,初始值为0x55
//2.resb、resw、resd、resq预留空间通用,只是大小不一样,变量名可选的
resb 100 //预留100字节的空间
buffer resb 100 //预留100字节的空间,名为buffer
buffer resw 100 //预留100字的空间,名为buffer
buffer resd 100 //预留100双字的空间,名为buffer
buffer resq 100 //预留100四字的空间,名为buffer
//3.equ定义常量
MAX equ 100 //定义名为MAX的常量,值为100
MIN equ 1 //定义名为MIN的常量,值为1
//4.times:重复指令或伪指令的执行,常用于零填充缓冲区和扇区填充字节
times 100 db 0 //重复执行100次db 0
buffer times 100 db 0 //buffer处连续写入100个字节的0
times 100 nop //重复执行100次nop指令
times 510-($-$$) db 0 //填充引导扇区代码
dw 0xaa55
//5.incbin:引入外部二进制文件
incbin "file.bin" //引入file.bin文件
incbin "file.bin", 1024 //跳过文件前1024字节
incbin "file.bin", 1024, 512 //跳过文件前1024字节,仅包含接下来的512字节
//6.section/segment:自定义段,注意masm使用segment
section .text //定义代码段
section .data //定义数据段
section .bss //定义未初始数据段
//7.global:声明符号为全局可见,常用于定义程序入口点
section .text
global _start //告诉链接器:_start 是一个全局符号,是程序的入口点
_start:
...
//8. [bits]:指明汇编模式
[BITS 16] //告诉汇编器,接下来是16位汇编代码
[BITS 32] //告诉汇编器,接下来是32位汇编代码
[BITS 64] //告诉汇编器,接下来是64位汇编代码
6. 调用约定
调用约定是指一套在程序设计中定义函数如何被调用和返回的规则,它规定了参数传递的方式、堆栈清理责任、返回值处理以及函数名修饰。
6.1 X86调用约定
X86架构下的调用约定有很多种,常用的主要的就三种:__cdecl、__stdcall和__fastcall。
| 调用约定 | 参数入栈顺序 | 堆栈清理责任 | 主要用途 |
| __cdecl | 从右到左 | 调用者 | C/C++ 默认,可变参数函数 |
| __stdcall | 从右到左 | 被调用者 | Windows API |
| __fastcall | 从右到左 | 被调用者 | 主要用于性能优化 |
Windows平台用户空间C/C++使用__cdecl,但其核心操作系统API使用更高效的__stdcall。
Linux X86平台下,已成为标准的调用约定就只有一种:即基于System V ABI(Application Binary Interface) 的__cdecl约定,它是Linux32位系统的默认约定。
6.2 X64调用约定
X86-64模式下,调用约定就只有一种:__fastcall,但是在Windows和Linux平台下还是有些差异。
X64模式下WIndows平台和Linux平台系统调用约定细节:
| 特性 | X64-Linux(System V ABI) | X64 Windows(MS x64 CC) |
| 调用约定 | __fastcall | __fastcall |
| 参数 | rdi,rsi,rdx,rcx,r8,r9,栈,… | rcx,rdx,r8,r9,栈,… |
| 浮点参数 | XMM0~XMM7,栈 | XMM0~XMM3,栈 |
| 返回值 | rax/rdx | eax/rax |
| 系统调用指令 | syscall | syscall |
| 栈对齐 | 16字节 | 16字节 |
| 调用者保存的寄存器 | rax,rdi,rsi,rdx,rcx,r8~r11 | rax,rcx,rdx,r8~r11 |
| 被调用者保存的寄存器 | rbx,rbp,r12~r15 | rbx,rbp,rdi,rsi,r12~r15 |
Linux平台下,系统调用时,可使用r10寄存器代替rcx;如果参数个数大于6个,前5个参数是从左到右依次存放于RDI、RSI、RDX、RCX、RAX寄存器中,剩下的参数通过栈从右往左入栈传递。
具体的细节参考链接:WIndows-X64体系结构概述和寄存器-调用约定
7. AT&T风格
X86架构下,流行的汇编风格有两种:Intel风格和AT&T风格。
这两种风格的汇编生成的指令都是一样的,就是书写规则不同。Intel风格的汇编主要用于WIndows系统,而AT&T风格主要用于类Unix系统,根据不同指令对比一下这两种风格:
//1.寄存器名:intel风格直接写寄存器名,AT&T风格需要在寄存器前加%前缀:
push eax //Intel风格
push1 %eax //AT&T风格
//2.立即数前缀:intel风格直接写立即数,AT&T风格需要在立即数前加$前缀:
push 1 //Intel风格
push1 $1 //AT&T风格
//3.操作数顺序:Intel风格和AT&T风格的源操作数和目标操作数相反,
//Intel风格:指令 目标,源 (dest, src)
//AT&T风格:指令 源,目标(src, dest)
mov eax,1 //Intel风格
movl $1,eax
//4.操作数大小:
//Intel风格在指令中显示指定操作数大小:
mov al, byte ptr val //byte ptr指定操作数大小为字节
//AT&T风格在指令中的结尾包含操作数的长度:
//b:表示操作数为字节大小
//w:表示操作数为字大小
//l:表示操作数为双字大小(32位)
//q:表示操作数为四字大小(64位)
movb var, %al //movb指定操作数大小为字节
//5.内存寻址:
//Intel风格:指令 disp(base, index, scale)
//AT&T风格:指令 [base + index*scale + disp]
mov eax,[ebp-4] //Intel风格
movl -4(%ebp), %eax //AT&T风格
//6.远程转移指令jmp和子程序调用指令call:
jmp far 标号 //Intel风格
call far 子程序标号 //Intel风格
ljmp 标号 //AT&T风格
lcall 子程序标号 //AT&T风格
//7.间接跳转指令jmp和子程序调用指令call:
//Intel风格不做任何处理,AT&T风格需要在操作数前加上*前缀
//直接跳转则不需要加*前缀,仅限于间接跳转(目标地址存储在寄存器或内存位置中)
jmp eax //Intel风格
jmp *%eax //AT&T风格
jmp dword ptr [jump_table + EBX*4] //Intel风格
jmp *jump_table(,%ebx,4) //AT&T风格
jmp [function_ptr_var] //Intel风格
jmp *function_ptr_var //AT&T风格
call eax //Intel风格
call *%eax //AT&T风格
call dword ptr [function_pointer_var] //Intel风格
call *function_pointer_var //AT&T风格
1. 一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。