ChatGPT解决这个技术问题 Extra ChatGPT

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

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

#include <stdio.h>

double m[] = {7709179928849219.0, 771};

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

测试它here

@BoBTFish 从技术上讲,是的,但它在 C99 中的运行方式完全相同:ideone.com/IZOkql
@nurettin 我也有类似的想法。但这不是 OP 的错,而是人们投票支持这种无用的知识。承认,这个代码混淆的东西可能很有趣,但是在谷歌中输入“混淆”,你会得到大量你能想到的正式语言的结果。不要误会我的意思,我觉得在这里问这样的问题是可以的。这只是一个被高估的问题,因为它不是很有用的问题。
@detonator123 “你一定是新来的”——如果你看一下关闭的原因,你会发现事实并非如此。您的问题显然缺少所需的最低限度的理解 - “我不明白,解释一下”在 Stack Overflow 上不受欢迎。如果您自己先尝试过某些事情,那么问题是否还没有结束。谷歌“双重表示C”等是微不足道的。
我的大端 PowerPC 机器打印出 skcuS++C
我的话,我讨厌这样人为的问题。这是内存中的一个位模式,恰好与一些愚蠢的字符串相同。它对任何人都没有任何用处,但它为提问者和回答者都赢得了数百个代表点。同时,可能对人们有用的难题可能会获得一些分数,如果有的话。这是 SO 问题的典型代表。

g
gsamaras

数字 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()


为什么字符串是向后的?
@Derek x86 是小端
@Derek这是因为特定于平台的endianness:抽象IEEE 754表示的字节存储在内存中的递减地址,因此字符串打印正确。在具有大字节序的硬件上,需要以不同的数字开头。
@AlvinWong您是对的,该标准不需要IEEE 754或任何其他特定格式。这个程序几乎是不可移植的,或者非常接近它:-)
@GrijeshChauhan 我使用了 double-precision IEEE754 calculator:我粘贴了 7709179928849219 值,并取回了二进制表示。
A
Adam Stelmaszczyk

更易读的版本:

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,其中 standsC++Suc;C。在每次调用中,m[0] 都会翻倍,以“修复”最后两个字母。在最后一次调用中,m[0] 包含 C++Sucks 的 ASCII 字符表示,而 m[1] 仅包含零,因此它有一个 null terminator 代表 C++Sucks 字符串。所有假设 m[0] 存储在 8 个字节上,因此每个字符占用 1 个字节。

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

double m[] = {7709179928849219.0, 0};
for (int i = 0; i < 771; i++)
{
    m[0] *= 2;
}
printf((char*) m);

这是后缀减量。所以它会被调用771次。
A
Angew is no longer proud of SO

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

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

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

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

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


我必须补充一点,这不是 C++11 的发明——C++03 也有 basic.start.main 3.6.1/3 具有相同的措辞。
这个小例子的目的是说明 C++ 可以做什么。使用 UB 技巧或“经典”代码的巨大软件包的魔术示例。
@sharptooth 感谢您添加此内容。我没有其他意思,我只是引用了我使用的标准。
@Angew:是的,我明白,只是想说措辞很老。
@JimBalter 注意我说“正式地说,不可能推理”,不是“不可能正式推理。”您是对的,可以对程序进行推理,但您需要了解用于执行此操作的编译器的详细信息。 完全在编译器的权利范围内可以简单地消除对 main() 的调用,或者将其替换为 API 调用以格式化硬盘驱动器,或其他任何方式。
P
Peter Mortensen

也许理解代码的最简单方法是逆向处理。我们将从一个要打印的字符串开始——为了平衡,我们将使用“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 的返回值,这甚至没有返回值。更糟糕的是,在没有意识到(和思考)短路评估是如何工作的情况下,它如何避免无限递归甚至可能都不是很明显。

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

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

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


N
Nilay Vishwakarma

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

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

double 正好有 8 个字节

字节序


C
Cody Gray

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

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

Y
Yu Hao

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

C++11 3.6.1/3 主函数

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


我会说它甚至格式不正确(就像我在回答中所做的那样)-它违反了“应”。
J
Jack Aidley

代码可以这样重写:

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

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

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


A
Abhishek Ghosh

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

(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 

https://i.stack.imgur.com/Cx0Fg.jpg

s   k   c   u      S      +   +   C 

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

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

但是,此代码不可移植。