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 日本ユーザーグループ代表
- X: @yosuke_furukawa
- Github: yosuke-furukawa









