C言語コードの解明を楽しんでください。
課題:切り詰める前に、記事の見出しを頭の中で編集してください。出力では何が得られますか?エキスパートCプログラミングの本をもう一度見ていたとき、私は突然、最も複雑なCコード( IOCCC )の国際競争の光レリーフセクションに出会いました 。 これは、読み取り不可能なコードを可能な限り書くための競争です。 このようなコンテストがCのために開催されるという事実は、おそらくこの言語について何かを述べています。 このコンペティションの参加者の作品を見たかった。 インターネットで情報を見つけられなかったので、自分で検索することにしました。
IOCCCは、Cプリプロセッサを使用し、C言語でUnixシェルを作成することを決めたときにStephen Bornによって発明されました。ただし、ステートメントの明示的な末尾を含むAlgol-68に似ています。
if ... fi
彼はこれを次のようにして達成しました。
#define IF if( #define THEN ){ #define ELSE } else { #define FI ;}
彼が次のように書けるようになった理由:
IF *s2++ == 0 THEN return(0); FI
[出版物のサポート- エジソン 、 囚人向けの電子伝送サービスを開発し、 情報のバイラル配信を実装した企業。]
エキスパートCプログラミングは次のように述べています。
ベース言語を変更するCプリプロセッサの使用は避けてください。
1987年の最初の勝者の1人は、Kornシェルの作成者であるDavid Kornでした(これらのシェルライターの何が問題になっていますか?)。
main(){printf(&unix["\021%six\012\0"], (unix)["have"]+"fun"-0x60);}
以上です。 これをコンパイルしてみてください。 何が表示されますか?
このコードはMicrosoftでは実行されません(ヒント!)が、このタスクを処理するオンラインコンパイラへのリンクを次に示します。 動作させるためにそこに数行追加されましたが、残りは同じです。
コードは次のもののみを出力します。
unix
しかし、なぜですか? コードには
unix
と呼ばれる配列のように見えるものがありますが、宣言されていません。 では、
unix
はキーワードですか? それはどういうわけか変数名を表示しますか?
私はやみくもに以下を追加してこれを検証しようとしました:
printf(unix);
そして彼は、
printf
int
ではなく
char *
受け入れるというエラーをもたらしました。
この変数を
int
として推論すると、その値が1であることが明らかになりました。これにより、コードがUnixシステムでコンパイルされたかのようにオーバーライドされたと思いました。 gccソースコードを検索すると、 ランタイムターゲット仕様であることがわかりました。 これは、コードがWindowsで実行されない理由を説明しています。
unix
は1です。書き換えると、次のようになります。
main(){printf(&1["\021%six\012\0"], (1)["have"]+"fun"-0x60);}
そのため、
unix
変数名で
unix
ませんでした。 しかし、1 []はどのように機能しますか? これは以前に見たことがありますが、これはC言語に関する私のお気に入りの事実の1つです。
CはBCPL言語に由来します。 その作成者であるDr. Martin Richardsは次のように書いています。
間接呼び出し演算子! 引数としてポインタを取り、それが指すセルの内容を返します。 vがポインターの場合、!(V + i)はアドレスv + iのセルにアクセスします。 演算子のバイナリバージョン! v!i =!(v + i)となるように定義されています。 v!iはインデックス付き表現のように動作し、vは1次元配列、iは整数インデックスです。 BCPLでは、v5 =!(V + 5)=! (5 + v)= 5!V C言語でも同じことが起こります:v [5] = 5 [v]。
言い換えると、インデックスは単にポインタで加算され、加算は可換なので、インデックス演算子も可換です。 これも変更してみましょう。
int x[] = {1, 2, 3}; printf("%d\n%d\n", x[1], 1[x]);
それでは
1["\021%six\012\0"]
何ですか? 通常の形式で記述した後、インデックス演算子
"\021%six\012\0"[1]
介して配列の要素にアクセスすることがわかります。 とにかく非定型ではありませんが、
array[index]
であることは既に明らかです。ただし、原則として、文字列リテラルはこれを使用しません。 しかし、それは動作するので、次を試してください:
printf("%c\n", "hello, world"[1]);
これに対処しながら、最初の配列のみを書き換えましょう。
main() { char str[] = "\021%six\012\0"; printf(&str[1], (1)["have"]+"fun"-0x60); }
それでも同じように動作します。
str
を見て、
\0
について考えました
\0
これはヌル文字(またはNUL文字?)です。 Cの文字列リテラルにはデフォルトでヌル文字があると思った。 削除するとどうなるか見てみましょう。
printf("%s", "\021%six\012");
出力:
%six
出力しようとしている行にフォーマット文字
%
が含まれているため、
"%s"
フォーマットする文字列を使用します。 (
printf(myStr)
ヒント:フォーマット文字がある場合、
printf(myStr)
ような行を出力しないでください。
%s
を介した出力
%s
示されています。)
\0
なしでも動作するようです。 ANSI C以前では、文字列リテラルにヌル文字を自分で追加する必要があったのでしょうか? 私はそうは思わない、なぜならプログラムの他の行にはそれらがないからだ。 それとも、もっと複雑に見えますか? さて、
\0
この
\0
のままにしておきましょう。
この行で停止したので、残りを見てみましょう。
\xxx
は8進数の各文字の表現、
\021
は特定の制御文字、
\012
は出力行の最後にある改行文字または
\n
です。
\021
が1文字にすぎないことを知っていると、
str[1]
が
%
であることがわかり
str[1]
。 そして、
&str[1]
は
%
始まる行です。 そのため、文字列は実際には制御文字なしでちょうど
%six\n
になりますが、ここでなぜ必要なのかは明確ではありません。
main() { char str[] = "%six\n"; printf(str, (1)["have"]+"fun"-0x60); }
printf
渡される最初の行はフォーマット行です。
%s
は「代わりに次の行を置く」ことを意味します。 この行は
ix
で終わっているため、
printf
渡される次の行は何らかの形で
un
ように見えるはずです。 書式文字列を渡すために使用した文字の配列を取り除くだけで、次のようになります。
main() { printf("%six\n", (1)["have"]+"fun"-0x60); }
次の行には、
(1)["have"]+"fun"-0x60
ます。
fun
という単語に含まれる
un
があるので、それを解析しましょう。
繰り返しますが、次のインデックス作成のトリックがあります:
(1)["have"]
。 1前後の括弧は必要ありません。 繰り返しますが、これは古いCで必要でしたか、それとも読みにくくするために作成されたのですか?
"have"[1]
はです。 16進表現では、0x61のように見え、0x60を引きます。 その後、
1+"fun"
が残ります。
前と同じように、
"fun"
は
char *
表します。 1を追加すると、2番目の文字、つまり
un
始まる行が作成されます。 その後、すべてがこれに変わります:
main() { printf("%six\n", "un"); }
読み取り可能なコードは次のとおりです。
コードの混乱でセマンティクスが大きな役割を果たすとき、つまり、たとえば
unix
という特定の単語を使用して
unix
を混乱させ、オーバーライドされ、その名前が何らかの形で表示されると思わせるとき、それが好きです。
\021
文字は反転した
\012
文字に似ており、実際には使用されていませんが、必要だと思わせるかもしれません。 また、「six」という単語を含む
%six
書式設定文字列もあります。これは、書式設定ではなく、何か他のものに%sを使用するようです。
翻訳:アレナ・カルナウコワ