はじめに
機械語読みます。(Linux OSのsourceまでは追いません。ローダについては次回のTODO)
何がわかるのか?
- 機械語(Executable and Linkable Format: ELF)の読み方
- アドレスの割り当て方
- linkerの挙動
リンカ、ローダ実践開発テクニックという本をhello worldで理解してみました的な内容です。1
本記事で書いてないこと
- ローダの詳細
- macの実行形式について(linuxのと違う)
対象者
- C言語わかる以外は特に無いが、assemblyわからなければ、https://www.youtube.com/watch?v=VQAKkuLL31g をみておくと良い。
環境
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
参考文献
- http://refspecs.linux-foundation.org/elf/gabi4+/contents.html .. ELF形式のドキュメント
-
https://linux.die.net/man/1/ld .. ldコマンド(gccだと、
/usr/lib/gcc/x86_64-linux-gnu/7/collect2
と役割同じ) - https://www.amazon.com/Low-Level-Programming-Assembly-Execution-Architecture/dp/1484224027/ ( Low-Level Programming: C, Assembly, and Program Execution on Intel® 64 Architecture )
- http://www.cirosantilli.com/elf-hello-world/ .. sugoi
- リンカ、ローダ実践開発テクニック
- https://qiita.com/amama/items/1fa80c5156729f6f4ea9 .. 実行ファイルの構造とかがわかりやすい
- https://qiita.com/knknkn1162/items/bea6d06d6b6009a9773d .. hello world Programの実行命令をバイトコードで確認する 補足記事。
- Intel® 64 and IA-32 Architectures Software Developer’s Manual 特にvol.3
- https://github.com/knknkn1162/nasm_study
- Binary Hacksも良い本だが、今回はあんまり役立たなかった
準備
アセンブルとは
- アセンブリ命令を機械語に置き換え、単一のアセンブリファイル(.asm)をオブジェクトファイル(.o)に変換する
- このファイルは、先頭部分にヘッダ情報を持ち、ある特定のフォーマットになっている
リンカの役割
- 複数のオブジェクトファイルをセクション単位にまとめる
- 1で作成された複数のセクションをセグメント単位にまとめる
- まとめたセグメントに実際のアドレスを割り当てる
- この段階で各シンボルが配置されるアドレスが決定する(relocate: 再配置)
- シンボルのアドレスが未定のために未解決であった部分に、実際のアドレスが挿入される(名前(シンボル)解決)
- 実行形式として出力する
用語解説
- ダンプファイル ..下のような感じのやつ
$ 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の各章のはじめにいろいろ載っているので、気になる人は参考に。
構造
あんまり詳細に走りすぎてしまうと、森が全く見えなくなるので、雑に追いかけつつ、自分なりにまとめてみる。
今回扱う例
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.tbl か http://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(void) {
return 0;
}
大枠の構造4
バイナリダンプ自体を見るのなら、hexdump -vC ./hello.o
もしくは、hexdump -vC ./hello
としましょう(v: verbose, C: 標準的な 16進数 + ASCII での表示。)
+-------------------------------------------------------+ 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`
+-------------------------------------------------------+ 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はリンク時に使用されるので、実行時には使われない。具体的には
# 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/58325c37aa9f6554ca73c931012b0f1d と https://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で作成された複数のセクションをセグメント単位にまとめる
- まとめたセグメントに実際のアドレスを割り当て、各シンボルが配置されるアドレスが決定する(relocate: 再配置)
- シンボルのアドレスが未定のために未解決であった部分に、実際のアドレスが挿入される(名前(シンボル)解決)
(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の関係で00000000006000e5
と00000000006000e8
で値が違っている)詳しくは、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 で確認するとよい
このファイルに、アドレスに関する情報が詰まっているので、関係する部分のみ読み解こう!
まず、
# 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自体は、
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使って追ってみた。この後、実行ファイルを起動するためには、ローダが必要である:
ローダの役割
- 実行形式を読み込み、そのフォーマットにしたがってメモリ配置する
- BSS領域の0クリアを行う
- レジスタやスタック、引数の設定を行う
- 実行開始アドレス(Entry point)にジャンプする(
ld --verbose
で調べると、ENTRY(_start)
となっているので、_start関数にジャンプ)
「リンカ、ローダ実践開発テクニック」の8章に基本的な仕組みとソースコードがある。
ここらあたりも解説したかったが、linux kernelのコード追いながらの説明が良さそう & 自分が完全に把握できてない ため、機が熟したら書こうと思う。
TODO
-
どのようにlinux OS内で実行されるかソースを丹念に読んで理解する ( https://0xax.gitbooks.io/linux-insides/content/index.htmlとか、http://ukai.jp/debuan/2002w/elf.html を読めば良さそう )
-
アドレスってそもそも何ぞやみたいな疑問に対しては、paging知っておかねばならない。 -> http://www.cirosantilli.com/x86-paging/ わかりやすいので、記事にする優先度低め (後は、Intel® 64 and IA-32 Architectures Software Developer’s Manual のvol.3を読むと良い)
-
ここまで理解できたので、熱血! アセンブラ入門 (リンカ、ローダ実践開発テクニックと同じ著者)を十分理解できるレベルになっていそう。
-
http://www.cirosantilli.com/elf-hello-world/ がすごかったので、自分なりに理解した内容を忘れないうちにまとめておきたかった、というのが本音。 ↩
-
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がmacho64
でELF(Executable and Linkable file format)
と形式違ったりするので、扱わない。 ↩ -
https://gist.github.com/knknkn1162/a183b323ff11f70eef9f424665dea3cf においた ↩
-
図は手動でまとめました。 ↩
-
https://stackoverflow.com/questions/14361248/whats-the-difference-of-section-and-segment-in-elf-file-format とか ↩
-
vimでの編集は、https://qiita.com/urakarin/items/337a0433a41443731ad0 を見れば良い ↩
-
複数のオブジェクトファイルをリンクするには、
ld -o hello hello.o lib1.o lib2.o
みたいにすると良い。 ↩ -
readelf -l hello.o
とすると、There are no program headers in this file.
と出力される。 ↩ -
ちなみにこの対応表はProgram headerには書いていなく、Sectionに割り振られたアドレスから
readelf
自身が計算しているみたい。https://stackoverflow.com/questions/23018496/where-is-the-section-to-segment-mapping-stored-in-elf-files も参照。 ↩ -
名前解決もひっくるめてrelocation(再配置)するという言い方もしているみたい。 ↩
-
objdump -d
では標準でAT&T記法になっているので、intel記法に直したければ、-M intel-mnemonic
を追加する ↩ -
little endianかどうかは、
readelf -H hello.o
で2's complement, little endian
で確認できる。 ↩ -
readelf -r hello
の場合は、There are no relocations in this file.
の表示のみ。 ↩ -
https://gist.github.com/knknkn1162/aea089a66e879876ba6ead0697b55a28#file-ld_m-txt-L9-L11 ↩
-
https://sourceware.org/binutils/docs/ld/Builtin-Functions.html ↩
-
ちなみに、MAXPAGESIZE、COMMONPAGESIZEは
ld -z common-page-size=[MAX_SIZE] -z max-page-size=[COMMON_SIZE]
で変えられる。ld -help
を参照 ↩