私はいつも疑問に思っていました。コンパイラがあなたが書いたコードをバイナリに変換することは知っていますが、リンカは何をしますか? 彼らはいつも私にとって謎でした。
「リンク」とは何かを大まかに理解しています。ライブラリとフレームワークへの参照がバイナリに追加されるときです。それ以上のことはわかりません。私にとって、それは「うまくいく」だけです。動的リンクの基本も理解していますが、深すぎることはありません。
誰かが用語を説明できますか?
私はいつも疑問に思っていました。コンパイラがあなたが書いたコードをバイナリに変換することは知っていますが、リンカは何をしますか? 彼らはいつも私にとって謎でした。
「リンク」とは何かを大まかに理解しています。ライブラリとフレームワークへの参照がバイナリに追加されるときです。それ以上のことはわかりません。私にとって、それは「うまくいく」だけです。動的リンクの基本も理解していますが、深すぎることはありません。
誰かが用語を説明できますか?
リンカを理解するには、ソース ファイル (C または C++ ファイルなど) を実行可能ファイル (実行可能ファイルとは、マシン上で実行できるファイルまたは同じマシン アーキテクチャを実行している他の誰かのマシン)。
内部的には、プログラムがコンパイルされると、コンパイラはソース ファイルをオブジェクト バイト コードに変換します。このバイト コード (オブジェクト コードと呼ばれることもあります) は、コンピュータ アーキテクチャだけが理解できるニーモニック命令です。従来、これらのファイルには .OBJ 拡張子が付いています。
オブジェクト ファイルが作成されると、リンカーが機能します。多くの場合、何か役に立つことを行う実際のプログラムは、他のファイルを参照する必要があります。たとえば、C では、自分の名前を画面に出力する単純なプログラムは次のようになります。
printf("Hello Kristina!\n");
コンパイラがプログラムを obj ファイルにコンパイルするとき、printf
関数への参照を配置するだけです。リンカはこの参照を解決します。ほとんどのプログラミング言語には、その言語に期待される基本的な機能をカバーするルーチンの標準ライブラリがあります。リンカーは、OBJ ファイルをこの標準ライブラリにリンクします。リンカーは、OBJ ファイルを他の OBJ ファイルとリンクすることもできます。別の OBJ ファイルから呼び出すことができる関数を持つ別の OBJ ファイルを作成できます。リンカは、ワード プロセッサのコピー アンド ペーストのように機能します。プログラムが参照するすべての必要な関数を「コピー」し、単一の実行可能ファイルを作成します。コピーされた他のライブラリが、さらに他の OBJ またはライブラリ ファイルに依存している場合があります。リンカは、仕事をするためにかなり再帰的にならなければならないことがあります。
すべてのオペレーティング システムが単一の実行可能ファイルを作成するわけではないことに注意してください。たとえば、Windows では、これらすべての機能を 1 つのファイルにまとめる DLL が使用されています。これにより、実行可能ファイルのサイズが縮小されますが、実行可能ファイルはこれらの特定の DLL に依存するようになります。DOS は、オーバーレイ (.OVL ファイル) と呼ばれるものを使用していました。これには多くの目的がありましたが、1 つは一般的に使用される機能を 1 つのファイルにまとめることでした (別の目的は、ご参考までに、大規模なプログラムをメモリに収めることができるようにすることでした。DOS はメモリに制限があり、オーバーレイはメモリから「アンロード」され、他のオーバーレイはそのメモリの上に「ロード」される可能性があるため、「オーバーレイ」と呼ばれます)。Linux には共有ライブラリがありますが、これは基本的に DLL と同じ考え方です (私が知っている筋金入りの Linux 関係者は、多くの大きな違いがあると教えてくれます)。
これが理解に役立つことを願っています!
アドレスの再配置は、リンクの重要な機能の 1 つです。
それでは、最小限の例でどのように機能するかを見てみましょう。
概要: 再配置は、.text
翻訳するオブジェクト ファイルのセクションを編集します。
コンパイラは一度に 1 つの入力ファイルしか認識しないため、これはリンカが行う必要がありますが、次の方法を決定するには、すべてのオブジェクト ファイルを一度に把握する必要があります。
.text
複数のオブジェクト ファイルの複数および.data
セクションを衝突させない前提条件: 以下についての最低限の理解:
リンクは特に C や C++ とは関係ありません。コンパイラはオブジェクト ファイルを生成するだけです。次に、リンカーは、それらをコンパイルした言語を知らなくても、それらを入力として受け取ります。Fortranでも構いません。
地殻を減らすために、NASM x86-64 ELF Linux hello world を調べてみましょう:
section .data
hello_world db "Hello world!", 10
section .text
global _start
_start:
; sys_write
mov rax, 1
mov rdi, 1
mov rsi, hello_world
mov rdx, 13
syscall
; sys_exit
mov rax, 60
mov rdi, 0
syscall
コンパイルおよびアセンブル:
nasm -o hello_world.o hello_world.asm
ld -o hello_world.out hello_world.o
NASM 2.10.09 で。
まず.text
、オブジェクト ファイルのセクションを逆コンパイルします。
objdump -d hello_world.o
与える:
0000000000000000 <_start>:
0: b8 01 00 00 00 mov $0x1,%eax
5: bf 01 00 00 00 mov $0x1,%edi
a: 48 be 00 00 00 00 00 movabs $0x0,%rsi
11: 00 00 00
14: ba 0d 00 00 00 mov $0xd,%edx
19: 0f 05 syscall
1b: b8 3c 00 00 00 mov $0x3c,%eax
20: bf 00 00 00 00 mov $0x0,%edi
25: 0f 05 syscall
重要な行は次のとおりです。
a: 48 be 00 00 00 00 00 movabs $0x0,%rsi
11: 00 00 00
これにより、hello world 文字列のアドレスがrsi
レジスタに移動され、write システム コールに渡されます。
ちょっと待って!"Hello world!"
プログラムがロードされたときにメモリ内の最終的な場所をコンパイラが知るにはどうすればよいでしょうか?
特に、多数のファイルを複数のセクション.o
と一緒にリンクした後は、それはできません。.data
リンカだけがこれらのオブジェクト ファイルをすべて持つので、リンカだけがそれを行うことができます。
したがって、コンパイラは次のようになります。
0x0
コンパイルされた出力にプレースホルダー値を置きますこの「追加情報」は.rela.text
、オブジェクト ファイルのセクションに含まれています。
.rela.text
「.text セクションの再配置」の略です。
リンカーがアドレスをオブジェクトから実行可能ファイルに再配置する必要があるため、再配置という言葉が使用されます。
.rela.text
セクションを次のように分解できます。
readelf -r hello_world.o
を含む;
Relocation section '.rela.text' at offset 0x340 contains 1 entries:
Offset Info Type Sym. Value Sym. Name + Addend
00000000000c 000200000001 R_X86_64_64 0000000000000000 .data + 0
このセクションの形式は、http ://www.sco.com/developers/gabi/2003-12-17/ch4.reloc.html で文書化されています。
各エントリは、再配置が必要な 1 つのアドレスについてリンカに通知します。ここでは、文字列用に 1 つしかありません。
少し単純化すると、この特定の行について、次の情報が得られます。
Offset = C
.text
:このエントリが変更する最初のバイトは何ですか。
逆コンパイルされたテキストを振り返ると、それはまさに critical 内にあり、movabs $0x0,%rsi
x86-64 命令エンコーディングを知っている人は、これが命令の 64 ビット アドレス部分をエンコードしていることに気付くでしょう。
Name = .data
.data
: アドレスはセクションを指します
Type = R_X86_64_64
アドレスを変換するために正確にどのような計算を行う必要があるかを指定します。
このフィールドは実際にはプロセッサに依存するため、AMD64 System V ABI 拡張セクション 4.4「再配置」に記載されています。
そのドキュメントには、次のように記載されてR_X86_64_64
います。
Field = word64
: 8 バイト、したがって00 00 00 00 00 00 00 00
at アドレス0xC
Calculation = S + A
S
は、再配置されるアドレスの値です。したがって、00 00 00 00 00 00 00 00
A
ここにある加数0
です。これは移転エントリのフィールドです。そのS + A == 0
ため、セクションの最初のアドレスに移動します.data
。
ld
次に、生成された実行可能ファイルのテキスト領域を見てみましょう。
objdump -d hello_world.out
与えます:
00000000004000b0 <_start>:
4000b0: b8 01 00 00 00 mov $0x1,%eax
4000b5: bf 01 00 00 00 mov $0x1,%edi
4000ba: 48 be d8 00 60 00 00 movabs $0x6000d8,%rsi
4000c1: 00 00 00
4000c4: ba 0d 00 00 00 mov $0xd,%edx
4000c9: 0f 05 syscall
4000cb: b8 3c 00 00 00 mov $0x3c,%eax
4000d0: bf 00 00 00 00 mov $0x0,%edi
4000d5: 0f 05 syscall
したがって、オブジェクト ファイルから変更されたのは、重要な行だけです。
4000ba: 48 be d8 00 60 00 00 movabs $0x6000d8,%rsi
4000c1: 00 00 00
の代わりにアドレス0x6000d8
(d8 00 60 00 00 00 00 00
リトルエンディアン) を指すようになりました0x0
。
hello_world
これは文字列の正しい位置ですか?
決定するには、各セクションをロードする場所を Linux に指示するプログラム ヘッダーを確認する必要があります。
それらを次のように分解します。
readelf -l hello_world.out
与える:
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x00000000000000d7 0x00000000000000d7 R E 200000
LOAD 0x00000000000000d8 0x00000000006000d8 0x00000000006000d8
0x000000000000000d 0x000000000000000d RW 200000
Section to Segment mapping:
Segment Sections...
00 .text
01 .data
これ.data
は、2 番目のセクションがVirtAddr
=で始まることを示してい0x06000d8
ます。
データ セクションにあるのは、hello world 文字列だけです。
PIE
linking: gcc と ld の位置に依存しない実行可能ファイルの -fPIE オプションは何ですか?_start
エントリ ポイント:アセンブリ言語の global _start とは?etext
、edata
およびのようなリンカー スクリプト定義のシンボルend
:シンボル etext、edata、および end はどこで定義されていますか?'C'のような言語では、コードの個々のモジュールは、従来、オブジェクトコードのブロブに個別にコンパイルされます。これは、モジュールがそれ自体の外部(つまり、ライブラリまたは他のモジュール)で行うすべての参照以外のあらゆる点で実行する準備ができています。まだ解決されていません(つまり、誰かがやって来てすべての接続を確立するまで、空白になっています)。
リンカが行うことは、すべてのモジュールを一緒に調べ、各モジュールがそれ自体の外部に接続するために必要なものを調べ、エクスポートしているすべてのものを調べることです。次に、それをすべて修正し、実行可能な最終的な実行可能ファイルを生成します。
ダイナミックリンクも行われている場合、リンカーの出力はまだ実行できません-まだ解決されていない外部ライブラリへの参照がいくつかあり、アプリをロードするときにOSによって解決されます(またはおそらく実行中も後で)。
コンパイラがオブジェクト ファイルを生成すると、そのオブジェクト ファイルで定義されているシンボルのエントリと、そのオブジェクト ファイルで定義されていないシンボルへの参照が含まれます。リンカはそれらを取得してまとめ、(すべてが正常に機能する場合) 各ファイルからのすべての外部参照が、他のオブジェクト ファイルで定義されているシンボルによって満たされるようにします。
次に、これらすべてのオブジェクト ファイルを結合し、各シンボルにアドレスを割り当てます。あるオブジェクト ファイルが別のオブジェクト ファイルへの外部参照を持っている場合、別のオブジェクトで使用されている各シンボルのアドレスを埋めます。典型的なケースでは、使用される絶対アドレスのテーブルも構築するため、ローダーはファイルがロードされるときにアドレスを「修正」できます/修正します(つまり、ベースロードアドレスをそれぞれに追加します)すべてが正しいメモリアドレスを参照するようにします)。
かなりの数の最新のリンカーは、すべてのモジュールが表示された場合にのみ可能な方法でコードを最適化するなど、他の「もの」の一部 (いくつかのケースでは多く) を実行することもできます (たとえば、含まれていた関数を削除するなど)。他のモジュールがそれらを呼び出す可能性があったためですが、すべてのモジュールがまとめられると、何も呼び出されないことが明らかになります)。