markdown
はじめに (対象読者・この記事でわかること)
本記事は、Linuxカーネルの内部に興味があるシステムプログラマーや、メモリ診断ツールを自作したいエンジニアを対象としています。
特に、仮想アドレスがどのプロセスにマッピングされているかを動的に取得したい場面で役立ちます。この記事を読むことで、以下ができるようになります。
/procやpagemap、kprobesなどのカーネル提供APIを用いた仮想アドレス追跡の全体像が把握できるkernel_read、get_mm_struct、find_vmaといった具体的な関数呼び出し手順が理解できる- デバッグやパフォーマンス解析に便利なカスタムツールの雛形を作成できる
執筆のきっかけは、実務でメモリリークや不正アクセスをトレースする際に、手元のツールだけでは情報が不足したことです。カーネルレベルで直接情報を取得できれば、問題の根源に早く到達できると考え、調査・実装手順をまとめました。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
- C言語での Linux カーネルモジュール開発の基礎
- Linux のメモリ管理(ページテーブル、仮想アドレス空間)の概念
make,insmod,rmmodなどの基本的なビルド・ロード手順
カーネルAPIで仮想アドレスをプロセスに紐付ける概要
Linux カーネルは、各プロセスの仮想アドレス空間を struct mm_struct で管理し、個々の領域は struct vm_area_struct(VMA)として表現されます。仮想アドレスがどのプロセスで使用されているかを調べる主な手順は次の通りです。
-
ターゲットの PID を取得
pid = find_get_pid(pid_number);でstruct pid *を取得し、pid_task(pid, PIDTYPE_PID)でtask_structを得ます。 -
プロセスのメモリ管理構造を取得
mm = get_task_mm(task);でstruct mm_struct *を取得します。取得したらmmput(mm);で解放するのを忘れないようにします。 -
仮想アドレスが属する VMA を検索
find_vma(mm, addr);を呼び出すと、addrが所属するstruct vm_area_struct *が返ります。VMA が NULL であれば、アドレスは未マッピングです。 -
VMA の情報からプロセスを特定
VMA のvm_fileメンバがファイルマッピングかスタックかヒープかを示す手がかりになります。さらにtask->commやtask->pidと組み合わせて、どのプロセスがそのアドレスを保持しているかを判定します。
この流れをカーネルモジュールで実装すれば、任意の仮想アドレスに対して「誰が保持しているか」をリアルタイムに取得でき、デバッグや監査に有効です。
実装手順とサンプルコード
以下では、具体的なカーネルモジュールの実装例を示します。モジュールは modinfo で設定した対象 PID と仮想アドレスを受け取り、カーネルログに結果を出力します。
ステップ1:モジュールの雛形作成
C#include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> #include <linux/pid.h> #include <linux/sched.h> #include <linux/mm.h> #include <linux/fs.h> static int target_pid = 0; static unsigned long target_addr = 0; module_param(target_pid, int, 0444); MODULE_PARM_DESC(target_pid, "調査対象のプロセス PID"); module_param(target_addr, ulong, 0444); MODULE_PARM_DESC(target_addr, "調査対象の仮想アドレス (16進数で入力)"); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kousukei"); MODULE_DESCRIPTION("仮想アドレスがどのプロセスで使用されているかを調べるカーネルモジュール");
このコードは target_pid と target_addr をモジュールパラメータとして受け取ります。insmod 時に target_pid=1234 target_addr=0x7ffdf000 のように指定できます。
ステップ2:プロセスとアドレスの紐付けロジック
Cstatic int __init vaddr_lookup_init(void) { struct pid *pid_struct; struct task_struct *task; struct mm_struct *mm; struct vm_area_struct *vma; if (!target_pid || !target_addr) { pr_err("target_pid と target_addr を必ず指定してください\\n"); return -EINVAL; } pid_struct = find_get_pid(target_pid); if (!pid_struct) { pr_err("PID %d が見つかりません\\n", target_pid); return -ESRCH; } task = pid_task(pid_struct, PIDTYPE_PID); if (!task) { pr_err("PID %d の task_struct が取得できません\\n", target_pid); return -ESRCH; } mm = get_task_mm(task); if (!mm) { pr_err("PID %d のメモリ情報が取得できません\\n", target_pid); return -EFAULT; } vma = find_vma(mm, target_addr); if (!vma) { pr_info("アドレス 0x%lx は PID %d のプロセス空間にマッピングされていません\\n", target_addr, target_pid); } else { pr_info("PID %d (comm:%s) がアドレス 0x%lx を保持しています\\n", target_pid, task->comm, target_addr); pr_info(" VMA情報: start=0x%lx, end=0x%lx, flags=0x%x\\n", vma->vm_start, vma->vm_end, vma->vm_flags); if (vma->vm_file) { char buf[256]; snprintf(buf, sizeof(buf), "%.*s", 255, vma->vm_file->f_path.dentry->d_name.name); pr_info(" マッピングファイル: %s\\n", buf); } } mmput(mm); return 0; } static void __exit vaddr_lookup_exit(void) { pr_info("vaddr_lookup モジュールをアンロードしました\\n"); } module_init(vaddr_lookup_init); module_exit(vaddr_lookup_exit);
ポイント解説
find_get_pidとpid_taskでtask_structを取得。task_structが保持しているcommはプロセス名です。get_task_mmは対象プロセスのmm_structを取得し、参照カウントが上がります。必ずmmputで開放してください。find_vmaは引数のアドレスが所属する VMA を返します。VMA がNULLの場合、対象アドレスは未マッピングです。vma->vm_fileが非NULLなら、ファイルマッピング(例: 共有ライブラリや mmap ファイル)であることが分かります。dentry->d_nameからファイル名を取得。
ハマった点やエラー解決
| 発生した問題 | 原因 | 解決策 |
|---|---|---|
find_vma が常に NULL を返す |
target_addr がページ境界外(例: 0x7ffdf123)だった |
アドレスはページサイズ(4KB)単位で切り捨てるか、addr & PAGE_MASK で正規化 |
mmput を忘れた |
カーネルモジュールのアンロード時にメモリリーク警告が出た | mmput(mm) を必ず呼び、put_task_struct も必要に応じて実施 |
vm_file が NULL のとき dentry 参照でカーネルパニック |
vma->vm_file が NULL でも vma->vm_ops へアクセスしていた |
if (vma->vm_file) で保護した上で参照 |
解決策まとめ
- アドレスの正規化:
target_addr = target_addr & PAGE_MASK;でページアラインさせる。 - 参照カウントの管理:
get_task_mm/mmputの対になる呼び出しを忘れない。 - NULLチェック:
vma->vm_fileが NULL でも安全に処理できるようガードを書く。
これらの注意点を守れば、カーネルモジュールは安定して動作します。
まとめ
本記事では、Linux カーネルAPI を用いて任意の仮想アドレスがどのプロセスで使用されているかを取得する方法 を解説しました。
- PID と仮想アドレスから
task_structとmm_structを取得 find_vmaで対象アドレスの VMA を特定し、プロセス情報とマッピング情報を取得- 実装上の落とし穴(アドレス正規化、参照カウント、NULLチェック)とその対策
この記事を通して、読者は カーネルレベルでのメモリトラッキング が可能になり、メモリリークや不正アクセスの診断が格段に容易になります。今後は、複数プロセス横断検索や、kprobes・tracepoints と組み合わせたリアルタイム監視ツールの実装にも挑戦していく予定です。
参考資料
- Linux Kernel Documentation – Memory Management
- Understanding the Linux Virtual Memory System – LWN.net
- Robert Love, Linux Kernel Development, 3rd Edition, Addison‑Wesley, 2010.
