i386のアドレス変換の話
この記事はNetBSD Advent Calendar 2015 112日目の記事です(嘘)。
mi_switchに
apcb_ptbrやpcb_cr3をprintfさせる細工を入れて、
動いているプロセスのページテーブルがどうなっているか
観察してみたいんだけれど...
http://t.co/Zl4tO59VRu
http://t.co/YaAwvV1OgL
— lucky owner (@nullnilaki) April 20, 2015
で外部からの観察ができたので、まとめておきます。
まずmi_switch(9)に以下のような細工をすることで、
CR3レジスタの内容を出力させました。
mi_switch(9) - NetBSD Manual Pages
#include <machine/pcb.h> if(strcmp(curproc->p_comm,"hoge") == 0) { struct pcb *pcb = lwp_getpcb(l); printf("cr3 = %x\n", pcb->pcb_cr3); }
を
http://nxr.netbsd.org/xref/src/sys/kern/kern_synch.c#780
あたりに追記します。
カーネルをコンパイルし、mi_switchに細工をしたカーネルで再起動します。
とりあえず何かの仮想アドレスを取得したいので、
#include <stdio.h> char foo [] = "WELCOME TO NETBSD!"; int main(void){ printf("address foo = %d\n", foo); for(;;) {} }
というプログラムを書き、
gcc -o hoge xxxx.c
でコンパイルします。
また、物理アドレスのダンプを取りたいので、
デバイスドライバに頼らないハードウェア操作
を参考に、
int main(int argc,char **argv) { char buff[1024]; int fd; unsigned int st,len; int r; st=strtol(argv[1],NULL,16); len=strtol(argv[2],NULL,16); fd=open("/dev/mem",O_RDONLY); if(fd<0) { fprintf(stderr,"cannot open\n"); return 1; } lseek(fd,st,SEEK_SET); while(len) { r=read(fd,buff,len>1024?1024:len); if(r<1) break; write(1,buff,r); len-=r; } close(fd); return 0; }
のようなコードを書き(コピペ)、
gcc -o physdump xxxx.c
でコンパイルしました。
僕はあまりプログラムを書いた事が無いので、
物理アドレスとサイズを指定すると、その範囲のメモリのダンプをとれるコマンドって無いかなぁ?
— lucky owner (@nullnilaki) April 20, 2015
とつぶやいたところ、
@nullnilaki /dev/memとかgdbでtarget kvmとかという話?
— oshimaya (@oshimyja) April 20, 2015
とoshimayaさんに教えてもらいました!
1.mi_switchに細工をしてhogeというプログラムを実行すると、
hogeプロセスのCR3レジスタの内容を出力するカーネル
2.仮想アドレスを取得するプログラム(プログラム名はhoge)
3./dev/mem経由で物理アドレスをダンプするプログラム(プログラム名はphysdump)
の3点セットが揃ったので、動かしてみます。
はじめに、仮想アドレスとCR3レジスタの内容を取得します。
hogeを実行すると
$ ./hoge address foo = 134519060
とfooの仮想アドレスを表示し、ループに入ります。
すると、コンソールには
cr3 = 5f215000 cr3 = 5f215000 cr3 = 5f215000 cr3 = 5f215000 cr3 = 5f215000 cr3 = 5f215000
という表示が延々出力されるはずです。
これで、
fooの仮想アドレス(134519060)
と
CR3レジスタに格納されている物理アドレス(0x5f215000)が取得出来ました。
では仮想アドレス->物理アドレスの変換をやってみます!
まず、取得できた仮想アドレス134519060を2進数に変換します。
134519060 -> 0000.1000.0000.0100.1001.1001.0001.0100
CR3レジスタに格納されている物理アドレスはPD(ページディレクトリ)の開始物理アドレスです。
仮想アドレスの上位10ビットはPDのインデックスです。
PDのインデックスの値を2ビット左にシフトしたものと
CR3レジスタに格納されている物理アドレス(PDの開始物理アドレス=0x5f215000)を加算すると、
PDEが格納されている物理アドレスが得られます。
まず、仮想アドレス134519060のPDのインデックスは
0000.1000.00 -> 0000.1000.0000 -> 80
になります。
次に、取得したCR3に格納されている物理アドレス(PDの開始物理アドレス)を指定してダンプしてみます。
# ./physdump 5f215000 100 | hexdump -C
00000000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
...
00000080 67 20 75 5e 00 00 00 00 00 00 00 00 00 00 00 00 |g u^............|
00000090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
...
00000100
80の所には、67 20 75 5eが格納されていました。
i386はリトルエンディアンなので、逆転させて、
5e 75 20 67がPDEに格納されている内容になります。
5e 75 20 67のうち、下位12ビットはアクセス権やら、キャッシュやらの設定なので、オフします。
したがって、
0x5e752000がPT(ページテーブル)の開始物理アドレスになります。
さらに、PTEに格納されている内容を調べます。
仮想アドレスの中10ビットはPTのインデックスです。
PTのインデックスの値を2ビット左にシフトしたものと
PDEに格納されている物理アドレス(PTの開始物理アドレス=0x5e752000)を加算すると、
PTEが格納されている物理アドレスが得られます。
まず、仮想アドレス134519060のPTのインデックスは
00.0100.1001 -> 01.0010.0100 -> 124
になります。
次に、取得したPDEに格納されている物理アドレス(PTの開始物理アドレス)を指定してダンプしてみます。
# ./physdump 5e752000 200 | hexdump -C
00000000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
...
00000120 25 0c 2e 5e 67 94 68 5e 00 00 00 00 00 00 00 00 |%..^g.h^........|
00000130 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
...
00000200
124の所には、67 94 68 5eが格納されていました。
i386はリトルエンディアンなので、逆転させて、
5e 68 94 67がPTEに格納されている内容になります。
5e 68 94 67のうち、下位12ビットはアクセス権やら、キャッシュやらの設定なので、オフします。
したがって、
0x5e689000がページフレームの開始物理アドレスになります。
最後に、仮想アドレスの下位12ビットはページ内のオフセットアドレスになります。
1001.0001.0100 -> 914
ページフレームの開始物理アドレス(0x5e689000)にオフセットアドレス(0x914)を加算して
ダンプを取ってみると...
# ./physdump 5e689914 30 | hexdump -C 00000000 57 45 4c 43 4f 4d 45 20 54 4f 20 4e 45 54 42 53 |WELCOME TO NETBS| 00000010 44 21 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |D!..............| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 f0 ff bf bf |................|
となっています。
これはfooの中身ですね。
これで仮想アドレス->物理アドレスのマッピングが勉強出来ました!
参考サイトに以下をあげておきます。softwaretechnique.jp