SIer だけど技術やりたいブログ

プロセスの環境変数を取得する

linux

ターミナル

printenv を実行すれば、環境変数を取得できる。

$ printenv
SHELL=/bin/bash
HISTCONTROL=ignoredups
SYSTEMD_COLORS=false
HISTSIZE=1000
HOSTNAME=ip-172-31-23-157.ec2.internal
PWD=/home/ec2-user/rpmbuild/BUILD/procps-3.3.17
LOGNAME=ec2-user
XDG_SESSION_TYPE=tty
MOTD_SHOWN=pam
...

特定のプロセス

proc 配下の environ を参照すれば、環境変数を取得できる。

$ sleep 100 &
[1] 3301

$ strings /proc/3301/environ
SHELL=/bin/bash
HISTCONTROL=ignoredups
SYSTEMD_COLORS=false
HISTSIZE=1000
HOSTNAME=ip-172-31-23-157.ec2.internal
PWD=/home/ec2-user/rpmbuild/BUILD/procps-3.3.17

ただし注意点としては、プロセス起動時の環境変数(つまり初期値 This file contains the initial environment)であり、プロセス起動後に追加や変更された環境変数は表示できない

/proc/[pid]/environ
       This file contains the initial environment that was set when the currently executing program was started via execve(2).  The entries are separated by null bytes ('\0'), and there may  be  a
       null byte at the end.  Thus, to print out the environment of process 1, you would do:

           $ cat /proc/1/environ | tr '\000' '\n'

       If,  after  an execve(2), the process modifies its environment (e.g., by calling functions such as putenv(3) or modifying the environ(7) variable directly), this file will not reflect those
       changes.

proc(5) man から引用

試しに環境変数を追加したうえで表示してみたが、たしかに environ は更新されない。

$ export HOGE=XXX
$ strings /proc/$$/environ  | grep HOGE

このファイルは、ps コマンド等からも参照されていた(ps -auxe などで環境変数を表示できるオプションがある)ので、上記注意点はほとんどのユーザ空間のコマンドに当てはまると思う。

プロセスの環境変数をすべて取得する

じゃあどうすればすべての環境変数を取得できるかというと、簡単な方法はなさそう。

そもそもの話として、putenv(3) や environ(7) の man を見るに、環境変数は、ユーザ空間で extern char **environ で環境変数の値を管理しているだけで、とくにカーネルで環境変数は管理していなそう。

ためしに putenv(3) を呼び出すプログラムを用意し、

#include <stdlib.h>
#include <stdio.h>

void main() {
       if (putenv("HOGE=XXX") != 0 ){
               perror("failed");
       }

}

コンパイルしてから strace でシステムコールの呼び出しを調べたが、とくに環境変数をカーネルに渡してる様子がない。

# gcc main.c
# ltrace -ttf ./a.out
[pid 7138] 13:30:40.451683 putenv("HOGE=XXX")                                                                                    = 0
[pid 7138] 13:30:40.453289 +++ exited (status 0) +++

# strace -ttf ./a.out
13:31:16.161149 execve("./a.out", ["./a.out"], 0x7fff5f2710b8 /* 25 vars */) = 0
13:31:16.161636 brk(NULL)               = 0x1483000
13:31:16.161843 arch_prctl(0x3001 /* ARCH_??? */, 0x7ffe3343b9d0) = -1 EINVAL (Invalid argument)
13:31:16.162023 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
13:31:16.162198 openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
13:31:16.162316 newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=16087, ...}, AT_EMPTY_PATH) = 0
13:31:16.162425 mmap(NULL, 16087, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f2c7e7de000
13:31:16.162517 close(3)                = 0
13:31:16.162600 openat(AT_FDCWD, "/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
13:31:16.162695 read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0p\0\4\0\0\0\0\0"..., 832) = 832
13:31:16.162783 pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
13:31:16.162868 pread64(3, "\4\0\0\0@\0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0"..., 80, 848) = 80
13:31:16.162961 pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0\254%|\v\225\304\241Dh-\231\203\303\353\224\206"..., 68, 928) = 68
13:31:16.163045 newfstatat(3, "", {st_mode=S_IFREG|0755, st_size=2385592, ...}, AT_EMPTY_PATH) = 0
13:31:16.163137 mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f2c7e7dc000
13:31:16.163232 pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
13:31:16.163319 mmap(NULL, 2129808, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f2c7e400000
13:31:16.163406 mmap(0x7f2c7e428000, 1527808, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x28000) = 0x7f2c7e428000
13:31:16.163507 mmap(0x7f2c7e59d000, 360448, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x19d000) = 0x7f2c7e59d000
13:31:16.163600 mmap(0x7f2c7e5f5000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1f5000) = 0x7f2c7e5f5000
13:31:16.163702 mmap(0x7f2c7e5fb000, 53136, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f2c7e5fb000
13:31:16.163810 close(3)                = 0
13:31:16.163905 mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f2c7e7da000
13:31:16.164000 arch_prctl(ARCH_SET_FS, 0x7f2c7e7dd600) = 0
13:31:16.164081 set_tid_address(0x7f2c7e7dd8d0) = 7147
13:31:16.164156 set_robust_list(0x7f2c7e7dd8e0, 24) = 0
13:31:16.164239 rseq(0x7f2c7e7ddfa0, 0x20, 0, 0x53053053) = 0
13:31:16.164365 mprotect(0x7f2c7e5f5000, 16384, PROT_READ) = 0
13:31:16.164464 mprotect(0x403000, 4096, PROT_READ) = 0
13:31:16.164556 mprotect(0x7f2c7e815000, 8192, PROT_READ) = 0
13:31:16.164659 prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=10240*1024}) = 0
13:31:16.164761 munmap(0x7f2c7e7de000, 16087) = 0
13:31:16.164869 getrandom("\xc0\x93\xc0\x6a\xd3\x2b\x2b\x82", 8, GRND_NONBLOCK) = 8
13:31:16.164959 brk(NULL)               = 0x1483000
13:31:16.165037 brk(0x14a4000)          = 0x14a4000
13:31:16.165134 exit_group(0)           = ?
13:31:16.165270 +++ exited with 0 +++

じゃあどうやって子プロセスに環境変数引き継いでるんじゃい、と思ったら、普通に execve の引数で設定してた(カーネルはこれらの値を子プロセスのスタックに積んでおくっぽい)。

int execve(const char *pathname, char *const argv[],
           char *const envp[]);
...
envp is an array of pointers to strings, conventionally of the form key=value, which are passed as the environment of the new program.  The envp array must be terminated by a NULL pointer.

execve(2) man から引用

たしかに、適当なコマンドをターミナルから実行すると、execve で環境変数っぽいものを渡してるようす。

# strace -ttf sleep 1 2>&1 | head
14:38:52.897284 execve("/usr/bin/sleep", ["sleep", "1"], 0x7fffee7daad0 /* 24 vars */) = 0
14:38:52.897686 brk(NULL)               = 0x5602794e0000

ためしに execve でなにも渡さないと、printenv でなにも表示されない(なお execv を使うと内部で環境変数を埋めてから execve が呼ばれるので同じことかなと思った)。

# cat main.c
#include <unistd.h>

void main() {
        execve("/usr/bin/printenv",NULL,NULL);
}
# gcc main.c
# ./a.out

ということでカーネルからすれば、環境変数はユーザ空間のスタックに積まれたただの変数だしそんなもん更新されても知らんがなということになりそう。

なので gdb とか(じゃなくてもいいかもしれないけどとりあえず思いつくものとして)でメモリの中身を覗いてどうにかするしかないかなという気がする。試してみると、Bash で export した環境変数も取れてた。

# gdb -p 3604
...
(gdb) p environ[0]
$4 = 0x55dad199cfd0 "SHELL=/bin/bash"
...
(gdb) p environ[20]
$3 = 0x55dad197d300 "HOGE=XXX"

うーん、もうちょっといい方法ないもんか…

まあ実際問題は、たいていのコマンドは自身で環境変数を設定したりしないだろうから(と信じて)、とりあえず environ を見とけばいいかな。