LoginSignup
9
7

More than 1 year has passed since last update.

機械語を「直打ち」して Hello, World!

Last updated at Posted at 2022-10-17

この記事には憶測疑問点が大量に含まれます。

この記事は長いうえにガバガバなので、結論だけ見たい人は目次から「本題:カチャカチャカチャ...ッターン!ってする」まで飛んでいってください。

はじめに

私たち人類とコンピュータでは使える言葉が異なります。ゆえに私たちは、普段はコンパイラさんやインタプリタさんの力を借りてプログラミングをしています。

しかしコンパイラさんにもインタプリタさんにも頼らず、直接機械語を入力してプログラムを作れたら最高にクールではないでしょうか。7f 45 4c 46 02 01 01 00 ... (ッターン!) なんてできたら、すごくハッカーって感じがします。というわけで今すぐやりましょう。

なお、この記事では x86-64 命令セットを用いて ELF64 ファイルを作成します。なのでお使いの CPU の命令セットが異なる方や Linux を使っていない方は各人でアセンブルの方法を調べるなり仮想 OS 使うなりしてください。

機械語の用意

今回は伝統に従い、"hello, world" を作りましょう。helloworld をアセンブリ言語で書くとこんな感じになります。

hello.s
    .text
    mov $1, %rax    ; sys_write のシステムコール番号
    mov $1, %rdi    ; 標準出力のファイルディスクリプタ
    lea hello, %rsi ; 書き出すデータのアドレス
    mov $len, %rdx  ; 書き出す文字数
    syscall         ; sys_write の実行
    mov $60, %rax   ; sys_exit のシステムコール番号
    mov $0, %rdi    ; 終了ステータス
    syscall         ; sys_exit の実行

    .data
hello:
    .ascii "hello, world\n"
    .set len, . - hello

このアセンブリをバイナリ列に変換する方法を自分で調べてもよかったのですが、そこまでするのも面倒なので as コマンドでオブジェクトファイルに変換したものをのぞき見することにしましょう。

$ as hello.s -o hello.o
$ objdump -d hello.o

hello.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <.text>:
   0:   48 c7 c0 01 00 00 00    mov    $0x1,%rax
   7:   48 c7 c7 01 00 00 00    mov    $0x1,%rdi
   e:   48 8d 34 25 00 00 00    lea    0x0,%rsi
  15:   00
  16:   48 c7 c2 0d 00 00 00    mov    $0xd,%rdx
  1d:   0f 05                   syscall
  1f:   48 c7 c0 3c 00 00 00    mov    $0x3c,%rax
  26:   48 c7 c7 00 00 00 00    mov    $0x0,%rdi
  2d:   0f 05                   syscall

というわけで 48 c7 c0 ... 0f 05 という 47 バイトのバイナリ列になるようです。ただしここで注意しなければならないのがこの部分です。

   e:   48 8d 34 25 00 00 00    lea    0x0,%rsi
  15:   00

lea 命令の下位 4 バイト(今は 00 00 00 00 となっている部分)には hello, world という文字列が格納されているアドレスが入っていなければならないのですが、このアドレスはまだ確定していないので 0 が入っています。この部分は後で自分で書き直します。
ちなみに objdump コマンドはなんでこんな中途半端なところで改行しているのかというと、デフォルトでは 80 文字で改行する設定になっているからだそうです。-w オプションをつけると改行しなくなりますが、こんどはディスアセンブル結果が不揃いになって見づらくなります。もうちょっとどうにかならんのか

$ objdump -dw hello.o

hello.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <.text>:
   0:   48 c7 c0 01 00 00 00    mov    $0x1,%rax
   7:   48 c7 c7 01 00 00 00    mov    $0x1,%rdi
   e:   48 8d 34 25 00 00 00 00         lea    0x0,%rsi
  16:   48 c7 c2 0d 00 00 00    mov    $0xd,%rdx
  1d:   0f 05                   syscall
  1f:   48 c7 c0 3c 00 00 00    mov    $0x3c,%rax
  26:   48 c7 c7 00 00 00 00    mov    $0x0,%rdi
  2d:   0f 05                   syscall

ELF ファイルの構造

ELF ヘッダ

先ほども書いたように今回は ELF64 を作っていきます。ELF64 はまず 64 バイトのヘッダから始まります。このヘッダは以下のような構造になっています。

struct {
    unsigned char e_ident[16]; // ファイル情報
    uint16_t      e_type;      // ファイルの種類。実行ファイルの場合は 0x2
    uint16_t      e_machine;   // アーキテクチャ。x86-64 の場合は 0x3e
    uint32_t      e_version;   // バージョン。現時点では 0x1 で固定っぽい
    uint64_t      e_entry;     // エントリポイント(後述)
    uint64_t      e_phoff;     // プログラムヘッダテーブルまでのオフセット(後述)
    uint64_t      e_shoff;     // セクションヘッダテーブルまでのオフセット(後述)
    uint32_t      e_flags;     // プロセッサ固有のフラグ。とりあえず 0x0 を入れておけばいいっぽい・・・?
    uint16_t      e_ehsize;    // このヘッダのバイト数。前述のとおり 64 バイト
    uint16_t      e_phentsize; // プログラムヘッダテーブルのエントリ一つ当たりのバイト数(後述)
    uint16_t      e_phnum;     // プログラムヘッダテーブルのエントリ数(後述)
    uint16_t      e_shentsize; // セクションヘッダテーブルのエントリ一つあたりのバイト数(後述)
    uint16_t      e_shnum;     // セクションヘッダテーブルのエントリ数(後述)
    uint16_t      e_shstrndx;  // セクションヘッダ文字列テーブルに対応するセクションヘッダテーブルエントリのインデックス(後述)
};

このうち e_indet[16] ですが、これはこんな感じになっています。

  • マジックナンバー(4 バイト):0x7f 0x45 0x4c 0x46 で固定。
  • アーキテクチャ(1 バイト):64ビットの場合は 0x2。
  • エンディアン(1 バイト):リトルエンディアンの場合は 0x1。
  • バージョン(1 バイト):現時点では 0x1 で固定っぽい。
  • OS と ABI (1 バイト):UNIX System V ABI の場合は 0x1。
  • ABI のバージョン(1 バイト):現時点では ABI にかかわらず 0x0 が入っていなければならない(?)
  • 予約済みバイト(7 バイト):現時点では 0x0 で埋めなければならない。

ところでマジックナンバーの 0x7f 0x45 0x4c 0x46 ですが、下位 3 バイトを ASCII コードとして解釈すると "ELF" となります。じゃあ先頭の 0x7f はなんなんだって話ですが、これはよくわかっていないようです。「マジックナンバーに印字可能文字を使うとプレーンテキストと解釈される可能性があるから制御文字の中から DEL が選ばれた」とか「"ELF" に発音が似ている "LF" を leet 表記して "7f" になった」とか「意味なんてない」とかいろいろな説がありましたが、どれが本当かはわかりません。

プログラムヘッダテーブル

ELF ヘッダに関してまだ説明していないことはありますが、説明の順番の都合上とりあえず後回しにします。

ELF ヘッダの後ろには「プログラムヘッダテーブル」を記述します。これはプログラムを構成するセグメントの情報を記述するもので、一つのセグメントに対し必ず一つ存在します。さて今回作るプログラムでは以下のようにセグメントを分けていきたいと思います。

セグメント 1

セグメント 1 には機械語命令を入れます。さっき objdump でのぞき見したものを入れましょう。

48 c7 c0 01 00 00 00    | mov    $0x1,%rax
48 c7 c7 01 00 00 00    | mov    $0x1,%rdi
48 8d 34 25 XX XX XX XX | lea    0xXXXXXXXX,%rsi
48 c7 c2 0d 00 00 00    | mov    $0xd,%rdx
0f 05                   | syscall
48 c7 c0 3c 00 00 00    | mov    $0x3c,%rax
48 c7 c7 00 00 00 00    | mov    $0x0,%rdi
0f 05                   | syscall

ただし前述のとおり lea 命令に渡すアドレスはまだ確定していないのでとりあえず XX XX XX XX としていますが、セグメント全体の大きさは 47 バイトと確定しましたね。

セグメント 2

セグメント 2 には文字列 "hello, world" を入れます。ASCII コードを並べたらおしまいです。全部で 13 バイトです。

68 65 6c 6c 6f 2c 20 77 6f 72 6c 64 0a | "hello, world\n"

セグメントごとにエントリを書く

これで二つのセグメントはそろったので、それぞれの情報をプログラムヘッダに書いていきます。プログラムヘッダのエントリは 56 バイトでこのような構造になっています。

struct {
    uint32_t p_type;   // セグメントの種類
    uint32_t p_flags;  // フラグ
    uint64_t p_offset; // 対応するセグメントのオフセット
    uint64_t p_vaddr;  // セグメントが設置される仮想アドレス
    uint64_t p_paddr;  // 必要があれば物理アドレスも書く。必要なければ 0x0 でいい(今回は 0x0 でいいはず・・・?)
    uint64_t p_filesz; // ファイル内におけるセグメントのバイト数
    uint64_t p_memsz;  // セグメントがロードされるときのバイト数
    uint64_t p_align;  // セグメントを設置するときのアライメント。p_vaddr と p_offset は p_align を法として合同でなければならない
};

なにやらよくわかりませんが、分かる範囲で作っていきます。

まず p_type ですが、今回作る二つのセグメントはどちらも「ロード可能セグメント」というのにあたります。このとき p_type には 0x2 を入れます。

次に p_flags ですが、これはセグメントの属性に応じて次の値の論理和で表します。

  • 実行可能フラグ:0x1
  • 書き込み可能フラグ:0x2
  • 読み込み可能フラグ:0x4

セグメント 1 には命令が入るので、実行可能かつ読み込み可能でなければなりません。よってセグメント 1 のフラグは 0x1 | 0x4 = 0x5 となります。一方セグメント 2 は読み取り専用データなので 0x4 を入れます。

次に p_offset にはそれぞれのセグメントがファイル内のどこにあるのかを記述します。ここまでに作ってきたヘッダやらセグメントやらを並べるとこんな感じになります。

  • ELF ヘッダ(64 バイト)
  • プログラムヘッダ(56 バイトのエントリが二つで合計 112 バイト)
  • セグメント 1(47 バイト)
  • セグメント 2(13 バイト)

よってセグメント 1 はファイル先頭から 176 バイトの位置で始まるので p_offset0xb0、同様にセグメント 2 の p_offset0xdf となります。

次にちょっと飛ばして p_fileszp_memsz ですが、p_filesz にはセグメントのバイト数を入れます。それぞれ 47 バイトと 13 バイトだったので、0x2f0xd となります。次に p_memsz ですが、これはセグメントが実際にロードされるときのサイズです。p_filesz と違うことなんてあるの?と思いましたが、ここによると .bss セクションなどはロード時にのみ領域が確保されるため、その場合 p_memszp_filesz より大きくなるようです。今回は関係ないので p_memsz には p_filesz と同じ値を入れます。

最後に残った p_alignp_vaddr ですが、まずセグメント 1 から考えていきます。この辺がよくわかっていないのですが、実行ファイルはどうやら仮想アドレス 0x400000 にロードされるらしい(?)です。するとセグメント 1 は仮想アドレス 0x4000b0 から始まることになります(たぶん)。なのでセグメント 1 の p_vaddr には 0x4000b0 を指定します。あとで説明する p_align0x1 としておきます。

次にセグメント 2 ですが、セグメント 1 とセグメント 2 では権限が違う(前者は実行可能・読み取り可能、後者は読み取り可能)ので違うページに設置されなければならない(??)らしく、それを実現するためにアライメントを適切に指定する必要があるそうです。私の環境ではこのとおりページサイズは 4096 、十六進法では 0x1000 です。

$ getconf PAGE_SIZE
4096

そこでセグメント 2 では p_align には 0x1000 を入れ、設置される仮想アドレス p_vaddr0x400000 + 0x1000 + 0xdf = 0x4010df となるようです。

本当にこれでいいのか不安なところですが、とりあえず二つのエントリはこのようになりました。

エントリ 1

01 00 00 00             | ロード可能セグメント
05 00 00 00             | 実行可能、読み取り可能
b0 00 00 00 00 00 00 00 | ファイル先頭から 176 バイト
b0 00 40 00 00 00 00 00 | 仮想アドレスは 0x4000b0
00 00 00 00 00 00 00 00 | 物理アドレス(0 でいいはず)
2f 00 00 00 00 00 00 00 | セグメントは 47 バイト
2f 00 00 00 00 00 00 00 | ロードされるときも 47 バイト
01 00 00 00 00 00 00 00 | アライメントの必要なし

エントリ 2

01 00 00 00             | ロード可能セグメント
04 00 00 00             | 読み取り可能
df 00 00 00 00 00 00 00 | ファイル先頭から 223 バイト
df 10 40 00 00 00 00 00 | 仮想アドレスは 0x4010df
00 00 00 00 00 00 00 00 | 物理アドレス(0 でいいはず)
0d 00 00 00 00 00 00 00 | セグメントは 13 バイト
0d 00 00 00 00 00 00 00 | ロードされるときも 13 バイト
00 10 00 00 00 00 00 00 | ページサイズ(4096 バイト)でアライメントする

セグメント

プログラムヘッダテーブルを書き終わったら今度はセグメントを書いていきます。これはさっきチラッと出てきましたが、先ほど セグメント 2 が始まる仮想アドレスは 0x4010df であるとわかったので、ようやく lea 命令への引数も決まりました。なので二つのセグメントはこのようになります。

セグメント 1

48 c7 c0 01 00 00 00    | mov    $0x1,%rax
48 c7 c7 01 00 00 00    | mov    $0x1,%rdi
48 8d 34 25 df 10 40 00 | lea    0x4010df,%rsi
48 c7 c2 0d 00 00 00    | mov    $0xd,%rdx
0f 05                   | syscall
48 c7 c0 3c 00 00 00    | mov    $0x3c,%rax
48 c7 c7 00 00 00 00    | mov    $0x0,%rdi
0f 05                   | syscall

セグメント 2

68 65 6c 6c 6f 2c 20 77 6f 72 6c 64 0a | "hello, world\n"

セクションヘッダ文字列テーブル

この先はセクションヘッダ文字列テーブルとセクションヘッダテーブルを作っていきます。ただこの二つは実行ファイルでは省略可能です。なのでなくてもいいものではあるですが、せっかくなので作っていきます。

まずセクションヘッダ文字列テーブルには各セクションの名前が入ります。今回作る ELF には三つのセクションがあります。

  • .text セクション。これは機械語命令を書くセクションなので、セグメント 1 全体が .text セクションに当たる。
  • .rodata セクション。これは読み取り専用データを書くセクションなので、セグメント 2 全体が .rodata セクションに当たる。"rodata" は "Read Only DATA" の略。
  • .shstrtab セクション。セクションヘッダ文字列テーブル自体がこのセクションにあたる。".shstrtab" は "Section Header STRing TABle" の略。

セクションヘッダ文字列テーブルはこれら三つの名前をヌル文字で区切りながら並べたらおしまいです。ただし理由はわからないのですが、セクションヘッダ文字列テーブルの先頭にもヌル文字を入れなければならないというルールがあるのでそのようにしておきます。

00                            | "\0"
2e 74 65 78 74 00             | ".text\0"
2e 72 6f 64 61 74 61 00       | ".rodata\0"
2e 73 68 73 74 72 74 61 62 00 | ".shstrtab\0"

セクションヘッダテーブル

セクションヘッダテーブルでは各セクションの情報を書いていきます。セクションヘッダテーブルのエントリはこのような構造になっています。

struct {
    uint32_t sh_name;      // セクション名がセクションヘッダ文字列テーブルの何バイト目から書いてあるのか
    uint32_t sh_type;      // セクションの種類
    uint64_t sh_flags;     // フラグ
    uint64_t sh_addr;      // 対応するセクションがロードされる場合はその仮想アドレス。されない場合は 0x0
    uint64_t sh_offset;    // 対応するセクションのオフセット
    uint64_t sh_size;      // 対応するセクションのバイト数
    uint32_t sh_link;      // 関連するセクションのエントリのインデックス。今回は使わないので 0x0
    uint32_t sh_info;      // 追加情報。今回は使わないので 0x0
    uint64_t sh_addralign; // セクションを設置するときのアライメント。今回は関係ないので 0x1
    uint64_t sh_entsize;   // 固定サイズエントリを格納するようなセクションの場合はそのエントリのバイト数。今回は違うので 0x0
};

まず sh_name ですが、.text セクションはセクションヘッダ文字列テーブルの 1 バイト目から、.rodata セクションは 7 バイト目から、.shstrtab セクションは 15 バイト目から名前が書いてあるので、sh_name はそれぞれ 0x10x70xf となります。

次に sh_type ですが、各々のプログラムで定義される内容を含むセクションは 0x1、文字列テーブルを含むセクションの場合は 0x3 となります。なので .text セクションと .rodata セクションは 0x1、.shstrtab セクションは 0x3 になります。

次に sh_flags は以下の値の論理和で表します。

  • 書き込み可能フラグ:0x1
  • ロードフラグ:0x2
  • 実行可能フラグ:0x4

.text セクションはロードされ、かつ実行可能なので sh_flags0x2 | 0x4 = 0x6 となります。.rodata セクションはロードされるので 0x2 となり、.shstrtab セクションはどれにも当てはまらないので 0x0 になります。

次に sh_addr セクションはそのセクションがロードされる場合にはその仮想アドレスが入ります。なので .text セクションは 0x4000b0、.rodata セクションは 0x4010df、.shstrtab セクションはロードされないので 0x0 となります。

次に sh_offset にはそのセクションのオフセットが入ります。.text セクションは 0xb0、.rodata セクションは 0xdf、.shstrtab セクションは 0xec となります。

最後に sh_size にはセクションのバイト数が入ります。よって .text セクションは 0x2f、.rodata セクションは 0xd、.shstrtab セクションは 0x19 となります。

よってこの三つのエントリはこのようになります。

エントリ 1(.text セクションの情報)

01 00 00 00             | セクションヘッダ文字列テーブルの 1 バイト目からが名前
01 00 00 00             | プログラム定義の内容を含む
06 00 00 00 00 00 00 00 | ロードされ、実行可能
b0 00 40 00 00 00 00 00 | 仮想アドレスは 0x4000b0
b0 00 00 00 00 00 00 00 | ファイル先頭から 176 バイト
2f 00 00 00 00 00 00 00 | セクションは 47 バイト
00 00 00 00             | 他のセクションとは特に関係なし
00 00 00 00             | 追加情報なし
01 00 00 00 00 00 00 00 | アライメントなし
00 00 00 00 00 00 00 00 | 固定長エントリを含まない

エントリ 2(.rodata セクションの情報)

07 00 00 00             | セクションヘッダ文字列テーブルの 7 バイト目からが名前
01 00 00 00             | プログラム定義の内容を含む
02 00 00 00 00 00 00 00 | ロードされる
df 10 40 00 00 00 00 00 | 仮想アドレスは 0x4010df
df 00 00 00 00 00 00 00 | ファイル先頭から 223 バイト
0d 00 00 00 00 00 00 00 | セクションは 13 バイト
00 00 00 00             | 他のセクションとは特に関係なし
00 00 00 00             | 追加情報なし
01 00 00 00 00 00 00 00 | アライメントなし
00 00 00 00 00 00 00 00 | 固定長エントリを含まない

エントリ 3(.shstrtab セクションの情報)

0f 00 00 00             | セクションヘッダ文字列テーブルの 15 バイト目からが名前
03 00 00 00             | 文字列テーブルを含む
00 00 00 00 00 00 00 00 | ロードされない
00 00 00 00 00 00 00 00 | 仮想アドレスなし
ec 00 00 00 00 00 00 00 | ファイル先頭から 236 バイト
19 00 00 00 00 00 00 00 | セクションは 25 バイト
00 00 00 00             | 他のセクションとは特に関係なし
00 00 00 00             | 追加情報なし
01 00 00 00 00 00 00 00 | アライメントなし
00 00 00 00 00 00 00 00 | 固定長エントリを含まない

ただし、これも理由はわからないのですが、セクションヘッダテーブルには先頭に「エントリ 0」として 64 バイトの 0 を置いておかなければなりません。どうやら将来的に使うために予約しているようです。

エントリ 0

00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

エントリ 1

・・・

ELF ヘッダ、再び

さてこれでようやく ELF ファイル全体の構造と内容が概ね定まったので、ELF ヘッダのまだ値が決まっていない部分を決めていきます。ELF ヘッダをおさらいするとこんな感じになっていました。

struct {
    unsigned char e_ident[16]; // ファイル情報
    uint16_t      e_type;      // ファイルの種類。実行ファイルの場合は 0x2
    uint16_t      e_machine;   // アーキテクチャ。x86-64 の場合は 0x3e
    uint32_t      e_version;   // バージョン。現時点では 0x1 で固定っぽい
    uint64_t      e_entry;     // エントリポイント(後述)
    uint64_t      e_phoff;     // プログラムヘッダテーブルまでのオフセット(後述)
    uint64_t      e_shoff;     // セクションヘッダテーブルまでのオフセット(後述)
    uint32_t      e_flags;     // プロセッサ固有のフラグ。とりあえず 0x0 を入れておけばいいっぽい・・・?
    uint16_t      e_ehsize;    // このヘッダのバイト数。前述のとおり 64 バイト
    uint16_t      e_phentsize; // プログラムヘッダテーブルのエントリ一つ当たりのバイト数(後述)
    uint16_t      e_phnum;     // プログラムヘッダテーブルのエントリ数(後述)
    uint16_t      e_shentsize; // セクションヘッダテーブルのエントリ一つあたりのバイト数(後述)
    uint16_t      e_shnum;     // セクションヘッダテーブルのエントリ数(後述)
    uint16_t      e_shstrndx;  // セクションヘッダ文字列テーブルに対応するセクションヘッダテーブルエントリのインデックス(後述)
};

e_entry はエントリポイント、つまり命令が始まるところなので 0x4000b0 が入ります。プログラムヘッダテーブルまでのオフセット e_phoff は 64 バイトなので 0x40、セクションヘッダテーブルまでのオフセット e_shoff は 64 + 56 * 2 + 47 + 13 + 25 = 261 バイトなので 0x105、プログラムヘッダテーブルのエントリは一つ当たり 56 バイトだったので e_phentsize0x38、プログラムヘッダテーブルのエントリ数は二つなので e_phnum0x2、セクションヘッダテーブルのエントリは一つ当たり 64 バイトだったので e_shentsize0x40、セクションヘッダテーブルのエントリ数は(エントリ 0 を入れて)四つなので e_shnum0x4、セクションヘッダテーブルのエントリのうちセクションヘッダ文字列テーブルの情報を格納しているのはエントリ 3 なので e_shstrndx0x3 となります。

これでようやく ELF ファイルが完成しました。全部で 517 バイトとなります。

ヘッダ(64 バイト)

7f 45 4c 46             | マジックナンバー
02                      | 64 ビット
01                      | リトルエンディアン
01                      | バージョン(実質 1 で固定)
00                      | UNIX System V ABI
00                      | ABI のバージョン(実質 0 で固定)
00 00 00 00 00 00 00    | 予約領域(0 で埋める)
02 00                   | 実行ファイル
3e 00                   | x86-64
01 00 00 00             | バージョン(実質 1 で固定)
b0 00 40 00 00 00 00 00 | エントリポイントは 0x4000b0
40 00 00 00 00 00 00 00 | プログラムヘッダテーブルまで 64 バイト
05 01 00 00 00 00 00 00 | セクションヘッダテーブルまで 261 バイト
00 00 00 00             | プロセッサ固有のフラグ(たぶん 0 で OK)
40 00                   | このヘッダは 64 バイト
38 00                   | プログラムヘッダテーブルのエントリは一つ当たり 56 バイト
02 00                   | プログラムヘッダテーブルのエントリは二つ
40 00                   | セクションヘッダテーブルのエントリは一つ当たり 64 バイト
04 00                   | セクションヘッダテーブルのエントリは四つ
03 00                   | セクションヘッダテーブルのエントリ 3 がセクションヘッダ文字列テーブルの情報を格納している

プログラムヘッダテーブル(56 バイト × 2)

エントリ 1(56 バイト)

01 00 00 00             | ロード可能セグメント
05 00 00 00             | 実行可能、読み取り可能
b0 00 00 00 00 00 00 00 | ファイル先頭から 176 バイト
b0 00 40 00 00 00 00 00 | 仮想アドレスは 0x4000b0
00 00 00 00 00 00 00 00 | 物理アドレス(0 でいいはず)
2f 00 00 00 00 00 00 00 | セグメントは 47 バイト
2f 00 00 00 00 00 00 00 | ロードされるときも 47 バイト
01 00 00 00 00 00 00 00 | アライメントの必要なし

エントリ 2(56 バイト)

01 00 00 00             | ロード可能セグメント
04 00 00 00             | 読み取り可能
df 00 00 00 00 00 00 00 | ファイル先頭から 223 バイト
df 10 40 00 00 00 00 00 | 仮想アドレスは 0x4010df
00 00 00 00 00 00 00 00 | 物理アドレス(0 でいいはず)
0d 00 00 00 00 00 00 00 | セグメントは 13 バイト
0d 00 00 00 00 00 00 00 | ロードされるときも 13 バイト
00 10 00 00 00 00 00 00 | ページサイズ(4096 バイト)でアライメントする

セグメント 1(47 バイト)

.text セクション(47 バイト)

48 c7 c0 01 00 00 00    | mov    $0x1,%rax
48 c7 c7 01 00 00 00    | mov    $0x1,%rdi
48 8d 34 25 df 10 40 00 | lea    0x4010df,%rsi
48 c7 c2 0d 00 00 00    | mov    $0xd,%rdx
0f 05                   | syscall
48 c7 c0 3c 00 00 00    | mov    $0x3c,%rax
48 c7 c7 00 00 00 00    | mov    $0x0,%rdi
0f 05                   | syscall

セグメント 2(13 バイト)

.rodata セクション(13 バイト)

68 65 6c 6c 6f 2c 20 77 6f 72 6c 64 0a | "hello, world\n"

セクションヘッダ文字列テーブル(25 バイト)

00                            | "\0"
2e 74 65 78 74 00             | ".text\0"
2e 72 6f 64 61 74 61 00       | ".rodata\0"
2e 73 68 73 74 72 74 61 62 00 | ".shstrtab\0"

セクションヘッダテーブル(64 バイト × 4)

エントリ 0(64 バイト)

00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

エントリ 1(64 バイト)

01 00 00 00             | セクションヘッダ文字列テーブルの 1 バイト目からが名前
01 00 00 00             | プログラム定義の内容を含む
06 00 00 00 00 00 00 00 | ロードされ、実行可能
b0 00 40 00 00 00 00 00 | 仮想アドレスは 0x4000b0
b0 00 00 00 00 00 00 00 | ファイル先頭から 176 バイト
2f 00 00 00 00 00 00 00 | セクションは 47 バイト
00 00 00 00             | 他のセクションとは特に関係なし
00 00 00 00             | 追加情報なし
01 00 00 00 00 00 00 00 | アライメントなし
00 00 00 00 00 00 00 00 | 固定長エントリを含まない

エントリ 2(64 バイト)

07 00 00 00             | セクションヘッダ文字列テーブルの 7 バイト目からが名前
01 00 00 00             | プログラム定義の内容を含む
02 00 00 00 00 00 00 00 | ロードされる
df 10 40 00 00 00 00 00 | 仮想アドレスは 0x4010df
df 00 00 00 00 00 00 00 | ファイル先頭から 223 バイト
0d 00 00 00 00 00 00 00 | セクションは 13 バイト
00 00 00 00             | 他のセクションとは特に関係なし
00 00 00 00             | 追加情報なし
01 00 00 00 00 00 00 00 | アライメントなし
00 00 00 00 00 00 00 00 | 固定長エントリを含まない

エントリ 3(64 バイト)

0f 00 00 00             | セクションヘッダ文字列テーブルの 15 バイト目からが名前
03 00 00 00             | 文字列テーブルを含む
00 00 00 00 00 00 00 00 | ロードされない
00 00 00 00 00 00 00 00 | 仮想アドレスなし
ec 00 00 00 00 00 00 00 | ファイル先頭から 236 バイト
19 00 00 00 00 00 00 00 | セクションは 25 バイト
00 00 00 00             | 他のセクションとは特に関係なし
00 00 00 00             | 追加情報なし
01 00 00 00 00 00 00 00 | アライメントなし
00 00 00 00 00 00 00 00 | 固定長エントリを含まない

本題:カチャカチャカチャ...ッターン!ってする

これでようやく ELF ファイルの設計が終わったので、実際にファイルを作っていきます。十六進文字列から実際にファイルを作成するには xxd コマンドが使えます。こんな感じにして、1 バイトずつ地道に打っていきます。完成した後は chmod で実行権限を持たせるのをお忘れなく。

うおおおおおおおお!(カチャカチャカチャ

$ xxd -r -p - a.out
7f 45 4c 46 02 01 01 00
...

ッターン!

$ chmod 755 a.out
$ ./a.out
hello, world

🎊👑🥳🎉🍺優勝🍺🎉🥳👑🎊

おわりに

今回はリチギにセグメントを分けたりセクションヘッダテーブルを書いたりしたのでファイルサイズも 517 バイトとやや大きくなりましたが(それでも ld で作るよりははるかに小さいですが)、このページではなんと 120 バイトで作ってしまっています。ELF64 に必須のヘッダとプログラムヘッダで 64 + 56 = 120 バイトなので、これは ELF64 のサイズの下限でもあります。どうやっているのかというと、どうやら予約済み領域や未使用の領域に命令や文字列を強引に書き込むことで実現しているようです。そんなことしていいのか・・・?

9
7
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
7