冬の自由課題 ~ ハロー“Hello, World” OSと標準ライブラリのシゴトとしくみ ~ の読書感想文

書き出し

年の瀬も近くなり始めた1週間ほど前、本棚にふと目を向けると山になり溜まっていた積み本がそびえ立っていた。流石にこのまま積み本を放置しておくわけにはいかないなと思い手に取りました。

この本を買ったきっかけは、自分自身が 低いレイヤーの話を知らなすぎ、今後エンジニアとして成長する上で足かせになるのではないかと、思い早いうちに手を打っておこうと思い購入しました。

少しでもパソコンが動いている理由がつかめればなと思い、この本を読んだ筆者が思った事をブログに書いていく。

読んでわかった事

出力されたニーモニックコードと関数の呼び出し方(スタック/レジスタ等)

これは自分自身あまり自信がなかった箇所で、CTFの問題でRevやPwnなどがでてきても「わからないから触れない」と自ら避けていた一因でもある。

実際に読んでみて、軽いニーモニックコードならメモを取っていれば読めるようになり、レジスタの意味もほんの少しは理解できたと思うのでよかったと思っている。

読書時にまとめたノート

読書時にまとめたノート

アセンブラを読んでみる

今回はAT&T記法のアセンブラを読んでいく この記法ではmov %esp,%ebpと書かれている場合、mov [転送元],[転送先]と考えると良い。

それでは読んでいく。

下記はhelloworldをobjdumpの結果の一部を抽出したものである。

080482bc <main>:
 80482bc:       55                      push   %ebp
 80482bd:       89 e5                   mov    %esp,%ebp
 80482bf:       83 e4 f0                and    $0xfffffff0,%esp
 80482c2:       83 ec 10                sub    $0x10,%esp
 80482c5:       8b 45 0c                mov    0xc(%ebp),%eax
 80482c8:       8b 10                   mov    (%eax),%edx
 80482ca:       b8 0c 36 0b 08          mov    $0x80b360c,%eax
 80482cf:       89 54 24 08             mov    %edx,0x8(%esp)
 80482d3:       8b 55 08                mov    0x8(%ebp),%edx
 80482d6:       89 54 24 04             mov    %edx,0x4(%esp)
 80482da:       89 04 24                mov    %eax,(%esp)
 80482dd:       e8 7e 10 00 00          call   8049360 <_IO_printf>
 80482e2:       b8 00 00 00 00          mov    $0x0,%eax
 80482e7:       c9                      leave
 80482e8:       c3                      ret
 80482e9:       90                      nop
 80482ea:       90                      nop
 80482eb:       90                      nop
 80482ec:       90                      nop
 80482ed:       90                      nop
 80482ee:       90                      nop
 80482ef:       90                      nop

上記の左端に列挙されている16進数の列はメモリのアドレスであり、アドレスの横には機械語の命令が存在している。

また、右端の言語として読み取れる箇所は、ニーモニックコードやアセンブリ言語と呼ばれている。

レジスタ(GPR)の話

x86でのレジスタで代表的なものは、EAX/EBX/ECX/EDXといったものがある。 レジスタというものは、CPUが持っている記憶領域であり。CPU固定の変数だと考えればよい。

汎用レジスタ(GPR)の構造

31                              0
+-------+-------+-------+-------+
|              EAX              |
+-------+-------+-------+-------+
|      A X      |  A H  |  A L  |
+-------+-------+-------+-------+

EAX/EBX/ECX/EDXは32bitから8bitまでの幅広いアクセス帯を持っている。それではEAXを例にして説明していく。

例 : EAX

32bitアクセス : EAX

16bitアクセス : AX

8bitアクセス上位 : AH

8bitアクセス下位 : AL

他にESP/EBP/ESI/EDIは32bitと16bitのみのアクセスしか利用できない。

汎用レジスタ(GPR)の使用用途

自分の理解のために以後16bitでの説明をする。

汎用レジスタ 名称 使用目的 使用命令
AX アキュムレータレジスタ 算術演算の結果を格納 mov/add/xor/xchg
移動/スワップ/加算をはじめとした算術演算
BX ベースレジスタ アクセスするメモリの基底アドレスを記憶する 変数などにアクセスする際などに利用されるが汎用としても利用
CX カウンタレジスタ シフトローテート命令やループ命令に使用さ loop/rep/jcxz
繰り返しやジャンプの際に利用
DX データレジスタ 算術演算や操作・I/O操作・データの一時退避 mul/div
算術演算などに利用
SP スタックポインタレジスタ スタックのトップアドレスを指す push/pop
BP ベースポインタレジスタ スタックのベースポインタを指す
SI ソースレジスタ 読み込み元アドレス
DI デスティネーションレジスタ 書き込み先アドレス

レジスタの扱い方

先のhelloworldをobjdumpした結果の中に(%eax)のような表現がある。これはレジスタが保持する値がアドレスだった場合、その指すアドレスの値を示している。

またポインタの加算の際には0xc(%ebp)のように行う。この表現の解釈の仕方としてはebp+12の値渡しとなる。

スタックの扱い方

またもであるが、先の結果の中にaddやsub命令が出てきている。3行目のaddの場合は、スタックポインタに0xfffffff0論理積で掛け合わせることで、スタックポインタを16バイトに境界を揃えている。これは、キャッシュ効率を向上させて高速化することを目的にし、スタックの先頭をキャッシュラインに揃えている。

このように、ある値を特定の値の倍数に揃える行為を「アライメント」と呼ぶ。

4行目のsub命令はスタックポインタを0x10(16)バイト分の領域を確保するための命令である。x86でのスタックは下方伸長のため0方向へスタックポインタが減算することで、領域を確保している。

スタックフレーム

関数のためにスタック上に作成される領域を「スタックフレーム」と呼ぶ。

このmain関数の場合、16バイトのスタックフレームを確保している。

push命令は、スタックに値を積む命令であり、スタックポインタを4減算し、スタックを4バイト拡張する。

さらに2行目でそのスタックポインタをベースポインタに書き込んでいる。

ところでなぜ、4バイトなのか?という疑問が僕の中にはあったが、答えとしては、32bitCPUを前提にしているためint型が32bitつまり、4バイトということになることから、int型のアドレスを格納するための4バイトの領域確保を行なった。

関数呼び出しの手順

call命令が関数の呼び出し命令であり、今回の場合だと標準関数の_IO_printfを呼び出し、ジャンプしている。

このcallの前のmovの連続は、関数呼び出しの為に、引数の準備を行なっている。

先のアセンプリの内容を見るとわかるが、5行目でEBP(ペースポインタ)+12の位置にある値をEAXへ代入している。

x86の場合は、スタックを経由し関数へ引数を渡す。また関数を呼び出す際にスタックポインタの指す位置には、関数の戻り先のアドレスを格納する。

呼び出し直後のスタックイメージ

+---------------------+
|   第二引数(argv[0])   | ←SP+8
+---------------------+
|        第一引数       | ←SP+4
+---------------------+
|     戻り先アドレス     | ←SP
+---------------------+
0

呼び出し後にアライメントを行い、下方伸長による領域確保を行い、eaxに第二引数のアドレスを代入します。

eaxにはargv[0]のアドレスが割り当てられていることから、80482c8ではその値をebxに代入していることになる。

80482bf:       83 e4 f0                and    $0xfffffff0,%esp
80482c2:       83 ec 10                sub    $0x10,%esp
80482c5:       8b 45 0c                mov    0xc(%ebp),%eax
80482c8:       8b 10                   mov    (%eax),%edx

下方伸長による領域確保後のスタックイメージ

+---------------------+
|   第二引数(argv[0])   | ←0xc(%ebp)
+---------------------+
|        第一引数       | ←0x8(%ebp)
+---------------------+
|     戻り先アドレス     | ←0x4(%ebp)
+---------------------+
|呼び出し時のペースポインタ| ←(%ebp) SP+12
+---------------------+
|                     | ←SP+8
+---------------------+
|                     | ←SP+4
+---------------------+
|                     | ←SP
+---------------------+
0

ebxにeaxが指すアドレスの値を代入している為、eaxは自由に使えるようになり、0x80b360cを代入します。 その後、確保した領域に値を引数として格納します。

0x80b360cとは第一引数の格納されているアドレスとなる。

80482ca:       b8 0c 36 0b 08          mov    $0x80b360c,%eax
80482cf:       89 54 24 08             mov    %edx,0x8(%esp)
80482d3:       8b 55 08                mov    0x8(%ebp),%edx
80482d6:       89 54 24 04             mov    %edx,0x4(%esp)
80482da:       89 04 24                mov    %eax,(%esp)

call前までのスタックイメージ

+---------------------+
|   第二引数(*argv[])   | ←0xc(%ebp)
+---------------------+
|     第一引数(argc)    | ←0x8(%ebp)
+---------------------+
|     戻り先アドレス     | ←0x4(%ebp)
+---------------------+
|呼び出し時のペースポインタ| ←(%ebp) SP+12
+---------------------+
|     0xc(%ebp)の値    | ←SP+8
+---------------------+
|     0x8(%ebp)の値    | ←SP+4
+---------------------+
|      0x80b360c      | ←SP
+---------------------+
0

GDBのコマンド

アルバイト先でGDBを使えず困ってしまったことがあり、ここは少しでも学べればと思って本を読みながら注力していました。

触り程度にはなりますが、今使う分にはこのくらい覚えておけばいいかなというくらいには学べたと思っています。

読書時にまとめたノート

読書時にまとめたノート

GDBの使い方

起動

gdb ./example

実行

#プログラム実行
(gdb) run
(gdb) r
#引数付き実行
(gdb) run arg1 arg2
(gdb) r arg1 arg2
#デフォルトの引数を設定
(gdb) set args arg1 arg2
#デフォルトの引数を確認
(gdb) show args
#ソース別の行に制御到達するまで実行
(gdb) step
(gdb) s
(gdb) s <回数>
#カレントスタックフレームの次の行まで実行継続
(gdb) next
(gdb) n
(gdb) n <回数>
#マシン語命令を1行ごと実行(関数に入る)
(gdb) stepi
(gdb) si <回数>
(gdb) si
#マシン語命令を1行ごと実行(関数に入らない)
(gdb) nexti
(gdb) ni <回数>
(gdb) ni
#ブレークポイントまで走る
(gdb) continue
(gdb) c

終了

(gdb) quit
(gdb) q

shellコマンド実行

(gdb) shell <コマンド> -CF

ブレークポイント

#関数を設定
(gdb) break main
(gdb) b main
#行数指定
(gdb) b 30
(gdb) b hoge.c:30
#条件付き行数指定
(gdb) b 100 if test == 1
#ブレイクポイント通過回数条件
(gdb) ignore <ブレークポイント番号> <通過回数>
#ポインタ指定
(gdb) b *0xccccccc
#ブレークポイント一覧
(gdb) info breakpoints
(gdb) info break
#ブレークポイント削除
(gdb) delete <行数>
#ブレークポイント全削除
(gdb) delete

アセンブリコード

#記法の設定
(gdb) set disassembly-flavor <記法>
#関数をdisas
(gdb) disassemble main
(gdb) disas main
#現在のフレームを
(gdb) disas

レイアウト

#ソースコード
(gdb) layout src
#アセンブリ
(gdb) layout asm
#レジスタとアセンブリ
(gdb) layout prev

コアダンプを利用した解析

gdb <option> <バイナリファイル> <コアダンプファイル>

関数間移動

#アベンド場所表示
(gdb) where
#上位関数へ
(gdb) up
#下位関数
(gdb) down

表示

(gdb) <表示前>/<表示> <NAME>
形式 説明
x 16進数
d 10進数(デフォルト)
u 符号なし10進数
o 8進数
t 2進数
a アドレス
c 文字
f 浮動小数点数
i 命令

もっと詳しくはこちら

カーネルでの処理

自分自身、このレベルの低さまで来ると全く想像がつかずはじめはどのように動いているのかは想像がつきませんでした。

しかし実際に本を読んでみると、先ほどのアセンブラの知識を元にしながら、膨大なソースコードの絞り込み方や、目的のソースコードまでのたどり着き方などを優しく記載されており、大変わかりやすかった。

その中でも、カーネルへの引数の渡し方や戻り値の返し方などが自分自身の中ではしっくりときました。

読書時にまとめたノート

読書時にまとめたノート

今までのまとめ

printf()で出てきた`int $0x80`は下記の意味を持つ

カーネルを読む

ディレクトリ構造をみる

ディレクトリ名 内容
arch アーキテクチャ依存の処理
fs ファイルシステム関連
kernel カーネルアーキテクチャ共通処理
drivers 各種デバイスドライバ
include 各種ヘッダファイル
mm 仮想メモリ関連
net ネットワーク関連

int $0x80はシステムコール命令が呼んだ処理で、システムコールはCPUに対する例外発行となる。 いわゆるソフトウェア割り込みなので、割り込み処理の入口がある。そして割り込み処理はCPUごとの処理なのでアーキテクチャ依存となるのでarchディレクトリにあるのではないかと考える。

cd arch/x86

目的の処理を探す

カーネルなどのコードリーディングは目的を持って行えば、そこまで重いものではない。これはつまみ食い的な考えで、rubyの開発者である matzも語っている。

その原則を元に今回探すものをまとめる。

  1. syscallであるwriteを探す
    • syscallという文字列があると思われる
    • kernelディレクトリの中にあるので
  2. まずはヘッダーファイルを見つける
  3. 数が多かったら減らす

すると今回は5つのヘッダーファイルが見つかる。

その後は、ファイル名から絞る。

今回は32で調べているのでそれに絞りそれっぽいものを見つける。

割り込みハンドラ

entry_32.Sにはsyscall_callというラベルがあり、これはsys_call_tableへEAX*4した値を加算し関数を呼び出している。

syscall_table_32.Sでシステムコールのテーブルを配列で定義している。

つまるところ、この配列で定義している関数へ アクセスし関数を実行する。

その後、カーネルのコードを読むと、set_system_trap_gateという関数があることがわかる。おそらくこれは割り込みハンドラの登録処理であろう。つまり割り込みハンドラとしてsystem_callが登録されているというわけになる。

これが、int $0x80が実行された時にソフトウェア割り込み処理の入り口となる。

パラメータの渡し方を見てみる

hello実行ファイルを利用してパラメータの渡し方を見ていく。

2章でわかった事として、システムコールのwriteを利用していた。そこでgdbブレークポイントをwriteに設定してから初めていく。

次にシステムコールを呼び出した箇所にもブレイクポイントを貼り、レジスタの状態を見てみる。

するとEAX/EBX/ECX/EDXの4つのレジスタが利用され、そのレジスタに値が保持されていた。

現状の整理をしていこう - 現状実行しようとしているものとして、eipが指し示している__kernel_vsyscallであると推測できる - writeシステムコールは標準出力で、ファイルディスクリプタの値は1 - 表示される文字列は"Hello world! 1 /home/user/hello/hello"の38バイト ここから推測できる事としてEBXとECX,EDXは何かしらの関係はあるのではなかろうかという事である。

さらにESPの値とEDXの値が近いことからEDXの値はスタック上のものであると考えられる。そのことからスタックの中を確認する。

(gdb) x/16xw $esp

先頭スタックには+12のいちまで引数と思われる値が3つ連なっている。 このことから、スタック上にもシステムコールの引数が配置されていると考えられるが、この本で後述されているシステムコールラッパーの呼び出しのためのものであり、write()を呼び出した時にスタック経由で渡された引数である。

つまり、int $0x80そのものによって参照される箇所ではない。

実行を再開してみてレジスタを見てみよう。

EAXの値が変動している。これは2章で述べていた関数の戻り値は、EAXを経由して返されるということを鑑みるに戻り値なのであろう。

システムコール番号

システムコールのパラメータとしては、システムコールに渡されるものとシステムコール番号が存在する。

はじめにシステムコール番号から考えていく。

先のsys_call_tableは配列であるのではないかと先に推測していたが、これを考慮するとEAXに存在した4の数値は配列のインデックスなのではないかとなる。

また、それを裏付けるヘッダが存在しており、/arch/x86/include/asm/unistd_32.hの内部にはシステムコール番号が定義されている。

システムコールの引数

次はシステムコールの引数についてだ、システムコールの引数はどのように渡されているのか。

Linuxカーネルシステムコール処理では、system_callからsystem_callに入り、call命令によって処理関数を呼び出している。

先も述べた通り、x86では引数はスタックで引き渡す。つまりスタック上に値を保存している箇所があれば、そこが引数を渡している場所になる。

そのような視点で探すと、SAVE_ALLというマクロがentry_32.Sにて定義されている。

このマクロは、system_call内部で利用されている。

push命令で、EDX/ECX/EBXの値がスタックに渡されている。これが、スタック上に格納された状態でcall命令にシステムコールの処理関数が呼ばれているため、これがシステムコールに引数を渡す処理となるのであろう。

このことから、x86ではEAXでシステムコール番号を受け渡すことであろう。

Linux/x86では、EAXにシステムコール番号、EBX以降に引数を入れint %0x80を実行することいでシステムコールを実行することができると言える。

これは、Linux/x86でのシステムコール体系だからであり、FreeBSDであればまた異なる。

システムコールラッパー

システムコールを呼び出す作業はすべてアセンブラで記述する必要がある。そのことから、Cから呼び出せる関数という形でライブラリ化して、システム側から提供してもらう。

だからこそ、write()を利用すれば呼び出せるということになる。

そのような役割りのアセンブラで書かれた関数はシステムコールラッパーと呼ばれる。

1章で書いたスタックの図のように、戻り値の下には、積まれた値が存在することから 、これらが呼び出しの際に利用された引数だと考えられる。

このことから、システムコールを実行するラッパーでは引数の受け取りにスタックの値を利用しているのではないかと考える。

そしてそのラッパーを見てみると(__write_nocancel)はじめにスタックの値をレジスタに写している。

その後、アドレスを直接指定し関数を実行している。

ここの箇所が、int %0x80を実行していることがわかった。

戻り値の返し方

システムコールでの返し方

戻り値はEAXを通してシステム上では返すことは決まっている。

その原則から考えるとEAXから返すのは当然であろう。しかし、システムコール(int 0x80)を呼び出す箇所(__write_nocancel)をみるとcmp(比較命令)が存在し、その下には__syscall_errorなる関数が呼び出されている。

これは、エラー発生時の処理である、これが実行されることによりerrnoを設定することができる。

エラーハンドル

errnoを設定する関数であると言ったが、その理由としてカーネルがアプリケーションレイヤーのエラーハンドルを実施するのは、できなくはないがおかしな話である。そこで、標準のCライブラリ(システムコールラッパー)ではエラーハンドリングを行う関数がコンパイル時にリンクされる。

その際にリンクされるのがsyscall error である。

この理由として、カーネル、そしてAPIの移植性を確保するためである。

さらに、この関数がerrnoを設定する。

カーネルの問題点

  1. 引数はレジスタで渡すため、渡せる引数の個数に上限がある
  2. 正しい戻り値が負の値の場合、返すことができない

    引数問題

    これを解決する方法は、二つ存在する。

  3. スタックで設定する

  4. 構造体で設定する

しかし、上はLinuxでは行なっていない。これは制限が6までと制限されていることが挙げられる。

下は実際過去に行われていた。なぜそのようなことが行われていたかというと、カーネルの引数は4つまでであるという制限があった。そのためこれは一つの回避策として構造体を使い回避されていた。

代表例としてselectが挙げられる。

感想

正直、はじめはprintf()なんて表示するだけじゃないか、と思いながら1度目の本の目通を行なっていました。しかしその目通しの段階で、僕の知らない技術や未知の領域が広がっており、この本をやることに期待で胸いっぱいになっていました。

2度目を読む際には実際に手を動かし、ノートを取りながら読み進め多くの知識を得ました。

しかし読むにつれわからない単語や技術が多かったのでもう少し関連書籍を巡回してみようと思った次第です。