编译器不会自动插入死代码,需手动构造不可达但难被优化的junk代码。常用方法包括volatile变量、extern函数调用、模板递归生成及链接时注入自定义段。

为什么编译器不会帮你插死代码,得自己动手
C++ 标准和主流编译器(g++、clang++、MSVC)默认不提供“自动注入 junk code”功能。所谓虚假死代码块,本质是人为构造的、不可达但语法合法、能通过编译、又难被优化掉的逻辑片段。编译器的优化器(如 -O2)反而会主动删掉它们——这正是你要对抗的。
关键不是“让编译器插”,而是“写出让编译器 不敢删 的 junk”。
用 volatile + 无副作用函数调用绕过 DCE
最常用且可靠的方式:引入外部可见副作用,阻止死代码消除(Dead Code Elimination)。常见错误是只写 int x = 1 + 2; —— 这种纯计算在 -O2 下直接被抹掉。
正确做法:
立即学习“C++免费学习笔记(深入)”;
- 声明一个外部链接的
volatile变量或函数(即使不定义,链接阶段再处理) - 或调用带
<strong>attribute</strong>((used))/[[gnu::used]]的空函数
extern "C" void __junk_marker() __attribute__((used));
void fake_branch() {
int x = rand(); // 引入不确定值
if (x & 0x80000000) { // 条件永远不可静态判定
volatile int a = 1;
a += 2;
__junk_marker(); // 链接时存在即可,不需实现
}
}
要点:
-
volatile让编译器放弃对a的优化假设 -
rand()破坏常量传播(不用真随机,time(0) % 2也行) -
<strong>attribute</strong>((used))强制保留符号,防止 LTO 合并/删除
用模板递归 + constexpr 混淆控制流
适用于想批量生成 junk 块又不想手写几十个 if 的场景。利用模板实例化在编译期展开,但最终生成的汇编仍含跳转和栈操作。
template<int N>
struct JunkBlock {
static void emit() {
volatile int v = N * 7;
if constexpr (N > 1) {
JunkBlock<N-1>::emit();
}
asm volatile("" ::: "rax", "rbx"); // 内联汇编屏障,干扰寄存器分配
}
};
// 使用:JunkBlock<5>::emit();
注意点:
-
if constexpr是编译期分支,但生成的每个emit()实例仍是独立函数体 -
asm volatile("" ::: ...)告诉编译器“我用了这些寄存器”,强制插入保存/恢复逻辑 - MSVC 需用
__declspec(noinline)替代volatile函数属性,否则可能内联后被优化扁平化
链接时插入 junk:objcopy + 自定义 section
如果源码侵入性要降到最低,可在链接阶段把预编译好的 junk 二进制块塞进去。适合加壳、混淆工具链集成。
步骤:
- 写一段汇编 junk(比如循环加减、无效 SSE 指令),用
nasm -f elf64编译成junk.o - 在 C++ 中声明 extern char 数组指向该段:
extern "C" char __junk_start[], __junk_end[]; - 链接时加
-Wl,--section-start,.junk=0x1000000并合并段 - 编译器不会检查该段内容,反汇编器会照常列出垃圾指令
风险提示:
-
objcopy --add-section插入的段默认不加载到内存,需手动加ALLOC,LOAD,READONLY标志 - 若 junk 包含绝对地址跳转,在 PIE 二进制中会出错;应全用相对寻址或纯数据填充
混淆效果强,但调试和符号表维护成本陡增,上线前务必验证段权限和运行时映射。
真正难的不是生成 junk,而是让它既不崩溃、不拖慢运行时、又足够扰乱 IDA/Ghidra 的控制流图重建——尤其是当目标二进制启用了 LTO 和 -fPIE 时,很多看似“安全”的 junk 会被跨函数折叠或重排。


















