C言語のデータ配置とメモリ操作を、プログラムの実行結果とlldbによる観察で確認する。リバースエンジニアリング、セキュリティ、OS、組込み開発に必要な基礎知識を扱う。
実測環境はmacOS・Apple Silicon(ARM64)・Apple clangである。型のサイズ、アラインメント、呼び出し規約は環境によって異なる。
記事の目次
第1回から順に読むと、メモリ上のデータ配置から、CPUが実行する命令までを確認できる。
第1回: メモリ・ポインタ・スタック
アドレスとバイト列、2進数・16進数、型、変数、ポインタ、配列、文字列を扱う。関数呼び出し時のスタックフレームとASLRも確認する。
第2回: 動的メモリ確保と構造体
mallocとfree、領域の有効期間、メモリリーク、二重解放、解放後使用を扱う。構造体のメンバー配置、パディング、アラインメントを確認する。
第3回: 連結リストとlldbによる検証
ノードの連結、走査、挿入、削除、全ノードの解放を実装する。lldbでポインタの値とノードのメモリ内容を確認する。
第4回: ARM64のレジスタと逆アセンブリ
汎用レジスタ、ロード・ストア命令、関数の引数と戻り値を確認する。アクセス幅とオフセットを手掛かりに、命令列とCの操作を対応付ける。
練習問題
各回の項目を開くと、練習問題を表示する。コードの実行前に結果を予測し、実測値と比較する。動的メモリを確保するコードでは、確保失敗を確認し、不要になった領域を解放する。
第1回: メモリ・ポインタ・文字列(3問)
int arr[5]の各要素のアドレスを表示し、要素間の距離をsizeof(int)と比較する。- 文字列の長さを返す
my_strlenを実装する。ヌル文字は長さに含めない。 char s[] = "hello";の全バイトを16進数で表示する。文字の5バイトと、終端のヌル文字1バイトを確認する。
第2回: 動的メモリ確保・構造体(4問)
malloc(5 * sizeof(int))で確保した領域について、各要素のアドレスが何バイトずつ増えるか確認する。- 文字列を動的に確保した領域へ複製する
my_strdupを実装する。ヌル文字の分も確保し、呼び出し側で解放する。 struct person { char name[16]; int age; };のサイズとメンバーのオフセットを予測し、sizeofとoffsetofで確認する。パディングの有無も調べる。- 意図的にメモリリークを起こす検証用プログラムを作る。macOSの
leaksで、解放されていない領域の確保位置を確認する。
第3回: 連結リスト(5問)
forによるリストの走査をwhileで書き直す。- リストの先頭へノードを挿入する関数を書く。
new->next = head; head = new;の各代入が更新するポインタを説明する。 - 全ノードの
valueを合計して返すlist_sumを実装する。 - lldbで先頭ノードの
nextを読み、そのアドレスから2番目のノードを確認する。別々に確保したノード間の距離は、sizeofから求められない。 prev、cur、nextの3つのポインタを使い、単方向連結リストの順序を逆にする。
第4回: レジスタ・逆アセンブリ(5問)
- リストの
valueを合計するプログラムを-g -O0でビルドする。lldbのdisassemble -n mainで、ロード、ストア、加算、分岐の各命令を探す。 struct { char a; char b; short c; int d; }のメンバー配置を予測し、sizeofとoffsetofで確認する。- int型の引数2つとint型の戻り値を持つ関数を作る。呼び出し直前・関数の先頭・復帰直後に停止し、
register read w0 w1で値を確認する。 - 第4回の
struct itemにdouble price;を追加し、配置を確認する。priceへの代入を逆アセンブリで調べ、使用するレジスタとストア命令を確認する。 - 同じコードを
-O0と-O2でビルドして比較する。命令数、メモリアクセス、レジスタの使い方がどう変わるか調べる。最適化によって処理が除去される場合もある。
今後の予定
第5回では、ページテーブルと、仮想アドレスから物理アドレスへの変換を扱う。その後は、メモリ保護と逆アセンブリの読解を進める。