私はいつも疑問に思っていました。コンパイラはあなたが書いたコードをバイナリに変換することを知っていますが、リンカは何をしますか?彼らはいつも私にとって謎でした。
「リンク」とは何かを大まかに理解しています。ライブラリとフレームワークへの参照がバイナリに追加されるときです。それ以上理解できません。私にとっては「うまくいく」だけです。動的リンクの基本も理解していますが、あまり深くはありません。
誰かが用語を説明できますか?
リンカを理解するには、ソースファイル(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
レジスタに移動され、書き込みシステムコールに渡されます。
ちょっと待って!コンパイラは、プログラムがロードされたときに"Hello world!"
がメモリのどこに到達するかをどのようにして知ることができますか?
それはできません。特に、多数の.o
ファイルを複数の.data
セクションとリンクした後はできません。
すべてのオブジェクトファイルを持っているのはリンカーだけなので、リンカーのみがこれを実行できます。
したがって、コンパイラは次のことを行います。
0x0
をコンパイル済み出力に配置しますこの「追加情報」は、オブジェクトファイルの.rela.text
セクションに含まれています
.rela.text
は「.textセクションの再配置」を表します。
Wordの再配置が使用されるのは、リンカーがオブジェクトのアドレスを実行可能ファイルに再配置する必要があるためです。
.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
の最初のバイトは何ですか。
逆コンパイルされたテキストを振り返ると、それは重要なmovabs $0x0,%rsi
の内部にあり、x86-64命令のエンコードを知っている人は、これが命令の64ビットアドレス部分をエンコードしていることに気付くでしょう。
Name = .data
:アドレスは.data
セクションを指します
Type = R_X86_64_64
。アドレスを変換するために正確にどのような計算を行う必要があるかを指定します。
このフィールドは実際にはプロセッサに依存しているため、 AMD64 System V ABI extension section 4.4 "Relocation"に文書化されています。
その文書は、R_X86_64_64
がすることを述べています:
Field = Word64
:8バイト、したがって00 00 00 00 00 00 00 00
アドレス0xC
Calculation = S + A
S
は、再配置されるアドレスでのvalueです。したがって、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
これは、2番目の.data
セクションがVirtAddr
= 0x06000d8
で始まることを示しています。
データセクションにあるのは、hello world文字列だけです。
「C」などの言語では、コードの個々のモジュールは伝統的にオブジェクトコードの塊に個別にコンパイルされ、モジュールがそれ自体の外部(つまりライブラリまたは他のモジュール)に行うすべての参照以外のあらゆる点で実行可能ですまだ解決されていません(つまり、空白で、誰かが一緒に来てすべての接続を保留しています)。
リンカは、すべてのモジュールをまとめて調べ、各モジュールが外部に接続するために必要なものを調べ、エクスポートするすべてのものを調べます。次に、それをすべて修正し、最終的な実行可能ファイルを生成し、実行可能にします。
動的リンクも行われている場合、リンカーの出力はstill実行できません-まだ解決されていない外部ライブラリへの参照がいくつかあり、それらはOSによって解決されますアプリを(または実行中に後で)ロードします。
コンパイラがオブジェクトファイルを生成するとき、そのオブジェクトファイルで定義されているシンボルのエントリと、そのオブジェクトファイルで定義されていないシンボルへの参照が含まれます。リンカはそれらを取得してまとめますので、(すべてが正常に機能する場合)各ファイルからのすべての外部参照は、他のオブジェクトファイルで定義されているシンボルによって満たされます。
次に、これらすべてのオブジェクトファイルを組み合わせて、各シンボルにアドレスを割り当てます。1つのオブジェクトファイルが別のオブジェクトファイルへの外部参照を持っている場合、別のオブジェクトが使用しているすべてのシンボルのアドレスを入力します。典型的なケースでは、使用される絶対アドレスのテーブルも構築するため、ローダーはファイルがロードされるときにアドレスを「修正」することができます(つまり、それぞれにベースロードアドレスを追加します)すべてのアドレスが正しいメモリアドレスを参照するようにします)。
非常に少数の最新のリンカは、すべてのモジュールが表示された後にのみ可能な方法でコードを最適化するなど、他の「もの」の一部(いくつかのケースではlot)も実行できます(たとえば、可能であったために含まれていた関数を削除しますが、他のモジュールがそれらを呼び出す場合がありますが、すべてのモジュールがまとめられると、何も呼び出さないことが明らかになります。