这四行棘手的C代码背后的概念


384

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

#include <stdio.h>

double m[] = {7709179928849219.0, 771};

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

在这里测试。


1
@BoBTFish从技术上讲是可以的,但是它在C99中的运行方式相同:ideone.com/IZOkql
nijansen 2013年

12
@nurettin我也有类似的想法。但这不是OP的错,而是人们投票支持这种无用的知识。诚然,这种代码混淆的东西可能很有趣,但是在Google中键入“混淆”会以您能想到的每种形式的语言获得大量的结果。不要误会我的意思,我可以在这里提出这样的问题。这只是被高估了,因为它不是一个非常有用的问题。
TobiMcNamobi 2013年

6
@ detonator123“您必须是这里的新手”-如果您查看关闭原因,则可以发现情况并非如此。您的问题显然缺少所需的最低限度的理解-“我不明白,请解释”在Stack Overflow上不受欢迎。如果您自己先尝试过某事,那么问题是否尚未解决。谷歌“双重代表C”之类的东西是微不足道的。

42
我的大端PowerPC机器打印出来了skcuS++C
亚当·罗森菲尔德

27
我的话,我讨厌这样的人为问题。这是内存中的一种位模式,恰好与某些愚蠢的字符串相同。它对任何人都没有任何用处,但它为发问者和答题者赢得了数百分。同时,可能对人们有用的棘手问题可能获得了很少的积分。这有点像SO的问题。
Carey Gregory

Answers:


494

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

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

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

由于该表示形式使用二进制指数和尾数,因此将数字加倍会使指数增加一。您的程序精确地执行了771次,因此从10​​75开始的指数(的十进制表示形式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()


22
为什么字符串向后退?
Derek

95
@Derek x86是低端的
Angew不再为

16
@Derek这是因为特定于平台的字节序:抽象IEEE 754表示形式的字节存储在内存中递减的地址中,因此字符串可以正确打印。在字节序大的硬件上,需要以不同的数字开头。
dasblinkenlight 2013年

14
@AlvinWong您是正确的,该标准不需要IEEE 754或任何其他特定格式。该程序几乎是不可移植的,或者非常接近它:-)
dasblinkenlight 2013年

10
@GrijeshChauhan我使用了双精度IEEE754计算器:我粘贴了该7709179928849219值,并取回了二进制表示形式。
dasblinkenlight

223

更具可读性的版本:

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);

8
后缀递减。因此它将被称为771次。
杰克·艾德利

106

免责声明:此答案已发布到问题的原始形式,该问题仅提及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()

在上面不满足某些条件的硬件上运行此命令将导致垃圾文本(甚至可能超出范围)。


25
我必须补充一点,这不是C ++ 11的发明-C ++ 03也有basic.start.main3.6.1 / 3的相同措辞。
Sharptooth

1
这个小例子的目的是说明C ++可以完成的工作。使用UB技巧或“经典”代码的大型软件包的魔术样本。
SChepurin

1
@sharptooth感谢您添加此内容。我并不是要暗示其他意思,我只是引用了我使用的标准。
Angew不再为2012年

@Angew:是的,我了解这一点,只是想说措辞很老。
Sharptooth

1
@JimBalter注意,我说“正式而言,这是不可能推理的”,而不是 “正式推理是不可能的”。没错,可以对程序进行推理,但是您需要了解用于执行该操作的编译器的详细信息。这将是完全内的编译器的权利,以消除简单地调用main(),或用API调用替换它格式化硬盘,或什么的。
Angew不再为SO

57

理解代码的最简单方法也许就是逆向工作。我们将从字符串开始打印-为了平衡起见,我们将使用“ C ++ Rocks”。关键点:与原著一样,长度恰好是八个字符。由于我们将(大致)像原始文件一样进行处理,并以相反的顺序打印出来,因此我们将其以相反的顺序开始。对于我们的第一步,我们将仅将该位模式视为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一些无意义的浮点数和的返回值的逻辑,main甚至不返回a值。更糟糕的是,如果没有意识到(和思考)短路评估是如何工作的,那么如何避免无限递归甚至可能不是很明显。

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

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

至少对我来说,这似乎已经很模糊了,所以我将其保留。


1
喜欢取证方法。
ryyker 2014年

24

它只是建立一个双精度数组(16字节),如果将其解释为char数组,则会为字符串“ C ++ Sucks”建立ASCII码。

但是,该代码无法在每个系统上运行,它依赖于以下一些未定义的事实:


12

打印以下代码C++Suc;C,因此整个乘法仅适用于最后两个字母

double m[] = {7709179928849219.0, 0};
printf("%s\n", (char *)m);

11

其他人已经相当详尽地解释了这个问题,我想补充一点,根据标准,这是未定义的行为

C ++ 11 3.6.1 / 3 主要功能

函数main不得在程序内使用。main的链接(3.5)是实现定义的。将main定义为delete或将main声明为嵌入式,静态或constexpr的程序格式错误。名称main否则不保留。[示例:成员函数,类和枚举可以称为main,其他命名空间中的实体也可以称为main。—结束示例]


1
我会说它甚至是格式错误的(就像我在回答中所做的那样)-它违反了“必须”。
Angew不再为SO

9

该代码可以这样重写:

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

它的作用是在double数组m中生成一组字节,这些字节恰好与字符“ C ++ Sucks”相对应,后跟一个空终止符。他们通过选择一个double值来使代码模糊不清,该值在标准表示中以771的两倍被生成时,会产生该字节集,并带有数组第二个成员提供的空终止符。

请注意,此代码在不同的字节序表示形式下将无法工作。另外,main()严格禁止拨打电话。


3
你为什么还f回来int
左右左转

1
嗯,因为我无精打采地复制int了问题的答案。我来解决这个问题。
杰克·艾德利

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的位)-1的偏置数, 即2 ^(11 -1)-1 = 1023,所以我们的指数变成52 + 1023 = 1075

现在我们的代码mutiplies的编号为2771倍,这使得指数由增加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 * ),并且在最后一个块中遇到000000000时,printf()停止...

但是,此代码不可移植。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.