参考
概要
リンカ&ローダーの仕組みについて上記本を見ながら学習した備忘録です
用語など
リンクとは
- アセンブラされた複数のオブジェクトファイル(.o)を結合する作業のこと。結合時にオブジェクトファイルの関数呼び出し箇所を実際の関数呼び出しのメモリ番地に置換する(結合するまでは関数呼び出しなどは関数呼び出ししますよのマークがされた状態になっている)。リンクすることで実行形式ファイルが作成される。
- リンクを行うアプリケーションのことをリンカと呼ぶ
ロードとは
- 実行形式を読み込み、プログラムヘッダをに従ってメモリ上に配置(仮想メモリにマッピング)
- BSS領域のゼロクリア
- レジスタやスタック、引数の設定
- エントリポイントにジャンプ(プログラムカウントをエントリポイントに設定)
- ロードを行うアプリケーションのことをローダと呼ぶ
リンカもローダもOSの機能
オブジェクトファイル(.o)
- アセンブルされたバイナリでELF形式
- .text、.rodata、.data、.bssセクションなどなどのセクションをもつ。アセンブラでは各セクションを作成する役割がある。自分でアセンブラプログラミングする時は明示的に指定するセクションは.text、.rodata、.data、.bssセクションくらいだと思っている。他セクションはアセンブラが作成してくれる。各セクションの情報を持つセクションヘッダをもつ。
- 各オブジェクトファイルをリンクすることで実行形式が作成される
リンカスクリプト
- .text .dataなど各領域のメモリ配置の情報が書かれている
- リンカが実行形式を作成する際に参照して、実行形式のプログラムヘッダなどを作成する
- GNUldでは実行時にリンカスクリプトを指定しないとldをビルドする時に組み込まれたデフォルトのリンカスクリプトが利用される。ld --verboseで確認できる。通常何も指定しないので下のデフォルトのリンカスクリプトを利用している。
# ld --verbose
GNU ld version 2.23.52.0.1-30.el7_1.2 20130226
サポートされているエミュレーション:
elf_x86_64
elf32_x86_64
elf_i386
i386linux
elf_l1om
elf_k1om
内部リンカスクリプトを使用しています:
==================================================
/* Script for -z combreloc: combine and sort reloc sections */
OUTPUT_FORMAT("elf64-x86-64", "elf64-x86-64",
"elf64-x86-64")
OUTPUT_ARCH(i386:x86-64)
ENTRY(_start)
SEARCH_DIR("/usr/x86_64-redhat-linux/lib64"); SEARCH_DIR("/usr/local/lib64"); SEARCH_DIR("/lib64"); SEARCH_DIR("/usr/lib64"); SEARCH_DIR("/usr/x86_64-redhat-linux/lib"); SEARCH_DIR("/usr/lib64"); SEARCH_DIR("/usr/local/lib"); SEARCH_DIR("/lib"); SEARCH_DIR("/usr/lib");
SECTIONS
{
/* Read-only sections, merged into text segment: */
PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); . = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS;
.interp : { *(.interp) }
.note.gnu.build-id : { *(.note.gnu.build-id) }
.hash : { *(.hash) }
.gnu.hash : { *(.gnu.hash) }
・
・
・
実行形式(.out)
- ELF形式
- 各オブジェクトファイルのセクションがセグメントにマージされる
- プログラムヘッダに記載される情報(一部)
- ロード対象フラグ
- ロード先の仮想アドレス
- セグメントのサイズ など
- ローダはプログラムヘッダを見て、各セグメントを仮想メモリにマッピングしていく
リンカについて
リンク動作順番
- 各オブジェクトファイルのセクションをマージしてまとめる。そしてマージしたセクション同士を意味のある単位でセグメント(ローダーがメモリにロードする単位)にまとめる。
- 関数、変数の実体が存在する各セクションをメモリのどの番地に配置されるかリンカスクリプトを参照してプログラムヘッダを作成する。結果、変数、関数の番地が決定する(再配置)
- 変数、関数のアドレスが決まったのでシンボルテーブル(.symtabセクション)の各変数、関数のアドレス欄を埋める。
- シンボルテーブルを元に各セクションで
空欄
(再配置情報)になっていた箇所にアドレスを補充
。 (名前解決=シンボル解決) - 実行形式として出力する。
ローダー
リンカが作成した実行形式をメモリ上に展開する(仮想メモリにマッピング)
補足
シンボルテーブル(.symtabセクション)
関数・変数が配置されるメモリ上のアドレスとシンボル名の表。オブジェクトファイルはシンボルを管理するシンボルテーブルを持っている。.symtabセクションに入っている。
実行形式のシンボルテーブル
# nm a.out
・
・
0000000000400440 T _start
0000000000601034 b completed.6337
0000000000601030 W data_start
0000000000400470 t deregister_tm_clones
0000000000400500 t frame_dummy
0000000000400530 T main
U printf@@GLIBC_2.2.5
00000000004004a0 t register_tm_clones
リンク後はmainにアドレスが割り当てられていて、printは共有ライブラリで実行時にアドレスが決定するので空欄。
再配置情報
オブジェクトファイルはリンク前の段階なので変数、関数を参照する箇所は空欄になっている(リンク前でアドレスを決められないので)。この空欄のことを再配置情報
と呼ぶ。.relxxx&.relaxxxに入ってる。xxxは各セクション名。この二つは再配置情報の格納方法が異るみたい。
- 再配置情報は空欄ごとにあるので、関数や変数を参照する数だけある。
-
シンボルテーブル
は関数、変数の定義の数だけ。
再配置情報を見てみる
# readelf -r hello.o
再配置セクション '.rela.text' (オフセット 0x600) は 10 個のエントリから構成されています:
オフセット 情報 型 シンボル値 シンボル名 + 加数
000000000023 00050000000a R_X86_64_32 0000000000000000 .rodata + 0
00000000002d 000a00000002 R_X86_64_PC32 0000000000000000 printf - 4
000000000037 00050000000a R_X86_64_32 0000000000000000 .rodata + 0
000000000041 000a00000002 R_X86_64_PC32 0000000000000000 printf - 4
00000000004b 00050000000a R_X86_64_32 0000000000000000 .rodata + 0
000000000055 000a00000002 R_X86_64_PC32 0000000000000000 printf - 4
00000000005f 00050000000a R_X86_64_32 0000000000000000 .rodata + 0
000000000069 000a00000002 R_X86_64_PC32 0000000000000000 printf - 4
00000000006e 00050000000a R_X86_64_32 0000000000000000 .rodata + 3
000000000078 000a00000002 R_X86_64_PC32 0000000000000000 printf - 4
printfが複数ある。printfを参照している数だけある。
# nm hello.o
0000000000000000 T main
U printf
シンボルテーブルは定義の数のみ。
リンカの実体
ソースコードから実行形式を作成するまでの流れで書いたように、CentOS7のGCCの場合だと/usr/libexec/gcc/x86_64-redhat-linux/4.8.3/collect2
ただcollcect2はGNUのldである/usr/bin/ld
を使っている。LinuxではGNUのldが一般的みたい。ただし、リンカはOSに強く依存するものなのでOS毎にリンカが存在している。Microsoftではlink、solarisならsolarisのld。なのでGCCは環境に応じてリンカを使い分けるので、collect2はその共通のラッパーになっている。
実行形式のプログラムヘッダ
これもELF形式。各セクションを意味的にまとめたプログラムヘッダが作られる。プログラムヘッダをみてみた。
# readelf -l a.out
Elf ファイルタイプは EXEC (実行可能ファイル) です
エントリポイント 0x400440
9 個のプログラムヘッダ、始点オフセット 64
プログラムヘッダ:
タイプ オフセット 仮想Addr 物理Addr
ファイルサイズ メモリサイズ フラグ 整列
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
0x00000000000001f8 0x00000000000001f8 R E 8
INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238
0x000000000000001c 0x000000000000001c R 1
[要求されるプログラムインタプリタ: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x000000000000077c 0x000000000000077c R E 200000
LOAD 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
0x0000000000000224 0x0000000000000228 RW 200000
DYNAMIC 0x0000000000000e28 0x0000000000600e28 0x0000000000600e28
0x00000000000001d0 0x00000000000001d0 RW 8
NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254
0x0000000000000044 0x0000000000000044 R 4
GNU_EH_FRAME 0x0000000000000654 0x0000000000400654 0x0000000000400654
0x0000000000000034 0x0000000000000034 R 4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 10
GNU_RELRO 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
0x00000000000001f0 0x00000000000001f0 R 1
セグメントマッピングへのセクション:
セグメントセクション...
00
01 .interp
02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame
03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag .note.gnu.build-id
06 .eh_frame_hdr
07
08 .init_array .fini_array .jcr .dynamic .got
00~08が各セクションを意味的にまとめている番号。上と下は順番が対応している。LOADはローダーがメモリにロードする箇所。オフセットはELFの先頭からの位置。