φ(.. )メモシテオコウ linuxのlong modeへの移行処理

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_64secondary_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()が呼ばれて本格的に初期化が始まります。