サブシェルとは
サブシェルとは、以下のサンプルコードで言うと、4〜7行目にわたって「()」で括られたリストを指します。
「シェルスクリプトを実行する」とは、シェルが新たなシェルを子プロセスとしてメモリ上にロードし、その子プロセスが引数に指定されたスクリプトファイルを読み込み、ファイルに書かれた命令を順次実行する…ということですが、サブシェルは、その子プロセスから生成された孫プロセスで実行されます。
1 #!/bin/bash 2 3 func() 4 ( 5 ps -ef | grep `basename $0` | grep -v grep 6 exit 10 7 ) 8 9 echo PID=$$ 10 ps -ef | grep `basename $0` | grep -v grep 11 12 echo "########" 13 func 14 15 echo "########" 16 ps -ef | grep `basename $0` | grep -v grep 17 18 exit 0
実行結果
$ echo $$ 14367 $ bash ./hello.sh PID=14471 yz2cm 14471 14367 0 03:38 pts/1 00:00:00 bash ./hello.sh ######## yz2cm 14471 14367 0 03:38 pts/1 00:00:00 bash ./hello.sh yz2cm 14476 14471 0 03:38 pts/1 00:00:00 bash ./hello.sh ######## yz2cm 14471 14367 0 03:38 pts/1 00:00:00 bash ./hello.sh $
元のシェルはPID=14367ですが、hello.shを実行するシェルはPID=14471としてforkされています。さらに、hello.sh内のサブシェルはPID=14476で起動しています。つまり、サブシェルは元の「bash ./hello.sh」とは別のプロセスとして起動しているため、サブシェル内6行目でexitコマンドを呼び出していますが、その子プロセスが終了するだけで、hello.shの処理は終了しません。
リニアアドレス→物理アドレスの変換方式
セグメントディスクリプタのリニアアドレスの算出式を疑似コードで表現すると、
QWORD segment_desc segment_desc := *(QWORD *)((gdtr AND 0xFFFFFFFF0000) + (segment_reg AND 0xFFF8))
上で求めたセグメントディスクリプタの値(segment_desc)から、飛び地で格納されたベース値を連結して、セグメントのベースアドレスのリニアアドレスを求めます。そのベースアドレスにオフセット値を加算して、論理アドレス上の目的のリニアアドレスを算出します。
32bitのリニアアドレスは、上位10bit・中10bit・下位12bitで以下の意味があります。
31 22 21 12 11 0 +--------------+----------+------------+ | ディレクトリ | テーブル | オフセット | +--------------+----------+------------+
CR3レジスタと、ページディレクトリ、ページテーブル、オフセット値の組み合わせで、リニアアドレスを段階的に物理アドレスに変換します。
- CR3レジスタに、ページディレクトリ(PD)のベースアドレスが格納される。
- リニアアドレスの上位10bitが、PD内のエントリのインデックス値を示す。
- PDは、4byteのPDE(構造体)を1024個格納した配列。
- 2^10=1024であるため、上位10bitで「0〜1023」の数、つまり任意のPDEのインデックス番号が表現可能である。
- CR3レジスタの値と、アドレス上位10bit(PDEのインデックス番号)の組み合わせで、任意のPDEを示す。
- PDEの下位12bitをマスクすると、ページテーブル(PT)のベースアドレスが取得できる。
- PTのベースアドレスと中10bit(PTEのインデックス番号)の組み合わせで、任意のPTEを示す。
- PTEの下位12bitをマスクすると、ページフレームのベースアドレスが取得できる。
- ページフレームのベースアドレスにオフセット(リニアアドレスの下位12bit)を加算することで、物理アドレスが示される。
セグメント・ディスクリプタ
63 56 55 54 53 52 51 48 47 46 44 43 41 40 39 32 +----------------+---+---+---+---+--------------------+---+---+---+------+---+-------------------+ |ベース(上位8bit)| G | D |res|AVL| リミット(上位4bit) | P |DPL| S |タイプ| A | ベース(中8ビット) | +------------------------------------------------------------------------------------------------+ | ベース(下位16bit) | リミット(下位16bit) | +------------------------------------------------------------------------------------------------+ 31 16 15 0
「ベース」(セグメントベース)は、8bit-8bit-16bitで分割されて飛び地で格納されています。
「タイプ」は、セグメントの読み書き実行の権限を示します。
「DPL」は、セグメントへのアクセスに必要な特権レベルを示します。
セグメント・セレクタ
セグメント・セレクタとは、ディスクリプタ・テーブル上、現在どのディスクリプタを使用しているかを示す「セレクタ値」のことです。セレクタ値は、セグメント・レジスタに格納されます。
15 3 2 1 0 +---------------+--+---+ | Index |TI|RPL| +---------------+--+---+
上位13bitで、ディスクリプタ・テーブル上の任意のディスクリプタのインデックス値(0,1,2,3,…)を示します。
ディスクリプタ・テーブルのエントリ1個分は8バイトであるので、「8*Index」で、ディスクリプタ・テーブルの先頭アドレスからのオフセット位置を示します。
実際には、セグメント・セレクタ(16bit)の下位3bitをマスクする(AND 0xFFF8)ことで、上記オフセットアドレスを求めることができます。3bitで表現可能な数は「0〜7」なので、下位3bitをマスクすれば8の倍数が表現されるためです。
GDTRに格納されたアドレス(ディスクリプタ・テーブルの先頭アドレス)にセレクタ値を加算することで、ディスクリプタ・テーブル上の任意のセグメント・ディスクリプタの位置を特定するわけです(これで判明するのはセグメントの「属性情報」が記述された位置であり、セグメントそのものの位置ではないことに注意)。
TIは「テーブルインジケータ」です。
0 … GDTを使用
1 … LDTを使用
RPLは「要求特権レベル」です。
13ビットのすべてが0の場合、「セグメント・レジスタを無効にする」という意味となります。
GDTR(Global Descriptor Table Register)
GDTRについて。
GDTR(Global Descriptor Table Register)とは、論理アドレス→物理アドレスのマッピングテーブルの先頭アドレスを格納するレジスタである。
47 16 15 0 +---------------------+-------------+ | Linear Base Address | Table Limit | +---------------------+-------------+
上位32bit(4byte)に、アドレス変換に使用するディスクリプタテーブルの先頭アドレスが格納される。
下位16bit(2byte)に、リミット値(ディスクリプタテーブル内のエントリ数)が格納される。
リンカによるシンボル解決(4)文字列リテラル
文字列リテラルのシンボル解決について見ていきます。
1 #include <stdio.h> 2 3 const char *dummy_string; 4 const char *string; 5 6 int main() 7 { 8 dummy_string = "123456"; 9 string = "ABCDEF"; 10 11 printf("string = %p\n", string); 12 13 return 0; 14 }
ソースコード中に出てくる文字列リテラルは、"123456"、"ABCDEF"、"string = %p\n"の3つです。
再配置テーブルを見てみると、
Relocation section '.rel.text' at offset 0x454 contains 7 entries: Offset Info Type Sym.Value Sym. Name 0000000b 00000901 R_386_32 00000004 dummy_string 0000000f 00000501 R_386_32 00000000 .rodata 00000015 00000a01 R_386_32 00000004 string 00000019 00000501 R_386_32 00000000 .rodata 0000001f 00000a01 R_386_32 00000004 string 00000024 00000501 R_386_32 00000000 .rodata 00000030 00000c02 R_386_PC32 00000000 printf
.text上の、文字列リテラルの参照箇所を示すエントリは、Offsetの0x0f、0x19、0x24の3つです。オブジェクトファイル中の実行コードを見てみると、
00000000: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 e4 f0 and $0xfffffff0,%esp 6: 83 ec 10 sub $0x10,%esp 9: c7 05 00 00 00 00 00 movl $0x0,0x0 10: 00 00 00 13: c7 05 00 00 00 00 07 movl $0x7,0x0 1a: 00 00 00 1d: 8b 15 00 00 00 00 mov 0x0,%edx 23: b8 0e 00 00 00 mov $0xe,%eax 28: 89 54 24 04 mov %edx,0x4(%esp) 2c: 89 04 24 mov %eax,(%esp) 2f: e8 fc ff ff ff call 30 34: b8 00 00 00 00 mov $0x0,%eax 39: c9 leave 3a: c3 ret
3箇所に埋め込まれている「仮の値」は、それぞれ「0x00」@0x0f「0x07」@0x19、「0x0e」@0x24となります。また、関連するシンボルテーブル・エントリの番号は、すべて5番(再配置テーブルのInfoの上位24ビット)ですが、
Symbol table '.symtab' contains 13 entries: Num: Value Size Type Bind Vis Ndx Name 0: 00000000 0 NOTYPE LOCAL DEFAULT UND 1: 00000000 0 FILE LOCAL DEFAULT ABS sample.c 2: 00000000 0 SECTION LOCAL DEFAULT 1 3: 00000000 0 SECTION LOCAL DEFAULT 3 4: 00000000 0 SECTION LOCAL DEFAULT 4 ★5: 00000000 0 SECTION LOCAL DEFAULT 5 6: 00000000 0 SECTION LOCAL DEFAULT 7 7: 00000000 0 SECTION LOCAL DEFAULT 8 8: 00000000 0 SECTION LOCAL DEFAULT 6 9: 00000004 4 OBJECT GLOBAL DEFAULT COM dummy_string 10: 00000004 4 OBJECT GLOBAL DEFAULT COM string 11: 00000000 59 FUNC GLOBAL DEFAULT 1 main 12: 00000000 0 NOTYPE GLOBAL DEFAULT UND printf
シンボルテーブルの5番エントリ(★)は、5番セクション(Ndxの値)に関連します。5番セクションは、以下の通り、.rodataセクションです。
Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .text PROGBITS 00000000 000034 00003b 00 AX 0 0 4 [ 2] .rel.text REL 00000000 000454 000038 08 11 1 4 [ 3] .data PROGBITS 00000000 000070 000000 00 WA 0 0 4 [ 4] .bss NOBITS 00000000 000070 000000 00 WA 0 0 4 [ 5] .rodata PROGBITS 00000000 000070 00001b 00 A 0 0 1 [ 6] .comment PROGBITS 00000000 00008b 00002b 01 MS 0 0 1 [ 7] .note.GNU-stack PROGBITS 00000000 0000b6 000000 00 0 0 1 [ 8] .eh_frame PROGBITS 00000000 0000b8 000038 00 A 0 0 4 [ 9] .rel.eh_frame REL 00000000 00048c 000008 08 11 8 4 [10] .shstrtab STRTAB 00000000 0000f0 00005f 00 0 0 1 [11] .symtab SYMTAB 00000000 000358 0000d0 10 12 9 4 [12] .strtab STRTAB 00000000 000428 00002a 00 0 0 1
つまり、前述の3つの「仮の値」は、.rodataセクションの先頭からのオフセットを示すものと推測されます。
そこで、.rodataセクションをダンプしてみます。
$ readelf -x 5 sample.o Hex dump of section '.rodata': 0x00000000 31323334 35360041 42434445 46007374 123456.ABCDEF.st 0x00000010 72696e67 203d2025 700a00 ring = %p..
先の「仮の値」は、「0x00」@0x0f「0x07」@0x19、「0x0e」@0x24でしたが、上記のダンプ結果を見てみると、.rodataのオフセットと文字列リテラルの対応関係は
- 0x00 → "123456"
- 0x07 → "ABCDEF"
- 0x0e → "string = %p"
となります。
実行形式での.rodataの先頭アドレスは、0x080484e8なので、
[15] .rodata PROGBITS 080484e8 0004e8 000023 00 A 0 0 4
文字列リテラルのアドレスは以下のようになると思うのですが、
- "123456" → 080484e8 + 0x00 = 080484e8
- "ABCDEF" → 080484e8 + 0x07 = 080484ef
- "string = %p" → 080484e8 + 0x0e = 080484f6
実行形式の.rodataを見てみると、
$ readelf -x 15 sample Hex dump of section '.rodata': 0x080484e8 03000000 01000200 31323334 35360041 ........123456.A 0x080484f8 42434445 46007374 72696e67 203d2025 BCDEF.string = % 0x08048508 700a00 p..
と、おそらくリンク工程でのセクション集約時に、「03000000 01000200」という8バイトのデータが追加されています。なので、文字列リテラルのアドレスは、正しくは
- "123456" → 080484e8 + 0x08 = 080484f0
- "ABCDEF" → 080484e8 + 0x0f = 080484f7
- "string = %p" → 080484e8 + 0x16 = 08048fe
となると推測されます。
実行コードを確認すると、上記の計算結果は、以下★1〜3のアドレスと一致しています。
080483e4: 80483e4: 55 push ebp 80483e5: 89 e5 mov ebp,esp 80483e7: 83 e4 f0 and esp,0xfffffff0 80483ea: 83 ec 10 sub esp,0x10 80483ed: c7 05 20 a0 04 08 f0 mov DWORD PTR ds:0x804a020,0x80484f0(★1) 80483f4: 84 04 08 80483f7: c7 05 1c a0 04 08 f7 mov DWORD PTR ds:0x804a01c,0x80484f7(★2) 80483fe: 84 04 08 8048401: 8b 15 1c a0 04 08 mov edx,DWORD PTR ds:0x804a01c 8048407: b8 fe 84 04 08 mov eax,0x80484fe(★3) 804840c: 89 54 24 04 mov DWORD PTR [esp+0x4],edx 8048410: 89 04 24 mov DWORD PTR [esp],eax 8048413: e8 e8 fe ff ff call 8048300 8048418: b8 00 00 00 00 mov eax,0x0 804841d: c9 leave 804841e: c3 ret 804841f: 90 nop
ついでなので、実行コードを簡単に追ってみます。
★2で"ABCDEF"へのポインタをstringに代入し、printf()の引数として(edxレジスタ経由で)スタックに積み、★3で"string = %p\n"へのポインタをeaxレジスタ経由でスタックに積んでいるのが分かります。引数の準備をした後で、call命令でprintf()にジャンプします。