リンカーは何をしますか?


127

私はいつも疑問に思っていました。コンパイラは、あなたが書いたコードをバイナリに変換することを知っていますが、リンカは何をしますか?彼らはいつも私にとって謎でした。

「リンク」とは大まかに理解しています。ライブラリとフレームワークへの参照がバイナリに追加されたときです。それ以上はわかりません。私にとってはそれは「うまくいく」だけです。動的リンクの基本も理解していますが、あまり深くありません。

誰かが用語を説明できますか?

回答:


160

リンカーを理解するには、ソースファイル(CまたはC ++ファイルなど)を実行可能ファイル(実行可能ファイルとは、マシンで実行できるファイルまたは同じマシンアーキテクチャを実行している他の誰かのマシン)。

内部的には、プログラムがコンパイルされると、コンパイラはソースファイルをオブジェクトバイトコードに変換します。このバイトコード(オブジェクトコードと呼ばれることもあります)は、コンピュータアーキテクチャのみが理解できるニーモニック命令です。従来、これらのファイルの拡張子は.OBJです。

オブジェクトファイルが作成された後、リンカが機能します。多くの場合、有用なことを行う実際のプログラムでは、他のファイルを参照する必要があります。たとえば、Cでは、名前を画面に出力する簡単なプログラムは次のように構成されます。

printf("Hello Kristina!\n");

コンパイラがプログラムをobjファイルにコンパイルすると、printf関数への参照が配置されます。リンカはこの参照を解決します。ほとんどのプログラミング言語には、その言語から期待される基本的なものをカバーするルーチンの標準ライブラリがあります。リンカは、OBJファイルをこの標準ライブラリにリンクします。リンカは、OBJファイルを他のOBJファイルとリンクすることもできます。別のOBJファイルから呼び出すことができる関数を持つ他のOBJファイルを作成できます。リンカは、ワードプロセッサのコピーアンドペーストのように機能します。プログラムが参照する必要なすべての機能を「コピー」して、単一の実行可能ファイルを作成します。コピーされる他のライブラリは、さらに他のOBJまたはライブラリファイルに依存している場合があります。時々、リンカはその仕事をするためにかなり再帰的にならなければなりません。

すべてのオペレーティングシステムが単一の実行可能ファイルを作成するわけではないことに注意してください。たとえば、WindowsはDLLを使用して、これらすべての機能を1つのファイルにまとめています。これにより、実行可能ファイルのサイズは小さくなりますが、実行可能ファイルはこれらの特定のDLLに依存します。DOSは、オーバーレイ(.OVLファイル)と呼ばれるものを使用していました。これには多くの目的がありましたが、1つは一般的に使用される関数を1つのファイルにまとめることでした(不思議なことに、別の目的は、大きなプログラムをメモリに収めることができるようにすることでした。DOSのメモリとオーバーレイには制限があります。メモリから「アンロード」され、他のオーバーレイがそのメモリの上に「ロード」される可能性があるため、「オーバーレイ」という名前です。Linuxは共有ライブラリを持っています。これは基本的にDLLと同じ考えです(私が知っているハードコアLinuxの連中は多くの大きな違いがあると私に告げるでしょう)。

これがあなたの理解に役立つことを願っています!


9
すばらしい答えです。さらに、ほとんどの最新のリンカーは、テンプレートのインスタンス化などの冗長なコードを削除します。
エドワード・ストレンジ

1
これは、これらの違いのいくつかを検討するのに適切な場所ですか?
John P

2
こんにちは、私のファイルが他のファイルを参照していないとします。2つの変数を単に宣言して初期化するとします。このソースファイルはリンカにも送られますか?
Mangesh Kherdekar、2016年

3
@MangeshKherdekar-はい、常にリンカーを経由します。リンカは外部ライブラリをリンクしない場合がありますが、リンクフェーズは実行可能ファイルを生成するために発生する必要があります。
Icemanind 2016年

78

アドレス再配置の最小限の例

アドレスの再配置は、リンクの重要な機能の1つです。

それでは、最小限の例でそれがどのように機能するかを見てみましょう。

0)はじめに

概要:再配置は、.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。

1).oのテキスト

まず.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、オブジェクトファイルのセクションに含まれています

2).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,%rsix86-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 00atアドレス0xC

    • Calculation = S + A

      • Sは、再配置されるアドレスの値なので、00 00 00 00 00 00 00 00
      • A0ここにある加数です。これは、再配置エントリのフィールドです。

      したがってS + A == 0.dataセクションの最初のアドレスに移動します。

3).outのテキスト

次に、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

これはの代わりに0x6000d8d8 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
あなたは素晴らしいですおい。「ELFファイルのグローバル構造」チュートリアルへのリンクが壊れています。
Adam Zahran

1
@AdamZahranありがとう!スラッシュを処理できない愚かなGitHubページのURL!
Ciro Santilli郝海东冠状病六四事件法轮功

15

「C」のような言語では、コードの個々のモジュールは伝統的にオブジェクトコードのblobに個別にコンパイルされます。これは、モジュールがそれ自体の外部(つまり、ライブラリまたは他のモジュール)から行うすべての参照以外のあらゆる点で実行する準備ができています。まだ解決されていません(つまり、何も表示されず、すべての接続が確立されるまで保留されます)。

リンカが行うことは、すべてのモジュールをまとめて確認し、各モジュールが外部に接続するために必要なものを確認し、エクスポートするすべてのものを確認することです。次に、それをすべて修正し、最終的な実行可能ファイルを生成して、実行できるようにします。

動的リンクも行われている場合、リンカーの出力はまだ実行できません-まだ解決されていない外部ライブラリへの参照がまだあり、アプリをロードするときにOSによって解決されます(または後で実行中にも)。


一部のアセンブラーやコンパイラーは、コンパイラーが必要なものすべてを(通常、単一のソースファイルと#includeに含まれるものすべてで)見た場合、実行可能ファイルを直接出力できることに注意してください。いくつかのコンパイラは、通常は小さなマイクロ向けであり、それを唯一の動作モードとして持っています。
スーパーキャット

はい、中途半端な答えを出そうとしました。もちろん、あなたの場合と同様に、逆も当てはまります。ある種のオブジェクトファイルでは、完全なコード生成さえも行われません。これはリンカによって行われます(MSVCのプログラム全体の最適化のしくみです)。
ウィルディーン

@WillDeanとGCCのリンク時最適化は、私が知る限り、すべての「コード」をGIMPLE中間言語として必要なメタデータとともにストリーミングし、それをリンカーが利用できるようにし、最後に一度で最適化します。(古いドキュメントが意味するものにもかかわらず、オブジェクトコードの両方の表現を持つ古い「ファット」モードではなく、GIMPLEのみがデフォルトでストリーミングされるようになりました。)
underscore_d

10

コンパイラがオブジェクトファイルを生成するとき、そのオブジェクトファイルで定義されているシンボルのエントリと、そのオブジェクトファイルで定義されていないシンボルへの参照が含まれます。リンカはそれらを受け取り、それらをまとめて(すべてが正しく機能する場合)、各ファイルからのすべての外部参照が他のオブジェクトファイルで定義されているシンボルによって満たされるようにします。

次に、それらすべてのオブジェクトファイルを結合し、各シンボルにアドレスを割り当てます。1つのオブジェクトファイルが別のオブジェクトファイルへの外部参照を持っている場合は、別のオブジェクトが使用している場所に各シンボルのアドレスを入力します。典型的なケースでは、使用される絶対アドレスのテーブルも作成するため、ローダーはファイルのロード時にアドレスを「修正」できます(つまり、それぞれにベースロードアドレスを追加します)。それらがすべて正しいメモリアドレスを参照するように、アドレス)。

かなりの数の最近のリンカは、他の "もの"のいくつか(場合によっては多く)を実行することもできます。他のモジュールがそれらを呼び出す可能性があったためですが、すべてのモジュールがまとめられると、何も呼び出さないことが明らかです)。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.