この記事では、プログラムのリンク原理を簡単に紹介します。リンクの原則を学ぶことは、プログラマがプログラムの性質を理解するのに役立ち、将来の大規模なソフトウェア コード開発のための強固な基盤を築くこともできます。リンクの原理を理解すると、日常の開発におけるいくつかの不可解な問題を解決するのに役立ちます。
簡単に言うと、リンクとは、プロジェクト内のさまざまなコードや部分データを収集し、それらを 1 つの実行可能ファイルに結合し、結合されたファイルをメモリにロードして実行するプロセスです。
リンクは次の 3 つの状況で発生する可能性があります。
1. コンパイル時間: ソースコードがマシンコードに変換される時間
2. ロード時:プログラムをメモリにロードして実行するとき
3. ランタイム: アプリケーションの実行時
1. 静的リンク
1.1 プログラムのコンパイル手順
//示例程序1
/* /code/link/main.c */
void swap();
int buf[2] = {1, 2};
int main()
{
swap();
return 0;
}
/* /code/link/swap.c */
extern int buf[];
int *bufp0 = &buf[0];
int *bufp1;
void swap()
{
int temp;
bufp1 = &buf[1];
temp = *bufp0;
*bufp0 = *bufp1;
*bufp1 = temp;
}
上記は単純な 2 番号交換プログラムであり、実行可能なターゲット ファイルを生成するプロセスは次のとおりです。
C 言語プリプロセッサ (cpp): C 言語ソース プログラム *.c を ASCII コード中間ファイル *.i に変換します。
C コンパイラー (ccl): *.i を ASCII コードアセンブリ言語ファイル *.s に変換します
アセンブラー (as): *.s を再配置可能なオブジェクト ファイル*.oに変換します。
最後に、リンカー プログラム ld は、すべての *.o ファイルといくつかの必要なシステム ファイルを結合して、実行可能オブジェクト ファイルを作成します。
1.2 リンカのタスク
リンカは、複数のオブジェクト ファイルをリンクして、ロード可能で実行可能な完全なオブジェクト ファイルを作成します。その入力は、再配置可能なターゲット ファイルのセットです。リンクの 2 つの主なタスクは次のとおりです。
1.シンボル解決: ターゲット ファイル内のシンボル参照とシンボル定義をリンクします。各関数および各変数はシンボルとみなすことができ、オブジェクト ファイル内の各シンボルはシンボル定義に関連付けられます。
2.再配置: リンカは各シンボルの定義を特定のメモリ (RAM) の場所に関連付け、これらのシンボルへのすべての参照を変更して、すべてがこのメモリの場所を指すようにします。
1.3 対象ファイル
ターゲット ファイルの 3 つの形式:
1.再配置可能なターゲットファイル
この種類のファイルには、コンパイルされて機械命令コードとデータに変換されたバイナリ コードとデータが含まれていますが、直接実行することはできません。これらの命令とデータは他のモジュール (オブジェクト ファイル) のシンボルを参照することが多いため、これら他のモジュールのシンボルはこのモジュールには認識されません。これらのシンボルを解決するには、リンカがすべてのモジュールをリンクする必要があります。この操作を再配置と呼ぶため、この対象ファイルを「再配置可能対象ファイル」と呼び、接尾辞は通常 *.o となります。
2.実行対象ファイル
このようなファイルにはバイナリ コードとデータも含まれています。違いは、このファイルがリンクされており、すべてのモジュール (オブジェクト ファイル) にリンクされていることです。リンカは、必要なすべての再配置可能オブジェクト ファイルを実行可能オブジェクト ファイルに連結します。この時点で、他のオブジェクト ファイルを参照する各オブジェクト ファイル内のシンボルは解決され、再配置されます。したがって、すべてのシンボルは既知であり、ファイルはマシンによって直接実行できます。
3. 対象ファイルの共有
これは、必要なプログラムが実行またはロードされるときにメモリに動的にロードして実行できる、特別な場所に配置可能なオブジェクト ファイルです。このようなファイルの接尾辞は通常 *.so です。共有オブジェクト ファイルは、「ダイナミック ライブラリ」ファイルまたは「共有ライブラリ」ファイルと呼ばれることがよくあります。
1.4 リロケータブルオブジェクトファイル
Linux 環境における一般的なリロケータブル ターゲット ファイルと実行可能ファイルは、通常ELF (Excutable Linkable File) 形式であり、ELF ファイルの一般的な構造は次のとおりです。
ターゲット ファイルは主に、ELF ファイル ヘッダーとターゲット ファイル セグメントの 2 つの部分で構成されます。ELF ファイル ヘッダーの最初の 16 バイトはバイト オーダーを構成し、生成されるファイル システムのワード長とバイト オーダーを記述します。残りの部分には、ELF ファイル ヘッダーのサイズ、ターゲット ファイルのタイプ、ターゲット マシンのタイプ、ターゲット ファイル内のセグメント ヘッダー テーブルのファイル オフセット位置など、ELF ファイルに関するその他の情報が含まれます。 。この情報は、ELF 形式のプログラムをリンクしてロードするときに重要です。
/*ELF文件头*/
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x4003e0
Start of program headers: 64 (bytes into file)
Start of section headers: 6736 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 31
Section header string table index: 28
ELF ファイルのヘッダーに加えて、残りの部分はオブジェクト ファイルのセグメントで構成されます。これらのセクションは ELF ファイルの中核部分です。これは次のセクションで構成されます。
●.text :コードセグメント、格納されたバイナリ機械命令で、機械によって直接実行できます。
● .rodata :読み取り専用のデータセグメント。プログラムで使用される文字列などの複雑な定数を保存します。
● .data :データ セグメント。プログラム内で明示的に初期化されたグローバル データを格納します。C言語のグローバル変数とスタティック変数を含みます。これらのグローバル データが 0 に初期化されている場合、データ セグメントには格納されず、ブロック ストレージ セグメントに格納されます。C 言語のローカル変数はスタックに格納され、データ セグメントには表示されません。
● .bss :明示的に初期化されていないグローバル データを格納するブロック ストレージ セグメント。このセクションはターゲット ファイル内の実際の領域を占有しませんが、グローバル データ用の領域を指定された場所に予約する必要があることを通知する単なるプレースホルダーです。ブロック ストレージ セグメントが存在する理由は、ディスク上のストレージ領域の利用率を向上させるためです。
● .symtab :シンボル テーブル。定義および参照された関数とグローバル変数を保存します。各再配置可能オブジェクト ファイルには、このようなテーブルが 1 つ存在する必要があります。このテーブルには、このモジュールで参照されるすべてのグローバル シンボル (関数とグローバル変数を含む) および他のモジュール (オブジェクト ファイル) のグローバル シンボルが登録されます。リンク内の再配置操作は、これらの参照されたグローバル シンボルの位置を決定することです。
● .rel.text : コードセグメントを再配置 (relocate) する必要があることを示す情報であり、再配置操作によって変更する必要があるシンボルの概要を格納します。これらのシンボルはコード セグメント内にあり、通常は関数名とラベルです。
● .rel.data : 再配置する必要があるデータセグメントに関する情報。再配置操作によって変更する必要があるシンボルの概要を保存します。これらのシンボルはデータ セグメント内にあり、グローバル変数です。
● .debug : デバッグ情報。デバッグ用のシンボル テーブルを保存します。プログラムのコンパイル時に gcc コンパイラの -g オプションを使用すると、このセクションが生成されます。このテーブルには、ソース プログラム内のすべてのシンボルの参照と定義が含まれています。このセクションを使用すると、gdb デバッガを使用してデバッグするときに出力して観察できます。プログラム、変数の値。
● .line : ソース プログラムの行番号マッピング。ソース プログラム内の各ステートメントの行番号が格納されます。プログラムをコンパイルするときに、gcc コンパイラーの -g オプションを使用すると、このセクションが生成されます。このセクションは、gdb デバッガーを使用してプログラムをデバッグするときに非常に役立ちます。
● .strtab : .symtab シンボル テーブルおよび .debug シンボル テーブル内のシンボルの名前を保存する文字列テーブル これらの名前は文字列であり、'\0' で終わります。
1.5 オブジェクトファイル内のシンボルとシンボルテーブル
シンボルの解決は、リンクの主なタスクの 1 つです。シンボルが正しく解析された後にのみ、参照されるシンボルの場所を変更できます。これにより、再配置が完了し、マシンに直接ロードして実行できる実行可能ターゲット ファイルが生成されます。各再配置可能オブジェクト ファイルには、シンボルを格納するシンボル テーブルがあり、これらのシンボルは 3 つのカテゴリに分類されます。
1. このモジュールで定義されるグローバルシンボル
2. このモジュールで参照される他のモジュールによって定義されたグローバル シンボル
3. このモジュールで定義および参照されるローカル シンボル
注: ローカル変数とローカルシンボルは同じものではありません。ローカル変数はスタックに格納され、メモリ内にのみ表示される概念です。ローカル シンボルには、ディスク ファイルにも表示される静的変数とローカル ラベルが含まれます。
シンボルテーブルの構造:
typedef struct{
int name; //目标符号的名字
int value; //符号的地址。对于可重定位模块:该值是距定义目标节的起始位置的偏移;
// 对于可执行目标文件:该值是一个绝对运行时地址。
int size; //目标符号的大小(字节为单位)
char type:4; //目标符号的类型
char binding:4; //目标符号是本地的还是全局的
char reserved; //保留
char section; //表示目标符号和目标文件的某个节关联(符号表中的Ndx字段)
}Elf_Symbol;
サンプルプログラム1main.c内のシンボルテーブル
Symbol table '.symtab' contains 11 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 6
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 5
8: 0000000000000000 8 OBJECT GLOBAL DEFAULT 3 buf
9: 0000000000000000 21 FUNC GLOBAL DEFAULT 1 main
10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND swap
シンボルテーブルの意味の説明:
**buf:** .data セクションのオフセット 0 (値) にある 8 バイトのターゲット、グローバル シンボル
**main:** .text セクションのオフセット 0 にある 21 バイトの関数、グローバル関数
**swap:**外部シンボルからの参照 スワップ、外部シンボル
シンボル テーブルでは、それぞれ異なるセクションを識別するために整数が使用されます。Ndx=1 は .text セクションを表し、Ndx=3 は .data セクションを表します。ABS は再配置すべきでないシンボルを表します。UNDEF は未定義のシンボルを表します。ターゲット モジュール内で参照され、他の場所で定義されているシンボル。COMMON は、まだ場所が割り当てられていない初期化されていないデータ ターゲット、つまり、初期化されていないグローバルまたはローカル静的変数を表します。LOCAL はローカル シンボルを表し、GLOBAL はグローバル シンボルを表します。
1.6 シンボル分析
リンカは、入力した再配置可能オブジェクト ファイルのシンボル テーブル内の明確なシンボル定義に各参照を関連付けることによって、シンボル参照を解決します。
1. ローカルシンボル解決
同じモジュール内で定義されたローカル シンボルへの参照の場合、シンボル解決は非常に簡単です。コンパイラは、各ローカル オブジェクト ファイル内の各ローカル シンボルの定義を 1 つだけ許可します。もちろん、ローカル静的変数の場合、コンパイラによってローカル リンカー シンボルが割り当てられ、一意の名前が付けられます。
2. グローバルシンボル解決
グローバル シンボルを解決するときに、コンパイラが現在のモジュールで定義されていないシンボル (変数または関数) を検出すると、そのシンボルが他のモジュールで定義されていると想定し、リンカ シンボル エントリ テーブルを生成し、それをリンカ シンボル エントリ テーブルに残します。リンカ。後続のリンク再配置プロセス中に、リンカが入力モジュールのいずれかで参照シンボルの定義を見つけることができない場合、コンパイルでエラーが報告されます。
3. 複数のオブジェクト ファイルで定義された同じグローバル シンボルに対するコンパイラの解析ルール
ルール 1: 複数の強いシンボルは許可されません
ルール 2: 1 つの強いシンボルと複数の弱いシンボルがある場合は、強いシンボルを選択します ルール
3: 複数の弱いシンボルがある場合は、これらの弱いシンボルから任意の強いシンボルを選択します
: 初期化されたグローバル シンボル
弱いシンボル: 初期化されていないグローバル シンボル
1.7 移転
シンボルの解析が完了すると、各シンボルの定義位置とサイズがわかります。再配置操作では、これらのシンボルをリンクするだけで済みます。このステップでは、リンカはリンクに参加しているすべてのオブジェクト ファイルをマージし、コンテンツを保存するためのランタイム アドレスを各シンボルに割り当てる必要があります。再配置は 2 つの手順で実行されます。
1. 再配置セクションとシンボルの定義
このステップでは、リンカは同じタイプのすべてのセクションを新しいセクションにマージします。たとえば、入力オブジェクト モジュール内のすべての .data セクションは、実行可能オブジェクト ファイル内の .data セクションにマージされ、リンカはランタイム メモリ アドレスを新しい .data セクションに割り当てます。他のセクションの処理も同様で、このステップが完了すると、プログラム内のすべての命令とグローバル変数に固有のランタイム メモリ アドレスが割り当てられます。
2. 再配置セクションのシンボル参照
このステップでは、リンカはコード セクションとデータ セクション内の各シンボルへの参照を変更して、正しい実行時メモリ アドレスを指すようにします。
コンパイラはオブジェクト ファイルを生成するとき、コードと変数の最終的な保存場所も、他のファイルで定義されている外部シンボルも知りません。したがって、アセンブラが最終位置が不明なターゲット参照に遭遇すると、コンパイラは各シンボルに関する情報を格納する再配置エントリを生成します。このエントリは、オブジェクト ファイルをマージするときに各オブジェクト ファイル内のシンボル参照を変更する方法をリンカに指示します。この再配置エントリは **.rel.text** セグメントと .rel.dataセグメントに保存されます。このエントリは、各シンボルの再配置情報を格納する構造体として理解できます。
typedef struct {
int offset;/*偏移值*/
int symbol;/*所代表的符号*/
int type;/*符号的类型*/
}symbol_rel;
/*
offset表示该符号在存储的段中的偏移值。symbol代表该符号的名称,字符串实际存储在.strtab段中,这里存储的是该字符串首地址的下标。type表示重定位类型,链接器只关心两种类型,一种是与PC相关的重定位引用,另一种是绝对地址引用。
*/
PC 関連の再配置参照は、現在の PC 値 (通常、この値は次のジャンプ命令の格納場所) にシンボルのオフセット値を加算することを意味します。絶対アドレス参照とは、現在の命令で指定されたアドレス参照をそのままジャンプアドレスとして使用することを意味します。
この情報を使用して、リンカは、再配置後のセグメントの新しいアドレスに記憶セグメント内のシンボルのオフセット値を加算して、新しい参照アドレスを取得し、この参照アドレスがシンボルの最終アドレスになります。同様に、このアドレスを参照するプログラムのすべての部分を、古いオフセット アドレスの代わりにこの新しい絶対アドレスを使用するように変更する必要があります。新しいシンボル アドレスが変更されると、リンカの作業は終了します。
1.8 実行可能オブジェクトファイル
実行可能オブジェクト ファイル (ELF) の形式:
ELF ヘッダーはファイル全体の形式を記述します。これは再配置可能オブジェクト ファイルの形式に似ていますが、プログラムのエントリ ポイントが含まれます。
セグメント ヘッダー テーブル: メモリのどの連続セグメントが実行可能ファイルの連続スライスにマップされるかを記述します。
.init は、プログラム初期化コードが呼び出す関数 _init を定義します。
.text、.rodata、および .data は、再配置可能オブジェクト ファイルの前のセクションと似ていますが、これらのセクションは最終的なランタイム メモリ アドレスに再配置されています。
セグメントヘッダーテーブルの例:
off: ファイル オフセット; vaddr: 仮想アドレス; Paddr: 物理アドレス; align: セグメント アライメント;
filesz: ターゲットファイル内のセグメントサイズ; memsz: メモリ内のセグメントサイズ; flags: 操作権限
説明する:
行 1 と行 2 は、最初のセグメント (コード セグメント) が 4KB 境界にアライメントされていて、読み取り/実行権限があり、メモリ アドレス 0x08048000 で始まり、合計メモリ サイズが 0x448 バイトであり、初期化されていることを示しています。これは最初の 0x448 バイトですELF ヘッダー、セグメント ヘッダー テーブル、.init、.text、および .rodata セクションを含む、実行可能オブジェクト ファイルのファイル。
行 3 と行 4 は、2 番目のセグメント (データ セグメント) が 4KB 境界にアライメントされ、読み取り/書き込み権限があり、メモリ アドレス 0x08049448 で開始し、合計メモリ サイズが 0x104 バイトで、0xe8 バイトが初期化されて使用されることを示しています。ファイル オフセット 0x448 で、この場合は .data セクションの始まりです。このセクションの残りのバイトは、実行時にゼロに初期化される .bss データに対応します。