記事一覧ページへ移動

Linux の /proc/self/exe はどのように実行ファイルのパスを指すのか

2025-12-07
2025-12-01

わたすけです。C++で実行ファイルのパスを実行時に知りたいなと思って調べていたとき、こんな Gist を発見しました:

Get EXE Location C++Get EXE Location C++https://gist.github.com/Jacob-Tate/7b326a086cf3f9d46e32315841101109Get EXE Location C++. GitHub Gist: instantly share code, notes, and snippets.

readlink("/proc/self/exe", path, FILENAME_MAX) らしいです。へ~。試してみました。

% ls -l /proc/self/exe
lrwxrwxrwx - watasuke watasuke 18 Nov 16:58 /proc/self/exe -> /usr/bin/eza

手元の環境ではls にエイリアスを張っているのですが、ちゃんとその正体が /usr/bin/eza であると見抜かれていますね。それはそうですが。カーネルドキュメントも存在していて、exe ファイルが Link to the executable of this process、つまり当該プロセスの実行形式ファイルへのリンクであることがちゃんと明記されています。

Everything is a file の精神に則って、ファイルの読み書きというインタフェースにより様々な事ができるおもしろディレクトリこと /proc について、また 1 つ詳しくなれたような気がして嬉しいです。ただ、これを見ると、やはり「これどうやって実装されてるんだ……?」と気になりますよね。今回はこのメカニズムをざっくり調べてきた1ので、ざっくり解説したいと思います。

ちなみに、この記事は以下のスレッドがもとになっています。Twitter は文字数が少ないので、細部が削ぎ落とされがちで、よくない。


この記事は、 mast Advent Calendar 2025 の1日目の記事です。最終日も枠をいただいているのでオセロだったら全員わたすけになりますね(?)。当記事では "情報メディア創成学類" の "情報" の部分をやっていきたいと思います。最終日は "メディア創成" の部分をやります。

明日はおかしさんがdetails/summaryタグについて書いてくれるらしいです~~(忘れてなかったらリンクを追記しておきます)~~。details/summaryタグは弊ポートフォリオでもふんだんに使わせていただいているので楽しみです。

(2025-12-07追記) 公開されました:2025年末のdetails/summaryタグを使ったアコーディオンの現状

調査対象

執筆時点で最新のバージョンである Linux v6.17.8 のソースコードを読みました2。ソースコードへのリンクは基本的に elixir.bootlin.com を用いています。いつもありがとうございますという感じですね。

/proc の定義

そもそも大元の /proc はどのように作られているのでしょうか?ArchWiki によると、/proc には、 procfs と呼ばれる疑似ファイルシステムがマウントされているようです。この「マウント」というのは言葉通りで、実際に以下のようにそれを確かめることが出来ます:

% mount | grep '^proc'
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)

それから、 sudo mount -t proc proc /path/to/mountpoint のようにすると、任意の場所に procfs を作ることが出来ます。

% mktemp -d
/tmp/tmp.c3uyobbQXf

% sudo mount -t proc proc /tmp/tmp.c3uyobbQXf

% ls -l /tmp/tmp.c3uyobbQXf/self/exe
lrwxrwxrwx - watasuke watasuke 20 Nov 09:36 󰡯 /tmp/tmp.c3uyobbQXf/self/exe -> /usr/bin/eza

さて、それでは procfs の実装を読んでみましょう。ファイルシステムということで、Linux カーネルのソースコードの中でも fs ディレクトリが怪しそうです。見てみると、fs/proc/root.cproc_root_init() という、いかにもな名前の関数があります。これは init/main.c@start_kernel から呼ばれるようです。

proc_root_init() が行う処理のうち、まず注目すべきは proc_self_init() でしょう。 /proc/self のための初期化処理を行っているようです。そして、最後には register_filesystem() を用いて、自身をファイルシステムへ登録しています。

register_filesystem()struct file_system_type* を受け取るようですが、このメンバとして fs_flags というものがあり、 procfs はこれを FS_USERNS_MOUNT | FS_DISALLOW_NOTIFY_PERM と設定しています。 USERNS というのはおそらく User NameSpace で、ユーザー空間でマウントできるよということなのでしょう。逆に言うと、明示的にマウントしない限り /proc は存在しないというわけです。

procfs を誰がマウントするのかはシステムに依存しますが、systemd をお使いであればこいつがやってくれています。systemd の src/shared/mount-setup.cstatic const MountPoint mount_table[] というそれっぽい変数があり、 "proc" の文字列が見えます。実際、src/core/main/c@main から呼ばれる mount_setup() において mount_points_setup() が呼ばれているのですが、ここで FOREACH_ARRAY というマクロにより mount_table をイテレートし、各要素を mount_one() に渡してマウントしているようです。

/proc/self の定義

/proc/self は、/proc/$$ へのシンボリックリンクになっています($$ は pid です)。ls -l /proc | grep ' self ' で確かめられますね。手元でやるとこのような実行結果になりました。 self214972 へのリンクになっていることがわかります。

% ls -l /proc | grep self
lrwxrwxrwx    - root             root             18 Nov 09:19 self -> 214972

この実装がある場所はかなりわかりやすく、即ち fs/proc/self.c です。ファイル名がそのまんますぎますね。まずは proc_setup_self() あたりを見てみましょう。注目すべき点を抜き出したものがこちらです:

static const struct inode_operations proc_self_inode_operations = {
	.get_link	= proc_self_get_link,
};

int proc_setup_self(struct super_block *s)
{
	struct dentry *self;
	struct proc_fs_info *fs_info = proc_sb_info(s);
	int ret = -ENOMEM;

	self = d_alloc_name(s->s_root, "self");
	if (self) {
		struct inode *inode = new_inode(s);
		if (inode) {
			inode->i_op = &proc_self_inode_operations;
			ret = 0;
		}
	}

	if (ret)
		pr_err("proc_fill_super: can't allocate /proc/self\n");
	else
		fs_info->proc_self = self;

	return ret;
}

inode->i_opstruct inode_operations 型である proc_self_inode_operations が格納されていますね。 struct inode_operations については VFS (Virtual FileSystem) に関するカーネルドキュメントに書いてあります。superblock というオブジェクトがマウントされたファイルシステムに対応しており、そこには directory entry (dentry) や、ファイルに対応する inode といった概念が存在します。なお、この文脈で file というと、これは open() によって開かれた、つまりファイルディスクリプタに対応するファイルを意味しているらしいです。

それと、引数として受け取った struct super_block*proc_sb_info() に渡して、struct proc_fs_info* を取得し、最終的にエラーがなければ fs_info->proc_selfself を代入していますね。これは伏線になります。

さて、 ->i_op->get_link で grep すると fs/name.c@vfs_readlink などで呼ばれているのが確認できます。この vfs_readlink() は例えば fs/stat.c@do_readlinkat で呼ばれていて、その真下にはこんな記述があります:

SYSCALL_DEFINE3(readlink, const char __user *, path, char __user *, buf,
		int, bufsiz)
{
	return do_readlinkat(AT_FDCWD, path, buf, bufsiz);
}

SYSCALL_DEFINE3(readlink) 3return do_readlinkat(...) という感じで呼んでいますね。つまり:

  1. /proc/self に対して readlink() システムコールを呼ぶ
  2. カーネルは指定されたパス /proc/self に対応する inode を取得し、 inode->i_op->get_link() を呼ぶ
  3. proc_self_inode_operations.get_link 、すなわち proc_self_get_link() が呼ばれる

ということが分かります。

そして、この const char* proc_self_get_link() が何をするのかというと、 pid_t tgid = task_tgid_nr_ns(current, ns); して char* name = kmalloc() して sprintf(name, "%u", tgid); からの return name; です。わかりやすいですね。カーネルドキュメントによると、 "tgid" とは Thread Group ID のことで、 "process" と同じように使われるらしく、つまり 1 つの tgid に対応する mm_struct が 1 つ存在する(同一の tgid を持つタスク間でメモリが共有される)といえる……ということだと思います。

tgid を文字列にしてそのまま返すだけで良いのかなあと不思議に思ったのですが、 /proc/$$/proc/self の階層が同じなので、これで相対リンクになるということなんでしょう。そういえば、先程 ls -l /proc | grep self した時も相対的なリンクになっていましたね4

/proc/$$ の定義

では、 /proc/$$ についてです。先ほど /proc の定義が fs/proc/root.c にあると述べました。そこで紹介した proc_root_init() を少しスクロールしてみると、以下の 2 つが定義されている箇所があります:

  • struct file_operations 型の proc_root_operations
  • struct inode_operations 型の proc_root_inode_operations

前述しましたが、 file というのは open() されたファイルであり、そして file_operations はファイルディスクリプタに対する処理を定義できるようです。これらは双方とも struct proc_dir_entry proc_root のメンバとして利用されており、 /proc に対する操作を定義しているのではないかと推測できます。

/proc/$$ に対する inode_operations を読む

まずは後者、 inode_operations のほうを見てみましょう。メンバは2つ定義されていますね。 lookup メンバに proc_root_lookup() が設定されているのでこれを見に行きます。fs/proc/base.c@proc_pid_lookupを呼び、エラーでなければ fs/proc/generic.c@proc_lookupの戻り値をそのまま return という処理をしているようです。

generic.c という特別感のないほうは無視して、proc_pid_lookup() のほうを見ましょう。ざっくり言うと、proc_sb_info()struct proc_fs_info* を取得し、そこで得た fs_info に含まれる名前空間(struct pid_namespace*)と pid を find_task_by_pid_ns() に渡して task_struct を取得し、 proc_pid_instantiate() を呼ぶという流れっぽいです。

/proc/$$ に対する file_operations を読む

次は前者、 file_operations のほうを見てみましょう。read および llseek メンバには "generic" と名の付く関数が設定されているのですが、 iterate_shared には proc_root_readdir() が設定されているので、これを見てみましょう。if 文はよくわからないのですが、おそらく /proc のエントリー番号(?)が FIRST_PROCESS_ENTRY 以上なら /proc/$$ に対応したファイル(未満なら /proc/uptime のようなその他のエントリ)、という意味なのではないでしょうか。というわけで if 文は読み飛ばします。

proc_root_readdir() は最終的に fs/proc/base.c@proc_pid_readdir の戻り値を return していますね。まず注目すべきは、pos == TGID_OFFSET - 2 のときに d_inode(fs_info->proc_self) として inode を作成し、 dir_emit(ctx, "self", 4, inode->i_ino, DT_LNK) を呼んでいる点です。先ほど伏線と述べましたが、fs/proc/self.c@proc_setup_self における初期化処理で作成された fs_info->proc_self がここで用いられています。include/linux/fs.h@dir_emit でおそらくエントリを作成しており、その名前は "self" であることが伺えます。DT_LNK は Dentry Type: LiNK ということなんでしょうね。これにより、 /proc/self が作られているといえます。作るといっても実際にファイルがあるわけではないのですが……。

次に注目すべきは for 文です。next_tgid() を用いて tgid をイテレートし、snprintf() で name に tgid を文字列として書き込み、 proc_fill_cache([略], proc_pid_instantiate, iter.task, NULL) と呼び出します。おや? proc_pid_instantiate() はさっきも登場していましたね。重要そうです。

共通して使われている関数を読む

というわけで fs/proc/base.c@proc_pid_instantiate を見に行きましょう。注目すべきは、 proc_pid_make_base_inode() を用いて inode を作成し、inode->i_op&proc_tgid_base_inode_operations を、inode->i_fop&proc_tgid_base_operations を代入している点です。

例に漏れず inode_operations から見ていきましょう。 lookup メンバにあるのは proc_tgid_base_lookup() です。やっているのはこれだけです:

return proc_pident_lookup(dir, dentry,
				tgid_base_stuff,
				tgid_base_stuff + ARRAY_SIZE(tgid_base_stuff));

では file_operations の方も見てみましょう。iterate_shared メンバに proc_tgid_base_readdir() があって、処理はこれだけです:

return proc_pident_readdir(file, ctx,
			tgid_base_stuff, ARRAY_SIZE(tgid_base_stuff));

似てますね。どちらも tgid_base_stuff を引数として渡しています。これは struct pid_entry の配列で、上記 2 つの関数はどちらもこの tgid_base_stuff を for 文で参照しています。定義を見てみると、こんな行があります:

LNK("exe",        proc_exe_link),

ようやく "exe" にたどり着きましたね!これが /proc/$$/exe を定義している箇所といって良いでしょう。もっと言うと、 tgid_base_stuff は、 /proc/$$ という(仮想的な)ディレクトリの下にあるファイルと、そのファイルに対する操作のペアを格納している配列である、と言えそうです。この例だと、"exe" というファイルに対する操作のハンドラとして proc_exe_link() を設定しているような雰囲気があります。

/proc/$$/exe の定義を読む

LNK マクロNOD マクロに展開されます。LNK("exe", proc_exe_link) は、最終的に以下のように展開されるようです:

{
    .name = "exe",
    .len  = sizeof("exe") - 1,
    .mode = S_IFLNK | S_IRWXUGO,
    .iop  = &proc_pid_link_inode_operations,
    .fop  = NULL,
    .op   = { .proc_get_link = proc_exe_link },
}

つまり、第 2 引数の proc_exe_link は、 op.proc_get_link に割り当てられるわけですね。この op というのは union proc_op です。 op.proc_get_link( で grep すると proc_pid_get_link()proc_pid_readlink() などで呼ばれているのが確認できます。これらは双方とも先ほどの LNK マクロを展開した際に出てきた proc_pid_link_inode_operations のメンバに置かれています。 /proc/$$/exe に対して get_link や readlink を行う際に proc_exe_link() が呼ばれるということでしょう。

/proc/$$/exe の処理を読む

それでは proc_exe_link() の実装を読みましょう。必要な部分だけ抜き出したものがこちらです:

static int proc_exe_link(struct dentry *dentry, struct path *exe_path)
{
	struct task_struct *task;
	struct file *exe_file;

	task = get_proc_task(d_inode(dentry));
	exe_file = get_task_exe_file(task);
	if (exe_file) {
		*exe_path = exe_file->f_path;
		return 0;
	} else
		return -ENOENT;
}

dentry から inode を取得し、そこから struct task_struct* を取得し、それを get_task_exe_file() に渡して struct file* を取得します。そのパスを exe_path に書き込んで終了です。

kernel/fork.c@get_task_exe_file は、 task_struct.mmget_mm_exe_file() に渡します。 mm_struct には struct file* exe_file というメンバがあるので、これをいい感じに取ってきてくれるわけです。ちなみに mm_struct.exe_file の定義箇所にあるコメントによると、mm_struct/proc/$$/exe のシンボリックリンクを作るためにこの exe_file を用意しているらしいです。そうなんだ。

ここまで来ると余談かもしれませんが、その mm_struct.exe_file は誰が設定するのでしょうか。 kernel/fork.c には set_mm_exe_file() という、いかにもという名前の関数があります。fs/exec.c@begin_new_exec で、set_mm_exe_file(bprm->mm, bprm->file); という感じで呼ばれています。bprmstruct linux_binprm* 型です。では、これはどこからやってくるのでしょうか。作成自体は fs/exec.c@alloc_bprm によって行われます。 struct filename* を引数として受け取り、これを do_open_execat() に渡して struct file* を取得し、これを bprm->file に入れて、あとは bprm に対して他の初期化処理などを行ってから返却する、という処理を行っています。そして、この alloc_bprm() がどこから呼ばれているのかというと、fs/exec.ckernel_execve()do_execveat_common() です。前者は init/main.c@run_init 、後者は execve() システムコールの定義部で呼ばれている do_execve() から呼ばれています。

まとめ

  • Linux カーネルが起動する際、fs/proc/root.c にある初期化関数によって procfs が作られる
  • /proc/selffs/proc/self.c によって作られており、 /proc/$$ へのシンボリックリンクとなる
  • /proc/$$ に対する操作は fs/proc/root.c にあり、 proc_pid_instantiate() という関数で /proc/$$ 下のファイルをつくる
  • /proc/$$/exetgid_base_stuff という配列をもとに生成される
  • /proc/$$/exe に対する readlink()proc_exe_link() によって処理される。タスク起動時に作られる mm_struct から実行形式ファイルを取り出して、そのパスを返すという処理が行われる

という感じの流れでした。いや~面白いですね。図らずも Linux の VFS について知ることが出来てよかったです。Linux の内部実装についての知識はほぼ皆無だったのですが、今回の調査でかなり理解が深まったように思います。また機会があればこういう調査をしてみたいです。

Footnotes

  1. AI にサクッと解説してもらって満足するか~と思っていたのですが、GitHub Copilot も DeepWiki も微妙な回答しかしてくれませんでした。これプロンプトエンジニアリング力が低すぎるせいなんですかね

  2. 記事を温めているうちに6.17.9が出てしまった

  3. 引数が 3 つあるsyscall の定義 (define) という意味。include/linux/syscalls.h にその定義がある。SYSCALL_DEFINEx(3, _##name, __VA_ARGS__) に展開され、これはマクロを再帰的?に利用して指定された数字だけ引数をつくるという面白マクロ __MAP を用いている

Comments

Powered by Giscus