391

为什么这段代码会给出输出C++Sucks?它背后的概念是什么?

#include <stdio.h>

double m[] = {7709179928849219.0, 771};

int main() {
    m[1]--?m[0]*=2,main():printf((char*)m);    
}

测试一下在这里一下。

4

9 回答 9

500

该数字7709179928849219.0具有以下 64 位二进制表示double

01000011 00111011 01100011 01110101 01010011 00101011 00101011 01000011
+^^^^^^^ ^^^^---- -------- -------- -------- -------- -------- --------

+显示标志的位置;^指数和-尾数(即没有指数的值)。

由于该表示使用二进制指数和尾数,因此将数字加倍会使指数加一。您的程序精确地执行了 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'

此模式对应于您看到打印的字符串,只是向后。同时,数组的第二个元素变为零,提供空终止符,使字符串适合传递给printf().

于 2013-08-01T11:33:33.520 回答
226

更易读的版本:

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 次。

一开始m[0] = 7709179928849219.0代表C++Suc;C在每次通话中,m[0]都会加倍,以“修复”最后两个字母。在最后一次调用中,m[0]包含 ASCII 字符表示C++Sucks并且m[1]仅包含零,因此它具有字符串的空终止符。C++Sucks所有假设m[0]存储在 8 个字节上,因此每个 char 占用 1 个字节。

如果没有递归和非法main()调用,它将如下所示:

double m[] = {7709179928849219.0, 0};
for (int i = 0; i < 771; i++)
{
    m[0] *= 2;
}
printf((char*) m);
于 2013-08-01T11:27:16.243 回答
107

免责声明:此答案已发布到问题的原始形式,其中仅提及 C++ 并包含 C++ 标头。问题到纯 C 的转换是由社区完成的,没有原始提问者的输入。


从形式上讲,不可能对这个程序进行推理,因为它的格式不正确(即它不是合法的 C++)。它违反了 C++11[basic.start.main]p3:

函数 main 不得在程序中使用。

除此之外,它依赖于这样一个事实,即在典型的消费类计算机上,a 的double长度为 8 个字节,并使用某种众所周知的内部表示。计算数组的初始值,以便在执行“算法”时,第一个的最终值double将使得内部表示(8 个字节)将是 8 个字符的 ASCII 代码C++Sucks。然后数组中的第二个元素是0.0,其第一个字节0在内部表示中,使其成为有效的 C 样式字符串。然后使用 将其发送到输出printf()

在上述某些内容不成立的硬件上运行此程序会导致垃圾文本(甚至可能是越界访问)。

于 2013-08-01T11:26:02.690 回答
57

也许理解代码的最简单方法是逆向处理。我们将从一个要打印的字符串开始——为了平衡,我们将使用“C++Rocks”。关键点:就像原版一样,它正好是八个字符长。由于我们将(大致)像原件一样做,并以相反的顺序打印出来,我们将从以相反的顺序开始。对于我们的第一步,我们将只将该位模式视为 a 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);
}

现在我们有很多强制转换,将参数传递给(递归)main,这些参数被完全忽略(但是获得增量和减量的评估是非常关键的),当然,这个完全任意的数字来掩盖我们正在做的事实真的很简单。

当然,由于整点是混淆,如果我们愿意,我们也可以采取更多步骤。举个例子,我们可以利用短路求值,把我们的if语句变成一个表达式,所以 main 的主体看起来像这样:

x[1] && (x[0] /= 2,  main(--x[1], (char **)++y));
putchar(*--y);

对于任何不习惯混淆代码(和/或代码高尔夫)的人来说,这确实看起来很奇怪——计算并丢弃and一些无意义的浮点数的逻辑和 from 的返回值main,甚至没有返回价值。更糟糕的是,如果没有意识到(和思考)短路评估是如何工作的,它如何避免无限递归甚至可能都不是很明显。

我们的下一步可能是将打印每个字符与查找该字符分开。我们可以很容易地做到这一点,方法是生成正确的字符作为 的返回值main,并打印出main返回的内容:

x[1] && (x[0] /= 2,  putchar(main(--x[1], (char **)++y)));
return *--y;

至少对我来说,这似乎已经足够模糊了,所以我将把它留在那里。

于 2013-08-03T03:33:15.377 回答
24

它只是建立一个双数组(16 个字节),如果解释为一个字符数组,它会为字符串“C++Sucks”建立 ASCII 代码

但是,该代码并非适用于每个系统,它依赖于以下一些未定义的事实:

于 2013-08-01T11:22:29.530 回答
13

下面的代码打印出来C++Suc;C,所以整个乘法只针对最后两个字母

double m[] = {7709179928849219.0, 0};
printf("%s\n", (char *)m);
于 2013-08-01T11:37:53.890 回答
11

其他人已经非常彻底地解释了这个问题,我想补充一点,根据标准,这是未定义的行为。

C++11 3.6.1/3主函数

函数 main 不得在程序中使用。main 的链接(3.5)是实现定义的。将 main 定义为已删除或将 main 声明为 inline、static 或 constexpr 的程序是格式错误的。名称 main 没有保留。[ 示例:成员函数、类和枚举可以称为 main,其他命名空间中的实体也可以。—结束示例]

于 2013-08-01T14:06:25.197 回答
9

代码可以这样重写:

void f()
{
    if (m[1]-- != 0)
    {
        m[0] *= 2;
        f();
    } else {
          printf((char*)m);
    }
}

double它所做的是在数组中生成一组字节,这些字节m恰好对应于字符“C++Sucks”,后跟一个空终止符。他们通过选择一个双精度值来混淆代码,当双精度值加倍 771 次时,会在标准表示中产生带有数组第二个成员提供的空终止符的字节集。

请注意,此代码在不同的字节序表示下不起作用。main()此外,不允许调用。

于 2013-08-01T11:26:30.933 回答
1

首先我们应该记得双精度数以二进制格式存储在内存中,如下所示:

(i) 1 位符号

(ii) 11 位用于指数

(iii) 幅度为 52 位

位的顺序从 (i) 到 (iii) 递减。

首先将十进制小数转换为等效的小数二进制数,然后将其表示为二进制的数量级形式。

所以数字7709179928849219.0变为

(11011011000110111010101010011001010110010101101000011)base 2


=1.1011011000110111010101010011001010110010101101000011 * 2^52

现在考虑幅度位1.被忽略,因为所有数量级方法都应从1 开始。

所以幅度部分变为:

1011011000110111010101010011001010110010101101000011 

现在 2 的幂52 我们需要将偏置数添加为2^(指数 -1 的位)-12^(11 -1)-1 =1023,所以我们的指数变为52 + 1023 = 1075

现在我们的代码将数字乘以2 , 771倍,这使得指数增加了771

所以我们的指数是(1075+771)= 1846其二进制等价物是(11100110110)

现在我们的数字是正数,所以我们的符号位是0

所以我们修改后的数字变为:

符号位 + 指数 + 幅度(位的简单串联)

0111001101101011011000110111010101010011001010110010101101000011 

由于 m 被转换为 char 指针,我们将从 LSD 中拆分为 8 块的位模式

01110011 01101011 01100011 01110101 01010011 00101011 00101011 01000011 

(其十六进制等效项是:)

 0x73 0x6B 0x63 0x75 0x53 0x2B 0x2B 0x43 

ASCII 图表 如图所示的字符图中是:

s   k   c   u      S      +   +   C 

现在一旦这被做了 m[1] 是 0 这意味着一个 NULL 字符

现在假设你在一个小端机器上运行这个程序(低位存储在低地址中),所以指针 m 指向最低地址位,然后继续占用 8 个卡盘中的位(作为类型转换为 char* ) 并且 printf() 在最后一个块中遇到 00000000 时停止...

但是,此代码不可移植。

于 2019-03-17T18:22:40.863 回答