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問)
  1. int arr[5]の各要素のアドレスを表示し、要素間の距離をsizeof(int)と比較する。
  2. 文字列の長さを返すmy_strlenを実装する。ヌル文字は長さに含めない。
  3. char s[] = "hello";の全バイトを16進数で表示する。文字の5バイトと、終端のヌル文字1バイトを確認する。
第2回: 動的メモリ確保・構造体(4問)
  1. malloc(5 * sizeof(int))で確保した領域について、各要素のアドレスが何バイトずつ増えるか確認する。
  2. 文字列を動的に確保した領域へ複製するmy_strdupを実装する。ヌル文字の分も確保し、呼び出し側で解放する。
  3. struct person { char name[16]; int age; };のサイズとメンバーのオフセットを予測し、sizeofとoffsetofで確認する。パディングの有無も調べる。
  4. 意図的にメモリリークを起こす検証用プログラムを作る。macOSのleaksで、解放されていない領域の確保位置を確認する。
第3回: 連結リスト(5問)
  1. forによるリストの走査をwhileで書き直す。
  2. リストの先頭へノードを挿入する関数を書く。new->next = head; head = new;の各代入が更新するポインタを説明する。
  3. 全ノードのvalueを合計して返すlist_sumを実装する。
  4. lldbで先頭ノードのnextを読み、そのアドレスから2番目のノードを確認する。別々に確保したノード間の距離は、sizeofから求められない。
  5. prev、cur、nextの3つのポインタを使い、単方向連結リストの順序を逆にする。
第4回: レジスタ・逆アセンブリ(5問)
  1. リストのvalueを合計するプログラムを-g -O0でビルドする。lldbのdisassemble -n mainで、ロード、ストア、加算、分岐の各命令を探す。
  2. struct { char a; char b; short c; int d; }のメンバー配置を予測し、sizeofとoffsetofで確認する。
  3. int型の引数2つとint型の戻り値を持つ関数を作る。呼び出し直前・関数の先頭・復帰直後に停止し、register read w0 w1で値を確認する。
  4. 第4回のstruct itemにdouble price;を追加し、配置を確認する。priceへの代入を逆アセンブリで調べ、使用するレジスタとストア命令を確認する。
  5. 同じコードを-O0と-O2でビルドして比較する。命令数、メモリアクセス、レジスタの使い方がどう変わるか調べる。最適化によって処理が除去される場合もある。

今後の予定

第5回では、ページテーブルと、仮想アドレスから物理アドレスへの変換を扱う。その後は、メモリ保護と逆アセンブリの読解を進める。