2


0

C ++プログラムのコンパイルの理解に助けが必要

C プログラムのコンパイルとリンクを正しく理解していません。 方法はありますか、C プログラムを(理解可能な形式で)コンパイルして生成されたオブジェクトファイルを見ることができます。 これは、オブジェクトファイルの形式、C ++クラスのコンパイル方法、コンパイラがオブジェクトファイルを生成するために必要な情報、および次のようなステートメントを理解するのに役立ちます。

クラスが入力パラメーターと戻り値の型としてのみ使用される場合、クラスヘッダーファイル全体を含める必要はありません。 前方宣言で十分ですが、派生クラスが基本クラスから派生する場合、基本クラスの定義を含むファイルを含める必要があります(「Exceptional C ++」から取得)。

オブジェクトファイルの形式を理解するために「リンクと読み込み」という本を読んでいますが、C ++ソースコード用に特別に調整されたものを好むでしょう。

ありがとう、

ジャグラティ

編集する

nmを使用すると、オブジェクトファイルに存在するシンボルを見ることができますが、オブジェクトファイルについて詳しく知りたいと思っています。

5 Answer


1


まず最初に。 コンパイラの出力を分解しても、おそらく、あなたが抱えている問題を理解する助けにはなりません。 コンパイラの出力は、もはやc ++プログラムではなく、単純なアセンブリであり、メモリモデルが何であるかわからない場合は、読むのが非常に困難です。

「派生」の基本クラスであると宣言するときに「ベース」の定義が必要な理由の特定の問題については、いくつかの異なる理由があります(おそらく、私は忘れています)。

  1. タイプ「派生」のオブジェクトが作成されると、コンパイラは 完全なインスタンスとすべてのサブクラスのメモリ:「ベース」のサイズを知っている必要があります

  2. メンバー属性にアクセスするとき、コンパイラはオフセットを知っている必要があります 暗黙的な「this」ポインタから、そしてそのオフセットは「base」サブオブジェクトがとるサイズの知識を必要とします。

  3. 識別子が「派生」のコンテキストで解析され、

    識別子が「派生」クラスに見つからない場合、コンパイラはそれが「ベース」で定義されているかどうかを確認してから、囲んでいる名前空間で識別子を探します。 コンパイラは、 foo();が base`クラスで宣言されている場合、 `foo();`が `derived

    function()`内で有効な呼び出しであるかどうかを知ることができません。

  4. `base`で定義されたすべての仮想関数の数とシグネチャ

    コンパイラーが `derived`クラスを定義するときに知っている必要があります。 動的ディスパッチメカニズム(通常はvtable)を構築し、「派生」のメンバー関数が動的ディスパッチにバインドされているかどうかを知るためにも、その情報が必要です。「base

    f()」が仮想の場合、次に、 derived :: f()`は、 `derived`の宣言に virtual`キーワードがあるかどうかに関係なく仮想になります。

  5. 多重継承は、他のいくつかの要件を追加します-相対的な メソッドの最終的なオーバーライドが呼び出される前に書き換える必要がある各「baseX」からのオフセットインスタンスの「base2」サブオブジェクト。継承リストの「base2」の前に宣言された他のベースによって相殺される場合があります。

コメントの最後の質問へ:

_ オブジェクトのインスタンス化(グローバルオブジェクトを除く)は実行時まで待機できないため、サイズやオフセットなどはリンク時まで待機する可能性があり、オブジェクトファイルを生成するときに必ずしも処理する必要はありませんか? _

void f() {
   derived d;
   //...
}

前のコードは、スタックにタイプ「派生」のオブジェクトを割り当てます。 コンパイラーは、アセンブラー命令を追加して、スタック内のオブジェクト用にある程度のメモリーを予約します。 コンパイラーがアセンブリを解析して生成した後、特にオブジェクトのトレースはありません(特に、PODタイプの簡単なコンストラクターを想定: 何も初期化されていません)、そのコードと `void f(){char array [sizeof(derived)]; } `はまったく同じアセンブラーを生成します。 コンパイラーは、スペースを予約する命令を生成するときに、その量を知る必要があります。


0


「readelf」でバイナリを調べてみましたか(Linuxプラットフォームを使用している場合)。 これにより、ELFオブジェクトファイルに関する非常に包括的な情報が提供されます。

しかし、正直なところ、これがコンパイルとリンクの理解にどの程度役立つかはわかりません。 適切な方法は、C ++コードがアセンブリのプリリンクおよびポストリンクにどのようにマッピングされるかを把握することだと思います。


0


通常、Objファイルの内部形式を詳細に知る必要はありません。ファイルは自動的に生成されるためです。 知っておく必要があるのは、作成するクラスごとに、コンパイラが生成するObjファイルです。これは、コンパイルするOSに適したクラスのバイナリバイトコードです。 次に、次のステップ(リンク)で、プログラムに必要なすべてのクラスのオブジェクトファイルを1つのEXEまたはDLL(またはWindows OS以外のその他の形式)にまとめます。 希望に応じて、EXE +複数のDLLも可能です。

最も重要なのは、クラスのインターフェース(宣言)と実装(定義)を分離することです。

常にクラスのヘッダーファイルインターフェイス宣言のみに入れてください。 他に何もありません-ここに実装はありません。 ポインターではないカスタム型のメンバー変数も避けてください。これらの場合、前方宣言では不十分であり、ヘッダーに他のヘッダーを含める必要があるためです。 ヘッダーにインクルードがある場合、デザインは匂いがし、構築プロセスも遅くなります。

クラスメソッドまたはその他の関数のすべての実装は、CPPファイルに含める必要があります。 これにより、誰かがヘッダーをインクルードし、CPPファイルのみに他のインクルードをインクルードできる場合、コンパイラーによって生成されたObjファイルは不要になります。

しかし、なぜ迷惑なのですか? 答えは、そのような分離がある場合、各Objファイルがクラスごとに1回使用されるため、リンクが高速になるということです。 また、クラスを変更すると、次のビルド中に少量の他のオブジェクトファイルも変更されます。

ヘッダーにインクルードがある場合、これは、コンパイラーがクラスのObjファイルを生成するときに、ヘッダーに含まれる他のクラスのObjファイルを最初に生成する必要があることを意味します。 循環依存であっても、コンパイルできません! または、クラス内の何かを変更した場合、コンパイラーは、他の多くのObjファイルを再生成する必要があります。これは、分離しないと、しばらくすると非常に密接に依存するためです。


0


`nm`は、オブジェクトファイル内のシンボルの名前を表示するUNIXツールです。

`objdump`はより多くの情報を表示するGNUツールです。

しかし、両方のツールは、リンカによって使用されるが、人間が読むように設計されていない非常に生の情報を表示します。 これはおそらく、C ++レベルで何が起こるかをよりよく理解するのに役立ちません。


0


「http://www.network-theory.co.uk/docs/gccintro/」を読んでいます-「GCCの紹介」。 これにより、リンクとコンパイルに関する優れた洞察が得られました。 初心者レベルですが、気にしません。