为什么这段代码给出了输出 C++Sucks ?它背后的概念是什么?
C++Sucks
#include <stdio.h> double m[] = {7709179928849219.0, 771}; int main() { m[1]--?m[0]*=2,main():printf((char*)m); }
测试here .
数字 7709179928849219.0 具有以下二进制表示形式为64位 double :
7709179928849219.0
double
01000011 00111011 01100011 01110101 01010011 00101011 00101011 01000011 +^^^^^^^ ^^^^---- -------- -------- -------- -------- -------- --------
+ 显示标志的位置;指数的 ^ 和尾数的 - (即没有指数的值) .
+
^
-
由于表示使用二进制指数和尾数,因此将指数加倍会使指数递增1 . 你的程序精确地完成了771次,所以从1075开始的指数( 10000110011 的十进制表示)最后变为1075 771 = 1846; 1846年的二进制表示是 11100110110 . 结果模式如下所示:
10000110011
11100110110
01110011 01101011 01100011 01110101 01010011 00101011 00101011 01000011 -------- -------- -------- -------- -------- -------- -------- -------- 0x73 's' 0x6B 'k' 0x63 'c' 0x75 'u' 0x53 'S' 0x2B '+' 0x2B '+' 0x43 'C'
此模式对应于您看到的打印字符串,仅向后 . 同时,数组的第二个元素变为零,提供null终止符,使字符串适合传递给 printf() .
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); } }
它以递归方式调用 main() 771次 .
main()
在开头, m[0] = 7709179928849219.0 ,其中stands为 C++Suc;C . 在每次通话中, m[0] 加倍,最后两个字母为"repair" . 在最后一次调用中, m[0] 包含 C++Sucks 的ASCII字符表示,而 m[1] 仅包含零,因此它具有null terminator用于 C++Sucks 字符串 . 所有这些都假设 m[0] 存储在8个字节上,因此每个char占用1个字节 .
m[0] = 7709179928849219.0
C++Suc;C
m[0]
m[1]
没有递归和非法 main() 调用它将如下所示:
double m[] = {7709179928849219.0, 0}; for (int i = 0; i < 771; i++) { m[0] *= 2; } printf((char*) m);
免责声明:此答案已发布到问题的原始形式,其中仅提到C并包含C Headers . 问题转换为纯C是由社区完成的,没有原始提问者的意见 .
从形式上讲,这个程序是不可能的,因为它是不正确的(即它不是合法的C) . 它违反了C 11 [basic.start.main] p3:
函数main不得在程序中使用 .
除此之外,它依赖于这样的事实:在典型的消费者计算机上, double 是8字节长,并且使用某种众所周知的内部表示 . 计算数组的初始值,以便在执行"algorithm"时,第一个 double 的最终值将使得内部表示(8个字节)将是8个字符 C++Sucks 的ASCII代码 . 然后,数组中的第二个元素是 0.0 ,其第一个字节在内部表示中为 0 ,使其成为有效的C样式字符串 . 然后使用 printf() 将其发送到输出 .
0.0
0
在硬件上运行此操作,其中上述某些操作不会导致垃圾文本(或者甚至是访问超出范围) .
也许理解代码的最简单方法是反过来处理事情 . 我们'll start with a string to print out -- for balance, we' ll使用"C++Rocks" . 关键点:就像原版一样,它(大致)会像原版一样,并以相反的顺序打印出来,我们只是将该位模式视为 double ,并打印出结果:
#include <stdio.h> char string[] = "skcoR++C"; int main(){ printf("%f\n", *(double*)string); }
这会产生 3823728713643449.5 . 因此,我们希望以某种方式操纵它,即半任意选择乘以256,这给了我们 978874550692723072 . 现在,我们只需要编写一些混淆代码来除以256,然后以相反的顺序打印掉它的各个字节:
3823728713643449.5
978874550692723072
#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); }
现在我们有很多转换,将参数传递给(递归) main ,这些参数完全被忽略(但是获得增量和减量的评估是完全至关重要的),当然还有完全随意的数字来掩盖我们所做的事实做的真的非常简单 .
main
当然,由于整点都是混淆,如果我们觉得这样,我们也可以采取更多步骤 . 例如,我们可以利用短路评估,将 if 语句转换为单个表达式,因此main的主体看起来像这样:
if
x[1] && (x[0] /= 2, main(--x[1], (char **)++y)); putchar(*--y);
对于那些不习惯混淆代码(和/或代码高尔夫)的人来说,这开始看起来确实很奇怪 - 计算和丢弃一些无意义的浮点数的逻辑 and 和来自 main 的返回值,这是不均匀的返回一个值 . 更糟糕的是,如果没有意识到(并思考)短路评估是如何工作的,那么它如何避免无限递归甚至可能不会立即显而易见 .
and
我们的下一步可能是将每个角色的打印与找到该角色分开 . 通过从 main 生成正确的字符作为返回值,并打印出 main 返回的内容,我们可以很容易地做到这一点:
x[1] && (x[0] /= 2, putchar(main(--x[1], (char **)++y))); return *--y;
至少在我看来,这似乎足够混淆了,所以我会留下它 .
它只是构建一个双数组(16个字节) - 如果解释为char数组 - 为字符串“C Sucks”构建ASCII代码
但是,代码不能在每个系统上运行,它依赖于以下某些未定义的代码事实:
double正好有8个字节
endianness
以下代码打印 C++Suc;C ,因此整个乘法仅适用于最后两个字母
double m[] = {7709179928849219.0, 0}; printf("%s\n", (char *)m);
其他人已经非常彻底地解释了这个问题,我想根据标准添加一个注释,这是 undefined behavior .
C 11 3.6.1 / 3主要功能
函数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 数组_590874中生成一组字节,恰好对应于字符'C++Sucks'后跟一个空终止符 . 他们通过选择一个double值来模糊代码,当加倍771次时,在标准表示中产生的字节集与数组的第二个成员提供的null终止符相同 .
请注意,此代码在不同的endian表示下不起作用 . 此外,不严格允许调用 main() .
8 回答
数字
7709179928849219.0
具有以下二进制表示形式为64位double
:+
显示标志的位置;指数的^
和尾数的-
(即没有指数的值) .由于表示使用二进制指数和尾数,因此将指数加倍会使指数递增1 . 你的程序精确地完成了771次,所以从1075开始的指数(
10000110011
的十进制表示)最后变为1075 771 = 1846; 1846年的二进制表示是11100110110
. 结果模式如下所示:此模式对应于您看到的打印字符串,仅向后 . 同时,数组的第二个元素变为零,提供null终止符,使字符串适合传递给
printf()
.更易阅读的版本:
它以递归方式调用
main()
771次 .在开头,
m[0] = 7709179928849219.0
,其中stands为C++Suc;C
. 在每次通话中,m[0]
加倍,最后两个字母为"repair" . 在最后一次调用中,m[0]
包含C++Sucks
的ASCII字符表示,而m[1]
仅包含零,因此它具有null terminator用于C++Sucks
字符串 . 所有这些都假设m[0]
存储在8个字节上,因此每个char占用1个字节 .没有递归和非法
main()
调用它将如下所示:免责声明:此答案已发布到问题的原始形式,其中仅提到C并包含C Headers . 问题转换为纯C是由社区完成的,没有原始提问者的意见 .
从形式上讲,这个程序是不可能的,因为它是不正确的(即它不是合法的C) . 它违反了C 11 [basic.start.main] p3:
除此之外,它依赖于这样的事实:在典型的消费者计算机上,
double
是8字节长,并且使用某种众所周知的内部表示 . 计算数组的初始值,以便在执行"algorithm"时,第一个double
的最终值将使得内部表示(8个字节)将是8个字符C++Sucks
的ASCII代码 . 然后,数组中的第二个元素是0.0
,其第一个字节在内部表示中为0
,使其成为有效的C样式字符串 . 然后使用printf()
将其发送到输出 .在硬件上运行此操作,其中上述某些操作不会导致垃圾文本(或者甚至是访问超出范围) .
也许理解代码的最简单方法是反过来处理事情 . 我们'll start with a string to print out -- for balance, we' ll使用"C++Rocks" . 关键点:就像原版一样,它(大致)会像原版一样,并以相反的顺序打印出来,我们只是将该位模式视为
double
,并打印出结果:这会产生
3823728713643449.5
. 因此,我们希望以某种方式操纵它,即半任意选择乘以256,这给了我们978874550692723072
. 现在,我们只需要编写一些混淆代码来除以256,然后以相反的顺序打印掉它的各个字节:现在我们有很多转换,将参数传递给(递归)
main
,这些参数完全被忽略(但是获得增量和减量的评估是完全至关重要的),当然还有完全随意的数字来掩盖我们所做的事实做的真的非常简单 .当然,由于整点都是混淆,如果我们觉得这样,我们也可以采取更多步骤 . 例如,我们可以利用短路评估,将
if
语句转换为单个表达式,因此main的主体看起来像这样:对于那些不习惯混淆代码(和/或代码高尔夫)的人来说,这开始看起来确实很奇怪 - 计算和丢弃一些无意义的浮点数的逻辑
and
和来自main
的返回值,这是不均匀的返回一个值 . 更糟糕的是,如果没有意识到(并思考)短路评估是如何工作的,那么它如何避免无限递归甚至可能不会立即显而易见 .我们的下一步可能是将每个角色的打印与找到该角色分开 . 通过从
main
生成正确的字符作为返回值,并打印出main
返回的内容,我们可以很容易地做到这一点:至少在我看来,这似乎足够混淆了,所以我会留下它 .
它只是构建一个双数组(16个字节) - 如果解释为char数组 - 为字符串“C Sucks”构建ASCII代码
但是,代码不能在每个系统上运行,它依赖于以下某些未定义的代码事实:
double正好有8个字节
endianness
以下代码打印
C++Suc;C
,因此整个乘法仅适用于最后两个字母其他人已经非常彻底地解释了这个问题,我想根据标准添加一个注释,这是 undefined behavior .
C 11 3.6.1 / 3主要功能
代码可以像这样重写:
它正在做的是在
double
数组_590874中生成一组字节,恰好对应于字符'C++Sucks'后跟一个空终止符 . 他们通过选择一个double值来模糊代码,当加倍771次时,在标准表示中产生的字节集与数组的第二个成员提供的null终止符相同 .请注意,此代码在不同的endian表示下不起作用 . 此外,不严格允许调用
main()
.