ptraceとデバッガ・トレーサの仕組み
gdb がブレークポイントで止め、strace が全syscallを覗ける理由を、ptraceという1本のシステムコールの内部動作から腑に落とせます。INT3挿入・レジスタ操作・停止と再開の原理まで押さえます。
- 1.デバッガは対象を直接いじらず、ptrace というシステムコールで「停止・レジスタ/メモリ読み書き・再開」をカーネル越しに依頼します。トレーサとトレーシーの関係はカーネルが仲介します。
- 2.ブレークポイントの正体は対象コードの1バイトを 0xCC(INT3)に書き換えることで、命中すると例外でカーネルに落ち、トレーサがSIGTRAPとして停止を受け取ります。
- 3.strace は PTRACE_SYSCALL で全システムコールの入口と出口の2回プロセスを止め、レジスタからsyscall番号と引数を読み出して記録します。
デバッガは対象を「直接」操作していない
gdb で実行を止め、変数を覗き、1行ずつ進める——この体験は、デバッガが対象プロセスのメモリやレジスタを直接いじっているように見えます。しかし実際には、別プロセスのアドレス空間やレジスタを勝手に読み書きすることは保護機構が許しません。デバッガ(トレーサ)は対象(トレーシー)に対して、ptrace() という1本のシステムコールを通じて「止めてくれ」「このレジスタを見せてくれ」「このメモリを書き換えてくれ」とカーネルに依頼しているだけです。
つまり ptrace は トレーサとトレーシーの間にカーネルを仲介役として立てる 仕組みです。トレーサが発行する各リクエストは システムコール としてカーネルに入り、カーネルが特権で対象プロセスの状態を操作します。gdb・strace・ltrace・各言語のプロファイラの多くが、この同じ土台の上に立っています。
アタッチと停止:トレーサが「親」になる
ptrace を使うには、まずトレーサがトレーシーに アタッチ して両者の関係を結びます。確立の経路は主に2つです。
- 子側から
PTRACE_TRACEMEを呼んでからexecする(gdb が新規にプログラムを起動して追う場合) - 走っている他プロセスに
PTRACE_ATTACH/PTRACE_SEIZEで外から取り付く(gdb -p PIDの場合)
アタッチが成立すると、カーネル上ではトレーサがトレーシーの 実質的な親 として扱われ、トレーシーで発生するイベント(後述のシグナルやsyscall停止)はすべて トレーサへの wait で通知 されます。ここが要で、ptrace はイベント駆動です。トレーシーが何らかの理由で 停止状態(traced かつ stopped) に入ると、トレーサ側の waitpid() が返り、トレーサは初めて対象の状態を読み書きできます。
ptraceの各操作(レジスタ取得・メモリ書き換え・再開)は、対象が停止している間だけ有効です。走行中のプロセスのレジスタを読むことはできません。だからデバッガの操作は「イベントで止まる→読み書きする→再開させる」というサイクルを必ず踏みます。wait が返らない=まだ止まっていない、という関係です。
ブレークポイントの正体:0xCC(INT3)の埋め込み
ソフトウェアブレークポイントは、ハードウェアの特別な機能ではなく、対象コードの1バイトを書き換える という素朴な仕掛けで実現されています。手順はこうです。
- トレーサは
PTRACE_PEEKTEXTでブレークしたいアドレスの元の命令バイトを読み、退避しておく - その先頭1バイトを
PTRACE_POKETEXTで0xCC(x86のINT3、1バイトのソフト割り込み命令)に書き換える - トレーシーを再開する
トレーシーの実行がそのアドレスに達すると、INT3 が トラップ例外 を起こしてカーネルに制御が移ります。カーネルはこれを SIGTRAP としてトレーシーに届けようとし、ptrace 下にあるため シグナルを配送する前にトレーシーを停止 させ、トレーサの wait を返します。こうしてブレークポイントが「命中」します。
元コード: 55 48 89 e5 ... ; push %rbp; mov %rsp,%rbp ...
│
先頭1バイトを退避し 0xCC で上書き
▼
仕込み後: CC 48 89 e5 ... ; INT3 -> トラップ -> SIGTRAP で停止
止まった後、トレーサが続行する際は 退避した元バイトを書き戻し て本来の命令を実行させます。INT3 実行後の命令ポインタ(RIP)は 0xCC の1バイト分だけ進んでいるため、トレーサは RIP を1つ戻してから元命令を実行させる必要があります。ブレークポイントを残したまま先へ進めたい場合は、いったん元バイトで1命令だけ進め(シングルステップ)、再び 0xCC を埋め直す、という出し入れを行います。
0xCC方式はコードを書き換えるため、読み取り専用ページや書き換え検知のある領域では使えません。これに対しCPUのデバッグレジスタ(x86の DR0〜DR3)を使うハードウェアブレークポイントはコードを改変せず、しかもデータの読み書き(ウォッチポイント)も検出できます。ただし同時に張れる数が数本に限られます。gdbのwatchは典型的にこのハードウェア機構を使います。
レジスタとメモリの読み書き
停止中、トレーサはトレーシーの状態を自由に読み書きできます。代表的なリクエストを整理します。
| リクエスト | 対象 | 用途 |
|---|---|---|
| PTRACE_GETREGSET | 汎用レジスタ群 | RIP/RSP/引数レジスタの取得(スタックトレースや次命令の特定) |
| PTRACE_SETREGSET | 汎用レジスタ群 | RIPの書き換えで実行位置を変える、戻り値を改ざんする等 |
| PTRACE_PEEKTEXT/DATA | メモリ(ワード単位) | 命令・変数の読み出し(INT3退避もこれ) |
| PTRACE_POKETEXT/DATA | メモリ(ワード単位) | ブレークポイント埋め込み・変数の書き換え |
| PTRACE_SINGLESTEP | 実行制御 | 1命令だけ実行して再び停止(ステップ実行) |
PEEK/POKE は1ワードずつと粒度が粗いため、大きな領域の読み書きには /proc/<pid>/mem を直接 read/write する経路が使われます(gdb はこちらを多用します)。変数の値を見られるのは、トレーサが 対象のアドレス空間 のレイアウトとデバッグ情報(DWARF 等)を持ち、シンボル名から番地を解決してメモリを読むからです。デバッガ自身は値を「計算」しているのではなく、止まった瞬間のメモリとレジスタをスナップショットとして読み出しているにすぎません。
シグナル停止とsyscall停止
ptrace でトレーシーが止まる契機は大きく2系統あります。
第一は シグナル配送停止 です。トレーシーに届く各シグナルは、ハンドラに渡される前にいったんトレーサへ通知されます。トレーサは中身を確認し、シグナルを握り潰すことも、そのまま注入して配送させることも、別のシグナルへ差し替えることもできます。デバッガが SIGSEGV(セグメンテーション違反)でちょうど止まり、クラッシュ箇所を見せられるのはこの仕組みです(シグナルの基礎)。前述のブレークポイント命中も、INT3 由来の SIGTRAP という形のシグナル停止として観測されます。
第二は syscall停止 です。PTRACE_SYSCALL で再開すると、トレーシーが システムコールに入る直前 と そこから戻る直前 の2回、停止してトレーサへ通知されます。トレーサはこの停止点でレジスタを読み、syscall 番号・引数・戻り値を取得します。
PTRACE_SYSCALL で再開した子の停止点:
……ユーザコード……
│ (システムコール命令)
▼ ← syscall-enter-stop : 番号と引数をレジスタから読む
[ カーネル内でsyscall処理 ]
│
▼ ← syscall-exit-stop : 戻り値をレジスタから読む
……ユーザコードへ復帰……
これが strace の原理そのもの です。strace は対象を PTRACE_SYSCALL でひたすら再開し続け、enter で open read 等の番号と引数(呼び出し規約に従ってレジスタへ並ぶ)を、exit で戻り値(成功時の値や -errno)を読み取り、人間可読な形に整形して出力します。プロセスが呼ぶ全syscallを丸見えにできるのは、syscall の入口・出口の2点でカーネルが律儀に止めてくれるからです。
全syscallで2回止めるのは重いため、近年は seccomp のフィルタで「興味のあるsyscallだけ」ptrace停止を発生させる手法(PTRACE_EVENT_SECCOMP)が使われます。監視対象を絞り、不要な停止のオーバーヘッドを避けられます。なお対象の挙動を無改変で広く観測したいだけなら、停止を伴わないeBPF系のトレーシング(パフォーマンスへの影響が小さい)が選ばれる場面も増えています。
制約とセキュリティ
ptrace は他プロセスのメモリとレジスタを丸ごと覗き書きできる強力な機構であり、悪用されれば認証情報の窃取やコード注入に直結します。そのためカーネルは厳しい制限を課します。同一ユーザかつ適切な権限が必要で、近年の Linux では Yama の ptrace_scope により「非子プロセスへのアタッチ」を既定で禁止できます。コンテナでは CAP_SYS_PTRACE が無いと外部からのアタッチが弾かれます。デバッグの便利さと、プロセス分離という カーネルの保護境界 の間で、ptrace は慎重に線引きされた特権操作なのです。
まとめ
OS Article
ptraceとデバッガ・トレーサの仕組みを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
ptrace
比較で見る軸
難易度: advanced / カテゴリ: OS / タグ数: 6
導入後に効く点
ブレークポイントの正体は対象コードの1バイトを 0xCC(INT3)に書き換えることで、命中すると例外でカーネルに落ち、トレーサがSIGTRAPとして停止を受け取ります。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- OS
- タグ数
- 6
判断チェックリスト
- 自社の用途が「ptrace / デバッガ」に近いか確認する。
- 強みである「デバッガは対象を直接いじらず、ptrace というシステムコールで「停止・レジスタ/メモリ読み書き・再開」をカーネル越しに依頼します。トレーサとトレーシーの関係はカーネルが仲介します。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。