web-dev-qa-db-ja.com

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

このコードはなぜC++Sucksを出力するのですか?その背後にある概念は何ですか?

#include <stdio.h>

double m[] = {7709179928849219.0, 771};

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

それをテストしてください ここ

377
codeslayer1

数値7709179928849219.0には、64ビットのdoubleとして次のバイナリ表現があります。

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

+は記号の位置を示します。指数の^、および仮数の-(つまり、指数のない値)。

表現では2進指数と仮数が使用されるため、数値を2倍にすると指数が1増加します。プログラムはそれを正確に771回実行するため、1075(10000110011の10進表現)で始まる指数は最後に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番目の要素がゼロになり、ヌルターミネータが提供され、printf()に渡すのに適した文字列になります。

490
dasblinkenlight

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

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]にはC++SucksのASCII char表現が含まれ、m[1]にはゼロのみが含まれるため、C++Sucksには nullターミネーター が含まれます文字列。 m[0]は8バイトで保存されるため、各文字は1バイトを使用します。

再帰と不正なmain()呼び出しを行わないと、次のようになります。

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

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


正式に言えば、このプログラムは不正な形式であるため(つまり合法的なC++ではないため)推論することはできません。 C++ 11 [basic.start.main] p3に違反しています。

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

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

上記のいくつかが保持されていないHWでこれを実行すると、代わりにガベージテキスト(または場合によっては境界外のアクセス)が発生します。

104
Angew

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

難読化されたコード(および/またはコードゴルフ)に慣れていない人には、これはかなり奇妙に見え始めます-意味のない浮動小数点数の論理andmainからの戻り値を計算して破棄します値を返します。さらに悪いことに、短絡評価がどのように機能するかを認識(および考慮)せずに、無限再帰を回避する方法がすぐには明らかでさえないかもしれません。

次のステップは、おそらく各文字の印刷とその文字の検索を分離することです。 mainからの戻り値として正しい文字を生成し、mainが返すものを出力することで、これを非常に簡単に行うことができます。

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

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

56
Jerry Coffin

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

ただし、コードは各システムで動作せず、次の未定義の事実に依存しています。

23
D.R.

次のコードは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として宣言するプログラムの形式が正しくありません。それ以外の場合、メイン名は予約されていません。 [例:メンバー関数、クラス、および列挙は、他の名前空間のエンティティと同様に、メインと呼ぶことができます。 —例の終了]

10
Yu Hao

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

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

実行しているのは、double配列mに一連のバイトを生成することです。これは、文字 'C++ Sucks'に続いてヌルターミネータが対応するものです。彼らは、標準表現では、配列の2番目のメンバーが提供するヌルターミネータを使用して、標準の表現でそのバイトセットを生成するdouble値を選択することにより、コードを難読化しました。

このコードは、異なるエンディアン表現では機能しないことに注意してください。また、main()の呼び出しは厳密に許可されていません。

9
Jack Aidley

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

(i)符号用の1ビット

(ii)指数用の11ビット

(iii)大きさは52ビット

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

最初に10進数の小数が同等の小数の2進数に変換され、次に2進数の大きさの形式として表されます。

したがって、数7709179928849219.

(11011011000110111010101010011001010110010101101000011)base 2


=1.1011011000110111010101010011001010110010101101000011 * 2^52

マグニチュードビットを考慮すると、1。は無視されます。これは、マグニチュードメソッドのすべての順序が1。で始まるためです。

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

1011011000110111010101010011001010110010101101000011 

2の力は52であり、2 ^(bits for exponent -1)-1すなわち2 ^(11 -1)-1 = 102なので、指数は52 + 1023 = 1075になります

これで、コードは2771で数値を複数回変更し、指数を771だけ増加させます

したがって、指数は(1075 + 771 1846)==であり、そのバイナリーの同等物は(11100110110)

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

したがって、修正された数は次のようになります。

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

0111001101101011011000110111010101010011001010110010101101000011 

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

01110011 01101011 01100011 01110101 01010011 00101011 00101011 01000011 

(その16進数は:)

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

ASCII CHART 示されている文字マップからは次のとおりです。

s   k   c   u      S      +   +   C 

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

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

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

1
Abhishek Ghosh