markdown

はじめに (対象読者・この記事でわかること)

本記事は、Linuxカーネルの内部に興味があるシステムプログラマーや、メモリ診断ツールを自作したいエンジニアを対象としています。
特に、仮想アドレスがどのプロセスにマッピングされているかを動的に取得したい場面で役立ちます。この記事を読むことで、以下ができるようになります。

  • /procpagemapkprobes などのカーネル提供APIを用いた仮想アドレス追跡の全体像が把握できる
  • kernel_readget_mm_structfind_vma といった具体的な関数呼び出し手順が理解できる
  • デバッグやパフォーマンス解析に便利なカスタムツールの雛形を作成できる

執筆のきっかけは、実務でメモリリークや不正アクセスをトレースする際に、手元のツールだけでは情報が不足したことです。カーネルレベルで直接情報を取得できれば、問題の根源に早く到達できると考え、調査・実装手順をまとめました。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。

  • C言語での Linux カーネルモジュール開発の基礎
  • Linux のメモリ管理(ページテーブル、仮想アドレス空間)の概念
  • make, insmod, rmmod などの基本的なビルド・ロード手順

カーネルAPIで仮想アドレスをプロセスに紐付ける概要

Linux カーネルは、各プロセスの仮想アドレス空間を struct mm_struct で管理し、個々の領域は struct vm_area_struct(VMA)として表現されます。仮想アドレスがどのプロセスで使用されているかを調べる主な手順は次の通りです。

  1. ターゲットの PID を取得
    pid = find_get_pid(pid_number);struct pid * を取得し、pid_task(pid, PIDTYPE_PID)task_struct を得ます。

  2. プロセスのメモリ管理構造を取得
    mm = get_task_mm(task);struct mm_struct * を取得します。取得したら mmput(mm); で解放するのを忘れないようにします。

  3. 仮想アドレスが属する VMA を検索
    find_vma(mm, addr); を呼び出すと、addr が所属する struct vm_area_struct * が返ります。VMA が NULL であれば、アドレスは未マッピングです。

  4. VMA の情報からプロセスを特定
    VMA の vm_file メンバがファイルマッピングかスタックかヒープかを示す手がかりになります。さらに task->commtask->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_pidtarget_addr をモジュールパラメータとして受け取ります。insmod 時に target_pid=1234 target_addr=0x7ffdf000 のように指定できます。

ステップ2:プロセスとアドレスの紐付けロジック

C
static 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_pidpid_tasktask_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) で保護した上で参照

解決策まとめ

  1. アドレスの正規化target_addr = target_addr & PAGE_MASK; でページアラインさせる。
  2. 参照カウントの管理get_task_mm / mmput の対になる呼び出しを忘れない。
  3. NULLチェックvma->vm_file が NULL でも安全に処理できるようガードを書く。

これらの注意点を守れば、カーネルモジュールは安定して動作します。

まとめ

本記事では、Linux カーネルAPI を用いて任意の仮想アドレスがどのプロセスで使用されているかを取得する方法 を解説しました。

  • PID と仮想アドレスから task_structmm_struct を取得
  • find_vma で対象アドレスの VMA を特定し、プロセス情報とマッピング情報を取得
  • 実装上の落とし穴(アドレス正規化、参照カウント、NULLチェック)とその対策

この記事を通して、読者は カーネルレベルでのメモリトラッキング が可能になり、メモリリークや不正アクセスの診断が格段に容易になります。今後は、複数プロセス横断検索や、kprobestracepoints と組み合わせたリアルタイム監視ツールの実装にも挑戦していく予定です。

参考資料