POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

0xAX

本記事は、原著者の許諾のもとに翻訳・掲載しております。

カーネルの展開

カーネルの起動処理( 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 に、プルリクエストを送ってください。

参考リンク