2015年12月11日
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
リアルモード構造体を指すポインタを保持しているからです(カーネルのセットアップの最初にこの構造体のフィールドを埋めたので、おそらくこの構造体の事は覚えているでしょう)。このコードの最後に、 rsi
に boot_params
を指すポインタを再度書き込みます。
次の2つの leaq
命令は、 rip
と rbx
に置かれているアドレスを _bss - 8
でオフセットして有効なアドレスになるよう計算し、 rip
と rbx
に置きます。なぜこれらのアドレスを計算するのでしょうか? 実際のところ、圧縮したカーネルイメージは、このコピーするコード( 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
フラグがセットされますが、これは rip
と rdi
がデクリメントされることを意味します。言い換えれば、逆向きにバイトをコピーすることになります。
最後に cld
命令で DF
フラグをクリアし、 rsi
へ boot_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
に、 _ebss
を rcx
に置き、 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
ここでもまた、ポインタと rsi
を boot_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_SIZE
は 0x400000
となり、そうでない場合は、 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
コマンドラインに kaslr
も nokaslr
も見当たらなかった場合、 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_image
と ext_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_image
と ext_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(®ion, &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_relocations
が choose_kernel_location
から取得したアドレスへとカーネルを再配置します。カーネルが再配置されたら、 decompress_kernel
から head_64.S
に戻ります。カーネルのアドレスは rax
レジスタにあるので、そこへジャンプします。
jmp *%rax
これで全てです。これでカーネルの展開ができました。
結論
これでLinuxカーネル起動処理の5つのパートが全て終了です。カーネルの起動についてはこれが最後の投稿になります(更新はするかもしれません)が、カーネル内部についての投稿は今後もしていきます。
次の章はカーネルの初期化についてです。Linuxのカーネル初期化コードの最初のステップを見ていきます。
ご質問やご意見があればコメントを書いていただくか、 twitter で呼びかけてみてください。
英語は私の第一言語ではないことをご承知おきください。誤りを見つけた方は linux-internals に、プルリクエストを送ってください。
参考リンク
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa