2015年12月10日
Linux Insides : カーネル起動プロセス part4
(2015-10-20)by 0xAX
本記事は、原著者の許諾のもとに翻訳・掲載しております。
64ビットモードへの移行
Kernel booting process
もパート4になりました。4回目の今回は、 プロテクトモード での最初の一歩についてご紹介します。CPUがサポートする ロングモード 、 SSE(ストリーミングSIMD拡張命令) 、 ページング方式 、そしてページテーブルの初期化やロングモードへの移行のお話しです。
注:このパートでは、アセンブリ言語のソースコードが頻出しますので、知識がない方は、事前に参考書を読むなどして理解を深めておいてください。
前回の パート では、 arch/x86/boot/pmjump.S 内にある32ビットのエントリーポイントにジャンプするところで終了しました。
jmpl *%eax
eax
レジスタには、32ビットのエントリーポイントのアドレスが含まれていたことを思い出してください。この点は、Linuxカーネルのx86ブートプロトコルから読み込むことができます。
When using bzImage, the protected-mode kernel was relocated to 0x100000
訳:bzImageを使うとき、保護モードのカーネルは0x100000に再配置されました
これが正しいか確認します。32ビットエントリーポイントでのレジスタ値を見てみましょう。
eax 0x100000 1048576
ecx 0x0 0
edx 0x0 0
ebx 0x0 0
esp 0x1ff5c 0x1ff5c
ebp 0x0 0x0
esi 0x14470 83056
edi 0x0 0
eip 0x100000 0x100000
eflags 0x46 [ PF ZF ]
cs 0x10 16
ss 0x18 24
ds 0x18 24
es 0x18 24
fs 0x18 24
gs 0x18 24
ここで、 cs
レジスタに 0x10
(前回のパートで説明したグローバルディスクリプタテーブルにある2つ目のインデックスです)が、 eip
レジスタに 0x100000
、そしてコードセグメントを含む全てのセグメントのベースアドレスにゼロが含まれているのが分かります。つまり、ブートプロトコルにあるように、 0:0x100000
もしくは 0x100000
といった物理アドレスを取得することができます。では、32ビットエントリーポイントから始めていきましょう。
32ビットエントリーポイント
32ビットエントリーポイントの定義は、 arch/x86/boot/compressed/head_64.S から見つけることができます。
__HEAD
.code32
ENTRY(startup_32)
....
....
....
ENDPROC(startup_32)
まず、なぜ compressed
ディレクトリなのでしょう? 実は bzimage
は、gzip圧縮された vmlinux + header + kernel setup code
です。カーネルのセットアップコードは、これまでの全てのパートにも出てきました。 head_64.S
の主な目的は、ロングモードに入る準備、ロングモードへ移行、そしてカーネルを展開することです。カーネルの展開と併せて全てのステップをこのパートで説明していきます。
なお、 arch/x86/boot/compressed
ディレクトリには、2つのファイルがあることに注意してください。
- head_32.S
- head_64.S
ここでは、 x86_64
のLinuxカーネルを学ぶので、 head_64.S
だけを使用します。 head_32.S
は、今回のケースではコンパイルしていません。では、 arch/x86/boot/compressed/Makefile を見ていきましょう。以下のターゲットがあります。
vmlinux-objs-y := $(obj)/vmlinux.lds $(obj)/head_$(BITS).o $(obj)/misc.o \
$(obj)/string.o $(obj)/cmdline.o \
$(obj)/piggy.o $(obj)/cpuflags.o
$(obj)/head_$(BITS).o
に注目してください。これは、head_{32,64}.oのコンパイルが $(BITS)
値に左右されることを示しています。他のMakefileにも同様のコードがあります – arch/x86/kernel/Makefile 。
ifeq ($(CONFIG_X86_32),y)
BITS := 32
...
...
else
...
...
BITS := 64
endif
どこから始めればいいのか分かったので、早速実践してみましょう。
必要に応じてセグメントを再読み込みする
上の項目で記載したように arch/x86/boot/compressed/head_64.S から始めます。まず、 startup_32
の定義の前を見ます。
__HEAD
.code32
ENTRY(startup_32)
__HEAD
は、 include/linux/init.h で定義されており、以下のようになっています。
#define __HEAD .section ".head.text","ax"
このセクションについては、 arch/x86/boot/compressed/vmlinux.lds.S リンカスクリプトから見つけられます。
SECTIONS
{
. = 0;
.head.text : {
_head = . ;
HEAD_TEXT
_ehead = . ;
}
ここで、 . = 0;
に注目してください。 .
は、ロケーションカウンタと呼ばれるリンカの特殊変数です。これに割り当てられる値は、セグメントのオフセットからのオフセットとなります。ここでは0を割り当てていますが、コメントには以下のように書かれています。
Be careful parts of head_64.S assume startup_32 is at address 0.
訳:Startup_32の一部は、head_64.Sがアドレス0(ゼロ)であると見なすことに注意する
これで、今どこにいるのかがわかります。。次に startup_32
関数を見ていきましょう。
startup_32
の最初に DF
フラグを消去するための cld
命令があります。この後、 stosb
といった文字列オペレーションや他のものが esi
または edi
インデックスレジスタをインクリメントします。
次に loadflags
から KEEP_SEGMENTS
フラグのチェックを見ます。 arch/x86/boot/head.S
(ここでは CAN_USE_HEAP
フラグをチェックしました)内にあった loadflags
を覚えていますか? ここでは、 KEEP_SEGMENTS
フラグをチェックする必要があります。このフラグの説明は、linuxブートプロトコルに掲載されています。
Bit 6 (write): KEEP_SEGMENTS
Protocol: 2.07+
- ゼロであれば、32ビットエントリーポイントでセグメントレジスタを再読み込みする。
- 1であれば、32ビットエントリーポイントでセグメントレジスタを再読み込みしない。
%cs %ds %ss %esは全て0(ゼロ)のベースと併せてフラットセグメント(または、それぞれの環境に同等)に設定すると見なす。
KEEP_SEGMENTS
が設定されていない場合は、以下のように ds
、 ss
、そして es
レジスタをベース0(ゼロ)のフラットセグメントに設定する必要があります。
testb $(1 << 6), BP_loadflags(%esi)
jnz 1f
cli
movl $(__BOOT_DS), %eax
movl %eax, %ds
movl %eax, %es
movl %eax, %ss
__BOOT_DS
が 0x18
(グローバルディスクリプタテーブルにあるデータセグメントのインデックス)であることを忘れないでください。 KEEP_SEGMENTS
が設定されていない場合は 1f
ラベルにジャンプするか、 __BOOT_DS
フラグが設定されている場合のみ、これと併せてセグメントレジスタをアップデートします。
前回の パート を読んだ方は、 arch/x86/boot/pmjump.S 内のセグメントレジスタがアップデートされているのを覚えているでしょう。ではなぜ、再度設定する必要があるのでしょうか。実際、Linuxカーネルは32ビットブートプロトコルを持っています。ですから、ブートローダがコントロールからカーネルへ移行した直後に実行される最初の関数は、 startup_32
となります。
KEEP_SEGMENTS
フラグをチェックし、正しい値をセグメントレジスタに置いたら、次は実行するために配置した場所とコンパイルした場所の差を計算します(セクションの最初には . = 0
が setup.ld.S
に含まれていることを思い出してください)。
leal (BP_scratch+4)(%esi), %esp
call 1f
1: popl %ebp
subl $1b, %ebp
ここでは、 esi
レジスタに boot_params 構造のアドレスが含まれており、 boot_params
にはオフセットである 0x1e4
と併せて特殊フィールドである scratch
が含まれています。 scratch
フィールドのアドレス + 4バイトを取得し、これを esp
レジスタに置きます(これらの計算のスタックとして使用します)。その後、コール命令とそのオペランドである 1f
ラベルを見ることができます。さて、 call
とはどういう意味でしょうか。これは、 ebp
値をスタック、 esp
値、そして関数の引数にプッシュし、最後にあるアドレスに返します。この後、スタックからのリターンアドレスを ebp
レジスタにポップし( ebp
にはリターンアドレスが含まれます)、先のラベル 1
のアドレスを差し引きます。
そうすると、 ebp
( 0x100000
)に配置した場所にアドレスが入ります。
次に、スタックをセットアップし、CPUがロングモードと SSE をサポートしていることを検証したいと思います。
スタックのセットアップとCPUの検証
カーネルを展開するための新しいスタックをセットアップするアセンブリ言語のコードを見ていきます。
movl $boot_stack_end, %eax
addl %ebp, %eax
movl %eax, %esp
boots_stack_end
は、 .bss
セクションにあります。この定義は、 head_64.S
の最後にあります。
.bss
.balign 4
boot_heap:
.fill BOOT_HEAP_SIZE, 1, 0
boot_stack:
.fill BOOT_STACK_SIZE, 1, 0
boot_stack_end:
まずは、 boot_stack_end
のアドレスを eax
レジスタに置き、それを ebp
の値に追加します(ここでは、 ebp
に 0x100000
を配置したアドレスが含まれていることを思い出してください)。最後に、 eax
値を exp
に置きます。これで、正しいスタックポインタが得られます。
次はCPUの検証です。CPUが long mode
と SSE
をサポートしていることをチェックする必要があります。
call verify_cpu
testl %eax, %eax
jnz no_longmode
cpuid
命令に対していくつかの呼び出しを含んでいる arch/x86/kernel/verify_cpu.S から verify_cpu
関数を呼び出すだけです。 cpuid
はプロセッサに関する情報を入手するために使われる命令です。今回の場合は、これでロングモードとSSEのサポートをチェックし、成功すれば 0
を、失敗すれば 1
を eax
レジスタに返します。
eax
がゼロでなければ、 no_longmode
ラベルにジャンプします。これによって hlt
命令がCPUを停止させ、どんなハードウェア割り込みもができなくなります。
no_longmode:
1:
hlt
jmp 1b
スタックのセットアップ、CPUの検証ができたので、次のステップに進みましょう。
再配置されたアドレスの計算
ここでは、展開が必要だった場合の再配置されたアドレスの計算について説明していきます。アセンブリ言語のコードは以下のようになります。
#ifdef CONFIG_RELOCATABLE
movl %ebp, %ebx
movl BP_kernel_alignment(%esi), %eax
decl %eax
addl %eax, %ebx
notl %eax
andl %eax, %ebx
cmpl $LOAD_PHYSICAL_ADDR, %ebx
jge 1f
#endif
movl $LOAD_PHYSICAL_ADDR, %ebx
1:
addl $z_extract_offset, %ebx
まずは、 CONFIG_RELOCATABLE
マクロに注目してください。この設定オプションは、 arch/x86/Kconfig で定義されており、以下の説明が記載されています。
これは、再配置情報を維持するカーネルイメージをビルドするので、デフォルトの1メガバイトに加えて、どこにでも配置することができます。
注:もし、CONFIG_RELOCATABLE=yであれば、カーネルは読み込まれたアドレスから起動し、コンパイル時の物理アドレス(CONFIG_PHYSICAL_START)は、最小値のロケーションとして使われます。
簡単に言うと、このコードはカーネルを移動させるためのアドレスの計算になります。カーネルは展開のために、カーネルが再配置可能、もしくはbzimageが LOAD_PHYSICAL_ADDR
の上で展開するのであれば ebx
レジスタにそれを置きます。
コードを見てみましょう。カーネルの設定ファイルに CONFIG_RELOCATABLE=n
があれば、 ebx
レジスタに LOAD_PHYSICAL_ADDR
を置き、 ebx
に z_extract_offset
を追加します。今は ebx
がゼロなので、これには z_extract_offset
が含まれます。では、これら2つの値を理解していきましょう。
LOAD_PHYSICAL_ADDR
は、 arch/x86/include/asm/boot.h で定義されたマクロです。以下のようになります。
#define LOAD_PHYSICAL_ADDR ((CONFIG_PHYSICAL_START \
+ (CONFIG_PHYSICAL_ALIGN - 1)) \
& ~(CONFIG_PHYSICAL_ALIGN - 1))
ここでは、アラインされたアドレスを計算します。これは、カーネルが読み込まれた( 0x100000
もしくは、この場合では1メガバイト)アドレスです。 PHYSICAL_ALIGN
は、カーネルがアラインされるべきアライメント値です。その値の領域は、x86_64であれば 0x200000
から 0x1000000
となります。デフォルト値では、 LOAD_PHYSICAL_ADDR
で2メガバイトを得ることができます。
>>> 0x100000 + (0x200000 - 1) & ~(0x200000 - 1)
2097152
アライメントユニットを抽出したら、2メガバイトに z_extract_offset
(この場合は、 0xe5c000
となります)を追加します。最終的に、17154048バイトのオフセットを得ることができます。 z_extract_offse
は、 arch/x86/boot/compressed/piggy.S
にあります。このファイルは、 mkpiggy プログラムによってコンパイル時に生成されます。
では、 CONFIG_RELOCATABLE
が y
である場合のコードを理解していきましょう。
まず、 ebp
値を ebx
に置きます( ebp
は、読み込んだアドレスが含まれていることを思い出してください)。そして、カーネルセットアップヘッダの kernel_alignment
フィールドを eax
レジスタに配置します。 kernel_alignment
は、カーネルで必要とされるアライメントの物理アドレスです。次に、前のケース(カーネルが再配置できない場合)と同様のことを行いますが、アラインユニットとして、 kernel_alignment
フィールドの値を、 CONFIG_PHYSICAL_ALIGN
と LOAD_PHYSICAL_ADDR
の代わりにベースアドレスとして ebx
(読み込んだアドレス)を使います。
アドレスを計算したら、 LOAD_PHYSICAL_ADDR
で比較し、これに再度 z_extract_offset
を追加するか、もし計算されたアドレスが必要以下の値であれば、 ebx
に LOAD_PHYSICAL_ADDR
を置きます。
これらの計算を行うことで、読み込んだアドレスを含む ebp
と、展開のために移動させられたカーネルのアドレスがある ebx
が得られます。
ロングモードに入る前の準備
64ビットモードに移行する前の最後の準備です。まずは、グローバルディスクリプタをアップデートする必要があります。
leal gdt(%ebp), %eax
movl %eax, gdt+2(%ebp)
lgdt gdt(%ebp)
ここでは、 ebp
からのアドレスを gdt
でオフセットしたものを eax
レジスタに置きます。次に、そのアドレスを gdt+2
でオフセットしたものを ebp
に置き、 lgdt
命令でグローバルディスクリプタテーブルを読み込みます。
グローバルディスクリプタテーブルの定義は以下の通りです。
.data
gdt:
.word gdt_end - gdt
.long gdt
.word 0
.quad 0x0000000000000000 /* NULL descriptor */
.quad 0x00af9a000000ffff /* __KERNEL_CS */
.quad 0x00cf92000000ffff /* __KERNEL_DS */
.quad 0x0080890000000000 /* TS descriptor */
.quad 0x0000000000000000 /* TS continued */
これは .data
セクションと同じファイル内に定義されており、中には5つのディスクリプタが含まれています。nullディスクリプタ、カーネルコードセグメント用のディスクリプタ、カーネルデータセグメント用のディスクリプタ、そして2つのタスクディスクリプタです。GDTは前の パート と同様の方法で読み込みますが、64ビットモードで実行するため、ディスクリプタのビットは CS.L = 1
、 CS.D = 0.
とします。
GDTを読み込んだ後は、 PAE(物理アドレス拡張) モードを有効にしないといけません。それには、以下のように cr4
レジスタの値を eax
に置いて、5ビット目を設定してから、再度 cr4
の中に読み込みます。
movl %cr4, %eax
orl $X86_CR4_PAE, %eax
movl %eax, %cr4
ここまで来れば、64ビットモードに移行する準備はほとんど終わりです。最後のステップはページテーブルの作成ですが、その前にロングモードについて少し説明しましょう。
ロングモード
ロングモードはx86_64プロセッサ用のネイティブモードです。まずは x86_64
と x86
の違いについて見てみましょう。
ロングモードでは、以下のような機能がサポートされています。
* r8
から r15
までの8つの新しい汎用レジスタと全ての汎用レジスタは64ビットです。
* RIP
(64ビットの命令ポインタ)
* ロングモード(新しいオペレーティングモード)
* 64ビットのアドレスとオペランド
* RIP相対アドレス指定(次のパートで例を紹介する予定です)
ロングモードはレガシーのプロテクトモードを拡張したもので、以下の2つのサブモードから構成されます。
- 64ビットモード
- 互換モード
64ビットモードに切り替えるには、次の処理が必要です。
- PAEの有効化(上述の通り、既に有効にしてあります)
- ページテーブルの作成と、
cr3
レジスタへのトップレベルページテーブルのアドレスの読み込み EFER.LME
の有効化- ページングの有効化
既に cr4
レジスタ内でPAEビットを設定しているので、 PAE
は有効になっています。次はページングについて説明します。
初期ページテーブルの初期化
64ビットモードに移行する前に、ページテーブルを作成する必要があります。では、初期の4Gブートページテーブルの作成について見ていきましょう。
注:ここでは仮想メモリの理論については説明しません。詳細を知りたい方は、本ページの一番下にあるリンクをご覧ください。
Linuxカーネルは4レベルのページングを使用しているので、通常は以下の6つのページテーブルを作成します。
- PML4テーブル1つ
- PDPテーブル1つ
- ページディレクトリテーブル4つ
では、実装を見ていきましょう。まずはメモリ内のページテーブル用のバッファをクリアします。各テーブルは4096バイトなので、24キロバイトのバッファが必要です。
leal pgtable(%ebx), %edi
xorl %eax, %eax
movl $((4096*6)/4), %ecx
rep stosl
ebx
に保存されているアドレスと( ebx
には、カーネルを再配置して展開するためのアドレスが含まれています)オフセットの pgtable
を edi
レジスタに置きます。 pgtable
は以下のような内容で、 head_64.S
の最後に定義されています。
.section ".pgtable","a",@nobits
.balign 4096
pgtable:
.fill 6*4096, 1, 0
これは .pgtable
セクションにあり、サイズは24キロバイトです。アドレスを edi
に置いた後は、 eax
レジスタをゼロアウトし、 rep stosl
命令でバッファにゼロを書き込みます。
これで、以下のようにするとトップレベルページテーブル PML4
が作成できます。
leal pgtable + 0(%ebx), %edi
leal 0x1007 (%edi), %eax
movl %eax, 0(%edi)
ここでは、オフセットの pgtable
と ebx
に格納されたアドレスを取得し、それを edi
に置いています。次に、このアドレスを 0x1007
でオフセットして eax
レジスタに置きます。 0x1007
は4096バイト(PML4のサイズ) + 7(PML4エントリーフラグの PRESENT+RW+USER
)です。そして、 eax
を edi
に置きます。この処理の後、 edi
には PRESENT+RW+USER
フラグの設定された最初のページ・ディレクトリ・ポインタ・エントリーのアドレスが格納されている状態になります。
次のステップでは、ページ・ディレクトリ・ポインタ・テーブル内に、4つのページディレクトリを作成します。ここでは、最初のエントリーには 0x7
のフラグが、その他のエントリーには 0x8
のフラグが設定されます。
leal pgtable + 0x1000(%ebx), %edi
leal 0x1007(%edi), %eax
movl $4, %ecx
1: movl %eax, 0x00(%edi)
addl $0x00001000, %eax
addl $8, %edi
decl %ecx
jnz 1b
ページ・ディレクトリ・ポインタ・テーブルのベースアドレスを edi
に、最初のページ・ディレクトリ・ポインタ・エントリーのアドレスを eax
に置きます。そして ecx
レジスタに4を置くと、これは以下のループ内のカウンタになり、最初のページ・ディレクトリ・ポインタ・テーブルのアドレスを edi
レジスタに書き込みます。
これで edi
には、 0x7
フラグが設定された最初のページ・ディレクトリ・ポインタ・エントリーのアドレスが入ります。そして、以下の 0x8
フラグが設定された以下のページ・ディレクトリ・ポインタ・エントリーのアドレスを計算して、そのアドレスを edi
に書き込みます。
次のステップでは、2メガバイトを使って 2048
ページ・テーブル・エントリーを作成します。
leal pgtable + 0x2000(%ebx), %edi
movl $0x00000183, %eax
movl $2048, %ecx
1: movl %eax, 0(%edi)
addl $0x00200000, %eax
addl $8, %edi
decl %ecx
jnz 1b
ここでは前の例とほぼ同じ処理をしています。1つ違うのは、最初のエントリーにはフラグ $0x00000183
、つまり PRESENT + WRITE + MBZ
が、他のエントリーには 0x8
が設定されていることです。これで、2メガバイトの2048ページが完成します。
作成した初期ページテーブルは、4ギガバイトのメモリをマッピングするので、 cr3
コントロールレジスタにあるハイレベルページテーブル PML4
のアドレスを格納できます。
leal pgtable(%ebx), %eax
movl %eax, %cr3
必要な処理は全て完了したので、これでロングモードに移行できます。
ロングモードへの移行
まずは MSR にある EFER.LME
フラグを、 0xC0000080
に設定する必要があります。
movl $MSR_EFER, %ecx
rdmsr
btsl $_EFER_LME, %eax
wrmsr
ここでは MSR_EFER
フラグ( arch/x86/include/uapi/asm/msr-index.h で定義されています)を ecx
レジスタに格納し、 rdmsr
命令を呼び出して、 MSR レジスタを読み込みます。 rdmsr
が実行されると、 ecx
値に応じて、 edx:eax
にあるデータが結果として得られます。 btsl
命令で EFER_LME
ビットをチェックし、 wrmsr
命令で eax
のデータを MSR
レジスタに書き込みます。
次のステップでは、カーネルセグメントコードのアドレスをスタック(GDTの中に定義されています)にプッシュして、 startup_64
ルーチンのアドレスを eax
に格納します。
pushl $__KERNEL_CS
leal startup_64(%ebp), %eax
その次は、このアドレスをスタックにプッシュして、 cr0
レジスタにある PG
ビットと PE
ビットを設定して、ページングを有効にします。
movl $(X86_CR0_PG | X86_CR0_PE), %eax
movl %eax, %cr0
そして、以下の呼び出しをします。
lret
前のステップでは、 startup_64
関数のアドレスをスタックにプッシュしました。そして、この lret
命令の後は、CPUがそのアドレスを抽出して、そこにジャンプします。
これらのステップを全て終えると、最終的に64ビットモードに移行できます。
.code64
.org 0x200
ENTRY(startup_64)
....
....
....
これでおしまいです!
まとめ
カーネル起動処理 パート4の記事は以上です。
質問やご意見がありましたら、 Twitter や Eメール でメ
ッセージを送信していただくか、こちらから issue を作成してください。
次のパートでは、カーネルの展開などについて説明します。
英語は私の第一言語ではないことをご承知おきください。誤りを見つけた方は
linux-internals に、プルリクエストを送ってください。
リンク
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa