【问题标题】:Concept behind these four lines of tricky C code这四行棘手的 C 代码背后的概念
【发布时间】:2026-01-29 06:05:01
【问题描述】:

为什么这段代码会输出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 问题的典型代表。

标签: c deobfuscation


【解决方案1】:

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

但是,代码并非在每个系统上都有效,它依赖于以下一些未定义的事实:

【讨论】:

    【解决方案2】:

    免责声明:此答案已发布到问题的原始形式,其中仅提及 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.main3.6.1/3 的相同措辞。
    • 这个小例子的目的是说明用 C++ 可以做什么。使用 UB 技巧或“经典”代码的巨大软件包的魔术示例。
    • @sharptooth 感谢您添加此内容。我没有其他意思,我只是引用了我使用的标准。
    • @Angew:是的,我明白了,只是想说措辞已经很老了。
    • @JimBalter 注意我说“正式地说,不可能推理”,不是“不可能正式推理。”您是对的,可以对程序进行推理,但您需要了解用于执行此操作的编译器的详细信息。 完全在编译器的权利范围内可以简单地消除对 main() 的调用,或者将其替换为 API 调用以格式化硬盘驱动器,或其他任何方式。
    【解决方案3】:

    代码可以这样重写:

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

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

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

    【讨论】:

      【解决方案4】:

      更易读的版本:

      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 次。
      【解决方案5】:

      数字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 值,并得到了二进制表示。
      【解决方案6】:

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

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

      【讨论】:

        【解决方案7】:

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

        C++11 3.6.1/3 主函数

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

        【讨论】:

        • 我会说它甚至是不正确的(就像我在回答中所做的那样)-它违反了“应”。
        【解决方案8】:

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

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

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

        至少对我来说,这似乎已经够模糊了,所以我就这样吧。

        【讨论】:

          【解决方案9】:

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

          (i) 1 位符号

          (ii) 11 位的指数

          (iii) 幅度为 52 位

          位的顺序从 (i) 到 (iii) 递减。

          首先将十进制小数转换为等效的小数二进制数,然后以二进制的数量级形式表示。

          所以数字7709179928849219.0变成了

          (11011011000110111010101010011001010110010101101000011)base 2
          
          
          =1.1011011000110111010101010011001010110010101101000011 * 2^52
          

          现在考虑幅度位 1. 被忽略,因为所有数量级方法都应从 1.

          所以幅度部分变为:

          1011011000110111010101010011001010110010101101000011 
          

          现在 2 的幂是 52 ,我们需要给它加上偏置数 2^(bits for exponent -1)-1强> 即 2^(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 
          

          如图所示的字符映射是:

          s   k   c   u      S      +   +   C 
          

          现在一旦 m[1] 变成 0,这意味着一个 NULL 字符

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

          此代码不可移植。

          【讨论】: