C++ リンクは実際にはどのように機能しますか? 私が探しているのは、リンクがどのように行われるかについての詳細な説明であり、どのコマンドがリンクを行うかではありません。
あまり詳細には触れていない、コンパイルに関する同様の質問が既にあります: How does the compilation/linking process work?
C++ リンクは実際にはどのように機能しますか? 私が探しているのは、リンクがどのように行われるかについての詳細な説明であり、どのコマンドがリンクを行うかではありません。
あまり詳細には触れていない、コンパイルに関する同様の質問が既にあります: How does the compilation/linking process work?
編集:この回答を複製に移動しました: https://stackoverflow.com/a/33690144/895245
この回答は、リンクの重要な機能の 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 -felf64 hello_world.asm # creates hello_world.o
ld -o hello_world.out hello_world.o # static ELF executable with no libraries
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 文字列だけです。
実際、リンクは比較的簡単だと言えます。
最も単純な意味では、それぞれのソースに含まれる関数/グローバル/データのそれぞれに対して発行されたアセンブリが既に含まれているオブジェクト ファイル1をまとめることです。リンカーはここで非常に愚かで、すべてをシンボル(名前) とその定義 (またはコンテンツ)として扱うだけです。
明らかに、リンカーは特定の形式 (Unix では一般的に ELF 形式) を尊重し、コード/データのさまざまなカテゴリをファイルのさまざまなセクションに分離するファイルを生成する必要がありますが、それは単なるディスパッチです。
私が知っている2つの合併症は次のとおりです。
シンボルの重複除去の必要性: 一部のシンボルは複数のオブジェクト ファイルに存在し、作成されるライブラリ/実行可能ファイルには 1 つだけを作成する必要があります。定義の1つだけを含めるのはリンカの仕事です
リンク時の最適化: この場合、オブジェクト ファイルには出力されたアセンブリではなく中間表現が含まれ、リンカーはすべてのオブジェクト ファイルをマージし、最適化パス (インライン化など) を適用し、これをアセンブリにコンパイルして、最終的にその結果を出力します。 .
1 : さまざまな翻訳単位のコンパイルの結果 (大まかに言えば、前処理されたソース ファイル)
すでに述べた「リンカーとローダー」に加えて、実際の最新のリンカーがどのように機能するかを知りたい場合は、ここから始めることができます。