抽象代码:eval 宏与魔法数字
先看这段 C 语言程序的入口:
1 | eval(int main)(eval(void)) |
eval(int main)?eval({)?这不是任何一门合法语言的语法——但它能编译、能运行。这一篇讲的是两个东西的合体:一个把一切”语法噪音”合法化的宏,和一组连作者自己都要注释自嘲的魔法数字。
eval:一个什么都能包的宏
谜底只有一行:
1 |
eval(x) 永远展开成 x 本身。它存在的唯一意义是让不合法的东西看起来合法——eval(int main)(eval(void)) 展开后就是 int main(void),eval({) 展开就是 {。一个什么都不做的宏,却能让整段代码的形态彻底失控。配合它的还有两个帮凶:
1 |
wscanf 这个标识符从此在源码里代表”wscanf 函数的地址“——为一个后面会震翻天的小把戏埋下伏笔。
魔法数字:地址算术调函数
程序的核心逻辑(节选,原文件里的注释已因多次转码损坏,此处省略):
1 | int magic_intArray[] = { 23884, (0b0100 << 0b0100) | 0b0100, 0x5730, 050545, 0123, eval((int)) - 0xffffffffa7, 0x5F0F, 1 +~ 0, eval((int)) - 037777777647, 0b0101011111111010, 0 }; |
拆开看每一层的狠活:
- 数据:
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。
魔法数字的物理意义:两个函数之间的距离
wscanf 和 wprintf 住在同一个运行库 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 | 这些魔法数字也不知道怎么来的,反正就这样写就对了 |
第一行是程序自己的吐槽;第二行”嵌入式基YYDS”正是上一篇把中文藏进 int 数组的同款暗语——同一句”嵌入式基地YYDS”,两个程序、两种藏法。
正经话
这一篇的”正经话”上一篇说了一半:函数地址是真实存在的数字,所以理论上可以从一个函数的地址走到另一个函数——但这依赖链接布局,标准不保证、工具链一升级就崩。eval 宏同理:它证明了宏可以构造出”看起来像另一门语言”的语法,代价是所有读代码的人(包括三个月后的你自己)都要重新学一遍。
抽象代码的价值从不在于可维护性——在于它逼你回答”这为什么能跑”,而答案往往是半本《链接器与加载器》。
系列其他文章见抽象代码宣言。
