この難読化されたCコードは、main()なしで実行されると主張していますが、実際には何をしているのでしょうか。


84
#include <stdio.h>
#define decode(s,t,u,m,p,e,d) m##s##u##t
#define begin decode(a,n,i,m,a,t,e)

int begin()
{
    printf("Ha HA see how it is?? ");
}

これは間接的に呼び出しmainますか?どうやって?


146
展開で定義されたマクロは「メイン」と言い始めます。それはただのトリックです。面白くも何とも。
rghome 2016

10
ツールチェーンには、前処理されたコードをファイル(コンパイルされる実際のファイル)に残すオプションが必要です。実際、そこにはmain()があります

@rghome答えとして投稿してみませんか?そして、賛成票の数を考えると、それは明らかに興味深いものです。
Matsemann 2016

3
@Matsemannうわー!私は賛成票に気づかなかった。私はそれを回答に変更することができ、コメントの賛成票が回答の賛成票だった場合、それは私の最高のスコアになりますが、すでに詳細な回答があります。私のコメントのポイントは、それはあまり面白くないので、答えに賛成票を投じたくない人々のための代替手段として機能するということだと思います。それを指摘してくれてありがとう。
rghome 2016

みんな、言語自体ではなく、エントリポイントを設定するオペレーティングシステムツールとしてのリンカー次第です。独自のエントリポイントを設定することもでき、実行可能なライブラリを作成することもできます。unix.stackexchange.com/a/223415/37799
Ho1

回答:


193

C言語は、独立型ホスト型の2つのカテゴリで実行環境を定義します。どちらの実行環境でも、プログラム起動のために環境から関数が呼び出されます。
自立環境プログラムの起動機能の実装でいる間に定義することができますホストされた環境を、それがあるべきmain。Cのプログラムは、定義された環境でプログラム起動機能なしで実行することはできません。

あなたの場合、mainはプリプロセッサ定義によって隠されています。begin()に展開され decode(a,n,i,m,a,t,e)、さらにに展開されmainます。

int begin() -> int decode(a,n,i,m,a,t,e)() -> int m##a##i##n() -> int main() 

decode(s,t,u,m,p,e,d)7つのパラメーターを持つパラメーター化されたマクロです。このマクロの置換リストはm##s##u##tです。m, s, u及びt4は番目、1番目、3番目及び2番目の置換リストで使用されるパラメータ。

s, t, u, m, p, e, d
1  2  3  4  5  6  7

残りは役に立たない(難読化するためだけに)。渡される引数decodeは「anim、a、t、e」であるため、識別子m, s, utはそれぞれ引数m, a, inに置き換えられます。

 m --> m  
 s --> a 
 u --> i 
 t --> n

11
@GrijeshChauhanすべてのCコンパイラがマクロを処理します。C89以降のすべてのC標準で必要です。
jdarthenay 2016

17
それは明らかに間違っています。Linuxでは使用できます_start()。または、さらに低レベルで、プログラムの開始を起動後にIPが設定されているアドレスに合わせるようにすることもできます。main()C標準ライブラリです。C自体はこれに制限を課しません。
ljrk 2016

1
@haccks標準ライブラリはエントリポイントを定義します。言語自体は気にしません
ljrk 2016

3
どうdecode(a,n,i,m,a,t,e)なるのm##a##i##nか説明してもらえますか?文字を置き換えますか?decode関数のドキュメントへのリンクを提供できますか?ありがとう。
AL

1
@AL Firstbeginは、decode(a,n,i,m,a,t,e)以前に定義されたものに置き換えられるように定義されています。この関数は引数s,t,u,m,p,e,dを受け取り、この形式でそれらを連結しますm##s##u##t##連結を意味します)。つまり、p、e、dの値を無視します。decodes = a、t = n、u = i、m = mで「呼び出す」と、事実上。に置き換えbeginられmainます。
ljrk 2016

71

を使用してみてくださいgcc -E source.c。出力は次で終わります。

int main()
{
    printf("Ha HA see how it is?? ");
}

したがって、main()関数は実際にはプリプロセッサによって生成されます。


37

問題のプログラムはありません通話をmain()伴うマクロ展開に、しかし、あなたの仮定は欠陥がある-それはしません呼び出す必要がありmain()、すべてに!

厳密に言えば、Cプログラムを作成して、mainシンボルなしでコンパイルすることができます。mainc library、独自の初期化が完了した後、ジャンプすることを期待しているものです。通常mainは、として知られているlibcシンボルからジャンプします_start。メインがなくても、アセンブリを実行するだけの非常に有効なプログラムを持つことは常に可能です。これを見てください:

/* This must be compiled with the flag -nostdlib because otherwise the
 * linker will complain about multiple definitions of the symbol _start
 * (one here and one in glibc) and a missing reference to symbol main
 * (that the libc expects to be linked against).
 */

void
_start ()
{
    /* calling the write system call, with the arguments in this order:
     * 1. the stdout file descriptor
     * 2. the buffer we want to print (Here it's just a string literal).
     * 3. the amount of bytes we want to write.
     */
    asm ("int $0x80"::"a"(4), "b"(1), "c"("Hello world!\n"), "d"(13));
    asm ("int $0x80"::"a"(1), "b"(0)); /* calling exit syscall, with the argument to be 0 */
}

上記をgcc -nostdlib without_main.cでコンパイルし、Hello World!インラインアセンブリでシステムコール(割り込み)を発行するだけで画面に出力されることを確認します。

この特定の問題の詳細については、kspliceブログをご覧ください。

もう1つの興味深い問題は、main記号をC関数に対応させずにコンパイルするプログラムを作成できることです。たとえば、次のものを非常に有効なCプログラムとして使用できます。これにより、警告レベルを上げたときにのみコンパイラが泣き言を言います。

/* These values are extracted from the decimal representation of the instructions
 * of a hello world program written in asm, that gdb provides.
 */
const int main[] = {
    -443987883, 440, 113408, -1922629632,
    4149, 899584, 84869120, 15544,
    266023168, 1818576901, 1461743468, 1684828783,
    -1017312735
};

配列の値は、HelloWorldを画面に出力するために必要な命令に対応するバイトです。この特定のプログラムがどのように機能するかについてのより詳細な説明については、このブログ投稿をご覧ください。ここでも最初に読んでいます。

これらのプログラムについて最後にお知らせしたいと思います。C言語の仕様に従って有効なCプログラムとして登録されているかどうかはわかりませんが、仕様自体に違反していても、コンパイルして実行することは確かに可能です。


1
_start定義された標準の一部の名前ですか、それとも実装固有ですか?確かに、「配列としてのメイン」はアーキテクチャ固有です。また重要なのは、セキュリティ制限のために実行時に「配列としてのメイン」トリックが失敗することは不合理ではないことです(ただし、const修飾子を使用しなかった場合はそれが発生する可能性が高く、多くのシステムで許可されます)。
mah

1
@mah:_startAMD64 psABIが参照含まれても、ELF標準にない_start3.4プロセスの初期化。公式には、ELFe_entryはELFヘッダーのアドレスについてのみ知っており_start、実装が選択した名前にすぎません。
ninjalj 2016

1
@mahまた重要なことですが、セキュリティ上の制限のために、実行時に「配列としてのメイン」トリックが失敗することは不合理ではありません(ただし、const修飾子を使用しなかった場合は、それが発生する可能性が高くなりますが、それでも多くのシステムで許可されます)それ)。 最終的な実行可能ファイルが何らかの方法で安全でないものとして区別できる場合にのみ、バイナリ実行可能ファイルは、そこに到達した方法に関係なく、バイナリ実行可能ファイルです。そしてconst、1ビットは関係ありません-そのバイナリ実行可能ファイルのシンボル名はmainです。それ以上でもそれ以下でもありません。 constは、実行時に何も意味しないC構造です。
アンドリューヘンレ2016

1
@Stewart:ARMv6lでは確かに失敗します(セグメンテーション違反)。ただし、x86-64アーキテクチャでは機能するはずです。
左回り2016

@AndrewHenleバイナリ実行可能ファイルは、どのようにしてそこに到達したかに関係なく、バイナリ実行可能ファイルです-正確には真実ではありません。バイナリ実行可能ファイルは、実行可能命令の単一のブロブではなく、慎重にマップされたパーティションのブロブです。その一部は命令であり、一部は読み取り専用データであり、一部は読み取り/書き込みデータに初期化されるデータです。(一部の)セキュリティハードウェアMMUは、そのようにマークされていないページからの実行を防ぐことができます。これは、たとえば、スタックオーバーフローがスタックでのコードの実行につながるのを防ぐための優れた機能ですが、残念ながら、それは正当な場合や有効になっていない場合があります。
mah

30

誰かが魔術師のように振る舞おうとしています。彼は私たちをだますことができると思っています。しかし、cプログラムの実行はmain()。で始まります。

int begin()置き換えられますdecode(a,n,i,m,a,t,e)プリプロセッサステージの1回のパスで。その後、再びdecode(a,n,i,m,a,t,e)m ## a ## i ## nに置き換えられます。マクロ呼び出しの位置関連付けと同様に、 swillの値は文字aです。同様に、u「i」tに置き換えられ、「n」に置き換えられます。そして、そういう風にm##s##u##tなりますmain

##マクロ展開の記号については、前処理演算子であり、トークンの貼り付けを行います。マクロが展開されると、各「##」演算子の両側にある2つのトークンが1つのトークンに結合され、マクロ展開の「##」と2つの元のトークンが置き換えられます。

私を信じていない場合は、-Eフラグを使用してコードをコンパイルできます。前処理後にコンパイルプロセスを停止し、トークンの貼り付けの結果を確認できます。

gcc -E FILENAME.c

11

decode(a,b,c,d,[...])最初の4つの引数をシャッフルし、それらを結合して、新しい識別子を順番に取得しdacbます。(残りの3つの引数は無視されます。)たとえばdecode(a,n,i,m,[...])、識別子を指定しmainます。これがbeginマクロの定義であることに注意してください。

したがって、beginマクロは単純にとして定義されmainます。


2

あなたの例では、main()関数が実際に存在します。beginこれは、コンパイラがdecodeマクロに置き換え、次に式m ## s ## u ## tに置き換えるマクロだからです。マクロ展開を使用##すると、mainから単語に到達しdecodeます。これはトレースです:

begin --> decode(a,n,i,m,a,t,e) --> m##parameter1##parameter3##parameter2 ---> main

持つのは単なるトリックですがmain()main()Cプログラミング言語では、プログラムの入力関数の名前を使用する必要はありません。それはあなたのオペレーティングシステムとそのツールの1つとしてのリンカーに依存します。

Windowsでは、常に使用するmain()わけではなくWinMainwWinMain、Microsoftのツールチェーンでも使用できますがmain()またはを使用します。Linuxでは、を使用できます_start

言語自体ではなく、エントリポイントを設定するのはオペレーティングシステムツールとしてのリンカー次第です。独自のエントリポイントを設定することもでき、実行可能なライブラリを作成することもできます


@vaxquisその通りですが、これは、main()関数をCプログラミング言語にバインドする最初の回答を補完/修正するために書いた部分的な回答です。これは正しくありません。
ho1 2016

@vaxquis「main()関数はCプログラムでは必須ではない」という説明は部分的な答えになると思いました。答えを完成させるために段落を追加しました。– Ho1 16分前
Ho1 2016
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.