💧

Linux kernelが0xffffffff80000000以下にマップされるまで

2023/12/10に公開

Linux kernelのコードは仮想アドレス0xffffffff80000000以下にロードされるようになっています。
https://www.kernel.org/doc/Documentation/x86/x86_64/mm.txt
もちろんブートローダからカーネルに移ってすぐ仮想アドレスにアクセスできるわけではなく、仮想アドレスにカーネルが読み込まれた物理アドレスをマップするページテーブルを作成し、プロセッサに伝える必要があります。この記事ではブートからページテーブルを作成し、仮想アドレスからカーネルにアクセスできるようになるまでを追っていきます。

前提知識

ページングについての基本的な知識があれば理解できます。
以下の記事がわかりやすく解説してくれています。
https://0xax.gitbooks.io/linux-insides/content/Theory/linux-theory-1.html

環境

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