这四行复杂的C代码背后的概念

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

#include <stdio.h>


double m[] = {7709179928849219.0, 771};


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

测试它在这里

38688 次浏览

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

然而,代码并不是在每个系统上都能工作,它依赖于以下一些未定义的事实:

免责声明:这个答案被发布到问题的原始形式中,该问题只提到了c++,并包含了一个c++头。问题转换为纯C是由社区完成的,没有来自最初提问者的输入。


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

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

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

在HW上运行这个,上面的一些不支持将导致垃圾文本(甚至可能是一个越界的访问)。

代码可以像这样重写:

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

它所做的是在double数组m中生成一组字节,恰好对应于字符' c++ Sucks'后跟一个空结束符。他们通过选择一个双精度值来混淆代码,当该双精度值乘以771倍时,在标准表示中会产生由数组的第二个成员提供的空结束符的字节集。

注意,这段代码不能在不同的端序表示下工作。另外,严格来说不允许调用main()

更易读的版本:

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]包含C++Sucks的ASCII字符表示,而m[1]只包含0,因此它有一个零终结者用于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);

数字7709179928849219.0具有以下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'

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

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

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

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

c++ 11 3.6.1/3 __abc0

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

也许理解代码最简单的方法是反向处理。我们将从一个字符串开始打印—为了平衡,我们将使用“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);
}

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

当然,因为重点在于混淆,如果我们喜欢,我们也可以采取更多步骤。例如,我们可以利用短路求值,将if语句转换为单个表达式,因此main的主体看起来像这样:

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

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

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

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

至少对我来说,这看起来很混乱,所以我就到此为止。

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

(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

现在我们的代码将这个数字与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 CHART 从字符映射显示的是:

s   k   c   u      S      +   +   C

现在,一旦这个已经完成,m[1]是0,这意味着一个NULL字符

现在假设你在低位优先机器上运行这个程序(低阶位存储在低位地址中),因此指针m指向最低位地址位,然后继续在8字节的卡盘中占用位(作为类型转换为char*),当在最后一个块中遇到00000000时printf()停止…

但是这段代码是不可移植的。