x86_64環境のブート処理でプロテクトモードからロングモードに移行しているのがhead_64.Sなのでこの辺をめも。
見ているカーネルのバージョンはv3.9です。
ファイル名で気をつけないといけないのはx86_64ではhead_64.Sは2個あるということ。
今回見るのはstartup_32 -> startup_64 -> x86_64_start_kernel()という流れの部分です。
ブートローダー依存になるけど、ブートローダーが64bitカーネルを直接起動できる場合(UEFI等かな)はstartup_32の処理は呼ばれずにstartup_64が呼ばれるとコメントに書かれています。
arch/x86/boot/compressed/head_64のstartup_64のエントリポイントの定義部分です。
193ENTRY(startup_64) 194 /* 195 * 64bit entry is 0x200 and it is ABI so immutable! 196 * We come here either from startup_32 or directly from a 197 * 64bit bootloader. 198 * If we come here from a bootloader, kernel(text+data+bss+brk), 199 * ramdisk, zero_page, command line could be above 4G. 200 * We depend on an identity mapped page table being provided 201 * that maps our entire kernel(text+data+bss+brk), zero page 202 * and command line. 203 */
arch/x86/boot/compressed/head_64.Sにはstartup_64の実装部分は無くてそれらはarch/x86/kernel/head_64.Sにあります。
startup_32から実行する場合は、arch/x86/boot/compressed/head_64.Sでプロテクトモード移行のため以下の処理を行います。
ページテーブルやGDTなどはモード移行用の暫定設定で本当の設定は別途行います。
- セグメントの設定
- スタックの設定
- GDTの設定
- ページングテーブルの設定
- PAEを有効に
- EFERでロングモードを有効に
- ldtの設定
- CR0のPGとPEビットを立てる
EFER(Extended Feature Enable Register)でロングモードを使うように設定しているのは以下の部分です。
157 /* Enable Long mode in EFER (Extended Feature Enable Register) */ 158 movl $MSR_EFER, %ecx 159 rdmsr 160 btsl $_EFER_LME, %eax 161 wrmsr
160行目のbit test and set命令でEFERのLMEビットを立てて、wrmsr命令でセットする形ですね。
startup_32の最後は以下のようにlret命令で終了するわけですが、この時の戻り先はstartup_32の中で設定しています。
187 /* Jump from 32bit compatibility mode into 64bit mode. */ 188 lret
戻り先を設定しているのはプロテクトモードに入る直前の部分です。
169 /* 170 * Setup for the jump to 64bit mode 171 * 172 * When the jump is performend we will be in long mode but 173 * in 32bit compatibility mode with EFER.LME = 1, CS.L = 0, CS.D = 1 174 * (and in turn EFER.LMA = 1). To jump into 64bit mode we use 175 * the new gdt/idt that has __KERNEL_CS with CS.L = 1. 176 * We place all of the values on our mini stack so lret can 177 * used to perform that far jump. 178 */ 179 pushl $__KERNEL_CS 180 leal startup_64(%ebp), %eax 181 pushl %eax
単純に言って、startup_64のアドレスをpushしてlretがこのアドレスを読んでそこに飛ぶようにするということですね。
では、ロングモードに移行するコードを見ていきます。今まではarch/x86/boot/compressed/head_64.S内の処理でしたが、ここからはarch/x86/kernel/head_64.Sになります。
ロングモードへの移行はstartup_64とsecondary_startup_64の2段階で行われます。
startup_64はSMPのApplication Processor(AP)の初期化時には使われずsecondary_startup_64から処理が開始となります。
startup_64は以下の処理を行います。ここでもページング関連の設定がありますが、ブート段階で使用するページの設定なので本来のページ設定はもっと先になります。
- ページテーブルの設定
と、ページテーブルの設定だけですが、この段階ではCR3にアドレスを設定するところまでは行いません。
その他、secondary_startup_64の最初の処理になる下記の部分はジャンプして実行しないようになっています。
175 movq $(init_level4_pgt - __START_KERNEL_map), %rax
この理由はstartup_64でBSP(Bootstrap Processor)に設定したページテーブルとAP初期化時に設定されているページテーブルの違いからです。
158 movq $(early_level4_pgt - __START_KERNEL_map), %rax
ブートローダーが直接64bitカーネルを起動する場合はcpuはロングモードになっているはずなのと、startup_32が実行された場合はEFERのLMEビットを立てたところでcpuはロングモードに移行しているのでstartup_64、secondary_startup_64はロングモード用の各種設定をするのが仕事になります。
本題に入りましてsecondary_startup_64ですが、以下の処理を行っています。
- CR4のPAEとPGEを有効に
- CR3にページテーブルのアドレスを設定(init_level4_pgtもしくはearly_level4_pgt)
- NXが使えるかチェック
- EFERのSCEビットを立ててSYSENTERを有効に
- EFERのNXビットを立ててNX機能を有効に
- CR0の色々なビットを立てる
- スタックの設定
- GDTの設定
- セグメントの設定
- gsセグメントをirqstackに設定
- リアルモードの情報がrsiレジスタにあるので、それをrdiにコピーしてC関数への引数として設定
- lretq命令の実行でx86_64_start_kernel()に飛ぶようにスタックを設定
- lretq命令の実行でx86_64_start_kernel()を実行
lretq命令の設定はこの辺りです。
282 movq initial_code(%rip),%rax 283 pushq $0 # fake return address to stop unwinder 284 pushq $__KERNEL_CS # set correct cs 285 pushq %rax # target address in negative space 286 lretq
282行目のinitial_code(%rip)がx86_64_start_kernelのアドレスになるところで307・308行目が変数宣言にあたる部分です。
304 /* SMP bootup changes these two */ 305 __REFDATA 306 .balign 8 307 GLOBAL(initial_code) 308 .quad x86_64_start_kernel 309 GLOBAL(initial_gs) 310 .quad INIT_PER_CPU_VAR(irq_stack_union) 311 312 GLOBAL(stack_start) 313 .quad init_thread_union+THREAD_SIZE-8 314 .word 0
lretqが実行されるとc関数のx86_64_start_kernel()が呼ばれます。
139void __init x86_64_start_kernel(char * real_mode_data) 140{ 141 int i; 142
secondary_startup_64でrdiにリアルモードのデータをセットしていたので、x86_64_start_kernel()の引数として渡ってきますね。
あとはいくつか処理をした後にinit/main.cのstart_kernel()が呼ばれて本格的に初期化が始まります。