web-dev-qa-db-ja.com

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

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

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

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

105
Kristina Brooks

リンカを理解するには、ソースファイル(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の人は、大きな違いがあると教えてくれるでしょう)。

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

134
Icemanind

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

アドレスの再配置は、リンクの重要な機能の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

最初に、オブジェクトファイルの.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セクションの再配置」を表します。

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セクションの最初のアドレスに再配置されます。

3).outの.text

次に、生成された実行可能ファイル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によって解決されますアプリを(または実行中に後で)ロードします。

15
Will Dean

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

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

非常に少数の最新のリンカは、すべてのモジュールが表示された後にのみ可能な方法でコードを最適化するなど、他の「もの」の一部(いくつかのケースではlot)も実行できます(たとえば、可能であったために含まれていた関数を削除しますが、他のモジュールがそれらを呼び出す場合がありますが、すべてのモジュールがまとめられると、何も呼び出さないことが明らかになります。

10
Jerry Coffin