在这四条棘手的C代码背后的概念
为什么这段代码会输出C++Sucks
? 它背后的概念是什么?
#include <stdio.h> double m[] = {7709179928849219.0, 771}; int main() { m[1]--?m[0]*=2,main():printf((char*)m); }
在这里testing。
数字7709179928849219.0
具有以下二进制表示forms作为64位double
:
01000011 00111011 01100011 01110101 01010011 00101011 00101011 01000011 +^^^^^^^ ^^^^---- -------- -------- -------- -------- -------- --------
+
显示标志的位置; 指数的^
,以及-
尾数(即没有指数的值)。
由于该表示使用二进制指数和尾数,因此数字加倍会使指数增加1。 你的程序正好771次,所以1075开始的指数( 10000110011
十进制表示)最后变成1075 + 771 = 1846。 1846的二进制表示是11100110110
。 结果模式如下所示:
01110011 01101011 01100011 01110101 01010011 00101011 00101011 01000011 -------- -------- -------- -------- -------- -------- -------- -------- 0x73 's' 0x6B 'k' 0x63 'c' 0x75 'u' 0x53 'S' 0x2B '+' 0x2B '+' 0x43 'C'
这种模式对应于您看到的打印string,只是向后。 同时,数组的第二个元素变为零,提供空终止符,使得string适合传递给printf()
。
更可读的版本:
double m[2] = {7709179928849219.0, 771}; // m[0] = 7709179928849219.0; // m[1] = 771; int main() { if (m[1]-- != 0) { m[0] *= 2; main(); } else { printf((char*) m); } }
它recursion调用main()
771次。
一开始, m[0] = 7709179928849219.0
, 代表 C++Suc;C
在每次调用中, m[0]
都会加倍,以修复最后两个字母。 在最后一个调用中, m[0]
包含C++Sucks
ASCII char表示, m[1]
仅包含零,因此它具有C++Sucks
string的空终止符。 所有假设m[0]
存储在8个字节,所以每个字符需要1个字节。
没有recursion和非法main()
调用它将看起来像这样:
double m[] = {7709179928849219.0, 0}; for (int i = 0; i < 771; i++) { m[0] *= 2; } printf((char*) m);
从forms上讲,这个程序的推理是不可能的,因为它是不合法的(即它不是合法的C ++)。 它违反了C ++ 11 [basic.start.main] p3:
函数main不能在程序中使用。
除此之外,它依赖于这样一个事实,即在典型的消费者计算机上,一个双字节长度为8个字节,并且使用某种众所周知的内部表示。 计算数组的初始值,以便在执行“algorithm”时,第一个double
的最终值将使得内部表示(8字节)将是8个字符C++Sucks
的ASCII码。 数组中的第二个元素是0.0
,其内部表示中的第一个字节是0
,这使得这是一个有效的C风格的string。 然后使用printf()
发送到输出。
在硬件上运行这个上面的一些不成立将导致垃圾文本(或者甚至可能访问超出边界),而不是。
也许理解代码的最简单的方法是反过来处理事情。 我们将从一个string开始打印 – 为了平衡,我们将使用“C ++ Rocks”。 关键点:就像原来的一样,它只有8个字符。 由于我们要做的(大致)和原文一样,并且以相反的顺序打印出来,所以我们先从相反的顺序开始。 对于我们的第一步,我们只是将该位模式视为double
,并打印出结果:
#include <stdio.h> char string[] = "skcoR++C"; int main(){ printf("%f\n", *(double*)string); }
这产生3823728713643449.5
。 所以,我们想用某种不明显的方式来操作,但很容易扭转。 我将半任意选乘256,这给了我们978874550692723072
。 现在,我们只需要编写一些混淆的代码除以256,然后以相反的顺序打印出各个字节:
#include <stdio.h> double x [] = { 978874550692723072, 8 }; char *y = (char *)x; int main(int argc, char **argv){ if (x[1]) { x[0] /= 2; main(--x[1], (char **)++y); } putchar(*--y); }
现在我们有了大量的投射,将(主观recursion的)主观参数完全忽略了(但是评估得到的增量和减量是非常重要的),当然这个完全随意的数字掩盖了事实,即我们这样做真的非常简单。
当然,由于整个问题是混淆的,如果我们觉得这样,我们可以采取更多的步骤。 举个例子,我们可以利用短路评估来把我们的if
语句变成一个单一的expression式,所以main的主体看起来像这样:
x[1] && (x[0] /= 2, main(--x[1], (char **)++y)); putchar(*--y);
对于任何不习惯混淆代码(和/或代码高尔夫)的人来说,这开始看起来确实很奇怪 – 计算和丢弃逻辑and
一些没有意义的浮点数以及来自main
的返回值,这不是偶数返回一个值。 更糟糕的是,没有意识到(和思考)短路评估是如何工作的,它可能不会立即明白它如何避免无限recursion。
我们的下一步可能是将每个字符与打印字符分开。 我们可以很容易地通过生成正确的字符作为main
的返回值,并打印出main
回报:
x[1] && (x[0] /= 2, putchar(main(--x[1], (char **)++y))); return *--y;
至less对我来说,这似乎已经足够混乱了,所以我会留下来。
它只是build立一个双数组(16字节),如果解释为一个字符数组,则为string“C ++ Sucks”build立ASCII码
但是,代码在每个系统上都不起作用,它依赖于下面一些未定义的事实:
- 双正好有8个字节
- 字节序
下面的代码打印C++Suc;C
,所以整个乘法只针对最后两个字母
double m[] = {7709179928849219.0, 0}; printf("%s\n", (char *)m);
其他人已经相当彻底地解释了这个问题,我想补充说明这是根据标准的未定义的行为 。
C ++ 11 3.6.1 / 3 主要function
函数main不能在程序中使用。 main的连接(3.5)是实现定义的。 将main定义为已删除或将main声明为inline,static或constexpr的程序是不合格的。 名称main不保留。 [例如:成员函数,类和枚举可以称为main,其他名称空间中的实体也可以称为main。 – 例子]
代码可以像这样重写:
void f() { if (m[1]-- != 0) { m[0] *= 2; f(); } else { printf((char*)m); } }
它所做的是在double
数组m
中产生一组字节,恰好对应于字符'C ++ Sucks'后跟一个空终止符。 他们通过select一个double值来混淆代码,这个double值在标准表示中以数组的第二个成员提供的null结束符产生了771次。
请注意,这个代码不会在不同的endian表示下工作。 此外,调用main()
不是严格允许的。
这基本上只是一个巧妙的方法来隐藏string“C ++ Sucks”(注意8个字节)在第一个double值,recursion乘以2,直到秒double值达到零(771次)。
如果将double的字节值解释为string,那么printf()对cast进行处理,将double值7709179928849219.0 * 2 * 711相乘得到“C ++ Sucks”。 和printf()不会失败,因为第二个double值是“0”,并被printf()解释为“\ 0”。