Linux kernelが0xffffffff80000000以下にマップされるまで
Linux kernelのコードは仮想アドレス0xffffffff80000000以下にロードされるようになっています。
もちろんブートローダからカーネルに移ってすぐ仮想アドレスにアクセスできるわけではなく、仮想アドレスにカーネルが読み込まれた物理アドレスをマップするページテーブルを作成し、プロセッサに伝える必要があります。この記事ではブートからページテーブルを作成し、仮想アドレスからカーネルにアクセスできるようになるまでを追っていきます。前提知識
ページングについての基本的な知識があれば理解できます。
以下の記事がわかりやすく解説してくれています。
環境
x86_64 (level4 paging)
linux kernel v6.6.4
コードフロー
grub2等のブートローダがカーネルを読み込んでjmpするのですが、そのエントリーポイントは主に start (arch/x86_64/boot/header.S)もしくは startup_64 (arch/x86/kernel)です。
以下はページテーブルに注目したコードの流れです。
- start (arch/x86/boot/header.S)
| |
\/ call main
- main (arch/x86/boot/main.c)
| |
\/ go_to_protected_mode
- startup_32 (arch/x86/boot/compressed/head_64.S)
| |
| | enable PAE, long mode,
| | initialize pgtable *1
| | set cr3, cr0
| |
\/
- startup_64
| |
| | (configure 5 level paging)
| | initialize_identity_maps *2
| | extract_kernel
| |
\/
- startup_64 (arch/x86/kernel/head_64.S)
| |
| | __startup_64 *3
| | set cr3
| |
\/
x86_64_start_kernel
startup_32において物理アドレス0~4GBをストレートマップするページテーブルを作成し、cr3にその先頭アドレスを格納します。そしてrdmsrでlme(ロングモード)、cr0にPG(ページング)ビットをセットすることでページングが有効になります。startup_64ではカーネルを解凍するにあたって必要なアドレスをマップし、__startup_64でカーネルが0xffffffff80000000以下に配置されるようなページテーブルを作成、cr3にセットします。
*1 pgtableの初期化
まず初めに0~4GBの領域をストレートマップするページテーブルを作成します。512エントリ(2048B)のテーブルをPML4,PDPT,PDに対してそれぞれ1個、1個、4個確保します。手間を省くために1ページ2MBとして、2MB*2048 = 4GBの領域を確保することになります。
初めに512エントリ(2048B)のテーブルを6つ初期化します。
/* Initialize Page tables to 0 */
leal rva(pgtable)(%ebx), %edi
xorl %eax, %eax
movl $(BOOT_INIT_PGT_SIZE/4), %ecx
rep stosl
level4のテーブル(PML4)を作成します。pgtableはページテーブルの先頭(PML4)の先頭を指しており、最初のエントリに次のテーブルPDPTのアドレス+7を格納します。7はフラグです。
/* Build Level 4 */
leal rva(pgtable + 0)(%ebx), %edi
leal 0x1007 (%edi), %eax
movl %eax, 0(%edi)
addl %edx, 4(%edi)
level3のテーブル(PDPT)を作成します。PML4と同様に先頭から4つのエントリにPDのテーブルを1つずつ格納していきます。
/* Build Level 3 */
leal rva(pgtable + 0x1000)(%ebx), %edi
leal 0x1007(%edi), %eax
movl $4, %ecx
1: movl %eax, 0x00(%edi)
addl %edx, 0x04(%edi)
addl $0x00001000, %eax
addl $8, %edi
decl %ecx
jnz 1b
level2のテーブル(PD)を作成します。このエントリに0x20000(2MB)刻みで物理アドレス0~4GBを入れていきます。0x183というフラグはページサイズが2MBであること等を示しています。
/* Build Level 2 */
leal rva(pgtable + 0x2000)(%ebx), %edi
movl $0x00000183, %eax
movl $2048, %ecx
1: movl %eax, 0(%edi)
addl %edx, 4(%edi)
addl $0x00200000, %eax
addl $8, %edi
decl %ecx
jnz 1b
そして、cr3にページテーブルのアドレスを入れ、ロングモードを設定します。この処理の前に既にPAEの設定もなされています。
/* Enable the boot page tables */
leal rva(pgtable)(%ebx), %eax
movl %eax, %cr3
/* Enable Long mode in EFER (Extended Feature Enable Register) */
movl $MSR_EFER, %ecx
rdmsr
btsl $_EFER_LME, %eax
wrmsr
startup_64に移る直前にcr0に書き込んでロングモードを有効にします。
*2 initialize_identity_maps
mapping_infoにマッピング情報を、pgt_dataにページテーブルの情報を格納します。
mapping_info.alloc_pgt_page = alloc_pgt_page;
mapping_info.context = &pgt_data;
mapping_info.page_flag = __PAGE_KERNEL_LARGE_EXEC | sme_me_mask;
mapping_info.kernpg_flag = _KERNPG_TABLE;
top_level_pgt = read_cr3_pa();
if (p4d_offset((pgd_t *)top_level_pgt, 0) == (p4d_t *)_pgtable) {
pgt_data.pgt_buf = _pgtable + BOOT_INIT_PGT_SIZE;
pgt_data.pgt_buf_size = BOOT_PGT_SIZE - BOOT_INIT_PGT_SIZE;
memset(pgt_data.pgt_buf, 0, pgt_data.pgt_buf_size);
}
先程の構造体を用いて、カーネルやブートパラメータのアドレスをマップしていきます。kernel_add_identity_mapからkernel_ident_mapping_initが呼ばれ、指定されたアドレスがページテーブルに存在しなければ作成します。
kernel_add_identity_map((unsigned long)_head, (unsigned long)_end);
boot_params_ptr = rmode;
kernel_add_identity_map((unsigned long)boot_params_ptr,
(unsigned long)(boot_params_ptr + 1));
cmdline = get_cmd_line_ptr();
kernel_add_identity_map(cmdline, cmdline + COMMAND_LINE_SIZE);
...
新しいページテーブルを読み込みます。
write_cr3(top_level_pgt);
*3 __startup_64
この関数で新しいページテーブルを作成します。これまでは仮想アドレスと物理アドレスが一致するようにマッピングしてきましたが、このページテーブルがロードされた以降はカーネルが0xffffffff80000000以下の仮想アドレスからアクセスされるようになります。このあとも都度ページテーブルは更新されます。
この関数で用いるシンボルはhead_64.Sで定義されています。カーネルのtextの仮想アドレスに関わる部分のみ抜粋すると以下のとおりです。__START_KERNEL_mapは0xffffffff80000000です。
__INITDATA
.balign 4
SYM_DATA_START_PAGE_ALIGNED(level3_kernel_pgt)
.fill L3_START_KERNEL,8,0
.quad level2_kernel_pgt - __START_KERNEL_map + _KERNPG_TABLE_NOENC
.quad level2_fixmap_pgt - __START_KERNEL_map + _PAGE_TABLE_NOENC
SYM_DATA_END(level3_kernel_pgt)
SYM_DATA_START_PAGE_ALIGNED(level2_kernel_pgt)
PMDS(0, __PAGE_KERNEL_LARGE_EXEC, KERNEL_IMAGE_SIZE/PMD_SIZE)
SYM_DATA_END(level2_kernel_pgt)
最初にkaslrによって生じるカーネルが実際にロードされたアドレスとの差異を計算します。
load_delta = physaddr - (unsigned long)(_text - __START_KERNEL_map);
続いてp4d,pud,pmdのそれぞれにload_deltaを加算します。
また、カーネルにアクセスするアドレスについて、early_top_pgd(pgd)がlevel3_kernel_pgdを指すようにします。
pgd = fixup_pointer(early_top_pgt, physaddr);
p = pgd + pgd_index(__START_KERNEL_map);
if (la57)
*p = (unsigned long)level4_kernel_pgt;
else
*p = (unsigned long)level3_kernel_pgt;
*p += _PAGE_TABLE_NOENC - __START_KERNEL_map + load_delta;
pud = fixup_pointer(level3_kernel_pgt, physaddr);
pud[510] += load_delta;
pud[511] += load_delta;
カーネルイメージ以外を指すアドレスを無効化します。
pmd = fixup_pointer(level2_kernel_pgt, physaddr);
for (i = 0; i < pmd_index((unsigned long)_text); i++)
pmd[i] &= ~_PAGE_PRESENT;
for (; i <= pmd_index((unsigned long)_end); i++)
if (pmd[i] & _PAGE_PRESENT)
pmd[i] += load_delta;
for (; i < PTRS_PER_PMD; i++)
pmd[i] &= ~_PAGE_PRESENT;
*fixup_long(&phys_base, physaddr) += load_delta - sme_get_me_mask();
return sme_postprocess_startup(bp, pmd);
これによって、
early_top_pgt[511] => level3_kernel_pgt
level3_kernel_pgt[510] => level2_kernel_pgt
level2_kernel_pgt => 512MB (kernel text mapping)
となり、0xffffffff80000000以下にカーネルのtextがマップされました。
Discussion