intel-8086データシート
intel 8086のデータシートを読んでてちょくちょくと詰まった所があったので、それのメモ。
読んでた箇所は"Table2. Instruction Set Summary"で、MOV命令の箇所。どういうビット列がどの命令と解釈されるかと、特定の役割を持ったビットの説明について。
命令表
とりあえず、問題の命令表はこんな感じ
MOV命令
76543210 | 76543210 | 76543210 | 76543210 | |
Register/Memory to/from Register | 100010dw | mod reg r/m | ||
---|---|---|---|---|
Immediate to Register/memory | 1100011w | mod 000 r/m | data | data if w = 1 |
Immediate to Register | 1011 w reg | data | data if w = 1 | |
Memory to Accumulator | 1010000w | addr-low | addr-high | |
Accumulator to Memory | 1010001w | addr-low | addr-high | |
Register/Memory to Segment Register | 10001110 | mod 0 reg r/m | ||
Segment Register to Register/Memory | 10001100 | mod 0 reg r/m |
MOV命令はどういうビットで成り立っているかがこれよりわかる。なんか表が崩れたりするし、実際にどう書かれているかはデータシートそのものを見てもらった方がいい。
例えば"Register/Memory to/from Register"、つまりは、レジスタかメモリのどちらかからレジスタへの値のコピー。またはレジスタかメモリのどちらかへ、レジスタから値をコピーする命令は"100010"というビットで始まり、以降の10ビットによりメモリかレジスタかといったことや、レジスタならどのレジスタかという事を指定する。
ここで各命令の挙動を指定するビットである、"mod"や"r/m"やらの説明がデータシートの最終ページにあり、それらと合わせてビット列を読むことでどういう命令かが具体的にわかる。
動的ビットの説明
前述の各命令の挙動を示すビットについて、以下引用。
データシートからの引用
NOTES: AL = 8-bit accumulator AX = 16-bit accumulator CX = Count register DS = Data segment ES = Extra segment Above/below refers to unsigned value Greater = more positive; Less = less positive (more negative) signed values if d = 1 then ‘‘to’’ reg; if d e 0 then ‘‘from’’ reg if w = 1 then word instruction; if w e 0 then byte instruction if mod = 11 then r/m is treated as a REG field if mod = 00 then DISP = 0*, disp-low and disp-high are absent if mod = 01 then DISP = disp-low sign-extended to 16 bits, disp-high is absent if mod = 10 then DISP = disp-high; disp-low if r/m = 000 then EA = (BX) + (SI) + DISP if r/m = 001 then EA = (BX) + (DI) + DISP if r/m = 010 then EA = (BP) + (SI) + DISP if r/m = 011 then EA = (BP) + (DI) + DISP if r/m = 100 then EA = (SI) + DISP if r/m = 101 then EA = (DI) + DISP if r/m = 110 then EA = (BP) + DISP* if r/m = 111 then EA = (BX) + DISP DISP follows 2nd byte of instruction (before data if required) *except if mod = 00 and r/m = 110 then EA = disp-high; disp-low. if s w = 01 then 16 bits of immediate data form the operand if s w = 11 then an immediate data byte is sign extended to form the 16-bit operand if v e = then "count" = 1; if v = 1 then "count" in (CL) x = don’t care z is used for string primitives for comparison with ZF FLAG
略語
各要素は基本的に略語で書かれてるので、最初ちょっと途惑った。ということで、それらの説明。
略語 | 元の語 | 用法 |
---|---|---|
d | direction(多分) | レジスタへの操作か、レジスタからの操作かを示す |
w | word | データがword長か否かを示す |
mod | mode | 同命令内にあるdata部の扱いや、r/mの挙動を決める |
DISP | displacement | メモリの位置指定などに使用 |
r/m | register/memory(多分) | レジスタを使ってのメモリ指定パターン |
いくつか実例
MOV命令を読む際に使ったいくつかについて、実例を見てみる。
例1
16進数:b80100 2進数:10111000 00000001 00000000
の場合。先頭ビットから見ていくと、一致する命令は"Immediate to Register"であることがわかる。以降のビットを命令表のこの命令と照らし合わせてみると
w | 1 |
---|---|
reg | 000 |
data | 000000001 00000000 |
であることがわかる。regはレジスタを示すビット列で、これについてもデータシートの最終ページに対応表があるのでそちらを参照すると、wが1の場合、000はレジスタAXを示す。以上よりこの命令は
mov ax, 0x0001
となることがわかる。
例2
16進数:c7070200 2進数:11000111 00000111 00000010 00000000
の場合。同様に先頭ビットから見ていくと、一致する命令は"Immediate to Register/Memory"であることがわかる。以降のビットを命令表と照合してみると
w | 1 |
---|---|
mod | 00 |
r/m | 111 |
data | 00000010 00000000 |
であることがわかる。r/m=111の場合、"EA = (BP) + DISP"が当てはまる。また、mod=00の場合、DISPは0であるとして扱われるので、DISPを示す要素は存在しない。つまり"EA = (BP)"となり、dataはコピーされる即値に相当する。以上より
mov [bp], 0x0002
となることがわかる
例3
16進数:c78718010200 2真数:11000111 10000111 00011000 00000001 00000010 00000000
の場合。同様にしていくと、これもまた"Immediate to Register/Memory"である。命令決定以降のビットを照合していくと
w | 1 |
---|---|
mod | 10 |
r/m | 111 |
data | 00000010 00000000 |
w=1より、dataは末尾16ビットが相当するのだが、ここで丁度真ん中の16ビットが謎の存在になる。mod=01を見ると、"if mod = 10 then DISP = disp-high; disp-low"とあり、このDISPが9~32ビット目までに相当している。なぜここに差し込まれるかというと、"DISP follows 2nd byte of instruction (before data if required)"という説明にあるように、dataがあるならその前に入るようになっている。あとはr/m=111より"EA = (BX) + DISP"であることがわかる。以上より
mov [bx+0x118], 0x0002
となる
例4
16進数:c70600010a000 2進数:11000111 00000110 00000000 00000001 00001010 00000000
の場合、相変わらずこれも"Immediate to Register/Memory"である。以下同様にしていくと
w | 1 |
---|---|
mod | 00 |
r/m | 110 |
data | 00001010 00000000 |
例3と同様にしてdataは表の通りとわかる。mod=00なので"if mod = 00 then DISP = 0*, disp-low and disp-high are absent"に相当するかと思いきや、このアスタリスクが曲者。"*except if mod = 00 and r/m = 110 then EA = disp-high; disp-low."なんていう説明がありまして、mod=00かつr/m=110の場合は即値のコピー先が"EA = (BP) + DISP"から"EA = disp-high; disp-low"に変更となる。つまり、即値をDISPで指定されるメモリの位置にコピーしろという命令になる。よって以上より
mov [0x100], 0x000a
となる。
最後に
この8086のデータシートは"8086 datasheet"なんてググれば出てきますが、これって"mnemonic © intel, 1978"って、今更誰が使うんだレベルの古さですね。なんという誰得エントリ。
ELFファイルを作る part7
さて、前回のpart6でズルをする必要があったのは、分解する元のELFファイルがあり、各セクションの中身はその元のファイルのものをそのまま使用していた為である。
それ故、主に動的リンク関係のセクションについてのアドレスを全く同じになるよう調整する必要があった。翻せば、そのあたりを自分の理解の元に再構築出来れば問題はなくなる。
ということで、.dynamicセクションまわりがどうなってるのか調査してみる
目標
動的リンクに関するセクションの構造を把握する
調査
最初に
調査する対象は、part5で使っていたtest.out。
一応セクションの中身を再掲すると、以下の様になっている。
$ readelf -S test.out There are 15 section headers, starting at offset 0x4e8: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .interp PROGBITS 0000000000400190 00000190 000000000000001c 0000000000000000 A 0 0 1 [ 2] .note.gnu.build-i NOTE 00000000004001ac 000001ac 0000000000000024 0000000000000000 A 0 0 4 [ 3] .hash HASH 00000000004001d0 000001d0 0000000000000018 0000000000000004 A 4 0 8 [ 4] .dynsym DYNSYM 00000000004001e8 000001e8 0000000000000048 0000000000000018 A 5 1 8 [ 5] .dynstr STRTAB 0000000000400230 00000230 0000000000000024 0000000000000000 A 0 0 1 [ 6] .gnu.version VERSYM 0000000000400254 00000254 0000000000000006 0000000000000002 A 4 0 2 [ 7] .gnu.version_r VERNEED 0000000000400260 00000260 0000000000000020 0000000000000000 A 5 1 8 [ 8] .rela.plt RELA 0000000000400280 00000280 0000000000000030 0000000000000018 A 4 9 8 [ 9] .plt PROGBITS 00000000004002b0 000002b0 0000000000000030 0000000000000010 AX 0 0 16 [10] .text PROGBITS 00000000004002e0 000002e0 0000000000000014 0000000000000000 AX 0 0 16 [11] .eh_frame PROGBITS 00000000004002f8 000002f8 0000000000000000 0000000000000000 A 0 0 8 [12] .dynamic DYNAMIC 00000000006002f8 000002f8 0000000000000140 0000000000000010 WA 5 0 8 [13] .got.plt PROGBITS 0000000000600438 00000438 0000000000000028 0000000000000008 WA 0 0 8 [14] .shstrtab STRTAB 0000000000000000 00000460 0000000000000084 0000000000000000 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings), l (large) I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific)
各セクションの把握
まずは各セクションがどういう役割の下に作成されているのかをさらってみる。
セクション | 役割 |
---|---|
.interp | インタプリタセクション。実際には主に動的リンカが指定される。このセクションがあると(正しくはこのセクションを元にしたINTERPセグメントがあると)実行時にファイルを直接実行するのではなく、ここで指定される文字列の指すファイルへ引数として渡すことになり、実行させる。イメージとしてはシェルスクリプトのshebangのようなもの。ちなみにここで指定するファイルがld.soとかって感じならa.out形式。ld-linux.soという感じならELF形式の動的リンカ。 |
.note.gnu.build_id | ビルドされたファイルに対するユニークなIDが入っている。また、このセクションはプロセスイメージにロードされ、kernelがサポートしていればcore dumpに含まれることになる。つまり、core dumpから適切なバージョンのバイナリを探せるようになる。 |
.hash | 再配置時に動的リンカが使用するセクション。このhashはハッシュテーブルの意味。これにより.dynsymセクション内のシンボルを、線型検索をせず、迅速に見つけられる様になる。 |
.dynsym | 動的リンク用のシンボルテーブル。動的リンカはこのセクションの中身を元にリンクを行う。 |
.dynstr | 動的リンクに関連するシンボル名などを格納する文字列 |
.gnu.version | .dynsymで定義されているシンボルに対応するバージョンの一覧 |
.gnu.version_r | .gnu.versionが指すバージョン値についての情報が示されているセクション |
.rela.plt | .pltセクション用の再配置テーブル |
.plt | procedure linkage table。遅延リンクのために使われる。関数本体へのジャンプコードの集合。これにより関数呼び出しのシンボル解決を起動時ではなく実行時にすることで、起動時間が短縮される。 |
.text | プログラム本体 |
.eh_frame | 例外をサポートしている言語の場合、情報を保持しておくセクション。形式はDWARFのdebug_frameに似ているらしい。詳細はこちらを見てもらった方が。 |
.dynamic | 動的リンク用の諸々の情報 |
.got.plt | GOT(global offset table)は変数本体へのポインタの配列。.text領域に変数があるとシンボル解決時に.textの変更が必要になってしまうので、GOTにシンボル解決が必要なものを集めさせておく。これにより、.text領域を変更する必要を無くしている。.got.pltということで、.gotの関数版ということか? |
.shstrtbl | セクション名の文字列 |
dynamicセクションの中身
どう考えても動的リンクに関する最たるセクションと思われる、dynamicセクションについて中身を調べてみる。この中はreadelfすると以下の様になっている
$ readelf -d test.out Dynamic section at offset 0x2f8 contains 15 entries: Tag Type Name/Value 0x0000000000000001 (NEEDED) Shared library: [libc.so.6] 0x0000000000000004 (HASH) 0x4001d0 0x0000000000000005 (STRTAB) 0x400230 0x0000000000000006 (SYMTAB) 0x4001e8 0x000000000000000a (STRSZ) 36 (bytes) 0x000000000000000b (SYMENT) 24 (bytes) 0x0000000000000015 (DEBUG) 0x0 0x0000000000000003 (PLTGOT) 0x600438 0x0000000000000002 (PLTRELSZ) 48 (bytes) 0x0000000000000014 (PLTREL) RELA 0x0000000000000017 (JMPREL) 0x400280 0x000000006ffffffe (VERNEED) 0x400260 0x000000006fffffff (VERNEEDNUM) 1 0x000000006ffffff0 (VERSYM) 0x400254 0x0000000000000000 (NULL) 0x0
ぱっと見た感じでも、動的リンクに必要な情報を記したセクションのアドレスやサイズ等の情報をまとめたセクションであることが見て取れる。これらの各要素は
typedef struct { Elf64_Sxword d_tag; /* Dynamic entry type */ union { Elf64_Xword d_val; /* Integer value */ Elf64_Addr d_ptr; /* Address value */ } d_un; } Elf64_Dyn;
という構造体から成る(/usr/include/elf.hより)。d_tagでこの要素のタイプを表し、d_un共用体がこの要素の値として整数値かアドレスを保持することとなる。
また、この構造体の記述のすぐ下にd_tagの値が定義されており、それらは以下の通り
/* Legal values for d_tag (dynamic entry type). */ #define DT_NULL 0 /* Marks end of dynamic section */ #define DT_NEEDED 1 /* Name of needed library */ #define DT_PLTRELSZ 2 /* Size in bytes of PLT relocs */ #define DT_PLTGOT 3 /* Processor defined value */ #define DT_HASH 4 /* Address of symbol hash table */ #define DT_STRTAB 5 /* Address of string table */ #define DT_SYMTAB 6 /* Address of symbol table */ #define DT_RELA 7 /* Address of Rela relocs */ #define DT_RELASZ 8 /* Total size of Rela relocs */ #define DT_RELAENT 9 /* Size of one Rela reloc */ #define DT_STRSZ 10 /* Size of string table */ #define DT_SYMENT 11 /* Size of one symbol table entry */ #define DT_INIT 12 /* Address of init function */ #define DT_FINI 13 /* Address of termination function */ #define DT_SONAME 14 /* Name of shared object */ #define DT_RPATH 15 /* Library search path (deprecated) */ #define DT_SYMBOLIC 16 /* Start symbol search here */ #define DT_REL 17 /* Address of Rel relocs */ #define DT_RELSZ 18 /* Total size of Rel relocs */ #define DT_RELENT 19 /* Size of one Rel reloc */ #define DT_PLTREL 20 /* Type of reloc in PLT */ #define DT_DEBUG 21 /* For debugging; unspecified */ #define DT_TEXTREL 22 /* Reloc might modify .text */ #define DT_JMPREL 23 /* Address of PLT relocs */ #define DT_BIND_NOW 24 /* Process relocations of object */ #define DT_INIT_ARRAY 25 /* Array with addresses of init fct */ #define DT_FINI_ARRAY 26 /* Array with addresses of fini fct */ #define DT_INIT_ARRAYSZ 27 /* Size in bytes of DT_INIT_ARRAY */ #define DT_FINI_ARRAYSZ 28 /* Size in bytes of DT_FINI_ARRAY */ #define DT_RUNPATH 29 /* Library search path */ #define DT_FLAGS 30 /* Flags for the object being loaded */ #define DT_ENCODING 32 /* Start of encoded range */ #define DT_PREINIT_ARRAY 32 /* Array with addresses of preinit fct*/ #define DT_PREINIT_ARRAYSZ 33 /* size in bytes of DT_PREINIT_ARRAY */ #define DT_NUM 34 /* Number used */ #define DT_LOOS 0x6000000d /* Start of OS-specific */ #define DT_HIOS 0x6ffff000 /* End of OS-specific */ #define DT_LOPROC 0x70000000 /* Start of processor-specific */ #define DT_HIPROC 0x7fffffff /* End of processor-specific */ #define DT_PROCNUM DT_MIPS_NUM /* Most used by any processor */
まだまだあるが、全部載せると長くなるのでとりあえずここまで。
今回test.outから得られたタイプそれぞれについての意味は以下のとおり。
タイプ | 説明 |
---|---|
NEEDED | このファイルで必要とされる動的リンク対象の動的ライブラリ。この要素の値には、動的ライブラリの名前は.dynstrセクションが保持する文字列のどこから始まるかのオフセットを保持している。 |
HASH | .hashセクションのアドレス |
STRTAB | .dynstrセクションのアドレス |
SYMTAB | .dynsymセクションのアドレス |
STRSZ | .dynstrセクションが保持する文字列のサイズ |
SYMENT | .dynsymセクションが持つエントリ1つ分のサイズ |
DEBUG | デバッグ用。今回は未使用? |
PLTGOT | .got.pltセクションのアドレス |
PLTRELSZ | .pltセクションのサイズ |
PLTREL | .pltセクションのタイプ |
JMPREL | .rela.pltセクションのアドレス |
VERNEED | .gnu.version_rセクションのアドレス |
VERNEEDNUM | バージョン情報の数 |
VERSYM | .gnu.versionセクションのアドレス |
NULL | .dynamicセクションが保持する構造体の列はここで終端であることを示すための要素 |
専用セグメントの役割
test.outのプログラムヘッダーの部分を見てみると
readelf -l test.out Elf file type is EXEC (Executable file) Entry point 0x4002e0 There are 6 program headers, starting at offset 64 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040 0x0000000000000150 0x0000000000000150 R E 8 INTERP 0x0000000000000190 0x0000000000400190 0x0000000000400190 0x000000000000001c 0x000000000000001c R 1 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2] LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x00000000000002f8 0x00000000000002f8 R E 200000 LOAD 0x00000000000002f8 0x00000000006002f8 0x00000000006002f8 0x0000000000000168 0x0000000000000168 RW 200000 DYNAMIC 0x00000000000002f8 0x00000000006002f8 0x00000000006002f8 0x0000000000000140 0x0000000000000140 RW 8 NOTE 0x00000000000001ac 0x00000000004001ac 0x00000000004001ac 0x0000000000000024 0x0000000000000024 R 4
となっており、INTERPとDYNAMICは専用のセグメントを持っていることがわかる。実際の実行時、ローダーによって把握されるのはセグメントの単位であるため(セクション単位では見てくれないため)、動的リンクに関わってくるINTERPやDYNAMICセグメントをローダーから扱う為に設けられている。
ローダーが実行形式のファイルをロードした際、INTERPセグメントがあるならば、実行形式ファイルそのものを引数としてインタプリタを起動する。この場合は/lib64/ld-linux-x86-64.so.2がインタプリタである。
動的リンカがDYNAMICセグメントを読みに行くまでの流れは大体こんな感じ。4の段階で、既にtest.outは引数として渡されてる筈なんで、この図のように外へ見に行くのはちょっと違うと思うが。
動的リンク
では実際にtest.out実行時にはどのようにして必要とする共有ライブラリとリンクするのか?
動的リンカがDYNAMICセグメントを見つけるまでは先に示した通り。そして、このセグメント(=.dynamicセクション)の情報を元に動的リンクが行われていくことになる。
その挙動はこんな感じ。
再配置
さて、立ち返って考えてみると、そもそも動的リンクは何故動的にリンクしてるのだったか。そう、プログラム中から外部の共有ライブラリにある処理を呼び出す際に必要だから動的リンクしているんだった。
ということで、textセクションから考えてみる。
まずはtest.outのtextセクション部分を見てみよう。
$ objdump -S test.out test.out: file format elf64-x86-64 Disassembly of section .plt: 00000000004002b0 <putchar@plt-0x10>: 4002b0: ff 35 8a 01 20 00 pushq 0x20018a(%rip) # 600440 <exit@plt+0x200170> 4002b6: ff 25 8c 01 20 00 jmpq *0x20018c(%rip) # 600448 <exit@plt+0x200178> 4002bc: 0f 1f 40 00 nopl 0x0(%rax) 00000000004002c0 <putchar@plt>: 4002c0: ff 25 8a 01 20 00 jmpq *0x20018a(%rip) # 600450 <exit@plt+0x200180> 4002c6: 68 00 00 00 00 pushq $0x0 4002cb: e9 e0 ff ff ff jmpq 4002b0 <putchar@plt-0x10> 00000000004002d0 <exit@plt>: 4002d0: ff 25 82 01 20 00 jmpq *0x200182(%rip) # 600458 <exit@plt+0x200188> 4002d6: 68 01 00 00 00 pushq $0x1 4002db: e9 d0 ff ff ff jmpq 4002b0 <putchar@plt-0x10> Disassembly of section .text: 00000000004002e0 <.text>: 4002e0: bf 54 00 00 00 mov $0x54,%edi 4002e5: e8 d6 ff ff ff callq 4002c0 <putchar@plt> 4002ea: bf 2a 00 00 00 mov $0x2a,%edi 4002ef: e8 dc ff ff ff callq 4002d0 <exit@plt>
見ての通り、textセクションの処理ではputcharとexitをcallしている事がわかる。そして、そのcall先は0x4002c0と0x4002da。上記objdumpの結果にも書いてあるが、セクション一覧の方を確認してもこれらのアドレスは.pltセクションに位置していることがわかる。
先に示したとおり、.pltセクションは関数本体へのジャンプコードの集合である。これもまたobjdumpの結果より、exitは0x600458でputcharは0x600450へジャンプする指定がされていることを読み取れる。そしてこれらのアドレスは、.got.pltセクションに位置している。
ならば次は.got.pltセクションの中身。このセクションを見てみると
$ readelf -x 13 test.out Hex dump of section '.got.plt': 0x00600438 f8026000 00000000 00000000 00000000 ..`............. 0x00600448 00000000 00000000 c6024000 00000000 ..........@..... 0x00600458 d6024000 00000000 ..@.....
となっている。一番左の段落がアドレスで、右四つの段落がデータの16進表現。この表示ではリトルエンディアンになっていることに注意。
.pltでexitがジャンプ指定していた0x600458を見ると、d6024000つまり、0x4002d6のアドレスを得られる。このアドレスは先に見た.pltセクションにあった
4002d6: 68 01 00 00 00 pushq $0x1
という箇所に相当する。ここでpushしている1という値は.rela.pltセクションの要素を指すオフセット値であり、以下に示す.rela.pltセクションの中身を鑑みると0x1をpushすることによってexitの再配置エントリを表している。
Relocation section '.rela.plt' at offset 0x280 contains 2 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000600450 000100000007 R_X86_64_JUMP_SLO 0000000000000000 putchar + 0 000000600458 000200000007 R_X86_64_JUMP_SLO 0000000000000000 exit + 0
ここでoffset要素の値は0x600458と、.got.pltセクション中のexitの飛び先を示すアドレスを格納したスロットを指している。
先ほどの1をpushしている命令の次は0x4002b0
とまあ、この最後の動的リンカのアドレスが埋め込まれれば動的ライブラリとの関連付けは完了することになる。
.dynsym
さて、再配置やらリンクやらとは言うものの、動的リンクした共有ライブラリの全てを使うわけではもちろんない。ではどうやって必要な対象を判断しているのか?そこで.dynsymセクションの出番になる。
.dynamicセクションの要素には.dynsymセクションを示すアドレスを持つ要素(SYMTAB)が存在し、.dynsymセクションにはこのファイルがインポート、エクスポートしているシンボルすべての情報が含まれている。また、その要素はElf64_Symと同様で、次の構造体で定義されている。
typedef struct { Elf64_Word st_name; /* Symbol name (string tbl index) */ unsigned char st_info; /* Symbol type and binding */ unsigned char st_other; /* Symbol visibility */ Elf64_Section st_shndx; /* Section index */ Elf64_Addr st_value; /* Symbol value */ Elf64_Xword st_size; /* Symbol size */ } Elf64_Sym;
.dynsymセクションの情報をreadelfから閲覧してみると
Symbol table '.dynsym' contains 3 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND putchar@GLIBC_2.2.5 (2) 2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND exit@GLIBC_2.2.5 (2)
となっている。また、16進表示で閲覧してみると
Hex dump of section '.dynsym': 0x004001e8 00000000 00000000 00000000 00000000 ................ 0x004001f8 00000000 00000000 10000000 12000000 ................ 0x00400208 00000000 00000000 00000000 00000000 ................ 0x00400218 0b000000 12000000 00000000 00000000 ................ 0x00400228 00000000 00000000 ........
である。このセクションはエントリサイズが0x18なので、putcharについての情報は0x400200から0x400217であり、exitについての情報は0x400218から0x40022fまでが該当する。
これより、putcharについてのエントリを見ると、各値は
st_name : 0x10 st_info : 0x12 st_other : 0x00 st_shndx : 0x00 st_value : 0x00 st_size : 0x00
となっている。
- st_name
shstrtabと同じような指定でst_nameの値をオフセットとして.dynstrから取得してくる。
- st_info
elf.hより
/* How to extract and insert information held in the st_info field. */ #define ELF32_ST_BIND(val) (((unsigned char) (val)) >> 4) #define ELF32_ST_TYPE(val) ((val) & 0xf) #define ELF32_ST_INFO(bind, type) (((bind) << 4) + ((type) & 0xf)) /* Both Elf32_Sym and Elf64_Sym use the same one-byte st_info field. */ #define ELF64_ST_BIND(val) ELF32_ST_BIND (val) #define ELF64_ST_TYPE(val) ELF32_ST_TYPE (val) #define ELF64_ST_INFO(bind, type) ELF32_ST_INFO ((bind), (type))
というように解釈され、Bind, Typeの値を取得するのに使われる。
- st_other
/* How to extract and insert information held in the st_other field. */ #define ELF32_ST_VISIBILITY(o) ((o) & 0x03)/* For ELF64 the definitions are the same. */ #define ELF64_ST_VISIBILITY(o) ELF32_ST_VISIBILITY (o) /* Symbol visibility specification encoded in the st_other field. */ #define STV_DEFAULT 0 /* Default symbol visibility rules */ #define STV_INTERNAL 1 /* Processor specific hidden class */ #define STV_HIDDEN 2 /* Sym unavailable in other modules */ #define STV_PROTECTED 3 /* Not preemptible, not exported */
というように解釈され、今回の場合はreadelfした情報でVisの項目がDEFAULTとなっているので、ELF64_ST_VISIBILITYした結果が0になっているということになる
- st_shndx
全てのシンボルテーブルはいずれかのセクションと関連を持つことになり、また、全てのセクションは型を持ち、その型を表す値を保持している(SHT_SYMTAB、SHT_DYNSYMとか)。その型を示すインデックス値。
- st_value
関連付けられているシンボルの値。アドレスなど、状況によって保持する値が意味するものは変わる。
- st_size
シンボルに関連付けられる何かの大きさ。シンボルが大きさを持っていない場合または大きさが不明な場合、0を保持する。
また、複数の共有ライブラリをリンクするときにはどのファイルにお目当てのシンボルがあるのか、その判断が必要となるが、これについては以前に調査した.gnu.version周りが担当してくれてるのではないかな。多分。中身からしてそうっぽいので特に調べてないけど。
ということで
動的リンクに必要な所を追ってみたけど、結構ややこしくなってるのであとでもう一度見直してみようと思う。
ELFファイルを作る part6
part5からの続き
目標
ELFファイルから各セクションを抽出し、それらのセクションの情報からELFファイル全体を再構築する。
環境
- opensuse12.3
- ld-2.23.1
実装
元になるELFファイルや、各セクションの抽出についてはpart5を参照。やることはこの、part5で抽出してきたセクションの情報のみ(ELFヘッダーの変更無いところなどは流用)を元にELFファイル全体を再構築するということ。
今回作成したコードについてはそこそこファイル数があったのでこちらのgithubにまとめてUPしておいた。挙動は以降に記すとおり。test.asmをコンパイルしたtest.outがmain.pyに置いてあるという前提で
$ python main.py
で実行。
Restructor.py
- 21~24行目
se = SectionExtractor(self.byteList) for name, body, sh in se.extract(): secm.append(name, body, sh)
で、part5で取得したようなセクションのリストをtest.outのbyteListから抽出してseへ格納し、それらをelf.components.SectionManagerへそれぞれ渡している。
- 26行目
上でセクションを格納したSectionManagerのインスタンスをSegmentManagerのmappingへ渡す。このmappingで行っている事は、セクションとセグメントの対応part2で得た関係を元に、セクションがどこのセグメントへ当てはめられるべきなのかをマッピングしている。その辺の対応はelf.Relation.pyに記述してあるので、それを元に。ちなみにSegmentManagerの86,87行目はちょっとズルしてます。とりあえずはこのtest.outを再構築するってことで、ここを書いていた時点では算出方法がわかっていなかったので。
- 28行目
上でセグメントの数はわかるので、ELFヘッダーとプログラムヘッダーに使われるサイズを計算
- 29行目
SectionManagerのmakeBodyで本体部分のデータを作成。alignを調整したりして。ここは本来、リンカスクリプトを元にしてセクションの順番やら開始アドレス等々が決定されるが、これまたリンカスクリプトを使うようにしていると今回は不要なものも多いので、対応をRelation.pyに書いて逃げておく。また、このmakeBodyでも49行目辺りでズルをしているが、これについては後述。
- 30行目
プログラムヘッダーの作成
- 31行目
アドレス位置の調整
- 33行目
shstrtblの作成
- 35行目
ここまでのELFヘッダー、プログラムヘッダー、body部分、shstrtblを足したサイズを求め、これ以降に続くセクションヘッダーへのオフセットを取得しておく
- 37行目~
ELFヘッダーの作成や、ここまで作成していたもののbyteListをくっつける。55行目のはセクションヘッダー先頭のNULL要素を作成して追加している。
- 59行目~
ファイルへの出力部分
ズルの釈明
何があったのか?
さて、先に述べたズルしていた部分。これについて。
まずは今回の大元、test.asmからgccを使って作成したtest.outの中身をreadelfでのぞいてみると
$ readelf -l test.out Elf file type is EXEC (Executable file) Entry point 0x400340 There are 7 program headers, starting at offset 64 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040 0x0000000000000188 0x0000000000000188 R E 8 INTERP 0x00000000000001c8 0x00000000004001c8 0x00000000004001c8 0x000000000000001c 0x000000000000001c R 1 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2] LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x0000000000000358 0x0000000000000358 R E 200000 LOAD 0x0000000000000eb0 0x0000000000600eb0 0x0000000000600eb0 0x0000000000000178 0x0000000000000178 RW 200000 DYNAMIC 0x0000000000000eb0 0x0000000000600eb0 0x0000000000600eb0 0x0000000000000150 0x0000000000000150 RW 8 NOTE 0x00000000000001e4 0x00000000004001e4 0x00000000004001e4 0x0000000000000024 0x0000000000000024 R 4 GNU_RELRO 0x0000000000000eb0 0x0000000000600eb0 0x0000000000600eb0 0x0000000000000150 0x0000000000000150 R 1
というように、LOADセグメントについて、flagがREの方はオフセットが0でサイズ0x358となっている。順序的に続く事になるflagがRWのLOADセグメントはオフセットが0xeb0から始まっている。この箇所について、セクション単位ではどうなっているかを見ると
readelf -S test.out There are 16 section headers, starting at offset 0x10b0: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .interp PROGBITS 00000000004001c8 000001c8 000000000000001c 0000000000000000 A 0 0 1 [ 2] .note.gnu.build-i NOTE 00000000004001e4 000001e4 0000000000000024 0000000000000000 A 0 0 4 [ 3] .hash HASH 0000000000400208 00000208 0000000000000018 0000000000000004 A 5 0 8 [ 4] .gnu.hash GNU_HASH 0000000000400220 00000220 000000000000001c 0000000000000000 A 5 0 8 [ 5] .dynsym DYNSYM 0000000000400240 00000240 0000000000000048 0000000000000018 A 6 1 8 [ 6] .dynstr STRTAB 0000000000400288 00000288 0000000000000024 0000000000000000 A 0 0 1 [ 7] .gnu.version VERSYM 00000000004002ac 000002ac 0000000000000006 0000000000000002 A 5 0 2 [ 8] .gnu.version_r VERNEED 00000000004002b8 000002b8 0000000000000020 0000000000000000 A 6 1 8 [ 9] .rela.plt RELA 00000000004002d8 000002d8 0000000000000030 0000000000000018 A 5 10 8 [10] .plt PROGBITS 0000000000400310 00000310 0000000000000030 0000000000000010 AX 0 0 16 [11] .text PROGBITS 0000000000400340 00000340 0000000000000014 0000000000000000 AX 0 0 16 [12] .eh_frame PROGBITS 0000000000400358 00000358 0000000000000000 0000000000000000 A 0 0 8 [13] .dynamic DYNAMIC 0000000000600eb0 00000eb0 0000000000000150 0000000000000010 WA 6 0 8 [14] .got.plt PROGBITS 0000000000601000 00001000 0000000000000028 0000000000000008 WA 0 0 8 [15] .shstrtab STRTAB 0000000000000000 00001028 0000000000000088 0000000000000000 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings), l (large) I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific)
となっている。先のオフセットとなるのは.dynamicの箇所だが、アドレスが0x600000台になるのはわかるけど、このオフセットがよくわからない。
原因は?
リンク時の指示と言えばリンカスクリプトということで、リンカスクリプトをのぞいてみる。
問題の箇所は.eh_frameセクションと.dynamicセクションの間ということで、その間を調べてみると
/* Adjust the address for the data segment. We want to adjust up to the same address within the page on the next page up. */ . = ALIGN (CONSTANT (MAXPAGESIZE)) - ((CONSTANT (MAXPAGESIZE) - .) & (CONSTANT (MAXPAGESIZE) - 1)); . = DATA_SEGMENT_ALIGN (CONSTANT (MAXPAGESIZE), CONSTANT (COMMONPAGESIZE));
というのがある。
また、リンク時にアドレスの指定がどのような挙動になっているかを調べるため、gccを以下のようなオプション付きで実行する
$ gcc -Wl,-M -Wl,"-T/usr/lib64/ldscripts/elf_x86_64.x" -s -nostartfiles -o test.out test.o
この結果を見ると
0x0000000000400358 . = (ALIGN (0x200000) - ((0x200000 - .) & 0x1fffff)) 0x0000000000600eb0 . = DATA_SEGMENT_ALIGN (0x200000, 0x1000)
となっているので、どうもこのDATA_SEGMENT_ALIGNってのが一枚噛んでいるのでは。
DATA_SEGMENT_ALIGN
これについてはbinutilsのコードを調べてみると、ld/ld.infoにて
`DATA_SEGMENT_ALIGN(MAXPAGESIZE, COMMONPAGESIZE)' This is equivalent to either (ALIGN(MAXPAGESIZE) + (. & (MAXPAGESIZE - 1))) or (ALIGN(MAXPAGESIZE) + (. & (MAXPAGESIZE - COMMONPAGESIZE))) depending on whether the latter uses fewer COMMONPAGESIZE sized pages for the data segment (area between the result of this expression and `DATA_SEGMENT_END') than the former or not. If the latter form is used, it means COMMONPAGESIZE bytes of runtime memory will be saved at the expense of up to COMMONPAGESIZE wasted bytes in the on-disk file.
との記述が。DATA_SEGMENT_ALIGNはこの2式のどっちかと等価ということの様で。
このDATA_SEGMENT_ALIGNの実装はld/ldexp.cにいくつかあるcase文で、DATA_SEGMENT_ALIGNとなっている所。
それで?
ここまで追ってみたが、計算してみてもどうにも先の様にオフセットに余計?なスペースが出来る計算にはなってくれない。なぜか。
何の気なしに、ソースコードを見るために手元にあったbinutilsをインストールしてみて、ldはそっちを使うようにしてtest.outを作ってみた。そちらをreadelfしてみると・・・
$ readelf -l test.out Elf file type is EXEC (Executable file) Entry point 0x4002e0 There are 6 program headers, starting at offset 64 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040 0x0000000000000150 0x0000000000000150 R E 8 INTERP 0x0000000000000190 0x0000000000400190 0x0000000000400190 0x000000000000001c 0x000000000000001c R 1 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2] LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x00000000000002f8 0x00000000000002f8 R E 200000 LOAD 0x00000000000002f8 0x00000000006002f8 0x00000000006002f8 0x0000000000000168 0x0000000000000168 RW 200000 DYNAMIC 0x00000000000002f8 0x00000000006002f8 0x00000000006002f8 0x0000000000000140 0x0000000000000140 RW 8 NOTE 0x00000000000001ac 0x00000000004001ac 0x00000000004001ac 0x0000000000000024 0x0000000000000024 R 4
readelf -S test.out There are 15 section headers, starting at offset 0x4e8: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .interp PROGBITS 0000000000400190 00000190 000000000000001c 0000000000000000 A 0 0 1 [ 2] .note.gnu.build-i NOTE 00000000004001ac 000001ac 0000000000000024 0000000000000000 A 0 0 4 [ 3] .hash HASH 00000000004001d0 000001d0 0000000000000018 0000000000000004 A 4 0 8 [ 4] .dynsym DYNSYM 00000000004001e8 000001e8 0000000000000048 0000000000000018 A 5 1 8 [ 5] .dynstr STRTAB 0000000000400230 00000230 0000000000000024 0000000000000000 A 0 0 1 [ 6] .gnu.version VERSYM 0000000000400254 00000254 0000000000000006 0000000000000002 A 4 0 2 [ 7] .gnu.version_r VERNEED 0000000000400260 00000260 0000000000000020 0000000000000000 A 5 1 8 [ 8] .rela.plt RELA 0000000000400280 00000280 0000000000000030 0000000000000018 A 4 9 8 [ 9] .plt PROGBITS 00000000004002b0 000002b0 0000000000000030 0000000000000010 AX 0 0 16 [10] .text PROGBITS 00000000004002e0 000002e0 0000000000000014 0000000000000000 AX 0 0 16 [11] .eh_frame PROGBITS 00000000004002f8 000002f8 0000000000000000 0000000000000000 A 0 0 8 [12] .dynamic DYNAMIC 00000000006002f8 000002f8 0000000000000140 0000000000000010 WA 5 0 8 [13] .got.plt PROGBITS 0000000000600438 00000438 0000000000000028 0000000000000008 WA 0 0 8 [14] .shstrtab STRTAB 0000000000000000 00000460 0000000000000084 0000000000000000 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings), l (large) I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific)
さっきのような、余計?なオフセットが無い・・・。
ちなみにどちらもbinutilsのバージョンは同じ。
- 元からあった方
GNU ld (GNU Binutils; openSUSE 12.3) 2.23.1
- コードからインストールした方
GNU ld (GNU Binutils) 2.23.1
(追記)
出来上がった実行ファイルのサイズは
- コードからインストールしたldを使った場合:2216byte
- パッケージで入ってたldを使った場合:5296byte
と、結構な差があった
というわけで
結論。あのオフセットは今のところ謎のまま。
この謎がある状態のバイナリに再構築しようとしていたので、あのオフセットの算出方法が分からずにズルをしてたと。
実行
一応動くようにはなったものの、こういったズルの値は状況に因って異なってしまうので、おそらく自分の手元の環境でしか動きません。(あのズルの値を調整すれば動くはず)
$ ./elf.out T
つぎはその辺りをちゃんとどんな環境でも動くように、各セクションの中身を調整する、もしくは作り上げるようにするのが良さそう。
sh_addralign
ELFファイルを作る part5で取得した、セクション毎に分解されたデータを元にELFファイルを再構築していた際、出来上がったファイルをreadelf -aしたら
readelf: Error: Unable to read in 0x10 bytes of version need aux (2) readelf: Error: Unable to read in 0x10 bytes of version need
なんてのが大量に出まくった。
ということで、これの原因と共に近辺を調べてみる。
目標
readelf: Error: Unable to read in 0x10 bytes of version need aux (2)
の原因調査と解消
調査
前提
分解する元となったファイル(test.out)のreadlelf全体像はこちら。
問題のエラーはこのファイルのセクションを抽出、分解し、自分で再構築してみたファイル(elf.out)に対して発生したもの。
どこがおかしいのか?
先のエラーが出た際、readelfのオプションは -a で行っていた。部分毎に出力してみたら問題の箇所がわかるかな?と。
いくつかのオプションで試してみたところ正常に出力されるところもあるようで、問題のエラーが出てきたのは -V オプションで実行したとき。これらが表示するセクションはバージョン情報。正常に動作する分解元のファイルで試してみると、こんな感じ。
$ readelf -V test.out Version symbols section '.gnu.version' contains 3 entries: Addr: 00000000004002ac Offset: 0x0002ac Link: 5 (.dynsym) 000: 0 (*local*) 2 (GLIBC_2.2.5) 2 (GLIBC_2.2.5) Version needs section '.gnu.version_r' contains 1 entries: Addr: 0x00000000004002b8 Offset: 0x0002b8 Link: 6 (.dynstr) 000000: Version: 1 File: libc.so.6 Cnt: 1 0x0010: Name: GLIBC_2.2.5 Flags: none Version: 2
これらのセクションについてのセクションヘッダ情報は
$ readelf -S test.out ...略... [ 7] .gnu.version VERSYM 00000000004002ac 000002ac 0000000000000006 0000000000000002 A 5 0 2 [ 8] .gnu.version_r VERNEED 00000000004002b2 000002b2 0000000000000020 0000000000000000 A 6 1 8 ...略...
なにこれ?
.gnu.version
まずはググってみる。たどり着いたページがこちら。11.7.2より、.gnu.versionはSymbol version tableというものらしい。そして、このセクションは.dynsymセクションと同じ数の要素を持つとのこと。.gnu.versionセクションの中身はElf64_Half型の配列で、定義済みのバージョンか、.dynsymで定義されているシンボル番号と一致するものが入っている。
平たく言うと、.dynsymで定義されるシンボルに対応するバージョンの一覧は.gnu.versionに格納されている。
- .dynsym
Symbol table '.dynsym' contains 3 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND exit@GLIBC_2.2.5 (2) 2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND putchar@GLIBC_2.2.5 (2)
- .gnu.version
Version symbols section '.gnu.version' contains 3 entries: Addr: 00000000004002ac Offset: 0x0002ac Link: 5 (.dynsym) 000: 0 (*local*) 2 (GLIBC_2.2.5) 2 (GLIBC_2.2.5)
.dynsym各要素のNameパラメーター末尾にある(2)とか。この括弧内の数値が.gnu.versionの対応するインデックスが持つ値である。参考URLにもあるとおり、0と1は特殊な値。
参考として、他の実行ファイルでも確認してみると
$ readelf -a `which ld` Symbol table '.dynsym' contains 312 entries: Num: Value Size Type Bind Vis Ndx Name ...略... 40: 0000000000000000 0 FUNC GLOBAL DEFAULT UND getopt_long_only@GLIBC_2.2.5 (2) 41: 0000000000000000 0 FUNC GLOBAL DEFAULT UND fseek@GLIBC_2.2.5 (2) 42: 0000000000000000 0 FUNC GLOBAL DEFAULT UND bfd_elf_set_dyn_lib_class 43: 0000000000000000 0 FUNC GLOBAL DEFAULT UND mkstemps@GLIBC_2.11 (4) ...略... Version symbols section '.gnu.version' contains 312 entries: Addr: 0000000000403eb0 Offset: 0x003eb0 Link: 7 (.dynsym) ...略... 028: 2 (GLIBC_2.2.5) 2 (GLIBC_2.2.5) 0 (*local*) 4 (GLIBC_2.11)
という具合になる。.gnu.versionの方の見出しの値は16進なので、0x28 = 40。つまり、.dynsumのnum40から順に、2,2,0,4が対応する。ということで、各バージョンの値が対応する箇所の値になっていることが読み取れる。
.gnu.version_r
このセクションが持つ値の構造体はElf64_Verneed構造体の配列となる。また、オプションとして、Elf64_Vernaux構造体が続く。
typedef struct { Elfxx_Half vn_version; Elfxx_Half vn_cnt; Elfxx_Word vn_file; Elfxx_Word vn_aux; Elfxx_Word vn_next; } Elf64_Verneed;
typedef struct { Elfxx_Word vna_hash; Elfxx_Half vna_flags; Elfxx_Half vna_other; Elfxx_Word vna_name; Elfxx_Word vna_next; } Elf64_Vernaux;
ここで先の.gnu.versionが指すversionの値についての情報が示される。readelfより、
Version needs section '.gnu.version_r' contains 1 entries: Addr: 0x00000000004002b8 Offset: 0x0002b8 Link: 6 (.dynstr) 000000: Version: 1 File: libc.so.6 Cnt: 1 0x0010: Name: GLIBC_2.2.5 Flags: none Version: 2
となり、最終行の"Version: 2"というのが.gnu.versionで示されていた値と対応する。つまり、.gnu.versionで値が2となっているなら、.gnu.version_rのVersionが2となっているものと対応する。
先ほどと同様に、ldコマンドについてみてみると
$ readelf -V `which ld` ...略... Version needs section '.gnu.version_r' contains 2 entries: Addr: 0x0000000000404120 Offset: 0x004120 Link: 8 (.dynstr) 000000: Version: 1 File: libdl.so.2 Cnt: 1 0x0010: Name: GLIBC_2.2.5 Flags: none Version: 5 0x0020: Version: 1 File: libc.so.6 Cnt: 4 0x0030: Name: GLIBC_2.4 Flags: none Version: 6 0x0040: Name: GLIBC_2.11 Flags: none Version: 4 0x0050: Name: GLIBC_2.3.4 Flags: none Version: 3 0x0060: Name: GLIBC_2.2.5 Flags: none Version: 2
ということで、.gnu.version_rのVersionと先に提示したldコマンドの.gnu.versionの値は対応している事が読み取れる。
セクションの関連
このように、セクション間で関連を持つ場合はセクションヘッダーのsh_link要素にその関連が示される。
[ 5] .dynsym DYNSYM 0000000000400240 00000240 0000000000000048 0000000000000018 A 6 1 8 [ 6] .dynstr STRTAB 0000000000400288 00000288 0000000000000024 0000000000000000 A 0 0 1 [ 7] .gnu.version VERSYM 00000000004002ac 000002ac 0000000000000006 0000000000000002 A 5 0 2 [ 8] .gnu.version_r VERNEED 00000000004002b8 000002b8 0000000000000020 0000000000000000 A 6 1 8
問題のエラーの原因は?
さて、.gnu.version周りは.dymsymが保持するダイナミックリンクのシンボルについて、バージョン情報を保持していることはわかった。では、件のエラーメッセージは何なのか?
今一度見直してみると
readelf: Error: Unable to read in 0x10 bytes of version need aux (2)
この文章から、先に述べているElf64_Verneedのvn_aux要素が示す0x10が読めないとかなんとか。
このvn_auxについてはよく世話になってるこちらのoracleの資料より、「Elf64_Verneedエントリの先頭から、関連付けられているファイル依存性から要求されるバージョン定義の Elf64_Vernaux 配列までのバイトオフセット」だそうだ。回りくどいけど、vn_auxが含まれる構造体の先頭から、関連するElf64_Vernaux構造体までのオフセットということかな。
諸悪の根源
それは一体何かというと、自分の勘違いな訳で。
さて、最初に述べました再構築をしている際、セクションのaddress alignについて自分はこう捉えていた。
「offsetの示す場所からセクションが保持する情報(=本体)を配置し、その終了地点のアドレスがaddress alignの示す値で割り切れる値で無いなら0x00でパディングして割り切れる様に調整する」
正常に動作する方と異常な方の違いを見る
上で述べたような考えを元に再構築していた所、セクション情報は以下のようになっていた
$ readelf -S elf.out ...略... [ 7] .gnu.version VERSYM 00000000004002ac 000002ac 0000000000000006 0000000000000002 A 5 0 2 [ 8] .gnu.version_r VERNEED 00000000004002b2 000002b2 0000000000000020 0000000000000000 A 6 1 8
対して、正常に動作する方は以下のように。
$ readelf -S test.out ...略... [ 7] .gnu.version VERSYM 00000000004002ac 000002ac 0000000000000006 0000000000000002 A 5 0 2 [ 8] .gnu.version_r VERNEED 00000000004002b8 000002b8 0000000000000020 0000000000000000 A 6 1 8 ...略...
これらの該当箇所を実際にバイナリエディタで見ると
- elf.out
- test.out
となっている。赤い箇所が.gnu.versionに相当し、青い箇所が.gnu.version_rに相当する。赤い箇所の開始位置はどちらも同様で0x2ac開始でサイズ6となっている。ここで、elf.outの方、つまり自分で再構築した方はalignを先に述べたように計算するものと考えていたために、直後に.gnu.version_rが続く形になっている。その為、あとでtest.outと比較した際、test.out側にあるこの.gnu.versionと.gnu.version_rの間にある空白の意味が分からなかった。
改めてaddress alignについての説明を読んでみる
「セクションがメモリにロードされる際の、バイトアラインメントのサイズ。sh_addrはsh_addralignの倍数値になっている必要がある。」
つまり、alignはセクションの末尾を整えるものではなくて、セクションの開始位置を決めるものだったわけで・・・。
それなら合点がいく。先のバイナリ値にある空白は.gnu.versionセクションが設けたalignではなく、.gnu.version_rが持つaddress align = 8に合わせて8で割りきれるアドレスから開始するべく設けた空白だった。そしてめでたく.gnu.version_rは8で割りきれるアドレス、0x2b8から開始していると。
結果
ということで、上のalignについて修正したところ、問題のエラーは出なくなった。.gnu.version_rの持つsh_addralignには一致しないバイナリの配置となっていたため、vn_auxが示すオフセットについて齟齬が出ていたようだ。
実行したらsegmentation faultが出てきてくれたので、まだ他の調整は必要だが。
ELFファイルを作る part5
ELFファイルを作るpart4で、ライブラリとのリンクについて詳細を追ってみる手順の案に書いていた
ELFファイルを分解するプログラム、また、その分解したものから再生成するプログラム」
これについて考えてみる。
まずは先だって分解をしてみる。
目標
ELFファイルをsection単位に分解し、それを保持する。
実装
分解するファイル
今回対象とするコードはこんな感じ
システムコールではなく、libcから呼び出すputcharを使っている。
内容としては以前にやっていた、コード42でexitするのに加え、せっかくなのでputcharを使って"T"という文字を出力させている。実際に試してみると
$ nasm -f elf64 test.asm $ gcc -s -nostartfiles -o test.out test.o $ ./test.out T $ echo $? 42
という出力を得られる。先に述べてる様に、libcが必要な状態にしているので、当然実行可能ファイルはライブラリとリンクする。このtest.outが分解対象。
オブジェクトファイルを見てみる
実行可能ファイルを作成する前、test.oというオブジェクトファイルが作られるが、このファイルがリンクする前の状態であることはいろんな所に情報があるので特には触れない。
readelfコマンドでこのファイルのセクションを見てみると
$ readelf -S test.o There are 6 section headers, starting at offset 0x40: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .text PROGBITS 0000000000000000 000001c0 0000000000000014 0000000000000000 AX 0 0 16 [ 2] .shstrtab STRTAB 0000000000000000 000001e0 000000000000002c 0000000000000000 0 0 1 [ 3] .symtab SYMTAB 0000000000000000 00000210 0000000000000090 0000000000000018 4 3 4 [ 4] .strtab STRTAB 0000000000000000 000002a0 000000000000001e 0000000000000000 0 0 1 [ 5] .rela.text RELA 0000000000000000 000002c0 0000000000000030 0000000000000018 3 1 4
という結果を得られる。つまり、リンクも行った後にあるセクションで、この中に無いものがリンク時に追加されたセクションである。
リンク済みのファイルを分解
実行可能ファイルとして先に作成したtest.out。これを各セクションに分解してみる。分解していじった後は、セクションとセグメントの対応 part2で得られた対応関係を元にセグメントを作成、その後にELFヘッダーを作成してやれば元の実行ファイルに戻るはず。
分解に使うコードは以前にELFファイルを作る part4等で使ったものを流用する。主にヘッダー類の読み書きとか。
やってみた
コードはこんな感じ
パッケージで使ってるファイル達はこちら。各ファイルの階層は
/ |- teardown.py -- elf/ |- Utils.py -- components/ |- SectionAggregator.py |- Section.py -- headers/ |- Eh.py |- Header.py |- Ph.py -- Sh.py
という具合。gistで階層わかる様に置ける方法無いかな・・・。githubの方だとあとで書き換えちゃう可能性があるしな・・・。
teardown.py
ELFファイルからsectionを取り出してるこの処理の説明をば。
- 8~9行目
見ての通り、今回の処理対象であるtest.outを読み込んでます。そして、処理しやすいようにバイト毎の値を各要素として持つリスト、byteListへ格納。
- 12~13行目
ELF header用クラスのインスタンスを作成し、byteListの先頭から64byte相当のデータを読み込ませる。この処理でインスタンスに、ヘッダ内の各要素が持つべき値を振り分ける。
- 15~17行目
セクションサイズ、セクション数、セクションヘッダ開始位置オフセットの取得
- 20~27行目
shstrtabセクションのデータを取得し、セクション名テーブルを作成(各セクション名を\0区切りにした文字列となる)。
- 30~44行目
nullセクションとshstrtabセクション以外の全セクションについて、ヘッダ、名前、データを取得してSectionクラスのインスタンスを作成。それをSectionAggregatorへ追加。
比較してみる
readelfで得られる情報と、今回作成した処理で得られる結果を比較してみる
- readelf -S test.out
$ readelf -S test.out There are 15 section headers, starting at offset 0x1090: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .interp PROGBITS 00000000004001c8 000001c8 000000000000001c 0000000000000000 A 0 0 1 [ 2] .note.gnu.build-i NOTE 00000000004001e4 000001e4 0000000000000024 0000000000000000 A 0 0 4 [ 3] .hash HASH 0000000000400208 00000208 0000000000000018 0000000000000004 A 5 0 8 [ 4] .gnu.hash GNU_HASH 0000000000400220 00000220 000000000000001c 0000000000000000 A 5 0 8 [ 5] .dynsym DYNSYM 0000000000400240 00000240 0000000000000048 0000000000000018 A 6 1 8 [ 6] .dynstr STRTAB 0000000000400288 00000288 0000000000000024 0000000000000000 A 0 0 1 [ 7] .gnu.version VERSYM 00000000004002ac 000002ac 0000000000000006 0000000000000002 A 5 0 2 [ 8] .gnu.version_r VERNEED 00000000004002b8 000002b8 0000000000000020 0000000000000000 A 6 1 8 [ 9] .rela.plt RELA 00000000004002d8 000002d8 0000000000000030 0000000000000018 A 5 10 8 [10] .plt PROGBITS 0000000000400308 00000308 0000000000000030 0000000000000010 AX 0 0 4 [11] .text PROGBITS 0000000000400340 00000340 0000000000000014 0000000000000000 AX 0 0 16 [12] .dynamic DYNAMIC 0000000000600e98 00000e98 0000000000000150 0000000000000010 WA 6 0 8 [13] .got.plt PROGBITS 0000000000600fe8 00000fe8 0000000000000028 0000000000000008 WA 0 0 8 [14] .shstrtab STRTAB 0000000000000000 00001010 000000000000007e 0000000000000000 0 0 1
- teardown.py
$ python teardown.py ====== Section Header(.interp) ====== name_index: 11 type: 1 flag: 2 address: 4194760(0x4001c8) offset: 456(0x1c8) size: 28(0x1c) link: 0 info: 0 address_align: 1 entry_table_size:0 ====== Section Header(.note.gnu.build-id) ====== name_index: 19 type: 7 flag: 2 address: 4194788(0x4001e4) offset: 484(0x1e4) size: 36(0x24) link: 0 info: 0 address_align: 4 entry_table_size:0 ====== Section Header(.hash) ====== name_index: 42 type: 5 flag: 2 address: 4194824(0x400208) offset: 520(0x208) size: 24(0x18) link: 5 info: 0 address_align: 8 entry_table_size:4 ... 以下略
良さそうな感じだ。
セクションとセグメントの対応 part2
part1でセクションとセグメントを対応付けしていると思われる箇所は見つけた。次は実際にどうやって対応づけているのかを見てみる。
おさらい
簡単なプログラムをコンパイルして出来上がった実行ファイルはこんな感じになる。
$ readelf -l test.out Elf file type is EXEC (Executable file) Entry point 0x400340 There are 7 program headers, starting at offset 64 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040 0x0000000000000188 0x0000000000000188 R E 8 INTERP 0x00000000000001c8 0x00000000004001c8 0x00000000004001c8 0x000000000000001c 0x000000000000001c R 1 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2] LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x0000000000000354 0x0000000000000354 R E 200000 LOAD 0x0000000000000e98 0x0000000000600e98 0x0000000000600e98 0x0000000000000178 0x0000000000000178 RW 200000 DYNAMIC 0x0000000000000e98 0x0000000000600e98 0x0000000000600e98 0x0000000000000150 0x0000000000000150 RW 8 NOTE 0x00000000000001e4 0x00000000004001e4 0x00000000004001e4 0x0000000000000024 0x0000000000000024 R 4 GNU_RELRO 0x0000000000000e98 0x0000000000600e98 0x0000000000600e98 0x0000000000000168 0x0000000000000168 R 1 Section to Segment mapping: Segment Sections... 00 01 .interp 02 .interp .note.gnu.build-id .hash .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.plt .plt .text 03 .dynamic .got.plt 04 .dynamic 05 .note.gnu.build-id 06 .dynamic
上記の場合7つのプログラムヘッダーがあるが、セクションはそれぞれに分布し、また、複数セグメントにまたがる様に配置されているセクションもある。
実行可能ファイルを作成するときにはセクションを各セグメントへ振り分ける必要があるわけで、その振り分け方を調べてみる。
_bfd_elf_map_sections_to_segments()の調査
さて、part1で見つけたbinutilsのbfd/elf.cにあった_bfd_elf_map_sections_to_segments関数。まずはこれを見てみる。この関数だけを抜き出したのがこのコード。
PHDR、INTERPセグメント
まず74行目辺りを見ると
s = bfd_get_section_by_name (abfd, ".interp"); if (s != NULL && (s->flags & SEC_LOAD) != 0) {
ということで、セクション名が".interp"でSEC_LOADフラグを持っている場合は、このif文の中でPT_PHDRセグメントとPT_INTERPセグメントを作成している。翻せば、.interpセクションが無い場合はPHDRセグメントは不要と言うことか。
LOADセグメント
さて、最重要(だと思う)なLOADセグメント。このセグメントの作成は135行目から
for (i = 0, hdrpp = sections; i < count; i++, hdrpp++)
このループ内で行われる。そのちょっと後のコードを見た感じ、ループの初期化部で出てきてるsectionsが各セクションのアドレスを保持しているという事かな。sectionsを作ってる箇所は49行目からの
i = 0; for (s = abfd->sections; s != NULL; s = s->next) { if ((s->flags & SEC_ALLOC) != 0) { sections[i] = s; ++i; /* A wrapping section potentially clashes with header. */ if (((s->lma + s->size) & addr_mask) < (s->lma & addr_mask)) wrap_to = (s->lma + s->size) & addr_mask; } }
これ。つまりはSEC_ALLOCフラグを持ったセクションのみが集められてるということ。
145行目からしばらくは新しくセグメントを作るかどうかの判断。ここら辺はとりあえずスルーしておこう。新しくセグメントを作る必要が無い場合は223行目からのコードでフラグを調べてあれこれした後、234行目でcontinue。
新しくセグメントを作成した場合は240行目でmake_mappingとかってのをして、それに対してSEC_READONLYかどうかを見たりした上でアレコレ。このエントリの最初でreadelfより出したサンプルから、LOADセグメントは2つあってRW属性とRX属性の二種類があるのでそれらを分けてるのだろう。
260行目でこのループは終了。つまり、このループでSEC_ALLOCフラグを持ったセクションを、RW属性かRX属性どちらかの属性を持つLOADセグメントに振り分けていると。
もう一つのLOADセグメント
262行目のコメントを見ると、もう一つLOADセグメントを持つパターンもあるようだ。とりあえずスルー
DYNAMICセグメント
278行目からはDYNAMICセグメント。これについては
m = _bfd_elf_make_dynamic_segment (abfd, dynsec);
ということでDYNAMICセグメント作成用の関数が用意されていると。
NOTEセグメント
293行目からはNOTEセグメント。対象セクションは
if ((s->flags & SEC_LOAD) != 0 && CONST_STRNEQ (s->name, ".note"))
ということで、".note"がセクション名に含まれるものかな。readelfして出てきた結果では、".note.gnu.build-id"がNOTEセグメントに配置されていたし。
LOADセグメントの作成を掘り下げる
make_mapping関数
LOADセグメントの作成中に出てきたmake_mapping関数。これも同じくbfd/elf.cの中にあり、これを抜き出したコードはこれ。
コードを見ると、セグメントタイプとしてPT_LOADを指定してる所なんかを見たらもう、してやったりな気分ですね。
これで間違いあるまい。
SEC_ALLOC?
さて、ここまで見てみると、LOADセグメントへ配置される条件というのはセクションがSEC_ALLOCフラグを持つ事であることが読み取れる。そして、LOADセグメントのRX属性へ配置されるにはSEC_READONLYを持つ。そういえばこのSEC_ALLOCって何者か?セクションヘッダが持つ各値の定義を見ると、タイプの値はSHT_PORGBITSとかそんな感じだし、フラグはSHF_ALLOCとかそういうのだ(/usr/include/elf.hより)。
というわけでこのSEC_XXXXXを探してみると、bfd/bfd.hに定義がある。
抜粋すると
#define SEC_NO_FLAGS 0x000 /* Tells the OS to allocate space for this section when loading. This is clear for a section containing debug information only. */ #define SEC_ALLOC 0x001 /* Tells the OS to load the section from the file when loading. This is clear for a .bss section. */ #define SEC_LOAD 0x002 /* The section contains data still to be relocated, so there is some relocation information too. */ #define SEC_RELOC 0x004 /* A signal to the OS that the section contains read only data. */ #define SEC_READONLY 0x008 /* The section contains code only. */ #define SEC_CODE 0x010 /* The section contains data only. */ #define SEC_DATA 0x020 /* The section will reside in ROM. */ #define SEC_ROM 0x040
こんな感じ。以降、同様の定義が続いていく。
セクションとSEC_XXXXXとの関連
今までのをまとめると、各セクションにSEC_ALLOCを割り当てる基準があれば、それは即ちLOADセグメントへ含む指示を内包する事に他ならない。
ではその対応付けはどこでされているか?
ld/eelf_x86_64.cを調べてみると、gldelf_x86_64_place_orphan()という関数がある。コードはこんな感じ。
このコードにある以下のコード
static struct orphan_save hold[] = { { ".text", SEC_HAS_CONTENTS | SEC_ALLOC | SEC_LOAD | SEC_READONLY | SEC_CODE, 0, 0, 0, 0 }, { ".rodata", SEC_HAS_CONTENTS | SEC_ALLOC | SEC_LOAD | SEC_READONLY | SEC_DATA, 0, 0, 0, 0 }, { ".data", SEC_HAS_CONTENTS | SEC_ALLOC | SEC_LOAD | SEC_DATA, 0, 0, 0, 0 }, { ".bss", SEC_ALLOC, 0, 0, 0, 0 }, { 0, SEC_HAS_CONTENTS | SEC_ALLOC | SEC_LOAD | SEC_READONLY | SEC_DATA, 0, 0, 0, 0 }, { ".interp", SEC_HAS_CONTENTS | SEC_ALLOC | SEC_LOAD | SEC_READONLY | SEC_DATA, 0, 0, 0, 0 }, { ".sdata", SEC_HAS_CONTENTS | SEC_ALLOC | SEC_LOAD | SEC_DATA | SEC_SMALL_DATA, 0, 0, 0, 0 }, { ".comment", SEC_HAS_CONTENTS, 0, 0, 0, 0 }, };
これか!
こうやって各セクション名に対してSEC_XXXXXというフラグ割り当てを定義してある。orphanというのは「みなしご」とか「親のない」という意味らしいので、つまりは最上位のセクション名について定義しているという事かな。
他のセクション名についても調べればあれこれ出てくるはず。というか、出てきた。
結論
part1での予想通り、ldの内部にどのセクションをどのセグメントへ配置するかの定義があった。
先述のstatic struct orphan_save hold[]を見ると、例えば
- .textセクション:SEC_ALLOC、SEC_READONLYがあるのでLOADセグメントのRX属性
- .dataセクション:SEC_ALLOCがあり、SEC_READONLYは無いのでLOADセグメントのRW属性
- .interpセクション:SEC_ALLOC、SEC_READONLYがあるのでLOADセグメントのRX属性
といった事がわかる。
.interpセクションについては、専用のセグメントを作成している箇所もあった。
セクションの順番指定についてはbfd/elf.cにelf_sort_sections()なんて関数があるので、これが整理してくれるのかな。
セクションとセグメントの対応 part1
前回ELFファイルを作る part4にて、セクションをセグメントに割り当てる際は単純な構造ということもあって決め打ちでやってみてた。前回の様な規模ならまあいいとして、これがある程度セクションの数や種類が増えてきたらどうしようもなくなる。
これについては通常リンク時に解決されるわけだが、それならばセクションとセグメントの対応はリンカスクリプトに書いてあるのか?と思ってみたわけだが、どうもそれらしい記述を見つけられなかった。しかし何処かに必ずその設定はあるはず。と、いうことで、その辺を追ってみた。
疑問
どのセクションをどのセグメントへ配置するかは、どのように決まっているのか?
予想
セクションを見たとき、そのセクションをどのセグメントへ割り当てるかを決める際に使用できそうな値は
- セクションタイプ
- フラグ(属性)
- セクション名
がある。
基本的にはセクションタイプとフラグの組み合わせによって決まる部分が大きそうだが、それだと同じ組み合わせで異なるセグメントに配置と言うことに対応できなそう。
ということで、多分この三つともを使い、そのセクションがどのセグメントに配置されるかを決定してるのではないか。
調査
リンカスクリプトを調べる
セクションをセグメントへ配置する処理はリンク時に行われる。ということで、そのリンクの挙動を指定するリンカスクリプトを調べてみることに。前の記事でリンカスクリプトの場所にちょっと触れてたが、実際にldコマンドを実行したときにデフォルトで使用されているスクリプトについて見るには
$ ld --verbose
で得られる。
ここのページを読んでみた感じではPHDRSコマンドがあれば、どのセクションをどのセグメントに配置するかを指定できるようだが、上記の ld --verbose で得た結果にはPHDRSコマンドが見当たらなかった。
つまりはPHDRSに因らない決定方法があるはず。
では、どこで決めている?
このORACLEのページを見てみると、「エントランス基準」という項目に
リンカーは、定義済みのすべてのエントランス基準の内部リストを保持しています。
との記述がある。このドキュメントはsolarisのものだけど、基本的な所は大幅に違いはあるまい。多分・・・。
でも、このドキュメントに出てくるmapfileはlinuxについてググってみると、それらしいものは出てこないからsolaris用のものなのかな。そっちは色々出てくるし。
ということで、次に探す場所のヒントは得られた。リンカーが既にセクションとセグメントの配置を決める基準を保持していることがあるのだ。
ならば次に見るのはリンカそのもので決定。
ldコマンドを調べる
リンカといえばldなわけで、これのコードを調べてみればよい。ldコマンドはパッケージとしてはbinutilsに含まれるので、GNU binutilsのサイトからソースコードをDLしてくる。今回自分がDLして中身を見てみたのはbinutils-2.23.1。これを展開してみると、ldというディレクトリがある。まあこれがldコマンドの元になっているコードであることは間違い無いだろう。
さて、リンカとリンカスクリプトは当然関係があるはずなので、前回調べてみてたリンカスクリプトのパス(/usr/lib64/ldscripts/elf_x86_64.x)を元に、grepをかけてみて関連のありそうなファイルを探してみる。
$ grep -rl ldscripts /path/to/binutils/ld
結果を見てみて、怪しそうと思えた行は
./emultempl/elf32.em
コレ。これは怪しい。怪しすぎる。
さて、ここで改めてさっき出したデフォルトで使用しているリンカスクリプトを見直してみよう。すると、こんな一文がある。
/* .gnu.warning sections are handled specially by elf32.em. */
なるほど。リンカスクリプトとの関連は間違いなさそうだ。まず調査する対象はこのファイルに決定。
elf32.emから見始めて
とはいえ、ちょっと疑問思ったのは自分の環境は64bitマシン。でも出てきたファイルはelf32.em。elf64.emは無いのかな?
ファイルを開いてみてすぐの3行目にこんな記述がある
# This file is now misnamed, because it supports both 32 bit and 64 bit
なるほどね・・・。とりあえず自分の環境でも調べるべきファイルはこれで良さそうだ。
さて、このemultempl/elf32.em。ディレクトリ名からも分かるように、実際にファイルの中身を見てみると関数名に
static char * gld${EMULATION_NAME}_add_sysroot (const char *path)
といったように ${EMULATION_NAME}といったような変数名と思われる箇所が散見される。このEMULATION_NAMEを検索してみると
$ grep -rl EMULATION_NAME . | grep -v ChangeLog | grep -v emultempl ... ./ld/genscripts.sh ...
というのが見つかる。なるほど、さっきのelf32.emはこのgenscripts.shで作成されるスクリプトのベースとなるスクリプトということね。
さて、genscripts.shの中身を見てEMULATION_NAMEの使われてる箇所を見てみると
( echo "/* Default linker script, for normal executables */" . ${CUSTOMIZER_SCRIPT} . ${srcdir}/scripttempl/${SCRIPT_NAME}.sc ) | sed -e '/^ *$/d;s/[ ]*$//' > ldscripts/${EMULATION_NAME}.x
とかって記述が。これからリンカスクリプトを生成するときにファイル名として使われていることがわかるので、自分の使っていたリンカスクリプトである"/usr/lib64/ldscripts/elf_x86_64.x"より、自分が今求めている対象はEMULATION_NAME = elf_x86_64となっていたことが読み取れる。
他にもelf32.emを見てると
1909 /* Decide which segment the section should go in based on the 1910 section name and section flags. We put loadable .note sections 1911 right after the .interp section, so that the PT_NOTE segment is 1912 stored right after the program headers where the OS can read it 1913 in the first page. */
なんて文があったりしていい感じ。
他のファイルにも手を伸ばしてみると、ld/emultempl/elf-genericl.em(このファイルがelf32.emと関係してるのはMakefileを見れば間違いなさそうなのは読み取れる)にある
_bfd_elf_map_sections_to_segments
なんて関数が名前からして雰囲気出てるので、関数の実装を見てみるべくbfd/elf.cをのぞいてみると、まさにこれだろうと言うような処理。elf-generic.emにあるループで1セクションずつbfd/elf.cの_bfd_elf_map_sections_to_segmentsへ渡して、適切なセグメントに割り振ってるっぽい。
なにはともあれ、これらがそのままコードとして使われるわけじゃなく、makeしたときに動的に生成される為の元ファイルということで。それに現状だとどうやってここにたどり着くのかよく分からん。ならばまずはmakeしてみる。binutilsを展開したディレクトリにて
$ cd /path/to/binutils $ ./configure $ make
installはしない。コード見たいだけだし。
そして出来上がるお目当てのファイル、ld/eelf_x86_64.c。
ld/eelf_x86_64.cから見始めて
中身を見てみると、早速お目当ての関数を発見。
64 static void 65 gldelf_x86_64_map_segments (bfd_boolean need_layout) 66 { ... 84 if (!_bfd_elf_map_sections_to_segments (link_info.output_bfd, 85 &link_info)) 86 einfo ("%F%P: map sections to segments failed: %E\n");
ここにたどり着くまでの足跡を辿ってみる。まずは同じファイル内に
1802 static void 1803 gldelf_x86_64_after_allocation (void) 1804 { 1805 bfd_boolean need_layout = bfd_elf_discard_info (link_info.output_bfd, 1806 &link_info); 1807 gldelf_x86_64_map_segments (need_layout); 1808 }
という箇所で呼び出され、これまたこの関数は同じファイルにて
4801 struct ld_emulation_xfer_struct ld_elf_x86_64_emulation = 4802 { 4803 gldelf_x86_64_before_parse, 4804 syslib_default, 4805 hll_default, 4806 after_parse_default, 4807 gldelf_x86_64_after_open, 4808 gldelf_x86_64_after_allocation,
として定義されている。ld_elf_x86_64_emulationの使用箇所を探すと
$ grep -r ld_elf_x86_64_emulation . ./ldemul-list.h:extern ld_emulation_xfer_type ld_elf_x86_64_emulation;
とかって出てくるので、中を拝見する。
$ cat ldemul-list.h /* This file is automatically generated. DO NOT EDIT! */ extern ld_emulation_xfer_type ld_elf_x86_64_emulation; extern ld_emulation_xfer_type ld_elf32_x86_64_emulation; extern ld_emulation_xfer_type ld_elf_i386_emulation; extern ld_emulation_xfer_type ld_i386linux_emulation; extern ld_emulation_xfer_type ld_elf_l1om_emulation; extern ld_emulation_xfer_type ld_elf_k1om_emulation; #define EMULATION_LIST \ &ld_elf_x86_64_emulation, \ &ld_elf32_x86_64_emulation, \ &ld_elf_i386_emulation, \ &ld_i386linux_emulation, \ &ld_elf_l1om_emulation, \ &ld_elf_k1om_emulation, \ 0
エミュレーションリストとしてdefineされてる。次に探すはこのEMULATION_LISTを使ってる箇所
$ grep -r EMULATION_LIST . ./ldemul.c:ld_emulation_xfer_type *ld_emulations[] = { EMULATION_LIST };
このファイル内でld_emulationsを使っている箇所がいくつかあるので、ひとまず怪しげな箇所をピックアップ
276 void 277 ldemul_choose_mode (char *target) 278 { 279 ld_emulation_xfer_type **eptr = ld_emulations; 280 /* Ignore "gld" prefix. */ 281 if (target[0] == 'g' && target[1] == 'l' && target[2] == 'd')
この関数、ldemul_choose_modeを使ってる箇所を探すと
$ grep -r ldemul_choose_mode . ./ldmain.c: ldemul_choose_mode (emulation);
きた!遂にmainきた!
中身を見てみると
189 int 190 main (int argc, char **argv) 191 { 192 char *emulation; .... 293 emulation = get_emulation (argc, argv); 294 ldemul_choose_mode (emulation); 295 default_target = ldemul_choose_target (argc, argv);
これだ。ちゃんとmain関数内で呼ばれてる。こうやってリンクするモードを選んだりして、適切な実行ファイルとかを出力してるのね。
振り返ってみる
元々はセクションをどうやってセグメントに関連付けてるのか、そのパターンを読もうとしてたのに何故か関連付けの処理自体を途中から追ってしまってた。
まあ、その関連付けの処理にたどり着くまではわかったので、そこの詳細はまた後でかな・・・。