これら4行のトリッキーなCコードの背後にある概念


384

なぜこのコードは出力を与えるのC++Sucksですか?その背後にあるコンセプトは何ですか?

#include <stdio.h>

double m[] = {7709179928849219.0, 771};

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

ここでテストしてください


1
@BoBTFish技術的にはそうですが、C99でも同じように実行されます:ideone.com/IZOkql
nijansen

12
@nurettin私は同様の考えを持っていました。しかし、それはOPのせいではなく、この役に立たない知識に投票する人々です。確かに、このコード難読化は興味深いかもしれませんが、Googleで「難読化」と入力すると、考えられるすべての形式言語で大量の結果が得られます。誤解しないでください。ここでそのような質問をしても大丈夫です。ただし、あまり有用な質問ではないため、過大評価されています。
TobiMcNamobi 2013

6
@ detonator123「ここで新しくなければならない」-閉鎖の理由を見ると、そうではないことがわかります。必要な最小限の理解はあなたの質問から明らかに欠落しています-「私はこれを理解していません、それを説明してください」はスタックオーバーフローで歓迎されるものではありません。最初に自分で何かを試みたとしたら、質問は閉じていませんでしたか?グーグルの「二重表現C」などはささいなことです。

42
ビッグエンディアンのPowerPCマシンが出力しskcuS++Cます。
Adam Rosenfield 2013

27
私の言葉、私はこのような人為的な質問が嫌いです。これは、メモリ内のビットパターンで、たまたま文字列と同じです。それは誰にとっても有用な目的を果たしませんが、質問者と回答者の両方に対して何百もの担当者ポイントを獲得します。一方、人々に役立つかもしれない難しい質問は、もしあれば、一握りのポイントを獲得します。これは、SOの問題点のポスターの子のようなものです。
キャリーグレゴリー

回答:


494

数値7709179928849219.0は、64ビットとして次のバイナリ表現を持っていますdouble

01000011 00111011 01100011 01110101 01010011 00101011 00101011 01000011
+^^^^^^^ ^^^^---- -------- -------- -------- -------- -------- --------

+標識の位置を示します。^指数と-仮数(つまり、指数なしの値)の。

表現は2進数の指数と仮数を使用するため、数値を2倍にすると、指数が1増加します。プログラムはそれを正確に771回実行するため、1075で始まった指数(10進表記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'

このパターンは、印刷された文字列に対応し、逆方向のみです。同時に、配列の2番目の要素がゼロになり、nullターミネーターが提供されるため、文字列はに渡すのに適していprintf()ます。


22
なぜ文字列が逆向きになっているのですか?
デレク

95
@Derekのx86はリトルエンディアンである
Angewはもはや誇りSOのある

16
@Derekこれは、プラットフォーム固有のエンディアンが原因です。抽象IEEE 754表現のバイトは、メモリの減少するアドレスに格納されるため、文字列は正しく印刷されます。エンディアンが大きいハードウェアでは、別の番号から始める必要があります。
dasblinkenlight 2013

14
@AlvinWong正解です。この規格では、IEEE 754またはその他の特定の形式は必要ありません。このプログラムは、移植性がないか、非常に近いものです:-)
dasblinkenlight

10
@GrijeshChauhan倍精度のIEEE754 計算機を使用しました7709179928849219。値を貼り付け、バイナリ表現を取得しました。
dasblinkenlight 2013

223

より読みやすいバージョン:

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]最後の2文字を「修復」するために2倍になります。最後の呼び出しでは、m[0]のASCII文字の表現が含まれていますC++Sucksし、m[1]それが持っているので、ゼロのみが含まれているヌル・ターミネータのための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);

8
後置減少です。したがって、771回呼び出されます。
Jack Aidley、2013

106

免責事項:この回答は、C ++のみに言及し、C ++ヘッダーを含む質問の元の形式に投稿されました。質問の純粋なCへの変換は、元の質問者からの入力なしで、コミュニティによって行われました。


正式には、このプログラムは形式が正しくない(つまり、正当なC ++ではない)ため、このプログラムについて推論することは不可能です。C ++ 11 [basic.start.main] p3に違反しています:

関数mainはプログラム内で使用しないでください。

これはさておき、一般的なコンシューマコンピュータでdoubleは、a は8バイトの長さであり、よく知られた特定の内部表現を使用するという事実に依存しています。配列の初期値は「アルゴリズム」が実行されたときに最初の最終値がdouble内部表現(8バイト)が8文字のASCIIコードになるような値になるように計算されますC++Sucks。配列の2番目の要素はであり0.0、その最初のバイトは0内部表現にあり、これを有効なCスタイルの文字列にします。次に、これを使用して出力に送信されますprintf()

上記のいくつかが成り立たないHWでこれを実行すると、代わりにガベージテキスト(またはおそらく範囲外のアクセス)が発生します。


25
これはC ++ 11の発明ではないことを付け加えてbasic.start.mainおきます-C ++ 03にも同じ表現の3.6.1 / 3がありました。
シャープトゥース2013

1
この小さな例のポイントは、C ++で何ができるかを説明することです。UBトリックまたは「クラシック」コードの巨大なソフトウェアパッケージを使用したマジックサンプル。
シェプリン2013

1
@sharptoothこれを追加してくれてありがとう。私はそれ以外のことをほのめかすつもりはありませんでした。私が使用した規格を引用しただけです。
Angewは、2013

@Angew:うん、理解しました、言い回しがかなり古いと言いたかっただけです。
シャープトゥース2013

1
@JimBalterの通知「正式に推論することは不可能」ではなく、「正式に言えば、推論することは不可能」と述べました。プログラムについて推論することは可能ですが、それを行うために使用されるコンパイラの詳細を知る必要があります。への呼び出しを単に削除することは、完全にコンパイラの権利の範囲内ですmain()、またはハードドライブ、または任意の書式を設定するためのAPI呼び出しに置き換えます。
Angewは、2013

57

おそらく、コードを理解する最も簡単な方法は、逆の処理を行うことです。印刷する文字列から始めます。バランスのために、「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);

難読化コード(および/またはコードゴルフ)に慣れていない誰にもこの開始は確かにかなり奇妙に見えるために-論理的なコンピューティングおよび廃棄andいくつかの無意味なの浮動小数点数とからの戻り値mainでもAが返されていません、値。さらに悪いことに、短絡評価がどのように機能するかを認識(および考え)なければ、無限再帰を回避する方法がすぐに明らかになることすらありません。

次のステップは、各文字の印刷とその文字の検出を分離することでしょう。からの戻り値として適切な文字を生成し、main何をmain返すかを出力することで、これをかなり簡単に行うことができます。

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

少なくとも私には、それは十分に難読化されているように見えるので、それはそのままにしておきます。


1
科学捜査のアプローチが大好きです。
ryyker 14

24

これは、double配列(16バイト)を構築するだけです。これは、char配列として解釈される場合、文字列 "C ++ Sucks"のASCIIコードを構築します。

ただし、コードは各システムで機能しておらず、以下の未定義の事実のいくつかに依存しています。


12

次のコードはを出力C++Suc;Cするので、乗算全体は最後の2文字のみです。

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

11

他はかなり完全に質問を説明しました、私はこれが標準に従って未定義の動作であるというメモを追加したいと思います。

C ++ 11 3.6.1 / 3 メイン関数

関数mainはプログラム内で使用しないでください。mainのリンケージ(3.5)は実装定義です。mainを削除済みとして定義するプログラム、またはmainをインライン、静的、またはconstexprとして宣言するプログラムは、形式が正しくありません。mainという名前は、他に予約されていません。[例:メンバー関数、クラス、および列挙は、他の名前空間のエンティティと同様に、メインと呼ぶことができます。—例を終了]


1
それは(私が私の回答で行ったように)不正な形式でさえあります-それは「挑戦」に違反しています。
Angewは、2013

9

コードは次のように書き直すことができます。

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

それがしていることは、double配列にバイトのセットを生成することですm「C ++ Sucks」という文字に対応し、その後にnullターミネータが続きます。彼らは、771回2倍したときに、配列の2番目のメンバーによって提供されるnullターミネーターを持つバイトのセットを標準表現で生成するdouble値を選択することにより、コードを難読化しました。

このコードは別のエンディアン表現では機能しないことに注意してください。また、呼び出しmain()は厳しく禁止されています。


3
なぜあなたはf戻ってくるのintですか?
leftaroundabout

1
えっと、私はint問題のリターンをコピーするのを無知だった。修正させてください。
Jack Aidley、2013

1

最初に、倍精度の数値は次のようにバイナリ形式でメモリに格納されることを思い出してください。

(i)符号用の1ビット

(ii)指数用の11ビット

(iii)マグニチュード用の52ビット

ビットの順序は(i)から(iii)に減少します。

最初に、10進数の小数が同等の小数の2進数に変換され、次に2進数の桁数の形式で表されます。

したがって、7709179928849219.0という数値は

(11011011000110111010101010011001010110010101101000011)base 2


=1.1011011000110111010101010011001010110010101101000011 * 2^52

マグニチュードビットを考慮しながら、1。は無視されます

したがって、等級部分は次のようになります。

1011011000110111010101010011001010110010101101000011 

これで2のべき乗は52になります。バイアス値を2 ^(指数-1のビット)-1として追加する必要があります。 つまり2 ^(11 -1)-1 = 1023なので、指数は52 + 1023 = 1075になります。

これで、コードに2771倍の数値が乗算され、指数が771増加します。

したがって、指数は(1075 + 771)= 1846で、2進数の等価物は(11100110110)です。

これで数値は正なので、符号ビットは0です。

したがって、変更後の番号は次のようになります。

符号ビット+指数+大きさ(ビットの単純な連結)

0111001101101011011000110111010101010011001010110010101101000011 

mはcharポインタに変換されるため、LSDからビットパターンを8のチャンクに分割します。

01110011 01101011 01100011 01110101 01010011 00101011 00101011 01000011 

(16進数の同等物は:)

 0x73 0x6B 0x63 0x75 0x53 0x2B 0x2B 0x43 

アスキーチャート 示されているように文字マップからどれが:

s   k   c   u      S      +   +   C 

これが行われると、m [1]は0になり、これはNULL文字を意味します

ここで、このプログラムをリトルエンディアンマシン(下位ビットは下位アドレスに格納されています)で実行すると仮定すると、ポインターmは最下位アドレスビットを指し、ビットを8のチャックに取り込みます(char *にキャストされる型として)。 )そして最後のチャンクで00000000に遭遇するとprintf()は停止します...

ただし、このコードは移植できません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.