☰
M/o/Vfuscator 入门示例:用单条 mov 指令编译 prime.c,与 GCC 输出逐行对比
2026/10/14 8:10:46 网站建设 项目流程
  • 编译器

【免费下载链接】movfuscator

The single instruction C compiler

项目地址:https://gitcode.com/gh_mirrors/mo/movfuscator
点击查看免费下载

本文以 movfuscator 仓库中的overview/目录为例,展示如何用 M/o/Vfuscator 编译器(movcc)编译 C 语言素数判定程序,并将同一函数is_prime在 GCC 与 M/o/Vfuscator 下的汇编输出、控制流图进行对比。读完本文,你能复现完整的汇编/目标文件/可执行文件生成流程,理解 mov 单指令编译后代码体积膨胀、控制流消失的现象,并掌握验证编译结果的基本命令。

示例背景:同一个素数判定程序

overview/目录提供了 M/o/Vfuscator 最基础的示例:使用验证目录中的 prime.c 作为编译对象。该程序结构非常简洁——is_prime函数先处理 1 和 2 两个特例,再用for (i=2; i*i<=x; i++)循环试除判断能否整除,main中调用is_prime输出 1 到 10000 之间的素数:

int is_prime(int x) { int i; if (x==1) { return 0; } if (x==2) { return 1; } for (i=2; i*i<=x; i++) { if (x%i==0) { return 0; } } return 1; }

这段代码只包含赋值、比较、循环、函数调用和printf等最常规的 C 语言结构,是观察"普通编译器"与"单指令编译器"输出差异的理想样本。

汇编对比:27 条普通指令 vs 5743 条 mov

对同一份源码,两种编译器生成的is_prime函数汇编差异极大。overview目录直接附带了两份汇编产物供对照:

GCCM/o/Vfuscator

GCC 的输出(prime_gcc.asm)是一个标准 x86 函数:30 行,使用了push、sub、cmp、jne、idiv、imul、jle、leave、ret等 9 条mov之外的各类指令,控制流分支清晰可见:

<is_prime>: push ebp mov ebp,esp sub esp,0x10 cmp DWORD PTR [ebp+0x8],0x1 jne 8048490 <is_prime+0x13> mov eax,0x0 jmp 80484cf <is_prime+0x52> ... idiv DWORD PTR [ebp-0x4] mov eax,edx test eax,eax jne 80484ba <is_prime+0x3d> ...

M/o/Vfuscator 的输出(prime_mov.asm)则是 5743 条指令,全部是 mov(不含任何注释与标签后逐行统计,mov恰好出现 5743 次)。其中没有任何cmp、idiv、jne,算术与分支全部被拆解为查表式的mov序列:

<is_prime>: mov eax,ds:0x83fc638 mov edx,0x88048744 mov ds:0x81fc4c0,eax mov DWORD PTR ds:0x81fc4c4,edx mov al,ds:0x81fc4c0 mov ecx,DWORD PTR [eax*4+0x8056ad0] mov dl,BYTE PTR ds:0x81fc4c4 mov dl,BYTE PTR [ecx+edx*1] ...

可以观察到的实现特征(均可在 prime_mov.asm 中直接验证):

  • 查表做算术:如mov ecx,DWORD PTR [eax*4+0x8056ad0]这类按 4 字节缩放索引访问大地址空间,正是 README 中提到的"算术依赖查找表(lookup tables)"的具体体现,也是编译产物体积巨大的根本原因;
  • 寄存器仅作搬运:eax、edx、ecx在 mov 之间反复倒手数据,配合数据表完成取模、比较等操作,对应源码 movfuscator/mov.md 中reg: MODI4(reg,reg)、reg: EQI4(reg,reg)等规则的展开结果;
  • 无显式跳转:整段代码线性排列,函数返回、循环回跳都由 mov 序列(配合执行引擎的跳转寄存器加载)隐式完成,因此从源码结构看静态反汇编器很难还原出正常 CFG。

控制流图对比

同一函数的控制流图(CFG)对比进一步放大了这种差异:

GCCM/o/Vfuscator

GCC 的 CFG 是典型的"菱形分支 + 回边循环"结构;而 M/o/Vfuscator 的 CFG 退化为一条几乎无分支的直线——这正是 mov 混淆的执行模型:控制流不再体现为跳转指令,而是由数据驱动(跳转目标被当作数据搬运并加载)。仓库根 README 也明确说明 M/o/Vfuscator 不依赖 jmp 实现无限执行循环,而是利用"故障的 mov 指令"(faulting mov)达成,见 movfuscator/movfuscator.c 中设计注释与mov cs, ax相关说明。

复现 overview 目录的完整命令

overview目录中每个产物文件都对应一条可复现的构建命令,这里按原文档完整列出(movcc需先通过构建脚本安装,见下文"构建与验证"):

生成汇编

产物命令
prime_gcc.asm:GCC 为is_prime生成的汇编gcc -S ./overview/prime.c -o prime_gcc.asm
prime_mov.asm:M/o/Vfuscator 为is_prime生成的汇编movcc -S ./overview/prime.c -o prime_mov.asm

生成目标文件

产物命令
prime_gcc.o:GCC 编译出的目标文件gcc -c ./overview/prime.c -o prime_gcc.o
prime_mov.o:M/o/Vfuscator 编译出的目标文件movcc -c ./overview/prime.c -o prime_mov.o

生成完整可执行文件

产物命令
prime_gcc:GCC 编译的完整可执行文件gcc -s ./validation/prime.c -o prime_gcc
prime_mov:M/o/Vfuscator 编译的完整可执行文件movcc -s ./validation/prime.c -o prime_mov

注意两处细节:汇编/目标文件示例用的是./overview/prime.c,而可执行文件示例用的是./validation/prime.c——两者是同一份源码(overview/中的prime.c即验证程序 prime.c)。另外-s选项用于 strip 符号表,根 README 的"Recommendations"指出:由于算术查表使符号表极大,strip 后最终二进制体积可缩小约一半,这也是示例命令统一带-s的原因。

运行prime_mov的效果如下(movcc prime.c -o prime编译后执行,输出 1-100000 内的素数序列):

movcc prime.c -o prime运行实录

构建与验证

要实际执行上述命令,需要先构建 M/o/Vfuscator。其前端是 LCC,构建脚本 build.sh 会自动完成以下工作(均可在该脚本源码中逐行对应):

  1. 克隆 LCC 并固定到指定提交(git reset --hard 3b3f01b...);
  2. 依次打补丁将 mov 后端绑定到前端:bind.patch、makefile.patch、enode.patch、gen.patch、expr.patch、lcc.patch;
  3. 编译 LCC 驱动并软链出movcc;
  4. 编译 mov 运行库(crt0/crtf/crtd 及 softfloat 浮点仿真库)。

根 README 给出了构建入口与 32 位环境要求(在 64 位系统上需安装 32 位 libc,如apt-get install libc6-dev-i386或yum install glibc-devel.i686):

git clone <仓库地址> cd movfuscator ./build.sh sudo ./install.sh # 将 movcc 软链到 /usr/local/bin

构建完成后,仓库提供 check.sh 作为端到端验证:它下载一个开源 AES 实现,用movcc编译、objdump反汇编检查并实际运行。这与本文示例的验证思路完全一致——汇编中只有 mov,且程序输出正确:

./check.sh

从源码理解 movcc 的开关

编译 prime.c 时,movcc的行为由 movfuscator/mov.md 中的 LCC 后端规则文件与 movfuscator/movfuscator.c 中的发射器共同决定。mov.md是一份完整语法文件:从%term定义(如CNSTI4、MULI4、EQI4)到规则体(如reg: MULI4(reg,reg) "# emit2\n"),规定了每一个 IR 节点如何被展开为 mov 序列。prime_mov.asm里反复出现的查表模式,就是MULI4、EQI4等规则经emit2展开后的产物。

与 overview 示例相关的常用开关(通过-Wf前缀传给编译器本体,如movcc example.c -Wf--no-mov-id)在 movfuscator/movfuscator.c 中定义,参数解析逻辑见其progbeg函数(约 L2923 起):

标志作用默认
--mov-id/--no-mov-id为每个基本块添加调试参考指令 / 不添加不添加
--mov-flow/--no-mov-flow用 mov 实现控制流 / 用 jmp 实现控制流(后者可大幅加速大型项目,但浮点需改链 softfloat*_cf.o)mov
--mov-extern/--no-mov-extern用 mov 实现外部函数调用 / 用 jmp 实现(便于调试)mov
--mov-loop/--no-mov-loop主执行循环用 mov 实现 / 用 jmp 实现(便于调试)mov
--crt0/--crtf/--crtd/--crt分别构建_start例程、主执行循环例程、数据表库,--crt等价于三者全开关闭

构建脚本中同样用到了这些开关,例如用-Wf--crt0编译 crt0 运行库、用-Wf--no-mov-flow额外编译一份控制流未混淆的*_cf.o版本供调试链接(见 build.sh L49-L56)。

小结与延伸阅读

通过overview/这个最小示例可以完整看到单指令编译的核心效果:同一份prime.c,GCC 输出 30 行含分支的标准 x86 汇编,M/o/Vfuscator 输出 5743 条纯 mov 指令、CFG 退化为直线。复现路径非常直接——gcc -S/-c与movcc -S/-c对称操作即可得到四份产物,-s选项控制 strip。

若需继续深入,仓库中还有:

  • validation/ 目录下大量验证程序(含 AES、MD5 等密码学算法及 README),可参照 check.sh 的流程验证 movcc 输出;
  • post/ 目录的后处理器脚本(README),可将 mov 汇编进一步翻译为仅 XOR、SUB、PUSH/POP 等单一指令形式,或做指令乱序、寄存器重命名;
  • poc/ 目录中的 M/o/Vfuscator 1.0(面向 BrainF*@! 语言)原型;
  • slides/domas_2015_the_movfuscator.pdf 设计幻灯片,讲解 mov 图灵完备的实现机制。
  • 编译器

【免费下载链接】movfuscator

The single instruction C compiler

项目地址:https://gitcode.com/gh_mirrors/mo/movfuscator
点击查看免费下载

相关推荐

上一篇:Dagger TypeScript SDK 中的 DirectoryAsModuleOpts 与 sourceRootPath:从目录加载模块的完整指南
下一篇:在 MLX-VLM 中使用 Qwen4-Exp 原生 MTP 头进行投机解码:拆分、推理与量化实战

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询