グローバル変数を横から覗く
この記事はカレーのち ぴょこりんクラスタ Advent Calendar 2018のために書いたものです。
サマリ
Linuxの/proc/${pid}/mem
を使って、グローバル変数を読み書きするものを作りました。
ptraceを使わなくても読み書きできます。使い方は以下のとおりで、WRITE_VALUEを省略すると
リード、WRITE_VALUEを書くとライトします。1、2、4、8バイトの変数に対応できますが、
配列の途中とか構造体のメンバ変数はまだダメです。ELFバイナリを読むので、
C/C++などで作ったバイナリでないと動きません。
# ./gvtool.py </path/to/binary> <PID> <GLOBAL_VARIABLE_NAME> {<WRITE_VALUE>}
ソースコードはこちら。 github.com
動作はこんな感じです。
背景など
プログラムをチューニングする場合、パラメタをどこに置くかは結構悩ましい問題ですが、 ともかく手を抜くことを考えた場合、グローバル変数にえいやと書いてしまうことがあります*1。
そのグローバル変数をgdbなどのデバッガで書き換えながらいろいろ動作させるんですが、 デバッガを使うのが億劫なケースがたまにあります。例えば、他のプログラムと連動するようなものをチューニングする場合です。 デバッガを使う場合は、変数書き換え時に逐一プログラムをbreakする必要があるので、 タイミングによっては連動する他のプログラムが異常終了してしまうことがあります。 また、デバッガはインタラクティブに動かす必要があったりで、自動化がちょっとやりにくいというのもいまいちですね。
そういうちょっとした面倒さを解決するために、今回このgvtool.pyを作りました。
pet.py*2がELFを読むためのもの、gvtool.pyが/proc/${pid}/maps
を読んだり/proc/${pid}/mem
を読み書きするためのものです。
どうやって実現しているか
概要
要はメモリのある一部を書き換えるだけなので、メモリアドレス(=先頭アドレスとオフセット)さえわかればいいわけです。 つまり、シンプルに以下の3ステップでできます。
- ELFバイナリのシンボルテーブルからアドレスのオフセットを求める
/proc/${pid}/maps
で先頭アドレスを調べる- 先頭アドレスとオフセットがわかったので、
/proc/${pid}/mem
を読み書きする
ELF: Executable and Linkable Format
Linuxで採用している実行可能なバイナリのフォーマットです。 ELFには、シンボルテーブルと呼ばれる、変数の名前とメモリ空間上のオフセットを 保持しているセクションがありますので、そこを直接読みます。
シンボルテーブルのエントリのフォーマットは、
身近なところだと/usr/include/elf.h
に載ってます。64bitだと以下のようになっていて、
ここのst_value
のところがアドレスオフセットになります*3。
typedef struct { Elf64_Word st_name; /* Symbol name (string tbl index) */ unsigned char st_info; /* Symbol type and binding */ unsigned char st_other; /* Symbol visibility */ Elf64_Section st_shndx; /* Section index */ Elf64_Addr st_value; /* Symbol value */ Elf64_Xword st_size; /* Symbol size */ } Elf64_Sym;
シンボルテーブルはデバッグ情報ではないので、-g
をつけてコンパイルしていなくても読めます。
/proc/${pid}/maps
Linux Kernelが提供する、プロセスのメモリマップを教えてくれる疑似ファイルです。 例えばこんな感じで見えます。左から、
です。詳しくはここ。
/home/bisco% cat /proc/$(pidof a.out)/maps 5591a7f3e000-5591a7f3f000 r--p 00000000 00:15 103135 /home/bisco/test/a.out 5591a7f3f000-5591a7f40000 r-xp 00001000 00:15 103135 /home/bisco/test/a.out 5591a7f40000-5591a7f41000 r--p 00002000 00:15 103135 /home/bisco/test/a.out 5591a7f41000-5591a7f42000 r--p 00002000 00:15 103135 /home/bisco/test/a.out 5591a7f42000-5591a7f43000 rw-p 00003000 00:15 103135 /home/bisco/test/a.out 5591a9395000-5591a93b6000 rw-p 00000000 00:00 0 [heap] 7f32b4401000-7f32b4423000 r--p 00000000 00:15 46396 /usr/lib/libc-2.28.so 7f32b4423000-7f32b456e000 r-xp 00022000 00:15 46396 /usr/lib/libc-2.28.so 7f32b456e000-7f32b45ba000 r--p 0016d000 00:15 46396 /usr/lib/libc-2.28.so 7f32b45ba000-7f32b45bb000 ---p 001b9000 00:15 46396 /usr/lib/libc-2.28.so 7f32b45bb000-7f32b45bf000 r--p 001b9000 00:15 46396 /usr/lib/libc-2.28.so 7f32b45bf000-7f32b45c1000 rw-p 001bd000 00:15 46396 /usr/lib/libc-2.28.so 7f32b45c1000-7f32b45c7000 rw-p 00000000 00:00 0 7f32b45cf000-7f32b45d1000 r--p 00000000 00:15 46385 /usr/lib/ld-2.28.so 7f32b45d1000-7f32b45f0000 r-xp 00002000 00:15 46385 /usr/lib/ld-2.28.so 7f32b45f0000-7f32b45f8000 r--p 00021000 00:15 46385 /usr/lib/ld-2.28.so 7f32b45f8000-7f32b45f9000 r--p 00028000 00:15 46385 /usr/lib/ld-2.28.so 7f32b45f9000-7f32b45fa000 rw-p 00029000 00:15 46385 /usr/lib/ld-2.28.so 7f32b45fa000-7f32b45fb000 rw-p 00000000 00:00 0 7fff149b7000-7fff149d8000 rw-p 00000000 00:00 0 [stack] 7fff149d8000-7fff149db000 r--p 00000000 00:00 0 [vvar] 7fff149db000-7fff149dd000 r-xp 00000000 00:00 0 [vdso]
グローバル変数は、heapでもstackでもなく、bssという領域にあります。 bssという領域は読み書きできるので、上から5番目の領域にいそうだな、ということが何となくわかりますね。
シンボルテーブルに載っているオフセットは、x番目の領域のオフセットではなく、先頭からのオフセットです。 a.outのグローバル変数にアクセスしたければ、a.outの先頭アドレス(例で言うところの0x5591a7f3e000)がわかればよいです。 この先頭アドレスに、 シンボルテーブルから得られたオフセットを足せばメモリアドレスがわかります。
/proc/${pid}/mem
Linux Kernelが提供する、プロセスのメモリ空間へアクセスするための疑似ファイルです。
setuid-rootなバイナリ(例えば実行者がrootとか)であれば、
プロセス${PID}
をbreakすることなくメモリの読み書きが可能になります。
書き込みが可能になったのはKernel 2.6.38-rc8(2011/03/08)からであり、
昨今の大抵のディストリビューションで使えます。
以下、LKMLより抜粋。デバッグ用途で使ってくれとのことみたいですね。
This patch series enables safe writes to /proc/pid/mem. Such functionality is
useful as it gives debuggers a simple and efficient mechanism to manipulate a
process' address space. Memory can be read and written using single calls to
pread(2) and pwrite(2) instead of iteratively calling into *ptrace(2)*
In addition, /proc/pid/mem has always had write permissions enabled, so clearly it
*wants* to be written to
まとめ
/proc/${pid}/mem
、/proc/${pid}/maps
、シンボルテーブルの情報があれば、動いているプログラムのグローバル変数を読み書きできる