“What I cannot create, I do not understand.” —— Richard Feynman
在逆向工程(滴水逆向)与底层系统安全的学习过程中,“看懂反汇编器输出的汇编文本”只是第一步;唯有亲手用纯 C 语言把取指(Fetch)、表驱动解码(Table-Driven Decode)、复杂寻址(ModR/M + SIB Effective Address)、算术逻辑单元(ALU)、标志位硬件演化(EFLAGS)、**函数调用栈帧(Stack Frame)**以及 **x87 浮点环形栈(FPU Ring Stack)**逐行写出来,才能真正站在 CPU 硬件电路与编译器生成器的视角彻底吃透 x86 汇编。
TinyX86 是一个完全采用 标准 C11 语言从零编写的轻量级 x86-32 CPU 与 x87 FPU 指令集模拟器,配套 简易 PE32 节区映射 / 原始机器码加载器 与 类 GDB/OllyDbg/WinDbg 命令行交互式调试器。
为了让这套纯 C11 代码既能在本地终端调试,又能在浏览器中直观展示内部状态,项目通过 WASI Clang (wasm32-wasi) 将 core/*.c 100% 原生 C 代码直接编译为 WebAssembly (public/tinyx86.wasm),驱动在线可视化调试工作台——零其他语言重写,浏览器里的每一次单步执行、反汇编、寄存器修改和 printf 终端输出,全部直接运行自仓库中的 C 语言源码。
无需在本地安装编译器,点击下方链接即可在浏览器中直接运行由 core/*.c 编译出的 tinyx86.wasm,单步调试 power(2.0, 3) 递归栈帧、观察 EFLAGS 标志位与 x87 FPU 环形栈变化,或上传无外部依赖的 .exe / .bin 机器码:
- 👉 入口一:GitHub Pages 在线可视化调试工作台 (
https://582835293.github.io/TinyX86/) - 👉 入口二:在 StackBlitz 云端容器中打开并运行 (
stackblitz.com)
上图:TinyX86 可视化调试工作台真实运行截图
- 顶部工具栏:支持一键切换 3 套内置汇编载荷(
power(2.0, 3)递归浮点运算、1..10循环累加、DOS INT 21h字符串输出)、单步步入Step Into (s)、单步步过Step Over (n)、继续运行Continue (c)、重置Reset以及上传本地.EXE / .BIN(支持键盘快捷键s/n/c/r/u/k/h直接触发)。- 左侧硬件状态面板:实时读取 C 结构体
CPU_Context中的 8 个 32 位通用寄存器(EAX~EDI及高低位拆分视图)、EIP、6 个段寄存器(ES/CS/SS/DS/FS/GS)、完整EFLAGS标志位(CF/PF/AF/ZF/SF/TF/IF/DF/OF/NT/IOPL,点击即可调用 C 函数SetCpuState实时翻转)与x87 FPU环形寄存器栈(R0~R7物理寄存器与ST(0)~ST(7)逻辑栈顶映射)。- 中部反汇编与交互式终端:上方直接调用
core/disasm.c的ParseInstuction()与FormatInstruction()渲染反汇编列表(点击任意行即可调用DebuggerToggleBreakpoint切换断点);下方直接调用core/debugger.c的DebuggerExecuteCommand()并通过 WASIfd_write实时拦截 C 语言printf标准输出流。- 右侧多维分析面板:包含
Stack(实时栈帧ESP/EBP双指针标注)、Mem(任意虚拟地址 Hex + ASCII 内存转储)、PE32(由core/PETools.c解析的 DOS/NT 头与节区映射表)、C Tests(在 WASM 内直接运行 C 集成测试套件并报告sizeof(CPU_Context)与sizeof(DecodeContext))以及x86 & Build核心速查手册。
- 项目初心:为什么坚持用纯 C 语言手写 x86 模拟器?
- 13 个敏捷冲刺(Sprint 1 ~ Sprint 13)演进路线图
- 架构设计:纯 C11 内核直通跨平台命令行与 WebAssembly 前端
- 从 C 源码深挖 8 大 x86 汇编与 CPU 底层核心机制
- 4.1 通用寄存器的物理重叠(
REG32Union 与 8 位寄存器编码陷阱) - 4.2
EFLAGS标志寄存器完整位图与硬件加减法公式(CF / OF / AF / TF / NT / IOPL) - 4.3 表驱动指令解码器(Prefix 前缀状态机 + 单/双字节 Opcode 表 + Group 扩展)
- 4.4
ModR/M+SIB寻址公式与LEA/MOV的本质分野 - 4.5 条件指令族(
Jcc/CMOVcc/SETcc)低 4 位正交条件矩阵 - 4.6
cdecl函数栈帧生命周期、“烫烫烫” (0xCCCCCCCC) 与递归回溯剖析 - 4.7 x87 FPU 8 寄存器环形栈(Ring Stack)与经典
FNSTSW AX + SAHF浮点分支 - 4.8 32 位客户机虚拟地址转换层(让 32 位 x86 机器码安全运行在 64 位宿主与 WASM 上)
- 4.1 通用寄存器的物理重叠(
- 已实现的 x86 / x87 指令集全景矩阵
- 项目能力边界与当前局限性(重要说明)
- 跨平台构建指南(Windows / Linux / macOS / WASI-WASM)
- 交互式调试器(Debugger)完整命令手册与快捷键指南
- 进阶实验:如何自己编写并编译可在 TinyX86 中运行的 C / 汇编程序?
- 如何同步更新 Git 子模块(
Core-x86-Disassembler与PETools) - 自动化测试与验证体系
- 扩展文档与关联仓库生态
在学习 C 语言指针、内存布局与 x86 汇编逆向时,仅靠阅读教材往往会留下许多似懂非懂的盲区:
- 寄存器位宽与别名:为什么
mov al, 0xFF不会清空EAX的高 24 位?为什么 8 位寄存器编号0~3是AL/CL/DL/BL,到了4~7却变成了高字节AH/CH/DH/BH? - 标志位硬件逻辑:CPU 内部其实只有一套补码加法器,它根本不知道操作数是
int还是unsigned int。那么ADD、SUB、CMP是怎么同时算出无符号进借位CF和有符号溢出OF的?为什么INC和DEC更新全部算术标志位却偏偏保留CF不变? - 复杂寻址字节流:看到反汇编里的
mov eax, dword ptr [ebp + ecx*4 - 0x18],机器码里的ModR/M和SIB字节到底是怎么按位拆解并算出有效地址(Effective Address)的? - 栈帧与调用约定:函数开头的
push ebp; mov ebp, esp; sub esp, 0x40和结尾的mov esp, ebp; pop ebp; ret在栈内存里画出了怎样的图像?MSVC Debug 模式下的rep stosd是如何把未初始化局部变量填满0xCCCCCCCC(“烫烫烫”)的? - x87 浮点协处理器:为什么浮点指令操作的是
ST(0)、ST(1)而不是固定编号寄存器?一次FLD压栈后,原来的ST(0)为什么会自动变成ST(1)?
用 C 语言亲手把这套状态机写出来、跑通编译器生成的真实机器码,是打通 “C 语言源码 ↔ 编译器机器码 ↔ CPU 硬件执行” 闭环最硬核也最有效的路径。
本项目从最基础的寄存器结构体起步,历经 13 个递进式迭代完成(完整开发心路历程详见 docs/Kanxue_TinyX86_Blog.md):
| 阶段 | 迭代主题 | 核心交付成果 |
|---|---|---|
| Sprint 1 | CPU 上下文建模与基础取指循环 | 定义 REG32 共用体、CPU_Context、EIP 步进与基础 MOV r32, imm32 (0xB8~0xBF) |
| Sprint 2 | 接入自研反汇编器与表驱动分派 | 集成 ParseInstuction 解码器,分离“取指解码(Decode)”与“语义执行(Execute)” |
| Sprint 3 | ModR/M 与 SIB 寻址引擎 |
实现 GetEffectiveAddress、ReadOperand、WriteOperand,攻克 8/16/32 位内存与寄存器读写 |
| Sprint 4 | ALU 算术逻辑单元与 EFLAGS 演化 |
实现 ADD/OR/ADC/SBB/AND/SUB/XOR/CMP/TEST/INC/DEC/NEG/NOT 及 CF/PF/AF/ZF/SF/OF 精确位运算 |
| Sprint 5 | 硬件栈操作与函数调用基础设施 | 实现 PUSH/POP、PUSHA/POPA、PUSHF/POPF、ENTER/LEAVE 与栈顶指针 ESP 自动升降 |
| Sprint 6 | 控制流、分支跳转与全条件码矩阵 | 实现 JMP、CALL/RET、Jcc(16 种条件)、LOOP/LOOPE/LOOPNE、CMOVcc 与 SETcc |
| Sprint 7 | 乘除法与位扩展/移位指令群 | 实现 MUL/IMUL(单/双/三操作数)、DIV/IDIV、SHL/SHR/SAR/ROL/ROR/RCL/RCR、CBW/CWDE/CDQ |
| Sprint 8 | 串操作指令与 REP 前缀循环 |
实现 MOVS/STOS/LODS/CMPS/SCAS 与 REP/REPE/REPNE 硬件级块拷贝与扫描 |
| Sprint 9 | 简易 PE32 加载器 (PELoader.c) |
集成 PETools,实现 DOS/NT 头校验、节区虚拟地址拉伸映射与入口点 OEP 初始化 |
| Sprint 10 | x87 FPU 浮点协处理器子系统 | 实现 8 寄存器环形栈(SW.TOP)、FLD/FSTP/FILD/FISTP、四则运算与 FCOM/FNSTSW 条件码联动 |
| Sprint 11 | 交互式命令行调试器 (debugger.c) |
实现单步 s、步过 n、断点 b/bl/bc、寄存器 r、反汇编 u、栈回溯 k、内存查看与修改 m/e/w |
| Sprint 12 | GoogleTest / CTest 自动化验证 | 构建指令解码、ALU 标志位、相对分支与 FPU 环形栈回归测试套件 |
| Sprint 13 | 跨平台 64 位寻址改造与 C11→WASM 工作台 | 引入 32 位客户机虚拟地址转换层,支持 Windows/Linux/macOS 全平台编译,并通过 WASI Clang 将 C 内核直通浏览器 |
本项目的核心原则是 “单一事实来源(Single Source of Truth in C11)”——无论是本地编译出的原生命令行程序 TinyX86,还是浏览器中运行的在线可视化调试器,底层执行的都是完全相同的 core/*.c 代码:
flowchart TB
subgraph CCore["纯 C11 核心源码层 (core/*.c & include/*.h)"]
direction LR
Disasm["core/disasm.c\nParseInstuction()\nFormatInstruction()"]
CPU["core/TinyX86.c\nruncpu() / ALU / x87 FPU\nTinyX86_TranslateAddress()"]
Loader["core/PELoader.c & PETools.c\nLoadMem() / LoadExe()\nLoadExeFromMemory()"]
Dbg["core/debugger.c\nDebuggerLoop()\nDebuggerExecuteCommand()"]
end
subgraph NativeTarget["原生跨平台构建目标 (CMake / Make / MSVC / GCC / Clang)"]
MainC["core/main.c\n命令行交互式 Shell"]
CTest["tests/cpu_integration_tests.c\nCTest 自动化回归测试"]
end
subgraph WasmTarget["WebAssembly 浏览器目标 (wasi-sdk clang --target=wasm32-wasi)"]
Bridge["core/wasm_bridge.c\n导出 wasm_* C ABI 接口"]
WasmBin["public/tinyx86.wasm\n32-bit WASI 原生二进制模块"]
FFI["src/emulator/wasmEngine.ts\n薄 FFI 绑定 + WASI fd_write 拦截 printf"]
UI["src/App.tsx\nReact 可视化调试工作台"]
end
CCore --> MainC
CCore --> CTest
CCore --> Bridge
Bridge --> WasmBin
WasmBin --> FFI
FFI --> UI
TinyX86/
├── include/ # 核心 C11 公开头文件 (.h)
│ ├── TinyX86.h # CPU_Context、REG32、EFLAGS、FPU 结构体与虚拟内存转换声明
│ ├── PELoader.h # PE32 文件/内存映像加载 (LoadExe / LoadExeFromMemory / LoadMem)
│ ├── debugger.h # 交互式调试器循环与单条命令执行 API (DebuggerExecuteCommand)
│ └── compat/ # 跨平台内置 disasm.h 与 PETools.h 头文件(避免遮蔽 deps/ 子模块)
├── core/ # 核心 C11 实现源码 (.c) —— 本地 CLI 与 WebAssembly 共用同一套源码
│ ├── TinyX86.c # CPU 取指-解码-执行主循环、ModR/M+SIB 寻址、ALU、x87 FPU 实现
│ ├── disasm.c # 表驱动 x86-32 解码器与 Intel 汇编语法格式化器
│ ├── PELoader.c # 虚拟内存区域注册、PE32 节区拉伸映射与栈空间初始化
│ ├── PETools.c # 跨平台标准 C11 PE32 头部 (DOS/NT/Section) 解析实现
│ ├── debugger.c # 调试器核心实现 (s/n/c/b/bl/bc/r/u/k/m/w/e/ew/ed)
│ ├── main.c # 本地命令行交互式 Shell 入口与内置 power(2.0, 3) 机器码
│ └── wasm_bridge.c # WebAssembly C ABI 导出层 (供前端直接调用 C 函数与读取 CPU_Context)
├── deps/ # Git Submodules 外部子模块挂载目录 (可选)
│ ├── Core-x86-Disassembler/ # 独立 x86 反汇编引擎子模块仓库
│ └── PETools/ # 独立 PE 工具库子模块仓库
├── tests/ # 原生 C 自动化测试套件
│ └── cpu_integration_tests.c # CTest 集成测试 (指令解码、ALU、栈、分支、x87 FPU)
├── public/
│ └── tinyx86.wasm # 由 core/*.c 通过 WASI Clang 直接编译生成的 WebAssembly 模块
├── src/ # Web 可视化调试前端界面 (仅负责 UI 渲染与调用 tinyx86.wasm)
│ ├── emulator/
│ │ ├── wasmEngine.ts # WASI 实例化、C printf 标准输出流拦截与 C 导出函数绑定
│ │ └── types.ts # 与 C 结构体对应的前端状态显示类型定义
│ └── App.tsx # 三栏式调试器工作台界面与键盘快捷键绑定
├── docs/ # 深度技术文档与开发笔记
│ ├── images/web-debugger-screenshot.png
│ ├── architecture.md # 系统架构、内存转换层、WASM 桥接与能力边界详解
│ ├── project-relationships.md # Git 子模块同步更新保姆级指南
│ ├── validation.md # 跨平台构建与测试验证矩阵
│ └── Kanxue_TinyX86_Blog.md # 看雪技术长文:13 个敏捷冲刺手写 x86 模拟器全记录
├── CMakeLists.txt # 跨平台 CMake 构建脚本 (支持 Win32/x64, Linux, macOS)
├── CMakePresets.json # Visual Studio / CLion / CLI 跨平台预设配置
└── Makefile # GNU Make 构建脚本 (含 make / make test / make wasm / make web)
x86 架构自 16 位 8086 演进至 32 位 IA-32,要求 32 位通用寄存器(如 EAX)的低 16 位必须可直接作为 AX 访问,而 AX 又可拆分为高 8 位 AH(bits 8..15)与低 8 位 AL(bits 0..7)。
在 include/TinyX86.h 中,我们利用 C 语言小端序(Little-Endian)下的 union + struct 实现了零开销硬件级重叠:
typedef union {
uint32_t I32; // 32 位全长寄存器 (如 EAX)
uint16_t I16; // 低 16 位子寄存器 (如 AX)
struct {
uint8_t L; // 低 8 位 (bits 0..7, 如 AL)
uint8_t H; // 次低 8 位 (bits 8..15, 如 AH)
} I8;
} REG32;8 位寄存器索引编码陷阱(core/TinyX86.c 中的 ReadGPR / WriteGPR):
在 ModR/M 或 Opcode 的 3-bit 寄存器编号(0~7)中:
- 当操作数宽度为 16 位或 32 位 时,
0~7依次对应:EAX(0), ECX(1), EDX(2), EBX(3), ESP(4), EBP(5), ESI(6), EDI(7)。 - 当操作数宽度为 8 位 时,
0~3对应AL(0), CL(1), DL(2), BL(3),但4~7并不是ESP~EDI的低 8 位,而是回绕指向前四个寄存器的高 8 位:AH(4), CH(5), DH(6), BH(7)!
uint32_t ReadGPR(CPU_Context* ctx, int reg_index, int byte_len) {
if (byte_len == 4) return ctx->GPR[reg_index].I32;
if (byte_len == 2) return ctx->GPR[reg_index].I16;
if (byte_len == 1) {
if (reg_index < 4) return ctx->GPR[reg_index].I8.L; // 0..3 -> AL, CL, DL, BL
else return ctx->GPR[reg_index - 4].I8.H; // 4..7 -> AH, CH, DH, BH
}
return 0;
}在 include/TinyX86.h 与 core/debugger.c 中,TinyX86 完整建模了 32 位 EFLAGS 寄存器的状态标志、控制标志与系统标志位:
| 位 (Bit) | 缩写 | 英文全称 | 类别 | 核心作用与触发机制 |
|---|---|---|---|---|
| Bit 0 | CF |
Carry Flag | 状态标志 | 无符号数进位/借位:加法最高位产生进位或减法产生借位时置 1;INC/DEC 不修改此位 |
| Bit 1 | 1 |
Reserved | 保留位 | IA-32 架构规范要求 Bit 1 恒为 1(因此初始 Raw EFLAGS 为 0x00000202) |
| Bit 2 | PF |
Parity Flag | 状态标志 | 低 8 位偶校验标志:运算结果**最低字节(bits 0..7)**中 1 的个数为偶数时置 1 |
| Bit 4 | AF |
Auxiliary Carry Flag | 状态标志 | 半进位标志:Bit 3 向 Bit 4 产生进位或借位时置 1(用于 BCD 码调整指令) |
| Bit 6 | ZF |
Zero Flag | 状态标志 | 零标志:运算结果所有有效位全为 0 时置 1,否则置 0 |
| Bit 7 | SF |
Sign Flag | 状态标志 | 符号标志:等于运算结果的最高有效位(MSB,8 位看 Bit 7,16 位看 Bit 15,32 位看 Bit 31) |
| Bit 8 | TF |
Trap Flag | 系统/调试 | 单步陷阱标志:置 1 时 CPU 每执行完一条指令自动触发 #DB(INT 1)单步调试异常 |
| Bit 9 | IF |
Interrupt Enable Flag | 系统标志 | 外部可屏蔽中断允许标志:置 1 时允许响应外部硬件中断(STI 置 1,CLI 清 0) |
| Bit 10 | DF |
Direction Flag | 控制标志 | 串操作方向标志:0 表示地址递增(CLD),1 表示地址递减(STD,用于 MOVS/STOS 等) |
| Bit 11 | OF |
Overflow Flag | 状态标志 | 有符号数溢出标志:补码运算结果超出当前位宽有符号数表示范围时置 1 |
| Bits 12..13 | IOPL |
I/O Privilege Level | 系统标志 | I/O 特权级(0~3):定义当前任务执行 IN/OUT/CLI/STI 所需的最低环级(Ring) |
| Bit 14 | NT |
Nested Task Flag | 系统标志 | 嵌套任务标志:控制 IRET 指令是从栈弹出返回地址还是通过 TSS 切换回前一个任务 |
ALU 标志位核心位运算公式(见 core/TinyX86.c -> ALU_Calculate):
uint32_t sign_mask = (byte_len == 1) ? 0x80 : (byte_len == 2) ? 0x8000 : 0x80000000;
// 1. ZF 与 SF
ctx->EFLAGS.ZF = (truncated_res == 0) ? 1 : 0;
ctx->EFLAGS.SF = (truncated_res & sign_mask) ? 1 : 0;
// 2. AF (半进位:利用异或消除低 4 位向 Bit 4 的无进位加法结果)
ctx->EFLAGS.AF = ((dest ^ src ^ truncated_res) & 0x10) ? 1 : 0;
// 3. 加法 (ADD) 的 CF 与 OF
ctx->EFLAGS.CF = (truncated_res < dest) ? 1 : 0;
ctx->EFLAGS.OF = (((dest ^ truncated_res) & (src ^ truncated_res)) & sign_mask) ? 1 : 0;
// 4. 减法/比较 (SUB / CMP) 的 CF 与 OF
ctx->EFLAGS.CF = (dest < src) ? 1 : 0;
ctx->EFLAGS.OF = (((dest ^ src) & (dest ^ truncated_res)) & sign_mask) ? 1 : 0;core/disasm.c 采用工业级反汇编器通用的多阶段表驱动架构:
- 阶段一:指令前缀扫描(Prefix Loop):循环识别最大 4 个前缀字节:
- 重复前缀:
0xF3(REP/REPE)、0xF2(REPNE) - 操作数/地址宽度反转前缀:
0x66(32 位与 16 位操作数切换)、0x67(地址宽度切换) - 段超越前缀:
0x26(ES:),0x2E(CS:),0x36(SS:),0x3E(DS:),0x64(FS:),0x65(GS:)
- 重复前缀:
- 阶段二:主操作码查表(1-Byte vs
0x0F2-Byte Opcode Table):- 普通指令直接索引
g_OpcodeTable1[opcode](256 项); - 若首字节为转义码
0x0F,则再读一字节索引g_OpcodeTable2[opcode2](覆盖Jcc rel32、MOVZX、MOVSX、CMOVcc、SETcc、IMUL Gv, Ev等扩展指令)。
- 普通指令直接索引
- 阶段三:Group 扩展操作码(
0x80~0x83,0xC0/0xC1/0xD0~0xD3,0xF6/0xF7,0xFE/0xFF):- 当一字节 Opcode 不足以编码所有指令时,Intel 借用了紧随其后的
ModR/M字节中间 3 位(reg字段,0~7) 作为次级 Opcode:- Group 1 (
0x80~0x83):0=ADD, 1=OR, 2=ADC, 3=SBB, 4=AND, 5=SUB, 6=XOR, 7=CMP - Group 2 (
0xC0/0xC1/0xD0~0xD3):0=ROL, 1=ROR, 2=RCL, 3=RCR, 4=SHL, 5=SHR, 7=SAR - Group 3 (
0xF6/0xF7):0=TEST Ib/Iz, 2=NOT, 3=NEG, 4=MUL, 5=IMUL, 6=DIV, 7=IDIV - Group 5 (
0xFF):0=INC Ev, 1=DEC Ev, 2=CALL Ev, 4=JMP Ev, 6=PUSH Ev
- Group 1 (
- 当一字节 Opcode 不足以编码所有指令时,Intel 借用了紧随其后的
x86 最强大的内存寻址表达式 [Base + Index * Scale + Displacement] 由 ModR/M(1 字节)和可选的 SIB(1 字节)共同决定:
ModR/M Byte: [ Mod (2b) | Reg/Opcode (3b) | R/M (3b) ]
SIB Byte: [ Scale(2b)| Index (3b) | Base(3b) ]
在 core/TinyX86.c 的 GetEffectiveAddress(CPU_Context* ctx, DecodeContext* d_ctx) 中:
- 若
Mod != 3且R/M == 4 (0b100),则存在SIB字节:- 基址
Base:若SIB.base == 5且Mod == 0,则无基址寄存器(仅有 32 位偏移disp32);否则读取GPR[SIB.base]。 - 变址
Index × Scale:若SIB.index != 4(ESP不可作为变址寄存器),则加上GPR[SIB.index] << SIB.scale(乘数为1, 2, 4, 8)。
- 基址
- 若无
SIB字节:- 当
Mod == 0且R/M == 5 (0b101)时,直接返回 32 位绝对地址disp32;否则读取GPR[R/M]并加上 8 位或 32 位有符号偏移disp。
- 当
LEA与MOV的区别:MOV reg, [mem]:调用GetEffectiveAddress得到地址addr-> 调用MemRead(addr, size)读出内存值 -> 写入reg。LEA reg, [mem]:调用GetEffectiveAddress得到地址addr-> 直接把addr数值写入reg,完全不访问内存!
x86 指令集设计中最优雅的正交特性之一,就是短条件跳转 0x70~0x7F、近条件跳转 0x0F 0x80~0x8F、条件传送 0x0F 0x40~0x4F(CMOVcc)和条件字节置位 0x0F 0x90~0x9F(SETcc)的 低 4 位(opcode & 0x0F)完全共用同一个条件评估函数 CheckCondition(ctx, cc):
opcode & 0x0F |
助记符后缀 | CheckCondition 布尔表达式 |
语义解释 |
|---|---|---|---|
0x0 / 0x1 |
O / NO |
OF == 1 / OF == 0 |
溢出 / 未溢出 |
0x2 / 0x3 |
B (NAE, C) / NB (AE, NC) |
CF == 1 / CF == 0 |
无符号 < / 无符号 >= |
0x4 / 0x5 |
Z (E) / NZ (NE) |
ZF == 1 / ZF == 0 |
相等 == / 不相等 != |
0x6 / 0x7 |
BE (NA) / NBE (A) |
(CF || ZF) / (!CF && !ZF) |
无符号 <= / 无符号 > |
0x8 / 0x9 |
S / NS |
SF == 1 / SF == 0 |
负数(符号位为 1) / 非负数 |
0xA / 0xB |
P (PE) / NP (PO) |
PF == 1 / PF == 0 |
偶校验 / 奇校验 |
0xC / 0xD |
L (NGE) / NL (GE) |
SF != OF / SF == OF |
有符号 < / 有符号 >= |
0xE / 0xF |
LE (NG) / NLE (G) |
(ZF || (SF != OF)) / (!ZF && (SF == OF)) |
有符号 <= / 有符号 > |
本仓库默认加载的测试载荷 g_TestCode(83 字节)提取自 MSVC Debug 模式编译的递归函数 double power(double base, int exp):
double power(double base, int exp) {
if (exp <= 0) return 1.0;
return base * power(base, exp - 1);
}对应的完整机器码与栈帧演化如下:
; === 调用者 (Caller: main) ===
00400000: 6A 03 push 0x3 ; 压入参数2: int exp = 3
00400002: 68 00 00 00 40 push 0x40000000 ; 压入参数1高32位: double base = 2.0 (IEEE-754: 0x4000000000000000)
00400007: 6A 00 push 0x0 ; 压入参数1低32位
00400009: E8 06 00 00 00 call 0x00400014 ; 压入返回地址 0x0040000E 并跳转到 power()
0040000E: 83 C4 0C add esp, 0xC ; cdecl 调用者平栈 (释放 12 字节参数)
00400011: CC int3 ; 软断点停机,此时 FPU ST(0) = 8.000000!
; === 被调用者 (Callee: power) 函数序言与 Debug 栈初始化 ===
00400014: 55 push ebp ; 保存上一层函数的栈底 EBP
00400015: 8B EC mov ebp, esp ; 建立当前函数的栈帧基准点 EBP
00400017: 83 EC 40 sub esp, 0x40 ; 抬高栈顶,开辟 64 字节局部变量空间
0040001A: 8D 7D C0 lea edi, [ebp-0x40] ; EDI 指向局部变量区起始地址
0040001D: B9 10 00 00 00 mov ecx, 0x10 ; 循环 16 次 (16 * 4 = 64 字节)
00400022: B8 CC CC CC CC mov eax, 0xCCCCCCCC ; 0xCC 即 INT 3 软断点,双字节 0xCCCC 在 GBK 下即“烫”字
00400027: F3 AB rep stosd ; 将 [EBP-0x40 .. EBP] 填满 0xCCCCCCCC!当你在调试器中单步执行到 0x00400029 并输入 k 16 查看栈内存时,就能亲眼看到栈上整齐排列的 0xCCCCCCCC、保存的旧 EBP、返回地址以及传入的浮点/整型参数。
x87 浮点协处理器内部有 8 个 80 位(模拟器中以 double 64 位双精度表示)物理寄存器 R0 ~ R7。不同于通用寄存器的固定寻址,x87 通过状态字 SW.TOP(3-bit,取值 0~7)将其维护成一个环形栈:
- 逻辑寄存器
ST(i)到物理寄存器的映射公式:phys_idx = (SW.TOP + i) & 7。 - 压栈(
FPU_Push):先将SW.TOP = (SW.TOP - 1) & 7(如从0减为7),再写入Regs[SW.TOP]。 - 弹栈(
FPU_Pop):先读出Regs[SW.TOP],再将SW.TOP = (SW.TOP + 1) & 7。 - 浮点比较与条件跳转:早期 x86 在还没有
FCOMI指令时,浮点比较指令FCOM/FCOMP/FUCOMPP会将比较结果写入 FPU 状态字的C3(Bit 14)、C2(Bit 10)、C0(Bit 8),随后编译器生成经典的组合指令:fnstsw ax(Opcode0xDF 0xE0):把 FPU 状态字SW拷贝到AX寄存器;sahf(Opcode0x9E):将AH的特定位直接加载进EFLAGS的SF, ZF, AF, PF, CF(此时C3 -> ZF,C0 -> CF,C2 -> PF)!- 紧接着即可使用普通的
JE、JB、JA完成浮点数条件跳转。TinyX86 对这一完整链路进行了字节级精确还原。
在 32 位 x86 机器码中,代码段基址通常位于 0x00400000,栈顶位于 0x0010FFE0。如果在 64 位操作系统(Linux x86_64、macOS Apple Silicon、Windows x64)上直接把 uint32_t addr 强转为宿主指针 (uint8_t*)(uintptr_t)addr 解引用,必然触发段错误(Segmentation Fault)。
为此,core/TinyX86.c 实现了轻量级 客户机虚拟内存区域表(Guest Virtual Memory Region Table):
bool TinyX86_RegisterRegion(uint32_t guest_base, void* host_ptr, uint32_t size);
uint8_t* TinyX86_TranslateAddress(uint32_t guest_addr);- 当
LoadMem()或LoadExe()分配宿主堆内存(calloc)后,会将[guest_base, guest_base + size)与宿主指针host_ptr注册到映射表中。 - 无论宿主是 32 位 Windows、64 位 Linux/macOS 还是 WebAssembly 线性内存,所有
MemRead()、MemWrite()、取指解码与调试器内存转储均通过TinyX86_TranslateAddress()完成线性偏移换算,实现了 100% 平台无关的内存安全隔离。
| 指令分类 | 已实现指令助记符 | 关键机器码与实现细节 |
|---|---|---|
| 数据传送与类型扩展 | MOV (88~8E, A0~A3, B0~BF, C6/C7), MOVZX (0F B6/B7), MOVSX (0F BE/BF), LEA (8D), XCHG (86/87, 90~97), NOP (90), CBW/CWDE (98), CWD/CDQ (99), CMOVcc (0F 40~4F), SETcc (0F 90~9F), SAHF (9E), LAHF (9F) |
完整支持 0x66 16 位操作数前缀、段寄存器传送、零扩展与符号位扩展 |
| 算术与逻辑运算 (ALU) | ADD, OR, ADC, SBB, AND, SUB, XOR, CMP, TEST, INC, DEC, NOT, NEG, MUL, IMUL (单/双/三操作数 F6/F7, 0F AF, 69/6B), DIV, IDIV |
精确计算 8/16/32 位 CF, PF, AF, ZF, SF, OF;支持带进借位的 64 位长整数加减(ADC/SBB)与除零保护 |
| 移位与循环移位 (Group 2) | ROL, ROR, RCL, RCR, SHL (SAL), SHR, SAR (C0/C1, D0~D3) |
移位计数自动掩码 & 0x1F,精确维护最后移出位 CF 与单比特移位 OF |
| 硬件栈与函数栈帧 | PUSH (50~57, 68/6A, FF /6, 段寄存器), POP (58~5F, 8F /0, 段寄存器), PUSHA/PUSHAD (60), POPA/POPAD (61), PUSHF/PUSHFD (9C), POPF/POPFD (9D), ENTER (C8), LEAVE (C9) |
POPA 自动跳过 ESP 槽位;POPFD 精确写回全部状态标志与 TF/IF/DF/NT/IOPL |
| 控制流与循环跳转 | JMP (EB rel8, E9 rel32, FF /4 r/m32), Jcc (70~7F rel8, 0F 80~8F rel32), JCXZ/JECXZ (E3), CALL (E8 rel32, FF /2 r/m32), RET (C3, C2 imm16), LOOP (E2), LOOPE (E1), LOOPNE (E0) |
覆盖全部 16 种条件跳转码与 stdcall 被调用者平栈返回 RET imm16 |
| 串操作与块指令 | MOVSB/W/D (A4/A5), CMPSB/W/D (A6/A7), STOSB/W/D (AA/AB), LODSB/W/D (AC/AD), SCASB/W/D (AE/AF) + REP (F3) / REPNE (F2) |
根据 EFLAGS.DF 自动增减 ESI/EDI;支持 REPE CMPS/SCAS 与 REPNE CMPS/SCAS 条件提前终止 |
| 中断与标志位指令 | INT 3 (CC), INT Ib (CD), HLT (F4), CMC (F5), CLC (F8), STC (F9), CLI (FA), STI (FB), CLD (FC), STD (FD) |
内置 INT 21h 极简高层模拟(AH=02h 字符输出、AH=09h $ 结尾字符串输出、AH=4Ch 退出) |
| x87 FPU 浮点协处理器 | FLD, FST, FSTP, FILD, FIST, FISTP, FLDCW, FNSTCW, FSTSW, FNSTSW AX, FADD/P, FMUL/P, FSUB/R/P, FDIV/R/P, FIADD, FIMUL, FISUB/R, FIDIV/R, FCOM/P/PP, FUCOMPP, FCHS, FABS, FLD1, FLDZ, FLDPI, FLDL2T, FLDL2E, FLDLG2, FLDLN2, FCLEX, FINIT |
支持 32 位 float、64 位 double 及 16/32/64 位整型内存操作数与环形栈 ST(i) 运算 |
TinyX86 的定位是“面向 C 语言、x86 汇编与逆向工程学习者的指令级教学模拟器”,而非完整模拟操作系统内核与动态链接器的全系统虚拟机(如 QEMU)。为了帮助读者清晰了解本项目的能力边界,现将已实现范围与未实现限制说明如下:
| 维度 | ✅ 已实现能力 (Implemented) | ❌ 未实现 / 不在当前范围内 (Out of Scope) |
|---|---|---|
PE32 加载器 (PELoader.c) |
• 解析 IMAGE_DOS_HEADER、IMAGE_NT_HEADERS32 与节表(IMAGE_SECTION_HEADER)• 按 ImageBase + VirtualAddress 将各节区(.text/.data/.rdata)拉伸拷贝到虚拟内存• 分配 64KB 虚拟栈空间并将 EIP 指向入口点 ImageBase + AddressOfEntryPoint |
• 未实现导入表(IAT)与导出表(EAT)解析 • 未实现基址重定位表( .reloc)修复• 未实现 TLS 回调表(TLS Callbacks)、异常表(SEH)与资源表解析 |
| 操作系统 API 与系统调用 | • 在 Execute_INT 中通过极简高层模拟(HLE)拦截了 DOS INT 21h 的 3 个基础子功能:AH=02h(打印单字符)、AH=09h(打印 $ 结尾字符串)、AH=4Ch(设置 Halted = 1 正常退出)• 支持 INT 3 (0xCC) 软断点拦截 |
• 不支持任何 Windows Win32 API 或外部 DLL 动态链接(如 kernel32.dll、user32.dll、msvcrt.dll 等)• 不能直接运行包含标准 C 运行库(CRT 启动代码 mainCRTStartup)的常规编译 .exe(因为 CRT 初始化会调用大量系统 DLL API);仅支持无外部依赖的纯算法 PE32 程序(见第 9 节编译方法)或 .bin 原始机器码 |
| CPU 指令集与硬件架构 | • 32 位平坦内存模型(Flat Memory Model)下的通用寄存器(EAX~EDI)、完整 EFLAGS 标志位与核心算术/逻辑/控制流/栈/串操作指令• x87 FPU 8 寄存器环形栈与常用浮点算术/比较指令 |
• 不支持 Ring 0 特权级指令(如 LGDT、LIDT、MOV CR0、IN/OUT、IRET、SYSENTER)• 不支持 GDT/LDT 段描述符权限检查与页表(Paging / MMU)硬件级地址保护 • 不支持 MMX、SSE、AVX 等 SIMD 向量扩展指令集及 64 位(x86_64)长模式指令 |
项目根目录提供了标准 CMakeLists.txt 与 CMakePresets.json(Visual Studio 2022/2026 直接使用“打开本地文件夹”即可自动识别 Win32 与 x64 配置):
# 1. 克隆仓库(包含子模块)
git clone --recurse-submodules https://github.com/582835293/TinyX86.git
cd TinyX86
# 2. 配置并编译(Linux / macOS / Windows 通用)
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release --parallel
# 3. 运行 CTest 自动化集成测试套件
ctest --test-dir build -C Release --output-on-failure
# 4. 启动 TinyX86 原生命令行交互式调试器
./build/bin/TinyX86 # Linux / macOS
.\build\bin\Release\TinyX86.exe # Windows MSVCmake # 编译 TinyX86 主程序与 cpu_integration_tests 至 build/make/
make test # 编译并运行 C 语言集成测试
make run # 编译并启动命令行交互式 TinyX86 Shell
make wasm # 使用 WASI Clang 将 core/*.c 重新编译为 public/tinyx86.wasm
make web # 启动本地 Web 可视化调试工作台 (http://localhost:3000)
make clean # 清理构建产物如果你修改了 core/TinyX86.c、core/disasm.c 或 core/debugger.c,除了推送到 GitHub 让 Actions 自动编译外,也可以在本地使用 wasi-sdk 一条命令重新生成 public/tinyx86.wasm:
/path/to/wasi-sdk/bin/clang -std=c11 -O2 -Iinclude -Iinclude/compat \
-Wl,--export-dynamic -Wl,--no-entry \
core/TinyX86.c core/disasm.c core/PELoader.c core/PETools.c core/debugger.c core/wasm_bridge.c \
-lm -o public/tinyx86.wasm无论是原生命令行 TinyX86 还是 Web 可视化调试工作台,均由 core/debugger.c 驱动,支持以下完整调试命令与键盘快捷键:
| 调试命令 | Web 全局快捷键 | 参数示例 | 功能详解 |
|---|---|---|---|
s |
S / F7 / F11 |
s 或 s 5 |
Step Into(单步步入):执行 1 条或连续执行 N 条指令,遇到 CALL 会进入子函数内部 |
n |
N / F8 / F10 |
n |
Step Over(单步步过):遇到 CALL、LOOP、REP STOSD、INT 等块指令时全速执行至下一条指令地址(若中途命中断点则立即停下) |
c |
C / F5 / F9 |
c |
Continue(继续运行):持续执行直到命中用户断点、INT 3(0xCC)软断点或 HLT 停机 |
b |
点击反汇编行 | b 400014 |
Toggle Breakpoint:在指定十六进制地址设置或取消断点(最多 16 个) |
bl / bc |
- | bl / bc |
List / Clear Breakpoints:列出当前所有断点 / 清空所有断点 |
r |
R |
r |
Registers:打印全部 32 位通用寄存器、段寄存器、EFLAGS 激活标志位(含 TF/NT/IOPL)及 x87 FPU 8 个物理/逻辑寄存器状态 |
u |
U |
u 或 u 12 |
Unassemble(反汇编):从当前 EIP 向后反汇编 N 条指令(输出地址、机器码字节流与 Intel 汇编助记符) |
k |
K |
k 或 k 12 |
Stack Dump(栈回溯):显示当前 ESP 栈顶附近的双字(DWORD)内存与 ASCII 字符,自动标注 <--- ESP (Top) 与 <--- EBP (Frame) |
m |
- | m 400000 |
Memory Dump:以 16 字节×4 行 Hex + ASCII 格式转储指定虚拟地址内存 |
w |
点击左侧寄存器/标志位 | w EAX 2A / w TF 1 |
Write State:动态修改任意 32/16/8 位通用寄存器(EAX/AX/AL/AH 等)、段寄存器、标志位(CF/PF/AF/ZF/SF/TF/IF/DF/OF/NT/IOPL)或 FPU TOP |
e / ew / ed |
- | e 400000 90 CC |
Edit Memory:向指定虚拟地址写入字节序列(e)、16 位字(ew)或 32 位双字(ed) |
t |
顶部预设下拉框 | t |
重新加载默认内置测试 g_TestCode(递归幂运算 power(2.0, 3)) |
h / ? |
H |
h |
打印调试器命令帮助说明 |
由于 TinyX86 不包含 Windows 系统 DLL(kernel32.dll / msvcrt.dll),如果你想自己写一段 C 代码或汇编代码编译成 .exe 或 .bin 放进 TinyX86 单步调试,只需关闭编译器的标准库启动代码(No CRT)并指定入口函数即可!
新建 fib.asm(计算斐波那契数列第 10 项存入 EAX 并触发 INT 3):
BITS 32
ORG 0x00400000
start:
mov ecx, 10 ; 计算第 10 项
xor eax, eax ; f(0) = 0
mov ebx, 1 ; f(1) = 1
.loop:
mov edx, eax
add edx, ebx ; edx = f(n-2) + f(n-1)
mov eax, ebx
mov ebx, edx
loop .loop
int3 ; 停机供调试器观察 EAX (55 = 0x37)
hlt编译并在 TinyX86 中加载:
nasm -f bin fib.asm -o fib.bin
# 在命令行输入: b fib.bin ,或在 Web 调试器点击 "Load .EXE / .BIN" 上传 fib.bin新建 mini_pe.c:
// 不引用任何标准库头文件,直接定义入口点函数
int calc_sum(int n) {
int s = 0;
for (int i = 1; i <= n; i++) s += i;
return s;
}
void __stdcall TinyEntry(void) {
int result = calc_sum(100); // EAX = 5050 (0x13BA)
__asm {
mov ebx, result
int 3
hlt
}
}- 使用 MSVC (
cl.exex86) 编译:cl /c /GS- /Od /arch:IA32 mini_pe.c link /ENTRY:TinyEntry /SUBSYSTEM:CONSOLE /NODEFAULTLIB /FIXED mini_pe.obj /OUT:mini_pe.exe
- 使用 MinGW (
i686-w64-mingw32-gcc) 编译:i686-w64-mingw32-gcc -O0 -fno-stack-protector -nostdlib -Wl,-e,_TinyEntry@0 -Wl,--image-base,0x400000 mini_pe.c -o mini_pe.exe
生成的 mini_pe.exe 即可直接拖入 TinyX86(命令行 e mini_pe.exe 或 Web 端 Load .EXE / .BIN),完整观察 PE32 节表映射与函数调用栈帧!
本仓库通过 .gitmodules 关联了上游反汇编引擎仓库 deps/Core-x86-Disassembler 与 PE 解析库 deps/PETools。当你更新了子模块仓库并推送到 GitHub 后,只需在 TinyX86 根目录下执行以下 3 步:
# 第一步:确保子模块已初始化
git submodule update --init --recursive
# 第二步:从 GitHub 拉取子模块远端默认分支的最新提交
git submodule update --remote --merge deps/Core-x86-Disassembler
# 第三步:在 TinyX86 主仓库提交新的子模块版本指针并推送
git add deps/Core-x86-Disassembler
git commit -m "chore(submodule): sync Core-x86-Disassembler to latest"
git push origin main详细的多仓库协作与常见问题排查请阅读 👉 《项目关系与 Git 子模块保姆级同步指南》(docs/project-relationships.md)。
项目内置双重验证机制,确保每次修改 C 代码后指令解码与执行语义不发生退化:
- 原生 CTest 集成测试 (
tests/cpu_integration_tests.c):- 在 GitHub Actions 矩阵中自动跨 5 个操作系统/编译器环境(Ubuntu GCC、Ubuntu Clang、macOS Apple Clang、Windows Win32 MSVC、Windows x64 MSVC)编译并运行。
- WebAssembly 内置 C 集成自检 (
wasm_run_c_integration_testsincore/wasm_bridge.c):- 浏览器加载
public/tinyx86.wasm时,在 C 语言层直接执行 3 组共 13 项硬件状态断言(涵盖ParseInstuction长度校验、MOV/ADD/PUSH/POP/XOR/CMP与ZF标志位、JMP rel8分支跳过以及FLD1 + FLD1 + FADDP浮点栈计算),并在右侧C Tests (13/13)面板实时展示结果与 C 结构体字节大小。
- 浏览器加载
- 📘 系统架构、内存转换层、WASM 桥接与能力边界详解 (
docs/architecture.md) - 🔗 项目关系与 Git 子模块保姆级同步指南 (
docs/project-relationships.md) - 📝 看雪技术长文:13 个敏捷冲刺手写 x86 CPU 模拟器全记录 (
docs/Kanxue_TinyX86_Blog.md) - ✅ 跨平台构建与测试验证记录 (
docs/validation.md) - 🧩 作者关联逆向工程系列仓库:
- Core-x86-Disassembler —— 纯 C 编写的轻量级表驱动 x86-32 反汇编引擎
- PETools —— PE32 文件头、节表、导入导出表与重定位解析工具库
- JiaquanPEViewerV1.0 —— 基于 Win32 SDK 原生 GUI 编写的可视化 PE 结构查看器
