Linux
初心者
assembly
x86_64
ELF

assemblyからhello world programを追いかける

はじめに

機械語読みます。(Linux OSのsourceまでは追いません。ローダについては次回のTODO)

何がわかるのか?

  • 機械語(Executable and Linkable Format: ELF)の読み方
  • アドレスの割り当て方
  • linkerの挙動

リンカ、ローダ実践開発テクニックという本をhello worldで理解してみました的な内容です。1

本記事で書いてないこと

  • ローダの詳細
  • macの実行形式について(linuxのと違う)

対象者

環境

Dockerfile
FROM ubuntu:18.04

RUN apt-get update -y && apt-get install bsdmainutils binutils gdb nasm -y
WORKDIR /usr/src

で、Dockerfileのあるディレクトリに対して、

# When using docker itself
$ docker build -t nasm_study . # t .. tagged
# `--privileged` option avoids `Operation not permitted` error
$ docker run -it --privileged --rm -v $(pwd):/usr/src nasm_study # --rm : remote automatically when exit container.

とする。2

$ uname -a
Linux b7f1908d2a63 4.9.87-linuxkit-aufs #1 SMP Wed Mar 14 15:12:16 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux

参考文献

準備

アセンブルとは

  • アセンブリ命令を機械語に置き換え、単一のアセンブリファイル(.asm)をオブジェクトファイル(.o)に変換する
  • このファイルは、先頭部分にヘッダ情報を持ち、ある特定のフォーマットになっている

リンカの役割
1. 複数のオブジェクトファイルをセクション単位にまとめる
2. 1で作成された複数のセクションをセグメント単位にまとめる
3. まとめたセグメントに実際のアドレスを割り当てる
4. この段階で各シンボルが配置されるアドレスが決定する(relocate: 再配置)
5. シンボルのアドレスが未定のために未解決であった部分に、実際のアドレスが挿入される(名前(シンボル)解決)
6. 実行形式として出力する

用語解説

  • ダンプファイル ..下のような感じのやつ
$ hexdump -vC hello.o # v.. verbose, C .. hex+ASCII 表示
00000000  7f 45 4c 46 02 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|
00000010  01 00 3e 00 01 00 00 00  00 00 00 00 00 00 00 00  |..>.............|
00000020  00 00 00 00 00 00 00 00  40 00 00 00 00 00 00 00  |........@.......|
00000030  00 00 00 00 40 00 00 00  00 00 40 00 07 00 03 00  |....@.....@.....|
00000040  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000050  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
  • ELF .. Executable and Linkable Format. オブジェクトファイルのフォーマット。nasm -hfでフォーマット一覧を確認できる。

  • Relocatable object file .. assembleした直後のオブジェクトファイル。 assembleした直後は、関数呼び出しや変数のアドレスを割り当てていない。リンク(ld)により、初めてアドレスが割り当てられる。

    なんでそうなっているのかというと、ファイルが複数にまたがっている場合、単一のファイルのみでは、別ファイルにある関数呼び出しの実体がどこにあるかわからないため、アドレスを割り当てていないからだ。リンクによりld -o ./hello prog1.o prog2.o prog3.oみたいな感じでまとめることにより、外部からよばれている関数や変数のアドレスを決定できる。

  • linker script .. リンカの役割の1~5を実行する際の設定スクリプト.ld --verboseで使用されているlink scriptを見れる。ld -s [linker_script]で自前のリンカスクリプトを読み込める。

  • raw binary file .. ヘッダやフッタ情報などが含まれないデータのみが含まれているファイル(objcopy -O binary -S [inputfile] [outputfile]で作成できる)

その他用語に関しては、Binary Hacksの各章のはじめにいろいろ載っているので、気になる人は参考に。

構造

あんまり詳細に走りすぎてしまうと、森が全く見えなくなるので、雑に追いかけつつ、自分なりにまとめてみる。

今回扱う例

hello.asm
section .data
  hello_world db "Hello world!", 10
  ; See also https://www.tutorialspoint.com/assembly_programming/assembly_strings.htm
  hello_world_len equ $ - hello_world ; $(points to the byte after the last character of the string variable msg. ) - hello_world

section .text
  global _start

_start:
  mov rax, 1 ; sys_write
  mov rdi, 1
  mov rsi, hello_world
  mov rdx, hello_world_len
  syscall

  mov rax, 60 ; sys_exit
  mov rdi, 0
  syscall

x86_64 assemblyについては、 https://www.youtube.com/watch?v=VQAKkuLL31g のシリーズ が一番とっつきやすかった。 その次に、Low-Level Programming の前半(後半はC言語が中心)読みましょう。
システムコールの一覧は、https://github.com/torvalds/linux/blob/master/arch/x86/entry/syscalls/syscall_64.tblhttp://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/ を参照に。

Note) 今回C言語でやっていないのは、main関数がよばれる前後でいくつか前処理、後処理関数が挟まっているから。リンカ、ローダ実践開発テクニック 4章の図4.50(p114)に表がある。また、下記の一見なんにもしなさそうなC fileでさえも、gcc int_main.c -vとすることにより、/usr/lib/gcc/x86_64-linux-gnu/7/collect2 -plugin /usr/lib/gcc/x86_64-linu...みたいに、いろんなライブラリが読み込まれていることがわかる。(objdump -d [file]でlink前のobject fileとlink後のexecutable object fileの命令数を確認した3

int_main.c
int main(void) {
  return 0;
}

大枠の構造4

バイナリダンプ自体を見るのなら、hexdump -vC ./hello.oもしくは、hexdump -vC ./helloとしましょう(v: verbose, C: 標準的な 16進数 + ASCII での表示。)

hello.o
+-------------------------------------------------------+ 0x00000000
|ELF header (check with `readelf -h hello.o`)           |
+-------------------------------------------------------+ 0x00000040
|7 Section headers             Index 0(SHT_NULL) header |
|(check with `readelf -S hello.o`) .data section header |
|                                  .text section header |
|                              .shstrtab section header |
|                                .symtab section header |
|                                .strtab section header |
|                             .rela.text section header |
+-------------------------------------------------------+ 0x00000200
|6 Section (.data, .text, .shstrtab,                    |
| .symtab, .strtab, .rela.text)                         |
|(Each section can be  checked                          |
| with `readelf -x .data helo.o)                        |
|                                                       |
|                                                       |
|                                                       |
+-------------------------------------------------------+ 0x00000380
# Note) There are no program headers in this file. Try `readelf -l hello.o`
hello
+-------------------------------------------------------+ 0x00000000
|ELF header (`readelf -h hello.o`)                      |
+-------------------------------------------------------+ 0x00000040
|2 program headers(.text, .data)                        |
|(`readelf -l ./hello`)                                 |
+-------------------------------------------------------+ 0x000000b0
|.text section                                          |
|.data section,  .shstrtab section                      |
+-------------------------------------------------------+ 0x00000110
|6 Section headers             Index 0(SHT_NULL) header 
|                                  .data section header |
|                                  .text section header |
|                              .shstrtab section header |
|                                .symtab section header |
|                                .strtab section header |
+-------------------------------------------------------+ 0x00000280
|.symtab section & .strtab section                      |
|                                                       |
|                                                       |
|                                                       |
|                                                       |
|                                                       |
|                                                       |
+-------------------------------------------------------+ 0x000003d0
# .rela.text sectionがないが、リンカが.textセクションの文字列のアドレスを再配置(rellocation)したので、テーブル不要になり消滅している。後述。

object file

hello.o ./hello
生成コマンド nasm -felf64 hello.asm ld -o hello hello.o
object file type Relocatable Executable
ELF Header 必要 必要
Program Header table 存在しない(Segmentがないため) 必要
Section Header table 必要 存在するが実行には不要
Section 必要 使われていないSectionなら不要
Symbol table 必要 存在するが再配置(relocation)し終わったので不要
Relocation table(rela.* section) 必要 シンボル解決し終わったので存在しない
  • SegmentとSectionの違いは、Sectionはリンク時に使用されて、Segmentは実行時に使用される。複数のSectionがリンク時にSegmentにまとめられる(linker scriptにどうまとめるかの記載がある)。Segmentが異なれば、異なる仮想メモリにマッピングされる。5 リンク前のファイルには、Segmentはない。

Note) 実行形式ファイルのほうがfile size大きくってぎょっとするんだけれど、実際に使われているのは、ELF header,Program Header, .data, .text section である。Section headerはリンク時に使用されるので、実行時には使われない。具体的には

dump
# ELF header
00000000  7f 45 4c 46 02 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|
00000010  02 00 3e 00 01 00 00 00  b0 00 40 00 00 00 00 00  |..>.......@.....|
00000020  40 00 00 00 00 00 00 00  00 01 00 00 00 00 00 00  |@...............|
00000030  00 00 00 00 40 00 38 00  02 00 40 00 04 00 03 00  |....@.8...@.....|
# 2 program headers(.text, .data)
00000040  01 00 00 00 05 00 00 00  00 00 00 00 00 00 00 00  |................|
00000050  00 00 40 00 00 00 00 00  00 00 40 00 00 00 00 00  |..@.......@.....|
00000060  d7 00 00 00 00 00 00 00  d7 00 00 00 00 00 00 00  |................|
00000070  00 00 20 00 00 00 00 00  01 00 00 00 06 00 00 00  |.. .............|
00000080  d8 00 00 00 00 00 00 00  d8 00 60 00 00 00 00 00  |..........`.....|
00000090  d8 00 60 00 00 00 00 00  0d 00 00 00 00 00 00 00  |..`.............|
000000a0  0d 00 00 00 00 00 00 00  00 00 20 00 00 00 00 00  |.......... .....|
# 2 Section(.text, .data)
000000b0  b8 01 00 00 00 bf 01 00  00 00 48 be d8 00 60 00  |..........H...`.|
000000c0  00 00 00 00 ba 0d 00 00  00 0f 05 b8 3c 00 00 00  |............<...|
000000d0  bf 00 00 00 00 0f 05 00  48 65 6c 6c 6f 20 77 6f  |........Hello wo|
000000e0  72 6c 64 21 0a 00                                 |rld!..|

で実行可能。(#はコメントのつもり)6

さらに詳細の構造に関しては、 https://gist.github.com/knknkn1162/58325c37aa9f6554ca73c931012b0f1dhttps://gist.github.com/knknkn1162/ea1ab38081ceadc0d0c3d71db9418ca2 に自分なりにまとめてみた。(もしくは、http://www.cirosantilli.com/elf-hello-world/ を丹念に読む。)

コマンド確認

コマンド 備考
dump確認 hexdump -vC [file] or xxd [file]
ELF Header readelf -h [file]
Program Header readelf -l [file] or objdump -p [file](briefly) Section to Segment mappingが記載されてる
Section Header readelf -S [file] or objdump -h [file](briefly)
Section readelf -x.data [file] .dataは個別のSection名を指す。(他には、.text, .bss, .symtabとか)
disassemble objdump -d [file] .text section(実行命令列)がどんなふうになっているのか確認できる
raw binary file objcopy -O binary -S hello.o hello_raw.o
relocation table readelf -r [file] .rel.* sectionをパースしてる
Symbol table readelf -s [file] or nm [file] .symtab sectionをパースしてる
linker script ld --verbose
link map ld -M -o hello hello.o

いろいろ本やサイトを見ていると、readelfやらobjdumpやらnmやらバイナリ解析コマンドが入り組んでいるが、大体の場合は、readelfが上位互換。(disassembleしたいときだけobjdump使えばよい)

hello worldの確認

$ ./hello
Hello world!

文字列は

$ readelf -x.data ./hello

Hex dump of section '.data':
  0x006000d8 48656c6c 6f20776f 726c6421 0a       Hello world!.

で、命令の中で、hello_world文字列の先頭アドレスに当たる0x006000d8が参照されている。

$ readelf -x.text hello

Hex dump of section '.text':
  0x004000b0 b8010000 00bf0100 000048be d8006000 ..........H...`.
  0x004000c0 00000000 ba0d0000 000f05b8 3c000000 ............<...
  0x004000d0 bf000000 000f05                     .......

# 上記ではよくわからないと思うので、disassembleする
$ objdump -d ./hello

hello:     file format elf64-x86-64


Disassembly of section .text:

00000000004000b0 <_start>:
  4000b0:   b8 01 00 00 00          mov    $0x1,%eax
  4000b5:   bf 01 00 00 00          mov    $0x1,%edi
  4000ba:   48 be d8 00 60 00 00    movabs $0x6000d8,%rsi # mov rsi, hello_world
  4000c1:   00 00 00
  4000c4:   ba 0d 00 00 00          mov    $0xd,%edx
  4000c9:   0f 05                   syscall
  4000cb:   b8 3c 00 00 00          mov    $0x3c,%eax
  4000d0:   bf 00 00 00 00          mov    $0x0,%edi
  4000d5:   0f 05                   syscall

リンクの役割確認

リンカの役割
1. 複数のオブジェクトファイルをセクション単位にまとめる
2. 1で作成された複数のセクションをセグメント単位にまとめる
3. まとめたセグメントに実際のアドレスを割り当て、各シンボルが配置されるアドレスが決定する(relocate: 再配置)
4. シンボルのアドレスが未定のために未解決であった部分に、実際のアドレスが挿入される(名前(シンボル)解決)
(5. 実行形式として出力する)

だった。一つづつ確認していく。


複数のオブジェクトファイルをセクション単位にまとめる

今回、単一のオブジェクトファイルをリンクするだけなので、まとめるもなにも無い7には、が、セクション自体はreadelf -S ./hello(各Section headerをパースしたもの)で確認すると良い:

$ readelf -S hello.o
There are 7 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] .data             PROGBITS         0000000000000000  00000200
       000000000000000d  0000000000000000  WA       0     0     4
  [ 2] .text             PROGBITS         0000000000000000  00000210
       0000000000000027  0000000000000000  AX       0     0     16
  [ 3] .shstrtab         STRTAB           0000000000000000  00000240
       0000000000000032  0000000000000000           0     0     1
  [ 4] .symtab           SYMTAB           0000000000000000  00000280
       00000000000000a8  0000000000000018           5     6     8
  [ 5] .strtab           STRTAB           0000000000000000  00000330
       000000000000002e  0000000000000000           0     0     1
  [ 6] .rela.text        RELA             0000000000000000  00000360
       0000000000000018  0000000000000018           4     2     8

$ readelf -S hello
There are 6 section headers, starting at offset 0x240:

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         00000000004000b0  000000b0
       0000000000000027  0000000000000000  AX       0     0     16
  [ 2] .data             PROGBITS         00000000006000d8  000000d8
       000000000000000d  0000000000000000  WA       0     0     4
  [ 3] .symtab           SYMTAB           0000000000000000  000000e8
       00000000000000f0  0000000000000018           4     6     8
  [ 4] .strtab           STRTAB           0000000000000000  000001d8
       000000000000003f  0000000000000000           0     0     1
  [ 5] .shstrtab         STRTAB           0000000000000000  00000217
       0000000000000027  0000000000000000           0     0     1

Offsetは起点から0x000000b0のところに、.text sectionがあるよ〜っていうのを表す。最初のセクションはNULL固定。

バイトコードから直接読み取るのは、http://www.cirosantilli.com/elf-hello-world/#section-header-table を見よう。
.rela.textがないが、これは、linkされたことによって、役目を終えた(アドレスを再配置し終わった)ため存在しない。
(ちなみに、section Nameは'.shstrtab section'から引っ張ってきている(配置規則については、http://refspecs.linux-foundation.org/elf/gabi4+/ch4.strtab.html を参照のこと)


複数のセクションをセグメント単位にまとめる

readelf -l ./hello(Program headerをパースしてる) で確認できる8

$ readelf -l hello

Elf file type is EXEC (Executable file)
Entry point 0x4000b0
There are 2 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x00000000000000d7 0x00000000000000d7  R E    0x200000
  LOAD           0x00000000000000d8 0x00000000006000d8 0x00000000006000d8
                 0x000000000000000d 0x000000000000000d  RW     0x200000

 Section to Segment mapping:
  Segment Sections...
   00     .text
   01     .data
 Section to Segment mapping:
  Segment Sections...
   00     .text
   01     .data

はこのプログラムでは単に1:1対応しているように見えるが、もうちょっと複雑な場合だと、下のようにSegment:Sectionが1:多対応となる9

 Section to Segment mapping:
  Segment Sections...
   00
   01     .interp
   02     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame
   03     .init_array .fini_array .dynamic .got .data .bss
   04     .dynamic
   05     .note.ABI-tag .note.gnu.build-id
   06     .eh_frame_hdr
   07
   08     .init_array .fini_array .dynamic .got

まとめたセグメントに実際のアドレスを割り当て、各シンボルが配置されるアドレスが決定する(relocate: 再配置)

readelf -s hello.oを見る(.symtab section をパースするコマンド)
リンク前のファイルは下のようにアドレスが定まっていない。

$ readelf -s hello.o

Symbol table '.symtab' contains 7 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hello.asm
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    2
     4: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT    1 hello_world
     5: 000000000000000d     0 NOTYPE  LOCAL  DEFAULT  ABS hello_world_len
     6: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT    2 _start

リンクされると、アドレスが決定される。

$ readelf -s hello

Symbol table '.symtab' contains 10 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND # 最初のentryはnullに相当する
     1: 00000000004000b0     0 SECTION LOCAL  DEFAULT    1
     2: 00000000006000d8     0 SECTION LOCAL  DEFAULT    2
     3: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hello.asm #ファイル名なので、アドレス割り当てられない
     4: 00000000006000d8     0 NOTYPE  LOCAL  DEFAULT    2 hello_world
     5: 000000000000000d     0 NOTYPE  LOCAL  DEFAULT  ABS hello_world_len
     6: 00000000004000b0     0 NOTYPE  GLOBAL DEFAULT    1 _start
     7: 00000000006000e5     0 NOTYPE  GLOBAL DEFAULT    2 __bss_start
     8: 00000000006000e5     0 NOTYPE  GLOBAL DEFAULT    2 _edata
     9: 00000000006000e8     0 NOTYPE  GLOBAL DEFAULT    2 _end

Ndxは(readelf -S helloから得られる)Section header Indexを表す(本記事の場合、1なら.text section, 2なら、.data section, UND=UNDEF, ABS=an absolute value that will not change because of relocation.)
(ちなみに、Nameは'.strtab section'から引っ張ってきている(配置規則については、http://refspecs.linux-foundation.org/elf/gabi4+/ch4.strtab.html を参照のこと)

Note) _edata, __bss_start, _endとリンク前のファイルにはないsymbol nameが追加されているが、コレはlinker scriptによって定義されている。意味はそれぞれ、データ領域終端アドレス、BSS領域の先頭アドレス, BSS領域の終端アドレスである。(今回BSS領域内が、alignmentの関係で00000000006000e500000000006000e8で値が違っている)詳しくは、linker scriptの章も参照すること。


シンボルのアドレスが未定のために未解決であった部分に、実際のアドレスが挿入される(名前解決)10

まず、リンク前のオブジェクトファイルをdisassembleしよう11

# dump relocatable object file
$ objdump -d hello.o

hello.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <_start>:
   0:   b8 01 00 00 00          mov    $0x1,%eax
   5:   bf 01 00 00 00          mov    $0x1,%edi
   a:   48 be 00 00 00 00 00    movabs $0x0,%rsi # mov rsi, hello_world
  11:   00 00 00
  14:   ba 0d 00 00 00          mov    $0xd,%edx
  19:   0f 05                   syscall
  1b:   b8 3c 00 00 00          mov    $0x3c,%eax
  20:   bf 00 00 00 00          mov    $0x0,%edi
  25:   0f 05                   syscall

このバイトコードの詳細は補足の記事( https://qiita.com/knknkn1162/items/bea6d06d6b6009a9773d )で確認してほしいんだけど、いちばん重要なのが、0x0c~0x13まで(00 00 00 00 00 00 00 00の部分)(はアドレス(mov rsi, hello_worldのhello_worldの参照先)が入るはずだが、未決定のままになっている。
リンクされると、アドレスが決定される。(前節の4: 00000000006000d8 0 NOTYPE LOCAL DEFAULT 2 hello_worldの部分)

# リンク後のobject file(executable object file)
$ objdump -d ./hello

hello:     file format elf64-x86-64


Disassembly of section .text:

00000000004000b0 <_start>:
  4000b0:   b8 01 00 00 00          mov    $0x1,%eax
  4000b5:   bf 01 00 00 00          mov    $0x1,%edi
  4000ba:   48 be d8 00 60 00 00    movabs $0x6000d8,%rsi # mov rsi, hello_world
  4000c1:   00 00 00
  4000c4:   ba 0d 00 00 00          mov    $0xd,%edx
  4000c9:   0f 05                   syscall
  4000cb:   b8 3c 00 00 00          mov    $0x3c,%eax
  4000d0:   bf 00 00 00 00          mov    $0x0,%edi
  4000d5:   0f 05                   syscall

$0x6000d8となっているのが確認できる。(d8 00 60 00 00 00 00 00はintel x86_64はリトルエンディアン12なので、0x00000000006000d8であることに注意。)

置き換えるべきアドレスは分かっているが、置き換える位置(0x4000bc)をどうやって知っているのだろうか?
リンク前のrela.text section がその情報を持っている。
readelf -r hello.o(rela.text sectionをパースしてる) で確認できる13

$ readelf -r hello.o

Relocation section '.rela.text' at offset 0x360 contains 1 entry:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
00000000000c  000200000001 R_X86_64_64       0000000000000000 .data + 0

どう見るかというと、.data(Sym.Name)の0x00000000000c(Offset)番目なので、下のvvの部分を指す。

$ objdump -d ./hello
// skip
              vv
   a:   48 be 00 00 00 00 00    movabs $0x0,%rsi # mov rsi, hello_world
  11:   00 00 00

linker script

とりあえず、前節までで、readelf, objdumpを用いて、大体のELF(Executable and Linkable Format)の構造を見てきた。
リンクされる時に、アドレスが決定されることまでわかった。(まとめたセグメントに実際のアドレスを割り当て、各シンボルが配置されるアドレスが決定する(relocate: 再配置)の節を参照)
では、アドレスの番号(0x06000d8とか0x04000b0)ってどう決まっているの? というのが気になる。(もちろん、ランダムに決まっているわけではない。)
アドレスを決定するのに大事な役目を果たすのが、linker scriptというやつである。

ld --verboseでどんな感じか確認できる -> https://gist.github.com/knknkn1162/66ddd485e8fda0ad0f7b595d4a48e5c0

で、この期に及んでlinker scriptの文法規則をいろいろ説明するのは大変なので、link mapという、アドレスとscriptのマッピングで確認したい。リンクの際にMオプションをつけて、ld -M -o hello hello.oでexecutable object fileを作成すると、それが出力される。
-> 長いので、全体は https://gist.github.com/knknkn1162/aea089a66e879876ba6ead0697b55a28 で確認するとよい

このファイルに、アドレスに関する情報が詰まっているので、関係する部分のみ読み解こう!


まず、

sample.ld
# PROVIDE: 他のobject fileのシンボル名と衝突したとき、リンカよりもobject fileのシンボルを優先させるために使う。
LOAD hello.o
                [!provide]                        PROVIDE (__executable_start = SEGMENT_START ("text-segment", 0x400000))
                0x00000000004000b0                . = (SEGMENT_START ("text-segment", 0x400000) + SIZEOF_HEADERS)

から見る14。なぜ、0x400000なのかは、https://stackoverflow.com/questions/14314021/why-linux-gnu-linker-chose-address-0x400000 とか https://teratail.com/questions/48366 を見れば良いと思う。

text-segmentは初期値が、0x400000と決められている。カスタマイズしたければ、ld -Ttext-segment [ADDRESS]とすれば良い。SIZEOF_HEADERSはELFヘッダとProgram Headerの合計値。今回の場合、

$ hexdump -vC hello
00000000  7f 45 4c 46 02 01 01 00  00 00 00 00 00 00 00 00  |.ELF............| #<= ELF Header
00000010  02 00 3e 00 01 00 00 00  b0 00 40 00 00 00 00 00  |..>.......@.....|
00000020  40 00 00 00 00 00 00 00  40 02 00 00 00 00 00 00  |@.......@.......|
00000030  00 00 00 00 40 00 38 00  02 00 40 00 06 00 05 00  |....@.8...@.....|
00000040  01 00 00 00 05 00 00 00  00 00 00 00 00 00 00 00  |................| #<= 2 Program Headers
00000050  00 00 40 00 00 00 00 00  00 00 40 00 00 00 00 00  |..@.......@.....|
00000060  d7 00 00 00 00 00 00 00  d7 00 00 00 00 00 00 00  |................|
00000070  00 00 20 00 00 00 00 00  01 00 00 00 06 00 00 00  |.. .............|
00000080  d8 00 00 00 00 00 00 00  d8 00 60 00 00 00 00 00  |..........`.....|
00000090  d8 00 60 00 00 00 00 00  0d 00 00 00 00 00 00 00  |..`.............|
000000a0  0d 00 00 00 00 00 00 00  00 00 20 00 00 00 00 00  |.......... .....|
...

の部分に当たる。(http://www.cirosantilli.com/elf-hello-world/#program-header-table も見ると良さそう)

.textセッション自体は、https://gist.github.com/knknkn1162/aea089a66e879876ba6ead0697b55a28#file-ld_m-txt-L79-L87 の部分:

# 0x27 はサイズ
.text           0x00000000004000b0       0x27
 *(.text.unlikely .text.*_unlikely .text.unlikely.*)
 *(.text.exit .text.exit.*)
 *(.text.startup .text.startup.*)
 *(.text.hot .text.hot.*)
 *(.text .stub .text.* .gnu.linkonce.t.*)
 .text          0x00000000004000b0       0x27 hello.o
                0x00000000004000b0                _start
 *(.gnu.warning)

linker script自体は、

script.ld
SECTIONS
{
  /* Read-only sections, merged into text segment: */
  PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); . = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS;
// skip
  .text           :
  {
    *(.text.unlikely .text.*_unlikely .text.unlikely.*)
    *(.text.exit .text.exit.*)
    *(.text.startup .text.startup.*)
    *(.text.hot .text.hot.*)
    *(.text .stub .text.* .gnu.linkonce.t.*)
    /* .gnu.warning sections are handled specially by elf32.em.  */
    *(.gnu.warning)
  }

みたいになっている。

.data sectionも見よう。

                // . : location counter( current address)
                0x00000000006000d7                . = DATA_SEGMENT_ALIGN (CONSTANT (MAXPAGESIZE), CONSTANT (COMMONPAGESIZE))
// skip
.data           0x00000000006000d8        0xd
 *(.data .data.* .gnu.linkonce.d.*)
 .data          0x00000000006000d8        0xd hello.o

MAXPAGESIZE, COMMONPAGESIZEは、https://github.com/bminor/binutils-gdb/blob/92e68c1d65f844c0027f471a0c9e1722d781ef7d/bfd/elf64-x86-64.c#L4991-L4994 に定義があり、DATA_SEGMENT_ALIGNの定義15から、は0x4000d7(=0x4000b0+0x27(.text sectionのサイズ))+0x200000=0x6000d7となる。16

$ readelf -s hello
## skip
   Num:    Value          Size Type    Bind   Vis      Ndx Name
## skip
     1: 00000000004000b0     0 SECTION LOCAL  DEFAULT    1 # .text section
     2: 00000000006000d8     0 SECTION LOCAL  DEFAULT    2 # .data section

ということで、それぞれのsectionの先頭アドレスがわかった。

     7: 00000000006000e5     0 NOTYPE  GLOBAL DEFAULT    2 __bss_start
     8: 00000000006000e5     0 NOTYPE  GLOBAL DEFAULT    2 _edata
     9: 00000000006000e8     0 NOTYPE  GLOBAL DEFAULT    2 _end

とかもlink mapを参考にしてください。

最後に

実行ファイルがどうやってできているのかをhello world使って追ってみた。この後、実行ファイルを起動するためには、ローダが必要である:

ローダの役割
1. 実行形式を読み込み、そのフォーマットにしたがってメモリ配置する
2. BSS領域の0クリアを行う
3. レジスタやスタック、引数の設定を行う
4. 実行開始アドレス(Entry point)にジャンプする(ld --verboseで調べると、ENTRY(_start)となっているので、_start関数にジャンプ)

「リンカ、ローダ実践開発テクニック」の8章に基本的な仕組みとソースコードがある。
ここらあたりも解説したかったが、linux kernelのコード追いながらの説明が良さそう & 自分が完全に把握できてない ため、機が熟したら書こうと思う。

TODO


  1. http://www.cirosantilli.com/elf-hello-world/ がすごかったので、自分なりに理解した内容を忘れないうちにまとめておきたかった、というのが本音。 

  2. macOS X はsystem call (https://opensource.apple.com/source/xnu/xnu-1504.3.12/bsd/kern/syscalls.master) がlinuxのやつ https://github.com/torvalds/linux/blob/master/arch/x86/entry/syscalls/syscall_64.tbl と違ったり、http://thexploit.com/secdev/mac-os-x-64-bit-assembly-system-calls/ のように、システムコールを0×2000000 + unix syscallで呼ぶ必要があったり、object fileがmacho64ELF(Executable and Linkable file format)と形式違ったりするので、扱わない。 

  3. https://gist.github.com/knknkn1162/a183b323ff11f70eef9f424665dea3cf においた 

  4. 図は手動でまとめました。 

  5. https://stackoverflow.com/questions/14361248/whats-the-difference-of-section-and-segment-in-elf-file-format とか 

  6. vimでの編集は、https://qiita.com/urakarin/items/337a0433a41443731ad0 を見れば良い 

  7. 複数のオブジェクトファイルをリンクするには、ld -o hello hello.o lib1.o lib2.oみたいにすると良い。 

  8. readelf -l hello.oとすると、There are no program headers in this file.と出力される。 

  9. ちなみにこの対応表はProgram headerには書いていなく、Sectionに割り振られたアドレスからreadelf自身が計算しているみたい。https://stackoverflow.com/questions/23018496/where-is-the-section-to-segment-mapping-stored-in-elf-files も参照。 

  10. 名前解決もひっくるめてrelocation(再配置)するという言い方もしているみたい。 

  11. objdump -dでは標準でAT&T記法になっているので、intel記法に直したければ、-M intel-mnemonicを追加する 

  12. little endianかどうかは、readelf -H hello.o2's complement, little endianで確認できる。 

  13. readelf -r helloの場合は、There are no relocations in this file.の表示のみ。 

  14. https://gist.github.com/knknkn1162/aea089a66e879876ba6ead0697b55a28#file-ld_m-txt-L9-L11 

  15. https://sourceware.org/binutils/docs/ld/Builtin-Functions.html 

  16. ちなみに、MAXPAGESIZE、COMMONPAGESIZEはld -z common-page-size=[MAX_SIZE] -z max-page-size=[COMMON_SIZE]で変えられる。ld -helpを参照