わらばんし仄聞記

南の国で引きこもってるWeb屋さん

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は引数として渡されてる筈なんで、この図のように外へ見に行くのはちょっと違うと思うが。
f:id:warabanshi21:20130514224017j:plain

動的リンク

では実際にtest.out実行時にはどのようにして必要とする共有ライブラリとリンクするのか?
動的リンカがDYNAMICセグメントを見つけるまでは先に示した通り。そして、このセグメント(=.dynamicセクション)の情報を元に動的リンクが行われていくことになる。
その挙動はこんな感じ。

f:id:warabanshi21:20130518223709j:plain

再配置

さて、立ち返って考えてみると、そもそも動的リンクは何故動的にリンクしてるのだったか。そう、プログラム中から外部の共有ライブラリにある処理を呼び出す際に必要だから動的リンクしているんだった。
ということで、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 へのジャンプ、つまりは.pltセクションの先頭へのジャンプを行う。そこでは0x20018aという値をpushして0x600448へジャンプする命令があり、この0x600448のアドレスの.got.pltスロットが動的リンカのアドレスとなるが、こればかりは動的リンカがどのようなアドレスに配置されるかは不明なので0が入っていて、実行時に埋め込まれる事になる。
とまあ、この最後の動的リンカのアドレスが埋め込まれれば動的ライブラリとの関連付けは完了することになる。

.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) 
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は.dynsymに。.gnu.version_rは.dynstrに関連してることが読み取れる。

問題のエラーの原因は?

さて、.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

f:id:warabanshi21:20130411032938j:plain

  • test.out

f:id:warabanshi21:20130411032942j:plain
となっている。赤い箇所が.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セグメントに配置されていたし。

その他のセグメント

更に進むと、341行目からはTLSセグメント。367行目からはGNU_EH_FRAMEセグメントと、いくつかのセグメントを作成している事が見て取れる。GNU_RELROセグメントなんかは重要そうだけど、とりあえず今回はパス。

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関数内で呼ばれてる。こうやってリンクするモードを選んだりして、適切な実行ファイルとかを出力してるのね。

振り返ってみる

元々はセクションをどうやってセグメントに関連付けてるのか、そのパターンを読もうとしてたのに何故か関連付けの処理自体を途中から追ってしまってた。
まあ、その関連付けの処理にたどり着くまではわかったので、そこの詳細はまた後でかな・・・。