先看这段 C 语言程序的入口:

1
2
3
4
5
eval(int main)(eval(void))
eval({)
...
return eval(1 +~ 1 + (0xffffffff>>31));
eval(})

eval(int main)eval({)?这不是任何一门合法语言的语法——但它能编译、能运行。这一篇讲的是两个东西的合体:一个把一切”语法噪音”合法化的宏,和一组连作者自己都要注释自嘲的魔法数字

eval:一个什么都能包的宏

谜底只有一行:

1
#define eval(everything) everything

eval(x) 永远展开成 x 本身。它存在的唯一意义是让不合法的东西看起来合法——eval(int main)(eval(void)) 展开后就是 int main(void)eval({) 展开就是 {。一个什么都不做的宏,却能让整段代码的形态彻底失控。配合它的还有两个帮凶:

1
2
#define wscanf (&wscanf)
#define wprintf (&wprintf)

wscanf 这个标识符从此在源码里代表”wscanf 函数的地址“——为一个后面会震翻天的小把戏埋下伏笔。

魔法数字:地址算术调函数

程序的核心逻辑(节选,原文件里的注释已因多次转码损坏,此处省略):

1
2
3
4
5
6
7
8
9
int magic_intArray[] = { 23884, (0b0100 << 0b0100) | 0b0100, 0x5730, 050545, 0123, eval((int)) - 0xffffffffa7, 0x5F0F, 1 +~ 0, eval((int)) - 037777777647, 0b0101011111111010, 0 };
int magic_num = eval((int)) - 037777777770;
unsigned short magic_shortArray[sizeof(magic_intArray)];

setlocale(LC_ALL, "");
for (int magic_i = 0; magic_i != 10; magic_i += (1 << 1) + 1, magic_i > 10 ? magic_i %= 10 : magic_i)
magic_shortArray[magic_intArray[(sizeof(magic_intArray) >> ((1 >> 1? 0 : 1) << eval((int))4294967297)) - 1]++] = eval((unsigned short))magic_intArray[magic_i];

((int (*)(const wchar_t *, ...))(wscanf + magic_num))(L"%ls\n", magic_shortArray);

拆开看每一层的狠活:

  • 数据magic_intArray 的取值来自上一篇《把中文藏进 int 数组》的同款思路——一组混用五进制书写方式的数字,暗藏一句话的 UTF-16 编码;
  • 取数下标(sizeof(...) >> ((1 >> 1? 0 : 1) << 4294967297)) - 1,看着像密码,实际恒等于 0——用恒真恒假表达式表演”谁能看懂我在干嘛”;
  • 步长magic_i += (1 << 1) + 1,加 3 也要写成移位;magic_i > 10 ? magic_i %= 10 : magic_i 的折返(上一篇见过的同款洗牌循环);
  • 最疯的一行((int (*)(const wchar_t *, ...))(wscanf + magic_num))(L"%ls\n", ...)——把 wscanf 函数的地址加上一个魔法偏移,强转成函数指针直接调用。它在干什么?调用的其实是 wprintf

魔法数字的物理意义:两个函数之间的距离

wscanfwprintf 住在同一个运行库 DLL 里,它们的地址相差一个固定偏移(在特定的编译器和库版本下)。magic_num 就是这个偏移量的补码形式——wscanf 的地址 + magic_num 恰好落到 wprintf 的门口。于是这一行”看不懂的函数指针调用”等效于 wprintf(L"%ls\n", ...)

它为什么吓人?因为这个偏移不是标准保证的——换一个编译器版本、换一个 CRT,距离就变了,魔法立即失效。果不其然,为写这篇文章重新编译,程序直接崩了(退出码 127):新的地址布局里,wscanf + magic_num 不再指向任何有意义的地方。原版 exe(2025 年 3 月编译)还能正常输出,新编译的已经阵亡——魔法数字是有”工具链指纹”的

作者当年其实心里有数,源码里留了两行诚实的注释(原文已随多次转码损坏,大意如下):

这些数字不知道怎么来的,恰好契合就能打印出想要的结果。
这个代码好像不能随便乱删,比如我删掉下面那行的 wprintf 就什么都不会输出。

——典型的海森 bug:一动它就变样,不动态它就装死。

原版 exe 的运行效果

用 2025 年 3 月编译的原版 exe 跑,输出两行:

1
2
这些魔法数字也不知道怎么来的,反正就这样写就对了
嵌入式基YYDS

第一行是程序自己的吐槽;第二行”嵌入式基YYDS”正是上一篇把中文藏进 int 数组的同款暗语——同一句”嵌入式基地YYDS”,两个程序、两种藏法。

正经话

这一篇的”正经话”上一篇说了一半:函数地址是真实存在的数字,所以理论上可以从一个函数的地址走到另一个函数——但这依赖链接布局,标准不保证、工具链一升级就崩。eval 宏同理:它证明了宏可以构造出”看起来像另一门语言”的语法,代价是所有读代码的人(包括三个月后的你自己)都要重新学一遍。

抽象代码的价值从不在于可维护性——在于它逼你回答”这为什么能跑”,而答案往往是半本《链接器与加载器》。

系列其他文章见抽象代码宣言