Linux Insides : カーネル起動プロセス part5(終)

カーネルの展開

カーネルの起動処理(Kernel booting process)シリーズの第5弾です。前回は、64ビットモードへの移行を見てきましたが、今回はその続きを説明していきたいと思います。カーネル展開の前準備と再配置、実際のカーネル展開処理のコードにジャンプする前の、最後のステップを見ていきます。それでは、カーネルコードの世界に再び飛び込んでいきましょう。

カーネル展開の前準備

前回は64ビットのエントリポイントstartup_64にジャンプする直前まででした。これはarch/x86/boot/compressed/head_64.Sのソースコードファイルの中にあります。すでにstartup_32でのstartup_64へのジャンプは見てきましたね。

    pushl   $__KERNEL_CS
    leal    startup_64(%ebp), %eax
    ...
    ...
    ...
    pushl   %eax
    ...
    ...
    ...
    lret

前回のパートで、startup_64に入りました。新しいグローバル・ディスクリプタ・テーブルをロードし、他のモード(私たちのケースでは64ビットモード)へのCPUトランジションをしたので、startup_64の始めの部分でデータセグメントの設定を行います。

.code64
    .org 0x200
ENTRY(startup_64)
    xorl    %eax, %eax
    movl    %eax, %ds
    movl    %eax, %es
    movl    %eax, %ss
    movl    %eax, %fs
    movl    %eax, %gs

cs以外の全てのセグメントレジスタはx18であるdsを指しています(なぜx18かが分からない場合は、前回のパートを読んでください)。

次のステップでは、カーネルがコンパイルされた時とロードされた時の違いを計算していきます。

#ifdef CONFIG_RELOCATABLE
    leaq    startup_32(%rip), %rbp
    movl    BP_kernel_alignment(%rsi), %eax
    decl    %eax
    addq    %rax, %rbp
    notq    %rax
    andq    %rax, %rbp
    cmpq    $LOAD_PHYSICAL_ADDR, %rbp
    jge 1f
#endif
    movq    $LOAD_PHYSICAL_ADDR, %rbp
1:
    leaq    z_extract_offset(%rbp), %rbx

rbpは展開されたカーネルの先頭アドレスを含みます。このコードが実行された後は、rbpレジスタに展開のためのカーネルコードを再配置するアドレスが含まれています。すでにstartup_32で、このようなコードを見てきていますが(前回の再配置のアドレスを計算するの章で触れています)、この計算を再度行う必要があります。というのも、ブートローダは64ビットブートプロトコルを利用できますが、startup_32は今回のケースで単に実行されないかもしれないからです。

次のステップでは、フラグレジスタのスタックの設定とリセットについて見ていきます。

leaq    boot_stack_end(%rbx), %rsp

    pushq   $0
    popfq

上記のように、rbxレジスタにはカーネルコード展開の先頭アドレスが読み込まれており、boot_stack_endでこのアドレスをオフセットしてrspへ置きます。これによってスタックの値は正しくなります。compressed/head_64.Sファイルの最後に boot_stack_endの定義がされています。

.bss
    .balign 4
boot_heap:
    .fill BOOT_HEAP_SIZE, 1, 0
boot_stack:
    .fill BOOT_STACK_SIZE, 1, 0
boot_stack_end:

.pgtable直前の.bssセクションに位置します。arch/x86/boot/compressed/vmlinux.lds.Sを見れば見つけられます。

スタックをセットしたので、展開されたカーネルのアドレスを計算すれば、上記で取得したアドレスに圧縮されたカーネルをコピーできます。コードを見てみましょう。

pushq   %rsi
    leaq    (_bss-8)(%rip), %rsi
    leaq    (_bss-8)(%rbx), %rdi
    movq    $_bss, %rcx
    shrq    $3, %rcx
    std
    rep movsq
    cld
    popq    %rsi

最初に、スタックに対してrsiをプッシュします。rsiの値を保存する必要がある理由は、このレジスタがboot_paramsリアルモード構造体を指すポインタを保持しているからです(カーネルのセットアップの最初にこの構造体のフィールドを埋めたので、おそらくこの構造体の事は覚えているでしょう)。このコードの最後に、rsiboot_paramsを指すポインタを再度書き込みます。

次の2つのleaq命令は、riprbxに置かれているアドレスを_bss - 8でオフセットして有効なアドレスになるよう計算し、riprbxに置きます。なぜこれらのアドレスを計算するのでしょうか? 実際のところ、圧縮したカーネルイメージは、このコピーするコード(startup_32から現在のコードまで)と展開コードの間に配置されています。リンカスクリプトのarch/x86/boot/compressed/vmlinux.lds.Sを見れば、これを確認できます。

   . = 0;
    .head.text : {
        _head = . ;
        HEAD_TEXT
        _ehead = . ;
    }
    .rodata..compressed : {
        *(.rodata..compressed)
    }
    .text : {
        _text = .;  /* Text */
        *(.text)
        *(.text.*)
        _etext = . ;
    }

気を付けなければいけないのは、.head.textセクションはstartup_32を含むということです。前回のパートを思い出してください。

__HEAD
    .code32
ENTRY(startup_32)
...
...
...

textセクションは展開したコードを含みます。

アセンブリです。

   .text
relocated:
...
...
...
/*
 * Do the decompression, and jump to the new kernel..
 */
...

そして.rodata..compressedは圧縮されたカーネルイメージを含んでいます。

そのためrsiにはripのアドレス(_bss - 8からの相対アドレス)を含み、rdiは再配置位置(同じく_bss - 8からの相対アドレス)を含みます。レジスタにこれらのアドレスを保存したので、_bssアドレスをrcxレジスタへ置きます。vmlinux.lds.Sを見て分かるように、セットアップ/カーネルコードを含む全てのセクションの最後で配置されます。movsq命令でrsiからrdiまでのデータを8バイトごとにコピーし始められるようになりました。

データをコピーする前にstd命令があることに留意してください。DFフラグがセットされますが、これはriprdiがデクリメントされることを意味します。言い換えれば、逆向きにバイトをコピーすることになります。

最後にcld命令でDFフラグをクリアし、rsiboot_params構造体を復元します。

この後、.textセクションのアドレスを取得して、そこにジャンプします。

  leaq    relocated(%rbx), %rax
    jmp *%rax

カーネル展開前の最後の準備

.textセクションはrelocatedラベルから開始します。最初にbssセクションを次のようにクリアします。

  xorl    %eax, %eax
    leaq    _bss(%rip), %rdi
    leaq    _ebss(%rip), %rcx
    subq    %rdi, %rcx
    shrq    $3, %rcx
    rep stosq

ここではeaxをクリアし、_bssのRIP相対アドレスをrdiに、_ebssrcxに置き、rep stosq命令によってゼロフィルします。

最後のほうでdecompress_kernelルーチンが呼び出されています。

  pushq   %rsi
    movq    $z_run_size, %r9
    pushq   %r9
    movq    %rsi, %rdi
    leaq    boot_heap(%rip), %rsi
    leaq    input_data(%rip), %rdx
    movl    $z_input_len, %ecx
    movq    %rbp, %r8
    movq    $z_output_len, %r9
    call    decompress_kernel
    popq    %r9
    popq    %rsi

ここでもまた、ポインタとrsiboot_params構造体に保存し、arch/x86/boot/compressed/misc.cから7つの引数と共にdecompress_kernelを呼び出します。全ての引数はレジスタに渡されます。これで全ての準備が終わったので、カーネルの展開に移りましょう。

カーネルの展開

上述のように、decompress_kernel関数は、arch/x86/boot/compressed/misc.cソースコードファイルの中にあります。この関数は、前のパートで触れたvideo/consoleの初期化から始まります。この呼び出しでは、ブートローダが32ビットのプロトコルを使っているのか、それとも64ビットのプロトコルなのかを知る必要があります。この後、フリーメモリの先頭地点にポインタを保存し、最後には次のようにします。

   free_mem_ptr     = heap;
    free_mem_end_ptr = heap + BOOT_HEAP_SIZE;

decompress_kernel関数の2つめのパラメータがheapですが、これは次のようにして取得します。

leaq    boot_heap(%rip), %rsi

上記で見たとおり、boot_heapは次のように定義されます。

oot_heap:
    .fill BOOT_HEAP_SIZE, 1, 0

カーネルがbzip2で圧縮されている場合はBOOT_HEAP_SIZE0x400000となり、そうでない場合は、0x8000となります。

次のステップではarch/x86/boot/compressed/aslr.c.からchoose_kernel_locationを呼び出します。名称から分かるように、この関数はカーネルイメージが展開されるメモリの位置を選択します。見てみましょう。

スタート時、CONFIG_HIBERNATIONが設定されていれば、choose_kernel_locationはコマンドラインオプションにkaslrがないか捜そうとします。もしこのコンフィギュレーションオプションのCONFIG_HIBERNATIONが設定されていなければnokaslrオプションを捜します。

#ifdef CONFIG_HIBERNATION
    if (!cmdline_find_option_bool("kaslr")) {
        debug_putstr("KASLR disabled by default...\n");
        goto out;
    }
#else
    if (cmdline_find_option_bool("nokaslr")) {
        debug_putstr("KASLR disabled by cmdline...\n");
        goto out;
    }
#endif

コマンドラインにkaslrnokaslrも見当たらなかった場合、outラベルまでジャンプします。

out:
    return (unsigned char *)choice;

そして返ってきたoutputパラメータを、変更を加えずにchoose_kernel_locationへ渡します。kaslrについて考えてみましょう。documentationを参照してみてください。

kaslr/nokaslr [X86]

Enable/disable kernel and module base offset ASLR
(Address Space Layout Randomization) if built into
the kernel. When CONFIG_HIBERNATION is selected,
kASLR is disabled by default. When kASLR is enabled,
hibernation will be disabled.

つまりこれは、kaslrオプションをカーネルのコマンドラインに渡し、展開されたカーネル用のランダムなアドレスを取得できるということを意味します(aslrについてはここをお読みください)。

カーネルのコマンドラインがkaslrオプションを含んでいる場合について考えてみましょう。

同じaslr.cソースコードファイルから、mem_avoid_init関数が呼び出されます。この関数はunsafeなメモリ領域(initrdや、カーネルコマンドラインなど)を取得します。これらのメモリ領域を把握して、展開後にカーネルとオーバーラップしないようにする必要があります。例を挙げます。

initrd_start  = (u64)real_mode->ext_ramdisk_image << 32;
    initrd_start |= real_mode->hdr.ramdisk_image;
    initrd_size  = (u64)real_mode->ext_ramdisk_size << 32;
    initrd_size |= real_mode->hdr.ramdisk_size;
    mem_avoid[1].start = initrd_start;
    mem_avoid[1].size = initrd_size;

ここではinitrdの先頭アドレスとサイズを計算しています。ext_ramdisk_imageはブートヘッダのramdisk_imageフィールドの高位32ビットであり、ext_ramdisk_sizeは、boot protocolからのramdisk_sizeフィールドの高位32ビットです。

Offset  Proto   Name        Meaning
/Size
...
...
...
0218/4  2.00+   ramdisk_image   initrd load address (set by boot loader)
021C/4  2.00+   ramdisk_size    initrd size (set by boot loader)
...

ext_ramdisk_imageext_ramdisk_sizeについてはDocumentation/x86/zero-page.txtをご覧ください。

Offset  Proto   Name        Meaning
/Size
...
...
...
0C0/004 ALL ext_ramdisk_image ramdisk_image high 32bits
0C4/004 ALL ext_ramdisk_size  ramdisk_size high 32bits
...

さてext_ramdisk_imageext_ramdisk_sizeを使い、それらを32ビットのままにして(高位32ビットの中に低位32ビットが含まれています)、initrdの先頭アドレスとサイズを取得します。この後、これらの値をmem_avoid配列に保存します。これは次のように定義されます。

#define MEM_AVOID_MAX 5
static struct mem_vector mem_avoid[MEM_AVOID_MAX];

mem_vectorの構造体は次のようになります。

struct mem_vector {
    unsigned long start;
    unsigned long size;
};

mem_avoid配列から全てのunsafeなメモリ領域を収集した後、次のステップは、unsafeな領域とオーバーラップしないランダムなアドレスを捜すことです。ここではfind_random_addr関数を使います。

最初にfind_random_addr関数で出力アドレスが整列されます。

minimum = ALIGN(minimum, CONFIG_PHYSICAL_ALIGN);

前のパートでやったCONFIG_PHYSICAL_ALIGNコンフィギュレーションオプションを覚えていますね? このオプションはどのカーネルが整列されるべきか値を提供しますが、この値はデフォルトで0x200000です。整列された出力アドレスを取得できたら、メモリを探索して、展開したカーネルイメージに適した領域を収集します。

for (i = 0; i < real_mode->e820_entries; i++) {
    process_e820_entry(&real_mode->e820_map[i], minimum, size);
}

カーネル起動処理パート2の2番目のパートでe820_entriesを収集したのを思い出してください。

最初に、process_e820_entry関数はe820メモリ領域がnon-RAMではないというチェックを行います。つまり、そのメモリ領域の先頭アドレスが、許可されたaslrオフセットの最大値よりも小さく、メモリ領域がカーネル整列の値よりも小さくないことをチェックします。

struct mem_vector region, img;

if (entry->type != E820_RAM)
    return;

if (entry->addr >= CONFIG_RANDOMIZE_BASE_MAX_OFFSET)
    return;

if (entry->addr + entry->size < minimum)
    return;

この後、e820メモリ領域の先頭アドレスとサイズをmem_vector構造体に保存します(この構造体の定義は上記でやりました)。

region.start = entry->addr;
region.size = entry->size;

これらの値を保存したら、find_random_addr関数でしたようにregion.startを整列して、オリジナルのメモリ領域よりも大きなアドレスを取得しなかったことを確認します。

region.start = ALIGN(region.start, CONFIG_PHYSICAL_ALIGN);

if (region.start > entry->addr + entry->size)
    return;

次にオリジナルのアドレスと整列されたものとの差を取得し、メモリ領域の末尾のアドレスがCONFIG_RANDOMIZE_BASE_MAX_OFFSETよりも大きければメモリ領域サイズを減らし、カーネルイメージの末尾がaslrオフセットの最大値よりも小さくなるようにします。

region.size -= region.start - entry->addr;

if (region.start + region.size > CONFIG_RANDOMIZE_BASE_MAX_OFFSET)
        region.size = CONFIG_RANDOMIZE_BASE_MAX_OFFSET - region.start;

最後には全てのunsafeなメモリ領域を探索して、各領域がカーネルのコマンドラインやinitrdなどのunsafeなエリアとオーバーラップしていないことをチェックします。

for (img.start = region.start, img.size = image_size ;
         mem_contains(&region, &img) ;
         img.start += CONFIG_PHYSICAL_ALIGN) {
        if (mem_avoid_overlap(&img))
            continue;
        slots_append(img.start);
    }

メモリ領域がunsafeな領域とオーバーラップしていなければ、領域の先頭アドレスでslots_append関数を呼び出します。slots_append関数はslots配列からメモリ領域の先頭アドレスを収集します。

slots[slot_max++] = addr;

これは次のように定義されます。

static unsigned long slots[CONFIG_RANDOMIZE_BASE_MAX_OFFSET /
               CONFIG_PHYSICAL_ALIGN];
static unsigned long slot_max;

process_e820_entryが実行された後、展開されたカーネルにとって安全なアドレスの配列が分かりました。次は、slots_fetch_random関数を呼び出し、この配列から任意のアイテムを取得します。

if (slot_max == 0)
    return 0;

return slots[get_random_long() % slot_max];

ここではget_random_long関数がCPUのフラグを調べてX86_FEATURE_RDRANDまたはX86_FEATURE_TSCなのかをチェックし、乱数を得るためのメソッド(RDRAND命令、タイムスタンプカウンタ、プログラマブルインターバルカウンタ(PIT)などがあります)を選択します。ランダム化されたアドレスを取得したら、choose_kernel_locationの実行は終了です。

では misc.cに戻りましょう。カーネルイメージのアドレスを取得した後、ランダム化されたアドレスが正しく整列されているか、そしてアドレスが間違っていないか、ということをチェックします。

これらのチェックが全て終了すれば、次のような見慣れたメッセージが見られるでしょう。

Decompressing Linux... 

そしてdecompress関数を呼び、カーネルを展開します。decompress関数は、カーネルのコンパイル時にどの展開アルゴリズムが選ばれたかに依存します。

#ifdef CONFIG_KERNEL_GZIP
#include "../../../../lib/decompress_inflate.c"
#endif

#ifdef CONFIG_KERNEL_BZIP2
#include "../../../../lib/decompress_bunzip2.c"
#endif

#ifdef CONFIG_KERNEL_LZMA
#include "../../../../lib/decompress_unlzma.c"
#endif

#ifdef CONFIG_KERNEL_XZ
#include "../../../../lib/decompress_unxz.c"
#endif

#ifdef CONFIG_KERNEL_LZO
#include "../../../../lib/decompress_unlzo.c"
#endif

#ifdef CONFIG_KERNEL_LZ4
#include "../../../../lib/decompress_unlz4.c"
#endif

カーネルが展開されたら、最後の関数handle_relocationschoose_kernel_locationから取得したアドレスへとカーネルを再配置します。カーネルが再配置されたら、decompress_kernelからhead_64.Sに戻ります。カーネルのアドレスはraxレジスタにあるので、そこへジャンプします。

jmp *%rax

これで全てです。これでカーネルの展開ができました。

結論

これでLinuxカーネル起動処理の5つのパートが全て終了です。カーネルの起動についてはこれが最後の投稿になります(更新はするかもしれません)が、カーネル内部についての投稿は今後もしていきます。

次の章はカーネルの初期化についてです。Linuxのカーネル初期化コードの最初のステップを見ていきます。

ご質問やご意見があればコメントを書いていただくか、twitterで呼びかけてみてください。

英語は私の第一言語ではないことをご承知おきください。誤りを見つけた方はlinux-internalsに、プルリクエストを送ってください。

参考リンク