わたすけです。いろいろあってELFについて調べる機会があったので、ここにまとめておきます。
Linuxの実行ファイルとは
その名の通り、Linuxにおける実行可能なファイルです。基本的にはELF(Executable and Linkable Format)と呼ばれるフォーマットによって記述されています。
ELFのフォーマットは/usr/include/elf.hやman elfで読むことが出来ます。今回はこれらをもうちょっと噛み砕いていきます。
ELFの構成要素
ELFには大まかに分けて3つの構成要素があります。即ち、ELFヘッダ・プログラムヘッダ・セクションヘッダです。
ELFヘッダ
ELFヘッダには、そのELFファイルに関する基本的な情報が記載されています。
マジックナンバー・対応しているOSやABI・プログラムヘッダやセクションヘッダのサイズおよび個数など、基本的な情報が記載されています。
readelf -h
でこの部分を閲覧することが可能です。
セクションヘッダ
セクションヘッダにはリンク時に必要となるセクション情報が記載されています。セクションというのは、ELFにおける最小単位です。おおよそ以下のようなものが用いられることが多いです:
- .text:プログラムのコードが格納される
- .data:初期値を持つグローバル変数・static変数
- .rodata:Read-Onlyなデータ 文字列リテラル等
readelf -S
でこの部分を閲覧することが可能です。
プログラムヘッダ
プログラムヘッダには実行時に必要となるセグメント情報が記載されています。セグメントはいくつかのセクションを役割・性質ごとにまとめたものです。ここでの「性質」というのは、読み込み可能・書き込み可能・実行可能というものです。パーミッションみたいなものですね。
例えば、前述の.textセクションであれば、プログラムのコードであるため実行可能であるべきです。もちろん、読み込みも可能である必要があります。
対して、.dataセクションはグローバル変数・static変数であるため、読み込み・書き込みが可能であるべきです。
また、.rodataはその名の通りRead-Onlyなデータであるため、読み込みだけ可能であるべきです。
このようなまとまりを作るのがセグメントというものです。例えばLOADセグメントは、プログラムを実行する前にELFからメモリに読み出す必要があることを示すセグメントです。
readelf -l
でこの部分を閲覧することが可能です。
実際に表示してみる
C言語で以下のようなプログラムを作成して、readelfコマンドを使ってみます。
#include <stdio.h>
int global;
unsigned char init = 255;
int main(void) {
printf("Hello, world!\n");
return 0;
}
まずはビルドしてみます。clangのバージョンは13.0.1です。
$ clang a.c
$ ./a.out
Hello, world!
次に、readelfコマンドを使いましょう。
ELFヘッダを覗く
まずは-hオプションです。
ELF ヘッダ:
マジック: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
クラス: ELF64
データ: 2 の補数、リトルエンディアン
Version: 1 (current)
OS/ABI: UNIX - System V
ABI バージョン: 0
型: DYN (Position-Independent Executable file)
マシン: Advanced Micro Devices X86-64
バージョン: 0x1
エントリポイントアドレス: 0x1040
プログラムヘッダ始点: 64 (バイト)
セクションヘッダ始点: 14064 (バイト)
フラグ: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 13
Size of section headers: 64 (bytes)
Number of section headers: 30
Section header string table index: 29
UNIXおよびSystem V ABI環境で動作するELF64形式のファイルであること、56バイトサイズのプログラムヘッダが13個あること等がわかります。
日本語で「型」と書いてありますが、これはTypeのことです。今回のDYN以外にもいくつか種類があります:
- REL(RELocatable):オブジェクトファイル等がこれにあたる
- EXEC(EXECutable):ビルド時に-staticオプションを付与するとこれになる
- CORE:コアファイル(よくわかりませんでした)
セクションヘッダを覗く
-Sオプションです。
There are 30 section headers, starting at offset 0x36f0:
セクションヘッダ:
[番] 名前 タイプ アドレス オフセット
サイズ EntSize フラグ Link 情報 整列
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000000318 00000318
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.gnu.pr[...] NOTE 0000000000000338 00000338
0000000000000020 0000000000000000 A 0 0 8
[ 3] .note.ABI-tag NOTE 0000000000000358 00000358
0000000000000020 0000000000000000 A 0 0 4
[ 4] .hash HASH 0000000000000378 00000378
0000000000000030 0000000000000004 A 6 0 8
[ 5] .gnu.hash GNU_HASH 00000000000003a8 000003a8
000000000000001c 0000000000000000 A 6 0 8
[ 6] .dynsym DYNSYM 00000000000003c8 000003c8
00000000000000a8 0000000000000018 A 7 1 8
[ 7] .dynstr STRTAB 0000000000000470 00000470
000000000000008f 0000000000000000 A 0 0 1
[ 8] .gnu.version VERSYM 0000000000000500 00000500
000000000000000e 0000000000000002 A 6 0 2
[ 9] .gnu.version_r VERNEED 0000000000000510 00000510
0000000000000030 0000000000000000 A 7 1 8
[10] .rela.dyn RELA 0000000000000540 00000540
00000000000000c0 0000000000000018 A 6 0 8
[11] .rela.plt RELA 0000000000000600 00000600
0000000000000018 0000000000000018 AI 6 23 8
[12] .init PROGBITS 0000000000001000 00001000
000000000000001b 0000000000000000 AX 0 0 4
[13] .plt PROGBITS 0000000000001020 00001020
0000000000000020 0000000000000010 AX 0 0 16
[14] .text PROGBITS 0000000000001040 00001040
0000000000000125 0000000000000000 AX 0 0 16
[15] .fini PROGBITS 0000000000001168 00001168
000000000000000d 0000000000000000 AX 0 0 4
[16] .rodata PROGBITS 0000000000002000 00002000
0000000000000013 0000000000000000 A 0 0 4
[17] .eh_frame_hdr PROGBITS 0000000000002014 00002014
0000000000000024 0000000000000000 A 0 0 4
[18] .eh_frame PROGBITS 0000000000002038 00002038
000000000000007c 0000000000000000 A 0 0 8
[19] .init_array INIT_ARRAY 0000000000003dd8 00002dd8
0000000000000008 0000000000000008 WA 0 0 8
[20] .fini_array FINI_ARRAY 0000000000003de0 00002de0
0000000000000008 0000000000000008 WA 0 0 8
[21] .dynamic DYNAMIC 0000000000003de8 00002de8
00000000000001f0 0000000000000010 WA 7 0 8
[22] .got PROGBITS 0000000000003fd8 00002fd8
0000000000000028 0000000000000008 WA 0 0 8
[23] .got.plt PROGBITS 0000000000004000 00003000
0000000000000020 0000000000000008 WA 0 0 8
[24] .data PROGBITS 0000000000004020 00003020
0000000000000011 0000000000000000 WA 0 0 8
[25] .bss NOBITS 0000000000004034 00003031
000000000000000c 0000000000000000 WA 0 0 4
[26] .comment PROGBITS 0000000000000000 00003031
0000000000000027 0000000000000001 MS 0 0 1
[27] .symtab SYMTAB 0000000000000000 00003058
00000000000003a8 0000000000000018 28 19 8
[28] .strtab STRTAB 0000000000000000 00003400
00000000000001ea 0000000000000000 0 0 1
[29] .shstrtab STRTAB 0000000000000000 000035ea
0000000000000103 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
D (mbind), l (large), p (processor specific)
.textや.rodataなどがあるのがわかりますか?
プログラムヘッダを覗く
-lオプションです。
Elf ファイルタイプは DYN (Position-Independent Executable file) です
エントリポイント 0x1040
There are 13 program headers, starting at offset 64
プログラムヘッダ:
タイプ オフセット 仮想Addr 物理Addr
ファイルサイズ メモリサイズ フラグ 整列
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x00000000000002d8 0x00000000000002d8 R 0x8
INTERP 0x0000000000000318 0x0000000000000318 0x0000000000000318
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000618 0x0000000000000618 R 0x1000
LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000
0x0000000000000175 0x0000000000000175 R E 0x1000
LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000
0x00000000000000b4 0x00000000000000b4 R 0x1000
LOAD 0x0000000000002dd8 0x0000000000003dd8 0x0000000000003dd8
0x0000000000000259 0x0000000000000268 RW 0x1000
DYNAMIC 0x0000000000002de8 0x0000000000003de8 0x0000000000003de8
0x00000000000001f0 0x00000000000001f0 RW 0x8
NOTE 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000020 0x0000000000000020 R 0x8
NOTE 0x0000000000000358 0x0000000000000358 0x0000000000000358
0x0000000000000020 0x0000000000000020 R 0x4
GNU_PROPERTY 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000020 0x0000000000000020 R 0x8
GNU_EH_FRAME 0x0000000000002014 0x0000000000002014 0x0000000000002014
0x0000000000000024 0x0000000000000024 R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000002dd8 0x0000000000003dd8 0x0000000000003dd8
0x0000000000000228 0x0000000000000228 R 0x1
セグメントマッピングへのセクション:
セグメントセクション...
00
01 .interp
02 .interp .note.gnu.property .note.ABI-tag .hash .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
03 .init .plt .text .fini
04 .rodata .eh_frame_hdr .eh_frame
05 .init_array .fini_array .dynamic .got .got.plt .data .bss
06 .dynamic
07 .note.gnu.property
08 .note.ABI-tag
09 .note.gnu.property
10 .eh_frame_hdr
11
12 .init_array .fini_array .dynamic .got
下部の「セグメントマッピングへのセクション」と上部の「プログラムヘッダ」は対応しています。例えば、.textは「セグメントマッピングへのセクション」では03に位置しています。つまり、「プログラムヘッダ」の上から4番目のセグメントに所属しています。
上から4番目はLOADセグメントです。フラグを見るとR E
(Read / Executable)が割り当てられていることがわかります。
ちなみに、上から6つめのLOADセグメントに注目すると、ファイルサイズ(0x259)とメモリサイズ(0x268)が一致していないことがわかります。
このセグメントには.dataセクションと.bssセクションが含まれています。これらはどちらもグローバル変数およびstatic変数を格納する部分ですが、初期値の有無で区別されています。初期値があればdataセクション、なければbssという分類です。つまり、先程のコードは以下のようになります:
// 初期値がない、bss
int global;
// 初期値がある(255)、data
unsigned char init = 255;
どちらも変数として存在するためメモリ上に配置されるが、bssは初期値がない(=ファイルに初期値を記録する必要がない)という性質があるため、ファイルサイズとメモリ上のサイズが一致しないわけです。ちなみにグローバル変数およびstatic変数は、初期値を指定されなかった場合はだいたい0で初期化されます。
セクションを覗いてみる
先程のコードに printf("Hello, world!\n");
という文がありました。この文字列リテラルも、もちろんELFに含まれています。
文字列リテラルは読み込み専用であるため、.rodataに格納されていそうです。-xオプションを用いて、.rodataセクションを表示してみましょう。
$ readelf -x .rodata a.out
セクション '.rodata' の 十六進数ダンプ:
0x00002000 01000200 48656c6c 6f2c2077 6f726c64 ....Hello, world
0x00002010 210a00 !..
0aはASCIIコードではLF(改行、’\n’)を表します。そして、文字列リテラルは末尾にヌル文字(’\0’)が挿入されるため、48656c6c 6f2c2077 6f726c64 210a00
となるわけです。
おわり
ざっくりとした解説でしたが、いかがでしたか?
今回は紹介しませんでしたが、objdumpに-dオプションを渡すと、バイナリの逆アセンブルが可能です。つまり、実行ファイルのアセンブリを覗けるわけです。
こんな感じでバイナリを覗くのはわりと楽しいので、ぜひやってみてください。
コメント