4つの問題からのLinuxでのC ++コンパイルとリンクの分析

要約: コンパイルとリンクは、C&C ++プログラマーにとってはなじみがあり、なじみのないものです。なじみは、すべてのコードのコンパイルとリンクのプロセスにあります。なじみのなさは、ほとんどの人がコンパイルとリンクの原則に意図的に注意を払っていないことです。この記事では、開発プロセス中に発生する4つの一般的な問題を通じて、64ビットLinuxでのC ++のコンパイルとリンクの問題について説明します。

コンパイルの原則:

次の最も単純なC ++プログラム(main.cpp)は、実行可能なターゲットプログラムにコンパイルされます。実際、前処理、コンパイル、アセンブリ、およびリンクの4つのステップに分けることができます。

g ++ main.cpp -v詳細なプロセスを参照してください。ただし、コンパイラーは前処理プロセスとコンパイルプロセスをマージしました。

前処理: g ++ -E main.cpp -o main.ii、-Eは前処理のみを意味します。前処理は主に、さまざまなマクロの展開、コンパイラがデバッグ情報を生成しやすくするための行番号とファイルIDの追加、コメントの削除、コンパイラが使用するコンパイラ命令の保持などを扱います。

コンパイル: g ++ -S main.ii -o main.s、-Sはコンパイルのみを意味します。コンパイルとは、前処理されたファイルに基づいて、一連の字句分析、構文分析、および最適化の後にアセンブリコードを生成することです。

アセンブリ: g ++ -c main.s -o main.o. アセンブリとは、アセンブリコードをマシンで実行できる命令に変換することです。

リンク: g ++ main.o. リンクすると実行可能なプログラムが生成されます。リンクが必要な理由は、コードをmain.cppほど単純にすることができないためです。最新のソフトウェアには数億行あります。main.cppで記述した場合、分業や協力を助長することも、維持することもできません。 、したがって、通常は一連のcppファイルで構成されます。コンパイラは各cppを個別にコンパイルします。これらのcppは、他のモジュールの関数またはグローバル変数を参照します。単一のcppをコンパイルする場合、正確なアドレスを知ることはできません。コンパイルが完了した後、リンカーは、正確なアドレスを持たないさまざまなシンボル(関数、変数など)を正しい値に設定して、それらを組み立てて完全な実行可能プログラムを形成できるようにする必要があります。

問題1:ヘッダーファイルのオクルージョン

コンパイルプロセスで最も奇妙な問題は、ヘッダーファイルのオクルージョンです。次のコードでは、main.cppにヘッダーファイルcommon.hが含まれており、実際に使用するヘッダーファイルは、図の右端にある名前を含むものです。

メンバーのファイル(ディレクトリは./include)ですが、コンパイルプロセスの途中でcommon.h(ディレクトリは./include1)が最初に検出されたため、コンパイラはエラーを報告しました:テスト構造に名前メンバーがありません。プログラマーの場合、名前のメンバーを明確に定義しましたが、名前のメンバーはいないと言いました。このような状況に初めて遭遇した場合、あなたは自分の人生を疑うかもしれません。この奇妙な問題に対処するために、次の図に示すように、-Eパラメーターを使用して前処理後のコンパイラーの出力を確認できます。

前処理ファイルの形式は次のとおりです。#linenum filename flag。これは、filanameという名前のファイルのlinenum行から次のコンテンツが展開されることを意味します。flagの値は1、2、3、4で、スペースで区切ることができます。複数値、1は新しいファイルが次に展開されることを意味し、2はファイルが展開されることを意味し、3は次のコンテンツがシステムヘッダーファイルからのものであることを意味し、4は次のコンテンツがexternCの形式でインポートされる必要があることを意味します。

展開された出力から、Test構造がnameメンバーを定義しておらず、Test構造が./include1のcommon.hで定義されていることがはっきりとわかります。この時点で、真実が明らかになり、コンパイラはまったく役に立たなくなります。定義されたテスト構造は、同じ名前の別のヘッダーファイルによって切り捨てられました。-Iを調整するか、ヘッダーファイルに部分的なパスを追加して、ヘッダーファイルの場所をより詳細に指定することで、問題を解決できます。

ターゲットファイル:

コンパイルリンクは、最終的にさまざまなターゲットファイルを生成します。Linuxでのターゲットファイル形式はELF(Executable Linkable Format)です。詳細な定義については、ヘッダーファイル/usr/include/elf.hを参照してください。一般的なターゲットファイルには、再配置可能ターゲットファイルとつまり、.oで終わるオブジェクトファイルはもちろん、静的ライブラリもこのカテゴリに分類されます。デフォルトでコンパイルされるa.outファイルなどの実行可能ファイル、共有オブジェクトファイル.so、コアダンプ後に出力されるコアダンプファイルファイル。Linuxファイル形式は、fileコマンドで表示できます。

典型的なELFファイル形式を次の図に示します。このファイルには、セクションヘッダーテーブルをコア組織プログラムとして使用するコンパイルパースペクティブと、操作パースペクティブであるプログラムヘッダーテーブルがセグメントをコア組織プログラムとして使用する2つのパースペクティブがあります。これは主にストレージを節約するためです。断片化されたセクションの多くは、実行時の配置要件のために多くのメモリを浪費します。実行時に、同様の権限を持つセクションは通常、セグメントに編成され、一緒にロードされます。

ELFファイルの内容は、コマンドobjdumpおよびreadelfを介して表示できます。

再配置可能なターゲットファイルの一般的なセクションは次のとおりです。

シンボル解像度:

リンカーは、外部シンボルへの参照を参照シンボルの正しいアドレスに変更します。参照された外部シンボルに対応する定義が見つからない場合、リンカーはXXXXへの未定義の参照のエラーを報告します。もう1つのケースは、複数のシンボルの定義が見つかった場合です。この場合、リンカーには一連のルールがあります。ルールを説明する前に、強い記号と弱い記号の概念を理解する必要があります。簡単に言えば、関数と初期化されたグローバル変数は強い記号であり、初期化されていないグローバル変数は弱い記号です。

シンボルの複数の定義に対するリンカー処理ルールは次のとおりです(作成者は、ルール2と3がgcc 7.3.0では1として処理されるように見えます)。

1.複数の強力なシンボル定義は許可されていません。リンカーは、繰り返される定義のエラーのように見えることを報告します。

2.強い記号と複数の弱い記号の名前が同じ場合は、強い記号を選択します

3.すべてのターゲットファイルでシンボルが弱い場合は、最大のスペースを占めるものを選択します

これらの基盤を使用して、最初に静的リンクプロセスを見てみましょう

1.リンカーは、コマンドラインが左から右に表示される順序でオブジェクトファイルと静的ライブラリをスキャンします

2.リンカーは、オブジェクトファイルのコレクションE、未解決のシンボルのコレクションU、およびEで定義されたシンボルDのコレクションを維持します。初期状態E、U、およびDはすべて空です。

3.コマンドラインのファイルfごとに、リンカーはfがオブジェクトファイルであるか静的ライブラリであるかを判別します。オブジェクトファイルの場合、fはEに追加され、fの未定義のシンボルはUに追加され、シンボルが定義されます。 Dに追加し、次のファイルに進みます

4.静的ライブラリの場合、リンカーは静的ライブラリのオブジェクトファイル内のU内の未定義のシンボルを照合しようとします。U内のシンボルがm内で一致する場合、mは、各メンバーについて、前の手順のファイルfと同じ方法で処理されます。 UとDが変更されるまでファイルは順番に処理され、Eに含まれていないメンバーファイルは単に破棄されます。

5.すべての入力ファイルが処理された後、Uにまだシンボルがある場合はエラーが発生します。それ以外の場合、リンクは正常であり、実行可能ファイルが出力されます。

質問2:静的ライブラリの順序

次の図に示すように、main.cppはliba.aに依存し、liba.aはlibb.aに依存します。静的リンクアルゴリズムによれば、g ++ main.cpp liba.a libb.aを使用すると、liba.aが解決されるため、libb.aの順序を正常にリンクできます。上記のアルゴリズムのUに未定義のシンボルFunBが追加され、定義がlibb.aにある場合、g ++ main.cpp libb.a liba.aの順序でコンパイルすると、静的にリンクされているため、FunBの定義を見つけることができません。アルゴリズム、libb.aを解析するときUは空なので、分析を行う必要はなく、libb.aは単に破棄されますが、liba.aを解析すると、FunBが定義されていないことがわかり、Uが空ではなく、リンクエラーが発生します。したがって、静的リンクを行う場合は、ライブラリの順序に特に注意する必要があります。他のライブラリを参照する静的ライブラリを最初に配置する必要があります。多くのライブラリをリンクする場合は、依存関係を明確にするために、ライブラリを調整する必要があります。

動的リンク:

以前のコンテンツのほとんどは静的リンクに関連していますが、静的リンクには多くの欠点があります。ライブラリに変更がある限り、更新することはできません。再コンパイルする必要があります。共有することはできません。そして、ディスクは大きな無駄です。

動的リンクライブラリを生成するには、パラメータ「-shared -fPIC」を使用して、位置に依存しないPIC(位置に依存しないコード)共有オブジェクトファイルを生成することを示す必要があります。静的リンクの場合、実行可能オブジェクトファイルが生成されるとリンクプロセス全体が完了しますが、動的リンクの効果を実現するには、モジュールに応じてプログラムを比較的独立した部分に分割し、プログラムの実行時にそれらを1つにリンクする必要があります。完全なプログラム同時に、異なるプログラム間でのコード共有を実現するには、コードが位置に依存しないことを確認する必要があります(共有オブジェクトファイルの仮想アドレスは各プログラムにロードされるため、どこにいてもロードできることを確認してください)作業)、そして位置の独立性を達成するために、それは前提に依存しています:データセグメントとコードセグメントの間の距離は常に同じままです。

ターゲットモジュールがメモリにどのようにロードされても、データセグメントとコードセグメントの間の距離は変わらないため、コンパイラは、データセグメント、参照されるグローバル変数、またはの前にグローバルオフセットテーブルGOT(グローバルオフセットテーブル)を導入します。関数にはGOTにレコードがあり、コンパイラはGOTの各エントリの再配置レコードを生成します。データセグメントは変更できるため、動的リンカーはロード時にGOTの各エントリを再配置します。 PICを実現しました。

一般的な原則は基本的に同じですが、特定の実装では、関数とグローバル変数の処理が異なります。大規模なプログラム関数は数千あり、プログラムはそれらのごく一部しか使用できないため、ロード時にすべての関数を再配置する必要はなく、使用時にアドレスを変更するだけです。このため、コンパイラは、遅延バインディングを実現するために、プロシージャリンクテーブルPLT(プロシージャリンクテーブル)を導入しています。コードセグメントでは、PLTはGOT内の関数に対応するアドレスを指します。初めて呼び出されたとき、GOTは関数の実際のアドレスではなく、GOTコードの次の命令アドレスにジャンプするため、最初のパスがPLTはGOTにジャンプし、GOTを介してPLTの次の命令に戻ります。これは、何もしないことと同じです。PLTの直後のコードは、動的リンクに必要なパラメーターをスタックに配置し、動的リンカーを呼び出してGOTを修正します。それ以降、PLTのコードがGOTにジャンプするアドレスが関数の実際のアドレスになり、いわゆる遅延バインディングが実現されます。

共有ターゲットファイルの場合、注意が必要なセクションがいくつかあります。

上記の基盤を使用して、動的リンクのプロセスを見てみましょう

1.プログラムの実行は、ロード中に動的リンカーにジャンプします

2.動的リンカーブートストラップは、GOTおよび.dynamic情報を介して独自の再配置作業を完了します

3.共有オブジェクトファイルをロードします。実行可能ファイルとリンカー自体のシンボルをグローバルシンボルテーブルにマージし、共有オブジェクトファイルを幅の順序でトラバースすると、それらのシンボルテーブルは継続的にグローバルシンボルテーブルにマージされます。複数の共有オブジェクトに同じものがある場合シンボル、最初にロードされた共有オブジェクトファイルは、次のシンボルをシールドします

4.再配置と初期化

質問3:グローバルシンボル介入

動的リンクプロセスの最も重要なステップ3は、複数の共有オブジェクトファイルに同じシンボルが含まれている場合、最初にロードされたシンボルがグローバルシンボルテーブルを占有し、後続の共有オブジェクトファイルの同じシンボルが無視されることを示しています。 。コードがネーミングを適切に処理しないと、非常に奇妙なエラーが発生します。運が良ければ、すぐにコアダンプが発生します。残念ながら、プログラムが長時間実行されるまで、説明のつかないコアダンプは取得されず、コアダンプは取得されませんが、結果は正しくありません。

次の図に示すように、2つの動的ライブラリlibadd.soとlibadd1.soのシンボルがmain.cppで使用されます。

Add関数の処理で、g ++ main.cpp libadd.so libadd1.soを使用してコンパイルすると、プログラムは「Add in add lib」を出力し、Addがlibadd.soのシンボル(add.cpp)であることを示します。 g ++ main.cpp libadd1.so libadd.soを使用してコンパイルすると、プログラムは「Add in add1 lib」を出力し、Addがlibadd1.soのシンボルであることを示します。現時点では、問題は深刻です。呼び出し元main.cppは、Addパラメータは2つしかなく、add1.cppはAddに3つのパラメータがあると考えています。プログラムにそのようなコードがある場合、大きな混乱を引き起こす可能性があると予測できます。特定のシンボル分析では、次の図に示すように、LD_DEBUG = all ./a.outを介してAddの解析プロセスを確認できます。左側は、コンパイル時にlibadd.soが前面に配置され、Addがlibadd.soにバインドされている状況です。右は、libadd1.soが前に置かれ、Addがlibadd1.soにバインドされている状況に対応します。

実行時に動的ライブラリをロードします。

Linuxは、動的リンクと共有オブジェクトファイルのサポートにより、より柔軟なモジュールロード方法を提供します。dlopen、dlsym、dlclose、およびdlerror APIを提供することにより、モジュールを実行時に動的にロードできるため、プラグインを実現できます。特徴。

次のコードは、Add関数を動的にロードするプロセスを示しています。add.cppは「g ++ -fPIC –shared –o libadd.so add.cpp」をlibadd.soの通常どおりにコンパイルし、main.cppは「g ++ main.cpp-ldl」でコンパイルします。 a.outとして。main.cppでは、ハンドルvoid * handleは最初にdlopenインターフェースを介して取得され、次にシンボルAddがハンドルからdlsymを介して検索され、見つかった後、Add関数に変換され、通常の関数に従って使用できるようになり、最後にdlcloseがハンドルを閉じます。エラーはdlerrorを介して取得できます。

問題4:静的グローバル変数と動的ライブラリがダブルフリーにつながる

動的リンクの知識を完全に理解した後、静的グローバル変数と動的ライブラリのエンタングルメントによって引き起こされる問題を見てみましょう。コードは次のとおりです。foo.cppに静的グローバルオブジェクトfoo_があり、foo.cppはlibfooにコンパイルされます。 a、bar.cppはlibfoo.aライブラリに依存し、libbar.soにコンパイルされ、main.cppはlibfoo.aとlibbar.soの両方に依存します。

コンパイルされたmakefileは次のとおりです。

a.outを実行すると、ダブルフリーエラーが発生します。これは、1つの場所でデストラクタを2回呼び出すことによって発生します。これは、リンク時に最初にリンクされる静的ライブラリがfoo_のシンボルを静的ライブラリのグローバル変数に解決するためです。libbar.soが動的にリンクされると、すでにグローバルにシンボルfoo_が存在するため、グローバルシンボルが含まれます。動的ライブラリ内のfoo_への参照は、静的ライブラリ内のバージョンを指し、同じオブジェクトが2回最終的に破棄されます。

解決策は次のとおりです。

1.グローバルオブジェクトを使用しないでください

2.コンパイル時にライブラリの順序を逆にし、動的ライブラリを最初に配置して、グローバルにfoo_オブジェクトが1つだけになるようにします。

3.すべて動的ライブラリを使用します

4.コンパイラパラメータを介してシンボルの可視性を制御します。

総括する:

コンパイルとリンクで発生する4つの問題を通して、基本的にこれらのコンパイルとリンクの問題をカバーしました。これらの基盤により、日常業務における一般的なコンパイルとリンクの問題に簡単に対処できるはずです。スペースが限られているため、この記事では、主に大規模なフレームワークの原則に焦点を当てて、多くの詳細を省略しています。関連する詳細をさらに深く掘り下げたい場合は、関連するリファレンスに参加して、elf.hに関連するヘッダーファイルを読むことができます。

参照:

1.「リンカーとローダー」

2.「コンピュータシステムの深い理解」

3.「プログラマーの自己修養」

4. http://www.gnu.org/software/binutils/

注1この記事に関連するツールは、詳細についてhttp://www.gnu.org/software/binutils/から入手できます

注2この記事のサンプルコード画像では、各ウィンドウの下の白い領域に、このコードに対応するファイル名があります。対応するテキストが説明されていることに注意してください。

 

クリックしてフォローし、Huawei Cloudの新しいテクノロジーについて初めて学びましょう〜

おすすめ

転載: blog.csdn.net/devcloud/article/details/108822827