Linux 6.18.37 LTS · x86-64 boot entry

x86-64 커널의 32비트 진입과 long mode·가상 주소 전환

startup_32, compressed startup_64, vmlinux startup_64를 한 경로로 연결해 boot protocol, KASLR 배치, 4/5-level paging과 CR3 전환을 읽습니다.

Series
01 / 4
Baseline
Linux 6.18.37 LTS
Architecture
x86-64
Source
arch/x86/boot/compressed/head_64.S

부트로더가 넘긴 CPU와 boot_params가 언제부터 x86-64 커널의 정상 C 실행 환경이 되는가?

x86-64 부팅에는 이름이 같은 startup_64가 두 개 있다. arch/x86/boot/compressed/head_64.S의 함수는 압축 해제기 실행 환경을 만들고, arch/x86/kernel/head_64.S의 함수는 압축이 풀린 vmlinux의 page table과 high virtual address 실행을 준비한다. 두 symbol을 같은 함수로 보면 relocation이 두 번 등장하는 이유를 설명할 수 없다.

32비트 boot protocol entry의 offset 0과 64비트 entry의 offset 0x200은 외부 부트로더가 의존하는 ABI다. 첫 경로는 4GiB 아래의 임시 주소 제약을 받고 직접 PAE와 long mode를 준비한다. 두 번째 경로는 이미 long mode와 identity mapping을 받은 대신 loader가 제공한 mapping 범위를 검증해야 한다.

핵심 상태는 세 가지다. %rsi가 가리키는 boot_params, 압축 해제 source와 destination이 겹치지 않는 메모리 배치, 그리고 CR3가 바뀐 뒤에도 다음 instruction fetch와 stack access가 성공하는 page-table overlap이다. 마지막 UART 문자열보다 이 세 값을 먼저 기록하면 초기 무출력 정지를 좁힐 수 있다.

들어오는 상태와 내보내는 상태

경계입력이 경계가 완성하는 상태
32-bit entryprotected mode, %esi=boot_paramsGDT, PAE, temporary page table, long mode
compressed 64-bit entrylong mode, identity map, %rsi=boot_paramsdecompress destination과 4/5-level paging
vmlinux startup_64압축 해제된 image, identity mapearly_top_pgt와 kernel virtual address
C entry정상 stack, 초기 page table, 보존된 boot_paramsx86_64_start_kernel()

주소와 register를 먼저 그려 본다

그림 1. 두 개의 startup_64가 맡는 서로 다른 일

bzImage / compressed

startup_32boot protocol 32-bit entry
startup_64decompressor의 stack·page table
extract_kernelvmlinux를 목적지에 해제

decompressed vmlinux

startup_64물리/가상 offset 계산
early_top_pgtCR3에 새 root 적재
common_startup_64high virtual address에서 계속

압축 image의 entry와 압축 해제된 vmlinux entry를 주소 공간 경계로 분리한다.

그림 2. 압축 해제 전후의 대표 메모리 배치
높은 주소
decompressed kernel destination2MiB 정렬·KASLR 후보
decompression output growth아래 주소부터 확장
compressed payload copybuffer 끝으로 역방향 복사
boot stack / page tables출력과 비중첩
boot_params / cmdlineloader가 넘긴 입력
낮은 주소

source를 buffer 끝으로 옮기는 이유는 출력이 커지면서 아직 읽지 않은 압축 입력을 덮는 일을 막기 위해서다.

그림 3. CR3 전환 때 동시에 유효해야 하는 주소
identity mapping현재 RIP와 early page table 접근
→
early_top_pgt fixup실제 물리 load offset 반영
→
mov %rax, %cr3활성 translation root 교체
→
high mappingcommon_startup_64의 링크 VA
→
x86_64_start_kernelC 초기화 진입

CR3 write 한 줄의 전후에서 PC, SP와 page-table page 자체가 모두 접근 가능해야 한다.

32비트 boot protocol 진입과 실제 load base 계산

startup_32는 loader의 DF와 interrupt 상태를 정리하고, call/pop으로 현재 실행 물리 주소를 구한 뒤 long mode에 필요한 GDT와 PAE를 준비한다.

원본: arch/x86/boot/compressed/head_64.S 82-170줄

82SYM_FUNC_START(startup_32)
83	/*
84	 * 32bit entry is 0 and it is ABI so immutable!
85	 * If we come here directly from a bootloader,
86	 * kernel(text+data+bss+brk) ramdisk, zero_page, command line
87	 * all need to be under the 4G limit.
88	 */
89	cld
90	cli
91 
92/*
93 * Calculate the delta between where we were compiled to run
94 * at and where we were actually loaded at.  This can only be done
95 * with a short local call on x86.  Nothing  else will tell us what
96 * address we are running at.  The reserved chunk of the real-mode
97 * data at 0x1e4 (defined as a scratch field) are used as the stack
98 * for this calculation. Only 4 bytes are needed.
99 */
100	leal	(BP_scratch+4)(%esi), %esp
101	call	1f
1021:	popl	%ebp
103	subl	$ rva(1b), %ebp
104 
105	/* Load new GDT with the 64bit segments using 32bit descriptor */
106	leal	rva(gdt)(%ebp), %eax
107	movl	%eax, 2(%eax)
108	lgdt	(%eax)
109 
110	/* Load segment registers with our descriptors */
111	movl	$__BOOT_DS, %eax
112	movl	%eax, %ds
113	movl	%eax, %es
114	movl	%eax, %fs
115	movl	%eax, %gs
116	movl	%eax, %ss
117 
118	/* Setup a stack and load CS from current GDT */
119	leal	rva(boot_stack_end)(%ebp), %esp
120 
121	pushl	$__KERNEL32_CS
122	leal	rva(1f)(%ebp), %eax
123	pushl	%eax
124	lretl
1251:
126 
127	/* Setup Exception handling for SEV-ES */
128#ifdef CONFIG_AMD_MEM_ENCRYPT
129	call	startup32_load_idt
130#endif
131 
132	/* Make sure cpu supports long mode. */
133	call	verify_cpu
134	testl	%eax, %eax
135	jnz	.Lno_longmode
136 
137/*
138 * Compute the delta between where we were compiled to run at
139 * and where the code will actually run at.
140 *
141 * %ebp contains the address we are loaded at by the boot loader and %ebx
142 * contains the address where we should move the kernel image temporarily
143 * for safe in-place decompression.
144 */
145 
146#ifdef CONFIG_RELOCATABLE
147	movl	%ebp, %ebx
148	movl	BP_kernel_alignment(%esi), %eax
149	decl	%eax
150	addl	%eax, %ebx
151	notl	%eax
152	andl	%eax, %ebx
153	cmpl	$LOAD_PHYSICAL_ADDR, %ebx
154	jae	1f
155#endif
156	movl	$LOAD_PHYSICAL_ADDR, %ebx
1571:
158 
159	/* Target address to relocate to for decompression */
160	addl	BP_init_size(%esi), %ebx
161	subl	$ rva(_end), %ebx
162 
163/*
164 * Prepare for entering 64 bit mode
165 */
166 
167	/* Enable PAE mode */
168	movl	%cr4, %eax
169	orl	$X86_CR4_PAE, %eax
170	movl	%eax, %cr4

주요 구문 해설

82SYM_FUNC_START(startup_32)

압축 커널의 32비트 진입 함수 startup_32를 정의합니다. 부트로더는 정해진 이미지 오프셋으로 이곳에 들어오므로, 일반 C 함수 호출로 시작하는 함수와 다릅니다.

84* 32bit entry is 0 and it is ABI so immutable!

offset 0은 Linux x86 boot protocol의 고정 ABI다. 코드를 앞에 삽입해 이 위치를 바꾸면 부트로더는 다른 byte를 entry로 실행한다.

89cld

방향 플래그 DF를 지웁니다. 이후 문자열 복사 명령이 낮은 주소에서 높은 주소로 진행하도록 부트로더가 남긴 상태를 정리합니다.

90cli

마스크 가능한 인터럽트를 끕니다. 커널의 예외·인터럽트 처리 환경을 아직 준비하지 않은 초기 부팅 구간입니다.

100leal (BP_scratch+4)(%esi), %esp

boot_params의 scratch 영역을 4-byte 임시 stack으로 사용한다. 정상 kernel stack이 아직 없다는 뜻이다.

101call 1f

다음 instruction 주소를 stack에 넣어 현재 load base를 역산한다. 32비트 모드에서는 일반 RIP-relative addressing을 쓸 수 없기 때문이다.

1021: popl %ebp

앞의 call이 임시 스택에 넣은 현재 실행 주소를 ebp로 꺼냅니다. 32비트 코드에서 자신의 실제 적재 위치를 알아내는 방법입니다.

103subl $ rva(1b), %ebp

실행 주소에서 이 레이블의 이미지 내부 오프셋을 빼 압축 이미지의 실제 시작 주소를 구합니다. 이후 심벌 주소를 계산하는 기준이 됩니다.

106leal rva(gdt)(%ebp), %eax

이미지 시작 주소 ebp에 GDT의 이미지 내부 오프셋을 더해 현재 메모리의 GDT 주소를 만듭니다. LEA는 유효 주소를 계산할 뿐 그 주소의 메모리를 읽지 않습니다.

107movl %eax, 2(%eax)

GDT를 설명하는 구조의 base 필드에 실제 GDT 주소를 기록합니다. 앞의 2바이트는 limit이므로 base는 주소 +2에서 시작합니다.

108lgdt (%eax)

준비한 GDT 주소와 크기를 GDTR에 읽습니다. 이후 세그먼트 선택자는 이 새 표를 기준으로 해석됩니다. LGDT는 메모리의 descriptor에서 GDT의 base와 limit를 GDTR에 적재합니다. 표를 메모리에 만든 것만으로 GDTR가 자동 변경되지는 않습니다.

111movl $__BOOT_DS, %eax

부팅용 데이터 세그먼트 선택자 __BOOT_DS를 eax에 준비합니다. 주소가 아니라 GDT 항목을 선택하는 값입니다.

112movl %eax, %ds

데이터 세그먼트 DS를 부팅용 GDT 항목으로 바꿉니다. 로더가 남긴 세그먼트 상태에 의존하지 않도록 합니다.

113movl %eax, %es

ES도 부팅용 데이터 세그먼트로 바꿉니다. 문자열 명령에서 사용하는 데이터 세그먼트를 맞추는 작업입니다.

114movl %eax, %fs

FS에 부팅용 데이터 세그먼트 선택자를 넣습니다. 여기서는 사용자 TLS를 준비하는 단계가 아닙니다.

115movl %eax, %gs

GS에 부팅용 데이터 세그먼트 선택자를 넣어 초기 세그먼트 환경을 정리합니다.

116movl %eax, %ss

스택 세그먼트 SS를 부팅용 데이터 세그먼트로 바꿉니다. 아래에서 새 스택 포인터를 준비합니다.

119leal rva(boot_stack_end)(%ebp), %esp

압축 해제기 내부 부팅 스택의 상단 주소를 esp에 넣습니다. 주소 계산에 실제 이미지 시작점 ebp를 사용합니다. LEA는 유효 주소를 계산할 뿐 그 주소의 메모리를 읽지 않습니다.

121pushl $__KERNEL32_CS

다음 far return이 사용할 32비트 커널 코드 세그먼트 선택자를 스택에 넣습니다. PUSHL는 esp를 4바이트 낮춘 뒤 새 스택 위치에 피연산자 값을 기록합니다.

122leal rva(1f)(%ebp), %eax

새 CS에서 실행할 바로 다음 레이블의 실제 주소를 eax에 만듭니다. 이미지 내부 오프셋에 적재 기준 주소를 더한 값입니다. LEA는 유효 주소를 계산할 뿐 그 주소의 메모리를 읽지 않습니다.

123pushl %eax

far return의 목적지 명령 주소를 스택에 넣습니다. 앞서 넣은 CS와 함께 하나의 전환 정보를 이룹니다. PUSHL는 esp를 4바이트 낮춘 뒤 새 스택 위치에 피연산자 값을 기록합니다.

124lretl

스택에서 EIP와 CS를 읽어 새 GDT의 32비트 코드 세그먼트로 계속 실행합니다. 여기서 곧바로 64비트 모드가 되는 것은 아닙니다.

1251:

CS를 새 GDT 기준으로 바꾼 뒤 실행을 이어가는 위치입니다. 다음 단계에서 초기 예외 처리와 CPU 기능을 확인합니다. 레이블은 현재 위치를 가리키는 이름으로, 별도의 CPU 명령을 추가하지 않습니다.

128#ifdef CONFIG_AMD_MEM_ENCRYPT

AMD 메모리 암호화 지원 빌드는 SEV-ES 환경에서 필요한 예외를 처리할 초기 IDT를 준비합니다. 암호화된 게스트의 CPU 기능 조회 등이 #VC를 일으킬 수 있어 후속 기능 검사보다 먼저 처리기를 갖춰야 합니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.

129call startup32_load_idt

SEV-ES 환경의 초기 예외를 처리할 32비트 IDT를 준비합니다. 이후 CPU 기능 확인 과정에서 발생할 수 있는 #VC 예외를 받을 수 있게 합니다.

130#endif

128행에서 시작한 선택 구간을 마칩니다. AMD 메모리 암호화 지원 빌드는 SEV-ES 환경에서 필요한 예외를 처리할 초기 IDT를 준비합니다. 암호화된 게스트의 CPU 기능 조회 등이 #VC를 일으킬 수 있어 후속 기능 검사보다 먼저 처리기를 갖춰야 합니다.

133call verify_cpu

64비트 커널을 실행할 수 있는 CPU인지 검사하는 루틴을 호출합니다. eax의 결과가 다음 분기의 기준입니다.

134testl %eax, %eax

검사 결과 eax가 0인지 확인하도록 상태 플래그를 설정합니다. 값 자체는 바꾸지 않습니다.

135jnz .Lno_longmode

eax가 0이 아니면 필요한 CPU 조건을 만족하지 못한 경로로 이동합니다. long mode 전환 준비를 계속하지 않습니다.

146#ifdef CONFIG_RELOCATABLE

재배치 가능한 압축 커널은 현재 적재 주소를 부트 헤더의 kernel_alignment에 맞추고 최소 물리 적재 주소도 확인하여 압축 해제 위치를 계산합니다. 고정 배치 빌드는 뒤의 LOAD_PHYSICAL_ADDR을 사용합니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.

147movl %ebp, %ebx

현재 이미지 적재 주소를 임시 목적지 계산용 ebx에 복사합니다. 뒤의 정렬 계산은 ebp 원본을 남겨 둔 채 ebx를 바꿉니다.

148movl BP_kernel_alignment(%esi), %eax

부팅 헤더의 커널 정렬 단위를 eax에 읽습니다. 이 값으로 압축 해제 위치를 올림 정렬합니다.

149decl %eax

정렬 단위에서 1을 뺍니다. (주소 + 정렬단위 - 1) & ~(정렬단위 - 1) 계산의 첫 준비입니다.

150addl %eax, %ebx

후보 주소에 정렬 단위 -1을 더합니다. 이미 정렬된 주소는 유지하고 나머지는 다음 경계로 올릴 준비입니다.

151notl %eax

정렬 단위 -1의 모든 비트를 반전해 하위 주소 비트를 지울 마스크를 만듭니다.

152andl %eax, %ebx

후보 주소의 하위 비트를 마스크하여 정렬된 목적지 기준 주소를 ebx에 얻습니다.

153cmpl $LOAD_PHYSICAL_ADDR, %ebx

정렬된 후보가 기본 적재 주소 LOAD_PHYSICAL_ADDR보다 낮은지 비교합니다.

154jae 1f

후보가 기본 적재 주소 이상이면 그대로 사용합니다. unsigned 비교이므로 주소의 크기를 부호 없는 값으로 판정합니다.

155#endif

146행에서 시작한 선택 구간을 마칩니다. 재배치 가능한 압축 커널은 현재 적재 주소를 부트 헤더의 kernel_alignment에 맞추고 최소 물리 적재 주소도 확인하여 압축 해제 위치를 계산합니다. 고정 배치 빌드는 뒤의 LOAD_PHYSICAL_ADDR을 사용합니다.

156movl $LOAD_PHYSICAL_ADDR, %ebx

후보가 너무 낮거나 재배치를 사용하지 않으면 기본 적재 주소를 목적지 기준으로 선택합니다.

1571:

목적지 기준 주소 선택이 끝난 위치입니다. 아래에서 압축 입력을 옮길 버퍼 끝부분의 주소를 계산합니다. 레이블은 현재 위치를 가리키는 이름으로, 별도의 CPU 명령을 추가하지 않습니다.

160addl BP_init_size(%esi), %ebx

기준 주소에 부팅 헤더의 초기 메모리 필요 크기를 더해 작업 버퍼의 끝을 구합니다.

161subl $ rva(_end), %ebx

버퍼 끝에서 압축 이미지의 전체 크기를 빼 임시 재배치 시작 주소를 구합니다. 압축 해제 출력이 아직 읽지 않은 입력을 덮지 않도록 끝부분에 배치합니다.

168movl %cr4, %eax

현재 CR4 값을 eax에 읽습니다. 기존 설정을 가져온 뒤 다음 줄에서 PAE 비트를 추가합니다.

169orl $X86_CR4_PAE, %eax

long mode page translation이 요구하는 PAE bit를 먼저 켠다. 이후 EFER.LME와 CR0.PG 순서가 이어진다.

170movl %eax, %cr4

PAE 비트를 켠 값을 CR4에 씁니다. long mode의 페이지 주소 변환에 필요한 조건을 준비하는 단계입니다.

64비트 compressed entry, paging level 조정과 안전한 source 이동

64비트 loader가 직접 들어오거나 startup_32에서 넘어온 두 경로를 합친다. 목적지 정렬을 다시 계산하고, 4/5-level paging 차이를 trampoline으로 정리한 뒤 압축 입력을 안전한 위치로 옮긴다.

원본: arch/x86/boot/compressed/head_64.S 278-442줄

278SYM_CODE_START(startup_64)
279	/*
280	 * 64bit entry is 0x200 and it is ABI so immutable!
281	 * We come here either from startup_32 or directly from a
282	 * 64bit bootloader.
283	 * If we come here from a bootloader, kernel(text+data+bss+brk),
284	 * ramdisk, zero_page, command line could be above 4G.
285	 * We depend on an identity mapped page table being provided
286	 * that maps our entire kernel(text+data+bss+brk), zero page
287	 * and command line.
288	 */
289 
290	cld
291	cli
292 
293	/* Setup data segments. */
294	xorl	%eax, %eax
295	movl	%eax, %ds
296	movl	%eax, %es
297	movl	%eax, %ss
298	movl	%eax, %fs
299	movl	%eax, %gs
300 
301	/*
302	 * Compute the decompressed kernel start address.  It is where
303	 * we were loaded at aligned to a 2M boundary. %rbp contains the
304	 * decompressed kernel start address.
305	 *
306	 * If it is a relocatable kernel then decompress and run the kernel
307	 * from load address aligned to 2MB addr, otherwise decompress and
308	 * run the kernel from LOAD_PHYSICAL_ADDR
309	 *
310	 * We cannot rely on the calculation done in 32-bit mode, since we
311	 * may have been invoked via the 64-bit entry point.
312	 */
313 
314	/* Start with the delta to where the kernel will run at. */
315#ifdef CONFIG_RELOCATABLE
316	leaq	startup_32(%rip) /* - $startup_32 */, %rbp
317	movl	BP_kernel_alignment(%rsi), %eax
318	decl	%eax
319	addq	%rax, %rbp
320	notq	%rax
321	andq	%rax, %rbp
322	cmpq	$LOAD_PHYSICAL_ADDR, %rbp
323	jae	1f
324#endif
325	movq	$LOAD_PHYSICAL_ADDR, %rbp
3261:
327 
328	/* Target address to relocate to for decompression */
329	movl	BP_init_size(%rsi), %ebx
330	subl	$ rva(_end), %ebx
331	addq	%rbp, %rbx
332 
333	/* Set up the stack */
334	leaq	rva(boot_stack_end)(%rbx), %rsp
335 
336	/*
337	 * At this point we are in long mode with 4-level paging enabled,
338	 * but we might want to enable 5-level paging or vice versa.
339	 *
340	 * The problem is that we cannot do it directly. Setting or clearing
341	 * CR4.LA57 in long mode would trigger #GP. So we need to switch off
342	 * long mode and paging first.
343	 *
344	 * We also need a trampoline in lower memory to switch over from
345	 * 4- to 5-level paging for cases when the bootloader puts the kernel
346	 * above 4G, but didn't enable 5-level paging for us.
347	 *
348	 * The same trampoline can be used to switch from 5- to 4-level paging
349	 * mode, like when starting 4-level paging kernel via kexec() when
350	 * original kernel worked in 5-level paging mode.
351	 *
352	 * For the trampoline, we need the top page table to reside in lower
353	 * memory as we don't have a way to load 64-bit values into CR3 in
354	 * 32-bit mode.
355	 */
356 
357	/* Make sure we have GDT with 32-bit code segment */
358	leaq	gdt64(%rip), %rax
359	addq	%rax, 2(%rax)
360	lgdt	(%rax)
361 
362	/* Reload CS so IRET returns to a CS actually in the GDT */
363	pushq	$__KERNEL_CS
364	leaq	.Lon_kernel_cs(%rip), %rax
365	pushq	%rax
366	lretq
367 
368.Lon_kernel_cs:
369	/*
370	 * RSI holds a pointer to a boot_params structure provided by the
371	 * loader, and this needs to be preserved across C function calls. So
372	 * move it into a callee saved register.
373	 */
374	movq	%rsi, %r15
375 
376	call	load_stage1_idt
377 
378#ifdef CONFIG_AMD_MEM_ENCRYPT
379	/*
380	 * Now that the stage1 interrupt handlers are set up, #VC exceptions from
381	 * CPUID instructions can be properly handled for SEV-ES guests.
382	 *
383	 * For SEV-SNP, the CPUID table also needs to be set up in advance of any
384	 * CPUID instructions being issued, so go ahead and do that now via
385	 * sev_enable(), which will also handle the rest of the SEV-related
386	 * detection/setup to ensure that has been done in advance of any dependent
387	 * code. Pass the boot_params pointer as the first argument.
388	 */
389	movq	%r15, %rdi
390	call	sev_enable
391#endif
392 
393	/* Preserve only the CR4 bits that must be preserved, and clear the rest */
394	movq	%cr4, %rax
395	andl	$(X86_CR4_PAE | X86_CR4_MCE | X86_CR4_LA57), %eax
396	movq	%rax, %cr4
397 
398	/*
399	 * configure_5level_paging() updates the number of paging levels using
400	 * a trampoline in 32-bit addressable memory if the current number does
401	 * not match the desired number.
402	 *
403	 * Pass the boot_params pointer as the first argument. The second
404	 * argument is the relocated address of the page table to use instead
405	 * of the page table in trampoline memory (if required).
406	 */
407	movq	%r15, %rdi
408	leaq	rva(top_pgtable)(%rbx), %rsi
409	call	configure_5level_paging
410 
411	/* Zero EFLAGS */
412	pushq	$0
413	popfq
414 
415/*
416 * Copy the compressed kernel to the end of our buffer
417 * where decompression in place becomes safe.
418 */
419	leaq	(_bss-8)(%rip), %rsi
420	leaq	rva(_bss-8)(%rbx), %rdi
421	movl	$(_bss - startup_32), %ecx
422	shrl	$3, %ecx
423	std
424	rep	movsq
425	cld
426 
427	/*
428	 * The GDT may get overwritten either during the copy we just did or
429	 * during extract_kernel below. To avoid any issues, repoint the GDTR
430	 * to the new copy of the GDT.
431	 */
432	leaq	rva(gdt64)(%rbx), %rax
433	leaq	rva(gdt)(%rbx), %rdx
434	movq	%rdx, 2(%rax)
435	lgdt	(%rax)
436 
437/*
438 * Jump to the relocated address.
439 */
440	leaq	rva(.Lrelocated)(%rbx), %rax
441	jmp	*%rax
442SYM_CODE_END(startup_64)

주요 구문 해설

278SYM_CODE_START(startup_64)

압축 커널의 64비트 진입 코드를 정의합니다. 32비트 시작 경로가 모드를 전환해 이곳에 오거나 64비트 부트로더가 직접 진입합니다. 이 표기는 빌드 도구에 코드 심벌과 정렬을 알려 주며 그 자체가 런타임 함수 호출은 아닙니다.

280* 64bit entry is 0x200 and it is ABI so immutable!

offset 0x200 역시 외부 bootloader가 직접 사용할 수 있는 고정 entry ABI다.

290cld

DF를 지워 기본 문자열 명령의 진행 방향을 주소 증가 방향으로 맞춥니다. 64비트 직접 진입도 같은 초기 상태에서 시작하게 합니다.

291cli

압축 해제기의 초기 환경을 준비하는 동안 마스크 가능한 인터럽트를 끕니다.

294xorl %eax, %eax

eax를 0으로 만듭니다. 아래 데이터 세그먼트 선택자를 초기화하는 데 사용할 값입니다.

295movl %eax, %ds

DS에 null 선택자를 넣습니다. long mode에서 일반 데이터 주소 계산은 주로 평탄한 주소 공간을 사용합니다.

296movl %eax, %es

ES에 null 선택자를 넣어 로더가 남긴 데이터 세그먼트 선택자를 정리합니다.

297movl %eax, %ss

SS에 null 선택자를 넣습니다. 이어서 커널이 사용할 64비트 스택 포인터를 별도로 준비합니다.

298movl %eax, %fs

FS 선택자를 0으로 만듭니다. FS 선택자와 long mode의 FS base는 별도 상태이므로 이 줄을 임의의 FS base 설정으로 해석하면 안 됩니다.

299movl %eax, %gs

GS 선택자를 0으로 만듭니다. 이후 정상 커널의 per-CPU GS base 초기화와는 다른 단계입니다.

315#ifdef CONFIG_RELOCATABLE

64비트 시작 경로에서도 재배치 빌드이면 현재 위치를 기준으로 정렬된 압축 해제 목적지를 계산합니다. 고정 배치 빌드는 지정된 물리 주소를 사용하며, 계산에는 부트 헤더의 정렬 요구가 반영됩니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.

316leaq startup_32(%rip) /* - $startup_32 */, %rbp

RIP 상대 주소 계산으로 현재 startup_32의 실제 주소를 rbp에 구합니다. 64비트 직접 진입에서는 앞선 32비트 주소 계산을 거쳤다고 가정할 수 없습니다. LEA는 유효 주소를 계산할 뿐 그 주소의 메모리를 읽지 않습니다.

317movl BP_kernel_alignment(%rsi), %eax

부팅 헤더에서 요구하는 커널 정렬 단위를 읽습니다. eax에 쓰면 rax의 상위 32비트도 0이 됩니다.

318decl %eax

정렬 단위에서 1을 빼 올림 정렬에 사용할 값을 만듭니다.

319addq %rax, %rbp

현재 적재 주소에 정렬 단위 -1을 더합니다. 다음 마스크 연산과 합쳐 다음 정렬 경계를 구합니다.

320notq %rax

정렬 단위 -1을 반전하여 하위 주소 비트를 지울 마스크를 만듭니다.

321andq %rax, %rbp

마스크를 적용해 rbp를 정렬된 압축 해제 기준 주소로 바꿉니다.

322cmpq $LOAD_PHYSICAL_ADDR, %rbp

정렬한 후보가 기본 적재 주소보다 낮은지 비교합니다. 주소를 unsigned 값으로 판단하는 다음 분기로 이어집니다.

323jae 1f

후보가 기본 적재 주소 이상이면 선택을 마칩니다. 낮으면 아래에서 기본 주소로 올립니다.

324#endif

315행에서 시작한 선택 구간을 마칩니다. 64비트 시작 경로에서도 재배치 빌드이면 현재 위치를 기준으로 정렬된 압축 해제 목적지를 계산합니다. 고정 배치 빌드는 지정된 물리 주소를 사용하며, 계산에는 부트 헤더의 정렬 요구가 반영됩니다.

325movq $LOAD_PHYSICAL_ADDR, %rbp

기본 적재 주소를 압축 해제 기준 rbp로 선택합니다. 재배치가 꺼진 빌드도 이 값을 사용합니다.

3261:

압축 해제 기준 주소를 결정한 두 경로가 합류합니다. 레이블은 현재 위치를 가리키는 이름으로, 별도의 CPU 명령을 추가하지 않습니다.

329movl BP_init_size(%rsi), %ebx

초기 부팅 작업에 필요한 메모리 크기를 ebx로 읽습니다. 임시 재배치 주소를 버퍼 끝부분에서 역산할 때 씁니다.

330subl $ rva(_end), %ebx

초기 메모리 크기에서 압축 이미지 크기를 빼 버퍼 내부의 재배치 오프셋을 계산합니다.

331addq %rbp, %rbx

압축 해제 기준 주소 rbp를 더해 압축 입력의 실제 재배치 시작 주소 rbx를 만듭니다.

334leaq rva(boot_stack_end)(%rbx), %rsp

재배치 목적지 기준으로 부팅 스택의 상단 주소를 rsp에 설정합니다. 아래 C 함수들을 호출할 때 사용할 스택입니다. LEA는 유효 주소를 계산할 뿐 그 주소의 메모리를 읽지 않습니다.

358leaq gdt64(%rip), %rax

64비트 GDT descriptor의 현재 주소를 rax에 만듭니다. 아래 4/5단계 페이징 전환에는 32비트 코드 세그먼트도 필요합니다. LEA는 유효 주소를 계산할 뿐 그 주소의 메모리를 읽지 않습니다.

359addq %rax, 2(%rax)

GDT descriptor에 저장된 상대 base에 현재 위치를 더해 실제 base 주소로 보정합니다.

360lgdt (%rax)

보정한 descriptor를 GDTR에 읽어 현재 실행과 모드 전환에 필요한 GDT를 선택합니다. LGDT는 메모리의 descriptor에서 GDT의 base와 limit를 GDTR에 적재합니다. 표를 메모리에 만든 것만으로 GDTR가 자동 변경되지는 않습니다.

363pushq $__KERNEL_CS

새 GDT에서 사용할 커널 코드 세그먼트 선택자를 스택에 넣습니다. PUSHQ는 rsp를 8바이트 낮춘 뒤 새 스택 위치에 피연산자 값을 기록합니다.

364leaq .Lon_kernel_cs(%rip), %rax

CS 전환 후 계속 실행할 .Lon_kernel_cs 주소를 rax에 준비합니다. LEA는 유효 주소를 계산할 뿐 그 주소의 메모리를 읽지 않습니다.

365pushq %rax

목적지 RIP를 스택에 넣습니다. 아래 far return이 이 값과 바로 앞의 CS를 읽습니다. PUSHQ는 rsp를 8바이트 낮춘 뒤 새 스택 위치에 피연산자 값을 기록합니다.

366lretq

RIP와 CS를 스택에서 읽어 새 GDT의 커널 코드 세그먼트로 넘어갑니다. 이 위치에서는 커널 권한을 유지하며 코드 세그먼트를 맞춥니다.

368.Lon_kernel_cs:

CS가 새 GDT의 커널 항목을 사용하게 된 뒤 합류하는 위치입니다. 레이블은 현재 위치를 가리키는 이름으로, 별도의 CPU 명령을 추가하지 않습니다.

374movq %rsi, %r15

loader가 넘긴 boot_params pointer를 callee-saved %r15에 보존한다. 이후 C 함수를 호출해도 살아 있어야 한다.

376call load_stage1_idt

초기 단계의 IDT를 설치합니다. SEV-ES에서 CPUID 실행 등에 의해 생길 수 있는 #VC 예외를 받을 준비를 합니다.

378#ifdef CONFIG_AMD_MEM_ENCRYPT

AMD 메모리 암호화 지원 빌드는 초기 예외 처리기가 준비된 다음 sev_enable로 SEV 상태와 SEV-SNP의 CPUID 정보를 준비합니다. 이후 CPUID 실행에 필요한 환경을 먼저 갖추며, 실제 사용 여부는 하드웨어와 게스트 상태를 확인합니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.

389movq %r15, %rdi

보존해 둔 boot_params 포인터를 첫 C 인자 레지스터 rdi로 옮깁니다. 다음 SEV 초기화 함수가 읽을 부팅 정보입니다.

390call sev_enable

SEV 사용 여부와 관련 초기 상태를 준비합니다. SEV-SNP에서는 뒤의 CPUID 실행보다 먼저 CPUID 테이블을 설정해야 합니다.

391#endif

378행에서 시작한 선택 구간을 마칩니다. AMD 메모리 암호화 지원 빌드는 초기 예외 처리기가 준비된 다음 sev_enable로 SEV 상태와 SEV-SNP의 CPUID 정보를 준비합니다. 이후 CPUID 실행에 필요한 환경을 먼저 갖추며, 실제 사용 여부는 하드웨어와 게스트 상태를 확인합니다.

394movq %cr4, %rax

현재 CR4를 rax에 읽습니다. 로더가 켜 둔 기능 중 부팅에 계속 필요한 비트만 남기려는 단계입니다.

395andl $(X86_CR4_PAE | X86_CR4_MCE | X86_CR4_LA57), %eax

PAE·MCE·LA57 비트만 남깁니다. 페이지 테이블 형식과 machine check 관련 상태를 유지하면서 다른 CR4 기능은 초기 상태로 정리합니다.

396movq %rax, %cr4

정리한 CR4 값을 CPU에 적용합니다. 아직 원하는 페이징 단계 수로 전환하는 작업 자체는 수행하지 않습니다.

399* configure_5level_paging() updates the number of paging levels using

현재 CR4.LA57과 kernel이 원하는 paging level이 다르면 long mode를 잠시 벗어나는 trampoline이 이를 맞춘다.

407movq %r15, %rdi

configure_5level_paging의 첫 인자로 부팅 정보 포인터를 준비합니다.

408leaq rva(top_pgtable)(%rbx), %rsi

두 번째 인자로 재배치 위치의 최상위 페이지 테이블 주소를 넘깁니다. 낮은 주소의 전환용 trampoline 페이지 테이블을 계속 사용할 필요가 없게 준비하는 값입니다. LEA는 유효 주소를 계산할 뿐 그 주소의 메모리를 읽지 않습니다.

409call configure_5level_paging

현재 CR4.LA57과 kernel이 원하는 paging level이 다르면 long mode를 잠시 벗어나는 trampoline이 이를 맞춘다.

412pushq $0

플래그 초기화에 사용할 0을 스택에 넣습니다. 아래 popfq가 이 값을 플래그 레지스터에 적용합니다. PUSHQ는 rsp를 8바이트 낮춘 뒤 새 스택 위치에 피연산자 값을 기록합니다.

413popfq

스택의 값으로 변경 가능한 RFLAGS 비트를 초기화합니다. 아키텍처상 고정된 예약 비트까지 모두 0으로 만든다는 뜻은 아닙니다.

419leaq (_bss-8)(%rip), %rsi

복사할 입력의 마지막 8바이트 주소를 rsi에 넣습니다. BSS 직전까지의 압축 이미지 데이터를 뒤에서부터 복사할 준비입니다. LEA는 유효 주소를 계산할 뿐 그 주소의 메모리를 읽지 않습니다.

420leaq rva(_bss-8)(%rbx), %rdi

새 위치에서 대응하는 마지막 8바이트 주소를 rdi에 계산합니다. rbx가 재배치 시작 기준 주소입니다. LEA는 유효 주소를 계산할 뿐 그 주소의 메모리를 읽지 않습니다.

421movl $(_bss - startup_32), %ecx

이미지 시작부터 BSS 전까지 복사할 바이트 수를 ecx에 넣습니다. BSS는 파일의 실제 초기 데이터 복사 범위에 포함하지 않습니다.

422shrl $3, %ecx

바이트 수를 8로 나누어 movsq가 복사할 8바이트 단위 개수를 만듭니다.

423std

DF를 켜서 반복 복사의 주소가 매번 감소하게 합니다. 입력과 출력이 겹치는 상황에서 아직 복사하지 않은 데이터를 보호합니다.

424rep movsq

DF를 세운 상태에서 높은 주소에서 낮은 주소 방향으로 compressed image를 복사한다. 아직 읽지 않은 source를 보호하는 핵심 구간이다.

425cld

역방향 복사가 끝났으므로 DF를 다시 지웁니다. 이후 C 코드가 기대하는 문자열 명령 방향을 복구합니다.

432leaq rva(gdt64)(%rbx), %rax

복사된 이미지 쪽 GDT descriptor의 주소를 rax에 구합니다. 원래 GDT는 뒤의 압축 해제 과정에서 덮일 수 있습니다. LEA는 유효 주소를 계산할 뿐 그 주소의 메모리를 읽지 않습니다.

433leaq rva(gdt)(%rbx), %rdx

새 복사본의 실제 GDT 주소를 rdx에 구합니다. descriptor가 가리킬 대상입니다. LEA는 유효 주소를 계산할 뿐 그 주소의 메모리를 읽지 않습니다.

434movq %rdx, 2(%rax)

새 descriptor의 base 필드에 새 GDT 주소를 기록합니다.

435lgdt (%rax)

GDTR를 복사된 GDT로 바꿉니다. 이후 원래 이미지 영역이 덮여도 세그먼트 표를 잃지 않도록 합니다. LGDT는 메모리의 descriptor에서 GDT의 base와 limit를 GDTR에 적재합니다. 표를 메모리에 만든 것만으로 GDTR가 자동 변경되지는 않습니다.

440leaq rva(.Lrelocated)(%rbx), %rax

재배치된 .Lrelocated의 주소를 rax에 구합니다. 바로 다음 간접 점프는 새 복사본에서 실행을 계속합니다. LEA는 유효 주소를 계산할 뿐 그 주소의 메모리를 읽지 않습니다.

441jmp *%rax

새 위치의 .Lrelocated로 비복귀 분기한다. GDT도 새 copy를 가리키도록 바꾼 뒤여야 한다.

442SYM_CODE_END(startup_64)

이 startup_64 코드 심벌의 범위를 끝냅니다. 실행 흐름은 앞의 점프로 이미 재배치 위치로 넘어갔습니다.

압축 해제된 vmlinux의 startup_64와 high virtual address 진입

두 번째 startup_64는 vmlinux의 실제 load offset을 page table에 반영하고 early_top_pgt를 CR3에 넣은 뒤 링크된 kernel virtual address로 이동한다.

원본: arch/x86/kernel/head_64.S 38-141줄

38SYM_CODE_START_NOALIGN(startup_64)
39	UNWIND_HINT_END_OF_STACK
40	/*
41	 * At this point the CPU runs in 64bit mode CS.L = 1 CS.D = 0,
42	 * and someone has loaded an identity mapped page table
43	 * for us.  These identity mapped page tables map all of the
44	 * kernel pages and possibly all of memory.
45	 *
46	 * %RSI holds the physical address of the boot_params structure
47	 * provided by the bootloader. Preserve it in %R15 so C function calls
48	 * will not clobber it.
49	 *
50	 * We come here either directly from a 64bit bootloader, or from
51	 * arch/x86/boot/compressed/head_64.S.
52	 *
53	 * We only come here initially at boot nothing else comes here.
54	 *
55	 * Since we may be loaded at an address different from what we were
56	 * compiled to run at we first fixup the physical addresses in our page
57	 * tables and then reload them.
58	 */
59	mov	%rsi, %r15
60 
61	/* Set up the stack for verify_cpu() */
62	leaq	__top_init_kernel_stack(%rip), %rsp
63 
64	/*
65	 * Set up GSBASE.
66	 * Note that on SMP the boot CPU uses the init data section until
67	 * the per-CPU areas are set up.
68	 */
69	movl	$MSR_GS_BASE, %ecx
70	xorl	%eax, %eax
71	xorl	%edx, %edx
72	wrmsr
73 
74	call	__pi_startup_64_setup_gdt_idt
75 
76	/* Now switch to __KERNEL_CS so IRET works reliably */
77	pushq	$__KERNEL_CS
78	leaq	.Lon_kernel_cs(%rip), %rax
79	pushq	%rax
80	lretq
81 
82.Lon_kernel_cs:
83	ANNOTATE_NOENDBR
84	UNWIND_HINT_END_OF_STACK
85 
86#ifdef CONFIG_AMD_MEM_ENCRYPT
87	/*
88	 * Activate SEV/SME memory encryption if supported/enabled. This needs to
89	 * be done now, since this also includes setup of the SEV-SNP CPUID table,
90	 * which needs to be done before any CPUID instructions are executed in
91	 * subsequent code. Pass the boot_params pointer as the first argument.
92	 */
93	movq	%r15, %rdi
94	call	__pi_sme_enable
95#endif
96 
97	/* Sanitize CPU configuration */
98	call verify_cpu
99 
100	/*
101	 * Derive the kernel's physical-to-virtual offset from the physical and
102	 * virtual addresses of common_startup_64().
103	 */
104	leaq	common_startup_64(%rip), %rdi
105	subq	.Lcommon_startup_64(%rip), %rdi
106 
107	/*
108	 * Perform pagetable fixups. Additionally, if SME is active, encrypt
109	 * the kernel and retrieve the modifier (SME encryption mask if SME
110	 * is active) to be added to the initial pgdir entry that will be
111	 * programmed into CR3.
112	 */
113	movq	%r15, %rsi
114	call	__pi___startup_64
115 
116	/* Form the CR3 value being sure to include the CR3 modifier */
117	leaq	early_top_pgt(%rip), %rcx
118	addq	%rcx, %rax
119 
120#ifdef CONFIG_AMD_MEM_ENCRYPT
121	mov	%rax, %rdi
122 
123	/*
124	 * For SEV guests: Verify that the C-bit is correct. A malicious
125	 * hypervisor could lie about the C-bit position to perform a ROP
126	 * attack on the guest by writing to the unencrypted stack and wait for
127	 * the next RET instruction.
128	 */
129	call	sev_verify_cbit
130#endif
131 
132	/*
133	 * Switch to early_top_pgt which still has the identity mappings
134	 * present.
135	 */
136	movq	%rax, %cr3
137 
138	/* Branch to the common startup code at its kernel virtual address */
139	ANNOTATE_RETPOLINE_SAFE
140	jmp	*.Lcommon_startup_64(%rip)
141SYM_CODE_END(startup_64)

주요 구문 해설

38SYM_CODE_START_NOALIGN(startup_64)

압축이 풀린 커널 본체의 startup_64를 정의합니다. 압축 해제기 안의 같은 이름 함수와 구분해야 하며, 이 시점에는 이미 64비트 모드와 초기 identity mapping이 준비되어 있습니다. 이 표기는 빌드 도구에 코드 심벌과 정렬을 알려 주며 그 자체가 런타임 함수 호출은 아닙니다.

39UNWIND_HINT_END_OF_STACK

스택 추적 도구에 이 부팅 진입점 아래로 일반 호출 프레임이 이어지지 않는다고 알립니다. 실행 중 CPU 레지스터를 바꾸는 명령은 아닙니다.

59mov %rsi, %r15

물리 주소인 boot_params를 C 호출 사이에 보존한다.

62leaq __top_init_kernel_stack(%rip), %rsp

검증 C 함수를 호출할 수 있도록 boot CPU의 초기 stack top을 선택한다.

69movl $MSR_GS_BASE, %ecx

GS base를 설정하는 MSR의 번호를 ecx에 넣습니다. wrmsr는 ecx로 대상 MSR을 선택합니다.

70xorl %eax, %eax

MSR에 쓸 64비트 값의 하위 절반 eax를 0으로 만듭니다.

71xorl %edx, %edx

MSR에 쓸 값의 상위 절반 edx도 0으로 만듭니다. 따라서 EDX:EAX 전체 값이 0이 됩니다.

72wrmsr

GS base를 0으로 설정합니다. 정상 per-CPU 영역을 마련하기 전 초기 커널 데이터 사용에 맞춘 상태입니다.

74call __pi_startup_64_setup_gdt_idt

초기 GDT와 IDT를 준비하는 위치 독립 초기화 함수를 호출합니다. 커널의 최종 가상 주소에서 아직 실행하고 있지 않기 때문에 __pi_ 경로를 사용합니다.

77pushq $__KERNEL_CS

현재 GDT의 커널 코드 세그먼트 선택자를 스택에 넣습니다. 코드 세그먼트와 GDT의 관계를 확실히 맞춥니다. PUSHQ는 rsp를 8바이트 낮춘 뒤 새 스택 위치에 피연산자 값을 기록합니다.

78leaq .Lon_kernel_cs(%rip), %rax

CS를 바꾼 뒤 이어갈 .Lon_kernel_cs의 현재 주소를 rax에 넣습니다. LEA는 유효 주소를 계산할 뿐 그 주소의 메모리를 읽지 않습니다.

79pushq %rax

far return이 읽을 목적지 RIP를 스택에 넣습니다. 그 아래에는 앞서 넣은 CS 선택자가 있습니다. PUSHQ는 rsp를 8바이트 낮춘 뒤 새 스택 위치에 피연산자 값을 기록합니다.

80lretq

준비한 RIP·CS를 읽어 커널 코드 세그먼트로 실행을 이어갑니다. 일반 사용자 모드 복귀나 인터럽트 복귀는 아닙니다.

82.Lon_kernel_cs:

코드 세그먼트 전환이 끝난 위치입니다. 이후 암호화 기능과 페이지 테이블을 준비합니다. 레이블은 현재 위치를 가리키는 이름으로, 별도의 CPU 명령을 추가하지 않습니다.

83ANNOTATE_NOENDBR

이 분기 목적지에 ENDBR 명령이 없어도 되는 위치임을 objtool에 알립니다. 명령을 실행하여 분기 보호를 끄는 동작은 아닙니다.

84UNWIND_HINT_END_OF_STACK

스택 추적 메타데이터에 부팅 스택의 경계를 다시 표시합니다. far return으로 도착했으므로 일반 호출자가 있다고 추정하지 않게 합니다.

86#ifdef CONFIG_AMD_MEM_ENCRYPT

AMD SME/SEV 지원 빌드는 boot_params를 전달해 암호화 사용 상태와 필요한 SEV-SNP CPUID 표를 먼저 준비합니다. 이후 초기화에서 CPUID를 사용할 수 있으므로 그보다 앞에 놓은 호출이며, 빌드 지원만으로 암호화가 무조건 활성화되지는 않습니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.

93movq %r15, %rdi

보존해 둔 부팅 정보 포인터를 첫 인자 rdi에 넣습니다. 아래 메모리 암호화 초기화 함수에 전달합니다.

94call __pi_sme_enable

지원과 설정 조건에 따라 SME·SEV 초기 상태를 준비합니다. SEV-SNP의 CPUID 테이블도 이후 CPUID 명령보다 먼저 사용할 수 있어야 합니다.

95#endif

86행에서 시작한 선택 구간을 마칩니다. AMD SME/SEV 지원 빌드는 boot_params를 전달해 암호화 사용 상태와 필요한 SEV-SNP CPUID 표를 먼저 준비합니다. 이후 초기화에서 CPUID를 사용할 수 있으므로 그보다 앞에 놓은 호출이며, 빌드 지원만으로 암호화가 무조건 활성화되지는 않습니다.

98call verify_cpu

커널 실행에 필요한 CPU 기능과 제어 상태를 검사·정리합니다. 초기 페이지 테이블 전환에 앞서 CPU 환경을 맞추는 단계입니다.

104leaq common_startup_64(%rip), %rdi

common_startup_64의 현재 실행 위치 기준 주소를 rdi에 구합니다. 다음 줄에서 링크상의 가상 주소를 빼 물리·가상 배치 차이를 계산합니다. LEA는 유효 주소를 계산할 뿐 그 주소의 메모리를 읽지 않습니다.

105subq .Lcommon_startup_64(%rip), %rdi

runtime RIP와 link-time virtual address 차이로 물리-가상 offset을 계산한다.

113movq %r15, %rsi

페이지 테이블 보정 함수의 두 번째 인자로 부팅 정보 포인터를 준비합니다. 첫 번째 인자 rdi에는 앞서 계산한 주소 차이가 있습니다.

114call __pi___startup_64

position-independent C helper가 초기 page table entry를 실제 load address에 맞게 고친다.

117leaq early_top_pgt(%rip), %rcx

초기 최상위 페이지 테이블 early_top_pgt의 현재 주소를 rcx에 구합니다. 현재 identity mapping으로 접근할 수 있는 물리 기준 주소입니다. LEA는 유효 주소를 계산할 뿐 그 주소의 메모리를 읽지 않습니다.

118addq %rcx, %rax

초기화 함수가 돌려준 CR3 보정값에 페이지 테이블 주소를 더합니다. SME가 활성화되어 있으면 암호화 비트도 최종 CR3 값에 포함됩니다.

120#ifdef CONFIG_AMD_MEM_ENCRYPT

AMD 암호화 지원 경로는 SEV 게스트에서 사용할 페이지 표의 C-bit 위치가 올바른지 확인합니다. 잘못된 암호화 비트 때문에 스택을 평문으로 노출하는 등의 문제를 막기 위한 검증이며, 일반 비암호화 빌드에는 없습니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.

121mov %rax, %rdi

설정하려는 CR3 값을 첫 인자로 복사합니다. 다음 함수가 SEV 암호화 C-bit의 유효성을 검사할 때 사용합니다.

129call sev_verify_cbit

SEV 환경에서 암호화 C-bit 위치가 올바른지 검사합니다. 잘못된 비트로 스택이나 페이지 테이블을 해석하는 일을 방지한 뒤 새 CR3를 적용합니다.

130#endif

120행에서 시작한 선택 구간을 마칩니다. AMD 암호화 지원 경로는 SEV 게스트에서 사용할 페이지 표의 C-bit 위치가 올바른지 확인합니다. 잘못된 암호화 비트 때문에 스택을 평문으로 노출하는 등의 문제를 막기 위한 검증이며, 일반 비암호화 빌드에는 없습니다.

136movq %rax, %cr3

고쳐진 early_top_pgt를 활성화한다. 아직 identity mapping을 유지하므로 바로 다음 fetch가 끊기지 않는다.

139ANNOTATE_RETPOLINE_SAFE

다음 간접 점프가 허용된 부팅 코드 전환임을 objtool에 표시합니다. 이 표시는 실제 점프 목적지를 바꾸거나 CPU 상태를 복원하지 않습니다.

140jmp *.Lcommon_startup_64(%rip)

이제 link-time kernel virtual address를 사용해 공통 시작 코드로 이동한다.

141SYM_CODE_END(startup_64)

커널 본체의 startup_64 코드 범위를 끝냅니다. 앞의 점프를 통해 높은 커널 가상 주소의 공통 시작 코드로 진행합니다.

extract_kernel이 실제 출력 크기와 KASLR 목적지를 확정하는 구간

assembly에서 계산한 임시 위치는 최종 kernel 위치가 아니다. extract_kernel()이 boot parameter를 보존하고 필요한 전체 범위를 PMD 크기로 올림한 뒤 KASLR 물리·가상 목적지를 선택하고 정렬을 다시 검증한다.

원본: arch/x86/boot/compressed/misc.c 407-499줄

407asmlinkage __visible void *extract_kernel(void *rmode, unsigned char *output)
408{
409	unsigned long virt_addr = LOAD_PHYSICAL_ADDR;
410	memptr heap = (memptr)boot_heap;
411	unsigned long needed_size;
412	size_t entry_offset;
413 
414	/* Retain x86 boot parameters pointer passed from startup_32/64. */
415	boot_params_ptr = rmode;
416 
417	/* Clear flags intended for solely in-kernel use. */
418	boot_params_ptr->hdr.loadflags &= ~KASLR_FLAG;
419 
420	parse_mem_encrypt(&boot_params_ptr->hdr);
421 
422	sanitize_boot_params(boot_params_ptr);
423 
424	if (boot_params_ptr->screen_info.orig_video_mode == 7) {
425		vidmem = (char *) 0xb0000;
426		vidport = 0x3b4;
427	} else {
428		vidmem = (char *) 0xb8000;
429		vidport = 0x3d4;
430	}
431 
432	lines = boot_params_ptr->screen_info.orig_video_lines;
433	cols = boot_params_ptr->screen_info.orig_video_cols;
434 
435	init_default_io_ops();
436 
437	/*
438	 * Detect TDX guest environment.
439	 *
440	 * It has to be done before console_init() in order to use
441	 * paravirtualized port I/O operations if needed.
442	 */
443	early_tdx_detect();
444 
445	early_sev_detect();
446 
447	console_init();
448 
449	/*
450	 * Save RSDP address for later use. Have this after console_init()
451	 * so that early debugging output from the RSDP parsing code can be
452	 * collected.
453	 */
454	boot_params_ptr->acpi_rsdp_addr = get_rsdp_addr();
455 
456	debug_putstr("early console in extract_kernel\n");
457 
458	free_mem_ptr     = heap;	/* Heap */
459	free_mem_end_ptr = heap + BOOT_HEAP_SIZE;
460 
461	/*
462	 * The memory hole needed for the kernel is the larger of either
463	 * the entire decompressed kernel plus relocation table, or the
464	 * entire decompressed kernel plus .bss and .brk sections.
465	 *
466	 * On X86_64, the memory is mapped with PMD pages. Round the
467	 * size up so that the full extent of PMD pages mapped is
468	 * included in the check against the valid memory table
469	 * entries. This ensures the full mapped area is usable RAM
470	 * and doesn't include any reserved areas.
471	 */
472	needed_size = max_t(unsigned long, output_len, kernel_total_size);
473#ifdef CONFIG_X86_64
474	needed_size = ALIGN(needed_size, MIN_KERNEL_ALIGN);
475#endif
476 
477	/* Report initial kernel position details. */
478	debug_putaddr(input_data);
479	debug_putaddr(input_len);
480	debug_putaddr(output);
481	debug_putaddr(output_len);
482	debug_putaddr(kernel_total_size);
483	debug_putaddr(needed_size);
484 
485#ifdef CONFIG_X86_64
486	/* Report address of 32-bit trampoline */
487	debug_putaddr(trampoline_32bit);
488#endif
489 
490	choose_random_location((unsigned long)input_data, input_len,
491				(unsigned long *)&output,
492				needed_size,
493				&virt_addr);
494 
495	/* Validate memory location choices. */
496	if ((unsigned long)output & (MIN_KERNEL_ALIGN - 1))
497		error("Destination physical address inappropriately aligned");
498	if (virt_addr & (MIN_KERNEL_ALIGN - 1))
499		error("Destination virtual address inappropriately aligned");

주요 구문 해설

407asmlinkage __visible void *extract_kernel(void *rmode, unsigned char *output)

부팅 정보와 압축 해제 목적지를 받아 커널을 추출하는 함수입니다. 초기 출력·메모리 배치를 준비하고, 마지막에는 추출된 커널 진입 주소를 돌려줍니다.

409unsigned long virt_addr = LOAD_PHYSICAL_ADDR;

가상 배치 오프셋의 초기값을 기본 적재 주소로 둡니다. KASLR 위치 선택 과정에서 이 값이 바뀔 수 있습니다.

410memptr heap = (memptr)boot_heap;

압축 해제기의 임시 동적 메모리 영역 boot_heap 시작 주소를 heap에 담습니다. 정상 커널 메모리 할당기가 생기기 전 사용하는 공간입니다.

411unsigned long needed_size;

압축 해제 결과와 커널의 BSS·초기 확장 영역 등을 수용하는 데 필요한 메모리 크기를 담을 변수를 선언합니다.

412size_t entry_offset;

추출한 이미지 안에서 실행 진입점까지의 오프셋을 담을 변수를 선언합니다. 함수 끝에서 실제 진입 주소를 계산할 때 사용합니다.

415boot_params_ptr = rmode;

assembly가 %rsi로 보존해 온 zero-page pointer의 소유권을 decompressor C 코드가 넘겨받는다. 이 4KiB가 출력 범위와 겹치면 뒤의 memory map과 command line 해석이 동시에 무너진다.

418boot_params_ptr->hdr.loadflags &= ~KASLR_FLAG;

부트로더가 남긴 loadflags에서 KASLR 상태 비트를 지웁니다. 이 부팅에서 실제로 무작위 배치를 했는지는 이후 커널 코드가 다시 기록합니다.

420parse_mem_encrypt(&boot_params_ptr->hdr);

명령줄의 mem_encrypt=on과 mem_encrypt=off 위치를 비교합니다. on이 더 뒤에 있으면 부팅 헤더의 XLF_MEM_ENCRYPTION 비트를 켜서 메모리 암호화 요청을 기록합니다.

422sanitize_boot_params(boot_params_ptr);

부트로더가 채운 boot_params를 정리합니다. 프로토콜상 유효하지 않거나 초기화되지 않았을 수 있는 필드가 뒤의 처리에 잘못 쓰이지 않게 합니다.

424if (boot_params_ptr->screen_info.orig_video_mode == 7) {

로더가 보고한 화면 모드가 흑백 텍스트 모드 7인지 검사합니다. 결과에 따라 화면 메모리 주소와 제어 포트가 달라집니다.

425vidmem = (char *) 0xb0000;

흑백 텍스트 화면의 메모리 주소 0xb0000을 선택합니다. 초기 콘솔이 문자와 속성을 기록할 위치입니다.

426vidport = 0x3b4;

흑백 화면 제어에 사용하는 포트 0x3b4를 선택합니다.

427} else {

화면 모드가 흑백 텍스트 모드가 아닌 경우 사용하는 초기 출력 경로입니다.

428vidmem = (char *) 0xb8000;

컬러 텍스트 화면 메모리 주소 0xb8000을 선택합니다.

429vidport = 0x3d4;

컬러 텍스트 화면의 제어 포트 0x3d4를 선택합니다.

432lines = boot_params_ptr->screen_info.orig_video_lines;

부팅 정보에 있는 화면의 행 수를 저장합니다. 초기 텍스트 출력의 줄 이동과 화면 범위를 계산할 때 사용합니다.

433cols = boot_params_ptr->screen_info.orig_video_cols;

화면의 열 수를 저장합니다. 텍스트 화면에서 다음 문자와 줄의 위치를 계산하는 기준입니다.

435init_default_io_ops();

초기 포트 I/O 연산을 기본 구현으로 설정합니다. 뒤의 기밀 VM 감지 결과에 따라 특수한 I/O 구현으로 바뀔 수 있습니다.

443early_tdx_detect();

Intel TDX 게스트인지 초기 감지를 수행합니다. 해당 환경에서는 직접 포트 I/O 대신 게스트용 경로가 필요할 수 있어 콘솔 초기화 전에 처리합니다.

445early_sev_detect();

SEV-ES 상태이면 화면 행·열 수를 0으로 만들어 비디오 메모리 출력을 막습니다. 이 단계의 #VC 핸들러가 화면 MMIO를 처리하지 못하므로, 특히 kexec가 화면 정보를 넘긴 경우 게스트 종료를 피하기 위한 처리입니다.

447console_init();

압축 해제 단계에서 메시지를 출력할 초기 콘솔을 준비합니다. 정상 커널의 콘솔 드라이버가 완성되기 전 사용하는 출력입니다.

454boot_params_ptr->acpi_rsdp_addr = get_rsdp_addr();

ACPI 테이블의 출발점인 RSDP 주소를 찾아 부팅 정보에 저장합니다. 콘솔 이후에 수행하므로 검색 중의 초기 디버그 출력도 볼 수 있습니다.

456debug_putstr("early console in extract_kernel\n");

압축 해제 함수의 초기 콘솔 단계에 도달했다는 디버그 문자열을 출력합니다. 메모리를 할당하거나 페이지 테이블을 바꾸는 함수는 아닙니다.

458free_mem_ptr = heap; /* Heap */

임시 힙에서 다음 할당을 시작할 위치를 힙 시작 주소로 설정합니다.

459free_mem_end_ptr = heap + BOOT_HEAP_SIZE;

힙 시작 주소에 정해진 힙 크기를 더해 사용 가능한 끝 경계를 설정합니다. 할당기는 이 범위를 넘겨 사용할 수 없습니다.

472needed_size = max_t(unsigned long, output_len, kernel_total_size);

압축 해제된 ELF byte 수만 보지 않고 relocation table, BSS와 brk까지 포함한 두 후보 중 큰 값을 선택한다. KASLR hole 검사는 이 전체 수명을 수용해야 한다.

473#ifdef CONFIG_X86_64

64비트 커널의 압축 해제 공간을 MIN_KERNEL_ALIGN 배수로 올립니다. 초기 큰 페이지 매핑의 정렬에 맞는 전체 크기를 확보하려는 계산이며 32비트 경로에는 이 추가 정렬이 없습니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.

474needed_size = ALIGN(needed_size, MIN_KERNEL_ALIGN);

x86-64 early mapping이 PMD 단위이므로 마지막 byte가 속한 2MiB frame까지 usable RAM인지 검사하기 위해 범위를 올림한다.

475#endif

473행에서 시작한 선택 구간을 마칩니다. 64비트 커널의 압축 해제 공간을 MIN_KERNEL_ALIGN 배수로 올립니다. 초기 큰 페이지 매핑의 정렬에 맞는 전체 크기를 확보하려는 계산이며 32비트 경로에는 이 추가 정렬이 없습니다.

478debug_putaddr(input_data);

압축 입력 데이터의 시작 주소를 디버그 출력합니다. 출력 목적지와의 겹침을 살필 때 필요한 값입니다.

479debug_putaddr(input_len);

압축 입력의 길이를 디버그 출력합니다. 함수 이름에 addr가 있지만 여기서는 크기 값을 출력합니다.

480debug_putaddr(output);

압축 해제 결과를 기록할 현재 목적지 주소를 디버그 출력합니다.

481debug_putaddr(output_len);

압축 해제 결과의 바이트 크기를 디버그 출력합니다.

482debug_putaddr(kernel_total_size);

BSS와 초기 확장 영역 등을 포함하여 커널 전체에 필요한 크기를 디버그 출력합니다.

483debug_putaddr(needed_size);

정렬 등을 반영한 실제 메모리 확보 크기 needed_size를 디버그 출력합니다.

485#ifdef CONFIG_X86_64

64비트 부팅에서 사용한 32비트 전환 trampoline의 주소를 디버그 출력에 포함합니다. 주소를 출력하는 진단 코드이고 이 줄이 trampoline을 실행하는 것은 아닙니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.

487debug_putaddr(trampoline_32bit);

4단계와 5단계 페이징을 전환할 때 쓰는 32비트 trampoline 주소를 출력합니다. 모드 전환용 코드가 낮은 주소에 제대로 놓였는지 확인하는 데 쓰입니다.

488#endif

485행에서 시작한 선택 구간을 마칩니다. 64비트 부팅에서 사용한 32비트 전환 trampoline의 주소를 디버그 출력에 포함합니다. 주소를 출력하는 진단 코드이고 이 줄이 trampoline을 실행하는 것은 아닙니다.

490choose_random_location((unsigned long)input_data, input_len,

입력 payload 범위와 필요한 출력 크기를 함께 넘겨 물리 destination과 kernel virtual address를 고른다. 반환된 두 주소는 같은 값일 필요가 없다.

491(unsigned long *)&output,

목적지 포인터 변수 자체의 주소를 위치 선택 함수에 넘깁니다. 함수가 새 물리 목적지를 골랐을 때 output 값을 바꿀 수 있게 합니다.

492needed_size,

선택할 위치에서 확보해야 하는 메모리 크기를 인자로 넘깁니다. 시작 주소 한 점만 비어 있는 것이 아니라 이 전체 범위가 사용 가능해야 합니다.

493&virt_addr);

가상 배치 값의 주소를 넘깁니다. 물리 목적지와 별도로 KASLR의 가상 배치 결과도 호출자에게 돌려받습니다.

496if ((unsigned long)output & (MIN_KERNEL_ALIGN - 1))

물리 목적지의 하위 비트에 값이 남아 있는지 검사하여 MIN_KERNEL_ALIGN 정렬 위반을 찾습니다. 위반하면 다음 줄에서 부팅 오류로 중단합니다.

497error("Destination physical address inappropriately aligned");

선택 결과를 신뢰하지 않고 assembly contract와 같은 MIN_KERNEL_ALIGN 제약을 C 경계에서 다시 검증한다.

498if (virt_addr & (MIN_KERNEL_ALIGN - 1))

선택한 가상 배치도 커널 최소 정렬 단위를 만족하는지 검사합니다. 물리 주소 정렬만 맞아서는 충분하지 않습니다.

499error("Destination virtual address inappropriately aligned");

가상 배치가 정렬 조건에 맞지 않으면 원인을 출력하고 부팅을 중단합니다. 잘못된 위치로 압축 해제를 계속하지 않습니다.

숫자를 넣어 끝까지 계산한다

01

compressed image를 16MiB에 적재한 경우의 주소 계산

예를 들어 compressed image runtime base가 0x01000000, link 기준 startup_32 offset이 0x200, 필요한 출력 범위가 0x01734000이라고 가정한다.

  1. runtime basecall/pop으로 얻은 다음 RIP에서 link-time label offset을 빼 0x01000000을 복원한다.
  2. output holeALIGN(0x01734000, 0x00200000) = 0x01800000, 즉 24MiB를 usable RAM으로 확보해야 한다.
  3. destination후보 0x02000000은 2MiB 정렬이고 끝 주소는 0x03800000이다.
  4. source protectioncompressed source 끝과 [0x02000000, 0x03800000)이 겹치면 source를 buffer 상단으로 역복사한 뒤 해제한다.

판정GDB에는 %rbp=destination, %rbx=decompression stack/buffer 기준, %r15=boot_params와 destination end를 함께 기록한다. 시작 주소 하나만 맞는 것으로는 overlap 안전성을 증명할 수 없다.

02

CR3에 넣는 값과 high virtual jump를 따로 검산한다

early page-table root의 physical address가 0x0019a000이면 x86 CR3의 base field는 그대로 0x0019a000이다. 하위 bit에는 PCID 또는 control flag만 들어간다.

  1. 전환 전 RIP현재 RIP가 0x03801234라면 새 root에도 이 물리 범위를 identity-map한 PTE가 있어야 한다.
  2. 전환 전 RSPRSP가 0x037ff000이면 같은 이유로 stack page도 새 root에서 접근 가능해야 한다.
  3. high alias링크 VA ffffffff81001234가 같은 physical text frame을 가리켜야 high-address jump가 성공한다.
  4. root self accesspage-table walk가 사용하는 0x0019a000과 하위 table page들은 usable RAM이며 덮어쓰이지 않아야 한다.

판정mov %rax,%cr3 직후 fault는 instruction page만 볼 문제가 아니다. RIP, RSP, page-table page 세 범위를 동시에 walk해야 한다.

코드에서 놓치기 쉬운 경계

01

boot_params는 단순 인자가 아니라 부트 프로토콜의 기록지다

struct boot_params에는 memory map, command line, initrd, loader type, kernel alignment가 들어 있다. %esi/%rsi 자체만 보존해도 그 pointer가 가리키는 zero page가 decompression destination이나 scratch에 덮이면 같은 결과가 난다. 주소와 함께 4KiB 범위를 memory map에 표시해야 한다.

32비트 entry에서는 zero page와 command line이 4GiB 아래에 있어야 하지만 64비트 direct entry는 그 제약이 완화된다. 같은 bzImage라도 loader가 어느 entry를 사용했는지에 따라 유효한 배치가 달라진다.

02

RIP-relative와 rva macro의 역할을 구분한다

compressed kernel은 PIE로 link될 수 있으므로 symbol reference가 불필요한 runtime relocation을 만들지 않게 rva(X)로 startup_32 기준 offset을 만든다. register에는 현재 runtime base를 넣고 instruction에는 offset만 남기는 방식이다.

disassembly에서 operand 숫자만 보지 말고 relocation section도 같이 본다. source의 rva()가 최종 binary에서 base+offset addressing으로 굳었는지 확인해야 한다.

03

KASLR 위치 선택과 decompression 안전성은 다른 문제다

KASLR은 실행 물리 위치 후보를 고르는 정책이고, source를 buffer 끝으로 옮기는 코드는 chosen destination에서 in-place decompression이 source를 덮지 않게 하는 안전 조건이다. KASLR을 꺼도 후자의 overlap 계산은 남는다.

destination은 alignment와 minimum load address를 만족해야 하고 init_size 전체가 RAM, reserved range, initrd와 겹치지 않아야 한다.

04

4-level과 5-level paging 변경은 CR4 bit 하나로 끝나지 않는다

long mode가 켜진 상태에서 CR4.LA57을 직접 바꾸면 #GP가 발생한다. 그래서 lower memory trampoline, 32비트로 표현 가능한 CR3, 임시 GDT가 필요하다. 이 구간에서 page fault가 나면 일반 kernel IDT와 console을 기대할 수 없다.

QEMU에서 -d int,cpu_reset을 사용하거나 GDB hardware breakpoint로 trampoline 전후 CR0, CR3, CR4, EFER를 기록하면 mode 전환을 확인할 수 있다.

05

identity map은 임시 다리이며 최종 kernel map과 같지 않다

entry 직후에는 현재 물리 RIP를 그대로 fetch하는 identity mapping과 링크된 high virtual address mapping이 겹쳐 존재한다. CR3를 바꾸고 high address로 분기한 뒤 identity mapping은 더 이상 일반 실행의 기반이 아니다.

초기 page table을 디버깅할 때 virtual address만 출력하지 말고 PTE가 가리키는 physical frame과 encryption C-bit까지 같이 적는다.

06

초기 fault를 마지막 출력 문자열로 판단하지 않는다

serial driver와 console이 완성되기 전에는 fault가 아무 문자열도 남기지 않을 수 있다. POST code, port 0x80, early debug register, QEMU trace, GDB로 RIP와 control register를 잡는 편이 더 직접적이다.

가장 먼저 비교할 checkpoint는 startup_32, compressed startup_64, .Lrelocated, vmlinux startup_64, common_startup_64다.

멈춘 위치보다 먼저 볼 값

조건관찰되는 증상첫 확인 값
boot_params 훼손memory map이나 command line이 무작위 값으로 보이거나 decompressor가 잘못된 크기를 사용한다.%rsi와 zero-page 4KiB 범위
destination/source overlapextract 중 정지하거나 압축 해제 뒤 instruction stream이 손상된다.%rbp, %rbx, _bss, init_size
잘못된 paging levelconfigure_5level_paging 또는 직후 #GP/triple fault가 발생한다.CR4.LA57, CR3, trampoline 주소
early page table fixup 오류CR3 write 직후 또는 high-address jump에서 무출력 reset이 난다.RIP/SP의 identity와 high mapping

직접 확인할 실험

  1. 01
    QEMU -S -s로 멈춘 뒤 startup_32, 두 startup_64, common_startup_64에 breakpoint를 건다.

    명령을 실행한 시점의 PC, stack, address-space root와 CPU/hart 번호를 함께 기록한다.

  2. 02
    각 checkpoint에서 CR0, CR3, CR4, EFER, RIP, RSP, RSI/R15를 기록해 표로 비교한다.

    명령을 실행한 시점의 PC, stack, address-space root와 CPU/hart 번호를 함께 기록한다.

  3. 03
    objdump -dr arch/x86/boot/compressed/vmlinux에서 rva() reference와 runtime relocation 유무를 확인한다.

    명령을 실행한 시점의 PC, stack, address-space root와 CPU/hart 번호를 함께 기록한다.

  4. 04
    readelf -lW vmlinux와 /proc/iomem을 대조해 실제 load range와 kernel text range를 확인한다.

    명령을 실행한 시점의 PC, stack, address-space root와 CPU/hart 번호를 함께 기록한다.

기준 원문

↑