为什么此代码给出输出C++Sucks
?它背后的概念是什么?
#include <stdio.h>
double m[] = {7709179928849219.0, 771};
int main() {
m[1]--?m[0]*=2,main():printf((char*)m);
}
在这里测试。
skcuS++C
。
为什么此代码给出输出C++Sucks
?它背后的概念是什么?
#include <stdio.h>
double m[] = {7709179928849219.0, 771};
int main() {
m[1]--?m[0]*=2,main():printf((char*)m);
}
在这里测试。
skcuS++C
。
Answers:
该数字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()
。
7709179928849219
值,并取回了二进制表示形式。
更具可读性的版本:
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);
免责声明:此答案已发布到问题的原始形式,该问题仅提及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()
。
在上面不满足某些条件的硬件上运行此命令将导致垃圾文本(甚至可能超出范围)。
basic.start.main
3.6.1 / 3的相同措辞。
main()
,或用API调用替换它格式化硬盘,或什么的。
理解代码的最简单方法也许就是逆向工作。我们将从字符串开始打印-为了平衡起见,我们将使用“ 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;
至少对我来说,这似乎已经很模糊了,所以我将其保留。
其他人已经相当详尽地解释了这个问题,我想补充一点,根据标准,这是未定义的行为。
C ++ 11 3.6.1 / 3 主要功能
函数main不得在程序内使用。main的链接(3.5)是实现定义的。将main定义为delete或将main声明为嵌入式,静态或constexpr的程序格式错误。名称main否则不保留。[示例:成员函数,类和枚举可以称为main,其他命名空间中的实体也可以称为main。—结束示例]
该代码可以这样重写:
void f()
{
if (m[1]-- != 0)
{
m[0] *= 2;
f();
} else {
printf((char*)m);
}
}
它的作用是在double
数组m
中生成一组字节,这些字节恰好与字符“ C ++ Sucks”相对应,后跟一个空终止符。他们通过选择一个double值来使代码模糊不清,该值在标准表示中以771的两倍被生成时,会产生该字节集,并带有数组第二个成员提供的空终止符。
请注意,此代码在不同的字节序表示形式下将无法工作。另外,main()
严格禁止拨打电话。
f
回来int
?
int
了问题的答案。我来解决这个问题。
首先我们应该记得,双精度数字以二进制格式存储在内存中,如下所示:
(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的编号为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
从字符图中显示的是:
s k c u S + + C
现在,一旦完成,m [1]为0,这意味着一个NULL字符
现在假设您在小端机上运行此程序(低位存储在低位地址),因此指针m指向最低地址位,然后通过占用8的卡盘中的位继续进行操作(按类型转换为char * ),并且在最后一个块中遇到000000000时,printf()停止...
但是,此代码不可移植。