サブシェルとは

サブシェルとは、以下のサンプルコードで言うと、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レジスタと、ページディレクトリ、ページテーブル、オフセット値の組み合わせで、リニアアドレスを段階的に物理アドレスに変換します。

  1. CR3レジスタに、ページディレクトリ(PD)のベースアドレスが格納される。
  2. リニアアドレスの上位10bitが、PD内のエントリのインデックス値を示す。
  3. PDは、4byteのPDE(構造体)を1024個格納した配列。
  4. 2^10=1024であるため、上位10bitで「0〜1023」の数、つまり任意のPDEのインデックス番号が表現可能である。
  5. CR3レジスタの値と、アドレス上位10bit(PDEのインデックス番号)の組み合わせで、任意のPDEを示す。
  6. PDEの下位12bitをマスクすると、ページテーブル(PT)のベースアドレスが取得できる。
  7. PTのベースアドレスと中10bit(PTEのインデックス番号)の組み合わせで、任意のPTEを示す。
  8. PTEの下位12bitをマスクすると、ページフレームのベースアドレスが取得できる。
  9. ページフレームのベースアドレスにオフセット(リニアアドレスの下位12bit)を加算することで、物理アドレスが示される。

論理アドレス空間上のLinuxカーネルの位置

Linuxカーネルは、4GBのアドレス空間上の「0xC0100000」にロードされます。これはおおよそ3Gの位置ですが、「0xC0100000 = 0xC0000000 + 0x100000」なので、正確には「3G+1M」の位置にロードされることになります。

論理アドレス上では上記の通りですが、物理アドレス空間上では、1M(0x100000)の位置にロードされます。

セグメント・ディスクリプタ

 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のオフセットと文字列リテラルの対応関係は

  1. 0x00 → "123456"
  2. 0x07 → "ABCDEF"
  3. 0x0e → "string = %p"

となります。

実行形式での.rodataの先頭アドレスは、0x080484e8なので、

  [15] .rodata           PROGBITS        080484e8 0004e8 000023 00   A  0   0  4

文字列リテラルのアドレスは以下のようになると思うのですが、

  1. "123456" → 080484e8 + 0x00 = 080484e8
  2. "ABCDEF" → 080484e8 + 0x07 = 080484ef
  3. "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バイトのデータが追加されています。なので、文字列リテラルのアドレスは、正しくは

  1. "123456" → 080484e8 + 0x08 = 080484f0
  2. "ABCDEF" → 080484e8 + 0x0f = 080484f7
  3. "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()にジャンプします。