【解説】Linuxの実行ファイルはどのような構造をしているのか

Linux

わたすけです。いろいろあって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オプションを渡すと、バイナリの逆アセンブルが可能です。つまり、実行ファイルのアセンブリを覗けるわけです。
こんな感じでバイナリを覗くのはわりと楽しいので、ぜひやってみてください。

参考サイト

コメント

タイトルとURLをコピーしました