为什么这段代码会给出输出C++Sucks
?它背后的概念是什么?
#include <stdio.h>
double m[] = {7709179928849219.0, 771};
int main() {
m[1]--?m[0]*=2,main():printf((char*)m);
}
测试一下在这里一下。
为什么这段代码会给出输出C++Sucks
?它背后的概念是什么?
#include <stdio.h>
double m[] = {7709179928849219.0, 771};
int main() {
m[1]--?m[0]*=2,main():printf((char*)m);
}
测试一下在这里一下。
该数字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()
.
更易读的版本:
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()
。
在上述某些内容不成立的硬件上运行此程序会导致垃圾文本(甚至可能是越界访问)。
也许理解代码的最简单方法是逆向处理。我们将从一个要打印的字符串开始——为了平衡,我们将使用“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;
至少对我来说,这似乎已经足够模糊了,所以我将把它留在那里。
它只是建立一个双数组(16 个字节),如果解释为一个字符数组,它会为字符串“C++Sucks”建立 ASCII 代码
但是,该代码并非适用于每个系统,它依赖于以下一些未定义的事实:
下面的代码打印出来C++Suc;C
,所以整个乘法只针对最后两个字母
double m[] = {7709179928849219.0, 0};
printf("%s\n", (char *)m);
其他人已经非常彻底地解释了这个问题,我想补充一点,根据标准,这是未定义的行为。
C++11 3.6.1/3主函数
函数 main 不得在程序中使用。main 的链接(3.5)是实现定义的。将 main 定义为已删除或将 main 声明为 inline、static 或 constexpr 的程序是格式错误的。名称 main 没有保留。[ 示例:成员函数、类和枚举可以称为 main,其他命名空间中的实体也可以。—结束示例]
代码可以这样重写:
void f()
{
if (m[1]-- != 0)
{
m[0] *= 2;
f();
} else {
printf((char*)m);
}
}
double
它所做的是在数组中生成一组字节,这些字节m
恰好对应于字符“C++Sucks”,后跟一个空终止符。他们通过选择一个双精度值来混淆代码,当双精度值加倍 771 次时,会在标准表示中产生带有数组第二个成员提供的空终止符的字节集。
请注意,此代码在不同的字节序表示下不起作用。main()
此外,不允许调用。
首先我们应该记得双精度数以二进制格式存储在内存中,如下所示:
(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
现在我们的代码将数字乘以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* ) 并且 printf() 在最后一个块中遇到 00000000 时停止...
但是,此代码不可移植。