QUESTION
부트로더가 넘긴 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 문자열보다 이 세 값을 먼저 기록하면 초기 무출력 정지를 좁힐 수 있다.
ENTRY CONTRACT
들어오는 상태와 내보내는 상태
| 경계 | 입력 | 이 경계가 완성하는 상태 |
|---|---|---|
| 32-bit entry | protected mode, %esi=boot_params | GDT, PAE, temporary page table, long mode |
| compressed 64-bit entry | long mode, identity map, %rsi=boot_params | decompress destination과 4/5-level paging |
| vmlinux startup_64 | 압축 해제된 image, identity map | early_top_pgt와 kernel virtual address |
| C entry | 정상 stack, 초기 page table, 보존된 boot_params | x86_64_start_kernel() |
ARCHITECTURE FIGURES
주소와 register를 먼저 그려 본다
bzImage / compressed
decompressed vmlinux
압축 image의 entry와 압축 해제된 vmlinux entry를 주소 공간 경계로 분리한다.
source를 buffer 끝으로 옮기는 이유는 출력이 커지면서 아직 읽지 않은 압축 입력을 덮는 일을 막기 위해서다.
CR3 write 한 줄의 전후에서 PC, SP와 page-table page 자체가 모두 접근 가능해야 한다.
SOURCE 01
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주요 구문 해설
SYM_FUNC_START(startup_32)압축 커널의 32비트 진입 함수 startup_32를 정의합니다. 부트로더는 정해진 이미지 오프셋으로 이곳에 들어오므로, 일반 C 함수 호출로 시작하는 함수와 다릅니다.
* 32bit entry is 0 and it is ABI so immutable!offset 0은 Linux x86 boot protocol의 고정 ABI다. 코드를 앞에 삽입해 이 위치를 바꾸면 부트로더는 다른 byte를 entry로 실행한다.
cld방향 플래그 DF를 지웁니다. 이후 문자열 복사 명령이 낮은 주소에서 높은 주소로 진행하도록 부트로더가 남긴 상태를 정리합니다.
cli마스크 가능한 인터럽트를 끕니다. 커널의 예외·인터럽트 처리 환경을 아직 준비하지 않은 초기 부팅 구간입니다.
leal (BP_scratch+4)(%esi), %espboot_params의 scratch 영역을 4-byte 임시 stack으로 사용한다. 정상 kernel stack이 아직 없다는 뜻이다.
call 1f다음 instruction 주소를 stack에 넣어 현재 load base를 역산한다. 32비트 모드에서는 일반 RIP-relative addressing을 쓸 수 없기 때문이다.
1: popl %ebp앞의 call이 임시 스택에 넣은 현재 실행 주소를 ebp로 꺼냅니다. 32비트 코드에서 자신의 실제 적재 위치를 알아내는 방법입니다.
subl $ rva(1b), %ebp실행 주소에서 이 레이블의 이미지 내부 오프셋을 빼 압축 이미지의 실제 시작 주소를 구합니다. 이후 심벌 주소를 계산하는 기준이 됩니다.
leal rva(gdt)(%ebp), %eax이미지 시작 주소 ebp에 GDT의 이미지 내부 오프셋을 더해 현재 메모리의 GDT 주소를 만듭니다. LEA는 유효 주소를 계산할 뿐 그 주소의 메모리를 읽지 않습니다.
movl %eax, 2(%eax)GDT를 설명하는 구조의 base 필드에 실제 GDT 주소를 기록합니다. 앞의 2바이트는 limit이므로 base는 주소 +2에서 시작합니다.
lgdt (%eax)준비한 GDT 주소와 크기를 GDTR에 읽습니다. 이후 세그먼트 선택자는 이 새 표를 기준으로 해석됩니다. LGDT는 메모리의 descriptor에서 GDT의 base와 limit를 GDTR에 적재합니다. 표를 메모리에 만든 것만으로 GDTR가 자동 변경되지는 않습니다.
movl $__BOOT_DS, %eax부팅용 데이터 세그먼트 선택자 __BOOT_DS를 eax에 준비합니다. 주소가 아니라 GDT 항목을 선택하는 값입니다.
movl %eax, %ds데이터 세그먼트 DS를 부팅용 GDT 항목으로 바꿉니다. 로더가 남긴 세그먼트 상태에 의존하지 않도록 합니다.
movl %eax, %esES도 부팅용 데이터 세그먼트로 바꿉니다. 문자열 명령에서 사용하는 데이터 세그먼트를 맞추는 작업입니다.
movl %eax, %fsFS에 부팅용 데이터 세그먼트 선택자를 넣습니다. 여기서는 사용자 TLS를 준비하는 단계가 아닙니다.
movl %eax, %gsGS에 부팅용 데이터 세그먼트 선택자를 넣어 초기 세그먼트 환경을 정리합니다.
movl %eax, %ss스택 세그먼트 SS를 부팅용 데이터 세그먼트로 바꿉니다. 아래에서 새 스택 포인터를 준비합니다.
leal rva(boot_stack_end)(%ebp), %esp압축 해제기 내부 부팅 스택의 상단 주소를 esp에 넣습니다. 주소 계산에 실제 이미지 시작점 ebp를 사용합니다. LEA는 유효 주소를 계산할 뿐 그 주소의 메모리를 읽지 않습니다.
pushl $__KERNEL32_CS다음 far return이 사용할 32비트 커널 코드 세그먼트 선택자를 스택에 넣습니다. PUSHL는 esp를 4바이트 낮춘 뒤 새 스택 위치에 피연산자 값을 기록합니다.
leal rva(1f)(%ebp), %eax새 CS에서 실행할 바로 다음 레이블의 실제 주소를 eax에 만듭니다. 이미지 내부 오프셋에 적재 기준 주소를 더한 값입니다. LEA는 유효 주소를 계산할 뿐 그 주소의 메모리를 읽지 않습니다.
pushl %eaxfar return의 목적지 명령 주소를 스택에 넣습니다. 앞서 넣은 CS와 함께 하나의 전환 정보를 이룹니다. PUSHL는 esp를 4바이트 낮춘 뒤 새 스택 위치에 피연산자 값을 기록합니다.
lretl스택에서 EIP와 CS를 읽어 새 GDT의 32비트 코드 세그먼트로 계속 실행합니다. 여기서 곧바로 64비트 모드가 되는 것은 아닙니다.
1:CS를 새 GDT 기준으로 바꾼 뒤 실행을 이어가는 위치입니다. 다음 단계에서 초기 예외 처리와 CPU 기능을 확인합니다. 레이블은 현재 위치를 가리키는 이름으로, 별도의 CPU 명령을 추가하지 않습니다.
#ifdef CONFIG_AMD_MEM_ENCRYPTAMD 메모리 암호화 지원 빌드는 SEV-ES 환경에서 필요한 예외를 처리할 초기 IDT를 준비합니다. 암호화된 게스트의 CPU 기능 조회 등이 #VC를 일으킬 수 있어 후속 기능 검사보다 먼저 처리기를 갖춰야 합니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
call startup32_load_idtSEV-ES 환경의 초기 예외를 처리할 32비트 IDT를 준비합니다. 이후 CPU 기능 확인 과정에서 발생할 수 있는 #VC 예외를 받을 수 있게 합니다.
#endif128행에서 시작한 선택 구간을 마칩니다. AMD 메모리 암호화 지원 빌드는 SEV-ES 환경에서 필요한 예외를 처리할 초기 IDT를 준비합니다. 암호화된 게스트의 CPU 기능 조회 등이 #VC를 일으킬 수 있어 후속 기능 검사보다 먼저 처리기를 갖춰야 합니다.
call verify_cpu64비트 커널을 실행할 수 있는 CPU인지 검사하는 루틴을 호출합니다. eax의 결과가 다음 분기의 기준입니다.
testl %eax, %eax검사 결과 eax가 0인지 확인하도록 상태 플래그를 설정합니다. 값 자체는 바꾸지 않습니다.
jnz .Lno_longmodeeax가 0이 아니면 필요한 CPU 조건을 만족하지 못한 경로로 이동합니다. long mode 전환 준비를 계속하지 않습니다.
#ifdef CONFIG_RELOCATABLE재배치 가능한 압축 커널은 현재 적재 주소를 부트 헤더의 kernel_alignment에 맞추고 최소 물리 적재 주소도 확인하여 압축 해제 위치를 계산합니다. 고정 배치 빌드는 뒤의 LOAD_PHYSICAL_ADDR을 사용합니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
movl %ebp, %ebx현재 이미지 적재 주소를 임시 목적지 계산용 ebx에 복사합니다. 뒤의 정렬 계산은 ebp 원본을 남겨 둔 채 ebx를 바꿉니다.
movl BP_kernel_alignment(%esi), %eax부팅 헤더의 커널 정렬 단위를 eax에 읽습니다. 이 값으로 압축 해제 위치를 올림 정렬합니다.
decl %eax정렬 단위에서 1을 뺍니다. (주소 + 정렬단위 - 1) & ~(정렬단위 - 1) 계산의 첫 준비입니다.
addl %eax, %ebx후보 주소에 정렬 단위 -1을 더합니다. 이미 정렬된 주소는 유지하고 나머지는 다음 경계로 올릴 준비입니다.
notl %eax정렬 단위 -1의 모든 비트를 반전해 하위 주소 비트를 지울 마스크를 만듭니다.
andl %eax, %ebx후보 주소의 하위 비트를 마스크하여 정렬된 목적지 기준 주소를 ebx에 얻습니다.
cmpl $LOAD_PHYSICAL_ADDR, %ebx정렬된 후보가 기본 적재 주소 LOAD_PHYSICAL_ADDR보다 낮은지 비교합니다.
jae 1f후보가 기본 적재 주소 이상이면 그대로 사용합니다. unsigned 비교이므로 주소의 크기를 부호 없는 값으로 판정합니다.
#endif146행에서 시작한 선택 구간을 마칩니다. 재배치 가능한 압축 커널은 현재 적재 주소를 부트 헤더의 kernel_alignment에 맞추고 최소 물리 적재 주소도 확인하여 압축 해제 위치를 계산합니다. 고정 배치 빌드는 뒤의 LOAD_PHYSICAL_ADDR을 사용합니다.
movl $LOAD_PHYSICAL_ADDR, %ebx후보가 너무 낮거나 재배치를 사용하지 않으면 기본 적재 주소를 목적지 기준으로 선택합니다.
1:목적지 기준 주소 선택이 끝난 위치입니다. 아래에서 압축 입력을 옮길 버퍼 끝부분의 주소를 계산합니다. 레이블은 현재 위치를 가리키는 이름으로, 별도의 CPU 명령을 추가하지 않습니다.
addl BP_init_size(%esi), %ebx기준 주소에 부팅 헤더의 초기 메모리 필요 크기를 더해 작업 버퍼의 끝을 구합니다.
subl $ rva(_end), %ebx버퍼 끝에서 압축 이미지의 전체 크기를 빼 임시 재배치 시작 주소를 구합니다. 압축 해제 출력이 아직 읽지 않은 입력을 덮지 않도록 끝부분에 배치합니다.
movl %cr4, %eax현재 CR4 값을 eax에 읽습니다. 기존 설정을 가져온 뒤 다음 줄에서 PAE 비트를 추가합니다.
orl $X86_CR4_PAE, %eaxlong mode page translation이 요구하는 PAE bit를 먼저 켠다. 이후 EFER.LME와 CR0.PG 순서가 이어진다.
movl %eax, %cr4PAE 비트를 켠 값을 CR4에 씁니다. long mode의 페이지 주소 변환에 필요한 조건을 준비하는 단계입니다.
SOURCE 02
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)주요 구문 해설
SYM_CODE_START(startup_64)압축 커널의 64비트 진입 코드를 정의합니다. 32비트 시작 경로가 모드를 전환해 이곳에 오거나 64비트 부트로더가 직접 진입합니다. 이 표기는 빌드 도구에 코드 심벌과 정렬을 알려 주며 그 자체가 런타임 함수 호출은 아닙니다.
* 64bit entry is 0x200 and it is ABI so immutable!offset 0x200 역시 외부 bootloader가 직접 사용할 수 있는 고정 entry ABI다.
cldDF를 지워 기본 문자열 명령의 진행 방향을 주소 증가 방향으로 맞춥니다. 64비트 직접 진입도 같은 초기 상태에서 시작하게 합니다.
cli압축 해제기의 초기 환경을 준비하는 동안 마스크 가능한 인터럽트를 끕니다.
xorl %eax, %eaxeax를 0으로 만듭니다. 아래 데이터 세그먼트 선택자를 초기화하는 데 사용할 값입니다.
movl %eax, %dsDS에 null 선택자를 넣습니다. long mode에서 일반 데이터 주소 계산은 주로 평탄한 주소 공간을 사용합니다.
movl %eax, %esES에 null 선택자를 넣어 로더가 남긴 데이터 세그먼트 선택자를 정리합니다.
movl %eax, %ssSS에 null 선택자를 넣습니다. 이어서 커널이 사용할 64비트 스택 포인터를 별도로 준비합니다.
movl %eax, %fsFS 선택자를 0으로 만듭니다. FS 선택자와 long mode의 FS base는 별도 상태이므로 이 줄을 임의의 FS base 설정으로 해석하면 안 됩니다.
movl %eax, %gsGS 선택자를 0으로 만듭니다. 이후 정상 커널의 per-CPU GS base 초기화와는 다른 단계입니다.
#ifdef CONFIG_RELOCATABLE64비트 시작 경로에서도 재배치 빌드이면 현재 위치를 기준으로 정렬된 압축 해제 목적지를 계산합니다. 고정 배치 빌드는 지정된 물리 주소를 사용하며, 계산에는 부트 헤더의 정렬 요구가 반영됩니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
leaq startup_32(%rip) /* - $startup_32 */, %rbpRIP 상대 주소 계산으로 현재 startup_32의 실제 주소를 rbp에 구합니다. 64비트 직접 진입에서는 앞선 32비트 주소 계산을 거쳤다고 가정할 수 없습니다. LEA는 유효 주소를 계산할 뿐 그 주소의 메모리를 읽지 않습니다.
movl BP_kernel_alignment(%rsi), %eax부팅 헤더에서 요구하는 커널 정렬 단위를 읽습니다. eax에 쓰면 rax의 상위 32비트도 0이 됩니다.
decl %eax정렬 단위에서 1을 빼 올림 정렬에 사용할 값을 만듭니다.
addq %rax, %rbp현재 적재 주소에 정렬 단위 -1을 더합니다. 다음 마스크 연산과 합쳐 다음 정렬 경계를 구합니다.
notq %rax정렬 단위 -1을 반전하여 하위 주소 비트를 지울 마스크를 만듭니다.
andq %rax, %rbp마스크를 적용해 rbp를 정렬된 압축 해제 기준 주소로 바꿉니다.
cmpq $LOAD_PHYSICAL_ADDR, %rbp정렬한 후보가 기본 적재 주소보다 낮은지 비교합니다. 주소를 unsigned 값으로 판단하는 다음 분기로 이어집니다.
jae 1f후보가 기본 적재 주소 이상이면 선택을 마칩니다. 낮으면 아래에서 기본 주소로 올립니다.
#endif315행에서 시작한 선택 구간을 마칩니다. 64비트 시작 경로에서도 재배치 빌드이면 현재 위치를 기준으로 정렬된 압축 해제 목적지를 계산합니다. 고정 배치 빌드는 지정된 물리 주소를 사용하며, 계산에는 부트 헤더의 정렬 요구가 반영됩니다.
movq $LOAD_PHYSICAL_ADDR, %rbp기본 적재 주소를 압축 해제 기준 rbp로 선택합니다. 재배치가 꺼진 빌드도 이 값을 사용합니다.
1:압축 해제 기준 주소를 결정한 두 경로가 합류합니다. 레이블은 현재 위치를 가리키는 이름으로, 별도의 CPU 명령을 추가하지 않습니다.
movl BP_init_size(%rsi), %ebx초기 부팅 작업에 필요한 메모리 크기를 ebx로 읽습니다. 임시 재배치 주소를 버퍼 끝부분에서 역산할 때 씁니다.
subl $ rva(_end), %ebx초기 메모리 크기에서 압축 이미지 크기를 빼 버퍼 내부의 재배치 오프셋을 계산합니다.
addq %rbp, %rbx압축 해제 기준 주소 rbp를 더해 압축 입력의 실제 재배치 시작 주소 rbx를 만듭니다.
leaq rva(boot_stack_end)(%rbx), %rsp재배치 목적지 기준으로 부팅 스택의 상단 주소를 rsp에 설정합니다. 아래 C 함수들을 호출할 때 사용할 스택입니다. LEA는 유효 주소를 계산할 뿐 그 주소의 메모리를 읽지 않습니다.
leaq gdt64(%rip), %rax64비트 GDT descriptor의 현재 주소를 rax에 만듭니다. 아래 4/5단계 페이징 전환에는 32비트 코드 세그먼트도 필요합니다. LEA는 유효 주소를 계산할 뿐 그 주소의 메모리를 읽지 않습니다.
addq %rax, 2(%rax)GDT descriptor에 저장된 상대 base에 현재 위치를 더해 실제 base 주소로 보정합니다.
lgdt (%rax)보정한 descriptor를 GDTR에 읽어 현재 실행과 모드 전환에 필요한 GDT를 선택합니다. LGDT는 메모리의 descriptor에서 GDT의 base와 limit를 GDTR에 적재합니다. 표를 메모리에 만든 것만으로 GDTR가 자동 변경되지는 않습니다.
pushq $__KERNEL_CS새 GDT에서 사용할 커널 코드 세그먼트 선택자를 스택에 넣습니다. PUSHQ는 rsp를 8바이트 낮춘 뒤 새 스택 위치에 피연산자 값을 기록합니다.
leaq .Lon_kernel_cs(%rip), %raxCS 전환 후 계속 실행할 .Lon_kernel_cs 주소를 rax에 준비합니다. LEA는 유효 주소를 계산할 뿐 그 주소의 메모리를 읽지 않습니다.
pushq %rax목적지 RIP를 스택에 넣습니다. 아래 far return이 이 값과 바로 앞의 CS를 읽습니다. PUSHQ는 rsp를 8바이트 낮춘 뒤 새 스택 위치에 피연산자 값을 기록합니다.
lretqRIP와 CS를 스택에서 읽어 새 GDT의 커널 코드 세그먼트로 넘어갑니다. 이 위치에서는 커널 권한을 유지하며 코드 세그먼트를 맞춥니다.
.Lon_kernel_cs:CS가 새 GDT의 커널 항목을 사용하게 된 뒤 합류하는 위치입니다. 레이블은 현재 위치를 가리키는 이름으로, 별도의 CPU 명령을 추가하지 않습니다.
movq %rsi, %r15loader가 넘긴 boot_params pointer를 callee-saved %r15에 보존한다. 이후 C 함수를 호출해도 살아 있어야 한다.
call load_stage1_idt초기 단계의 IDT를 설치합니다. SEV-ES에서 CPUID 실행 등에 의해 생길 수 있는 #VC 예외를 받을 준비를 합니다.
#ifdef CONFIG_AMD_MEM_ENCRYPTAMD 메모리 암호화 지원 빌드는 초기 예외 처리기가 준비된 다음 sev_enable로 SEV 상태와 SEV-SNP의 CPUID 정보를 준비합니다. 이후 CPUID 실행에 필요한 환경을 먼저 갖추며, 실제 사용 여부는 하드웨어와 게스트 상태를 확인합니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
movq %r15, %rdi보존해 둔 boot_params 포인터를 첫 C 인자 레지스터 rdi로 옮깁니다. 다음 SEV 초기화 함수가 읽을 부팅 정보입니다.
call sev_enableSEV 사용 여부와 관련 초기 상태를 준비합니다. SEV-SNP에서는 뒤의 CPUID 실행보다 먼저 CPUID 테이블을 설정해야 합니다.
#endif378행에서 시작한 선택 구간을 마칩니다. AMD 메모리 암호화 지원 빌드는 초기 예외 처리기가 준비된 다음 sev_enable로 SEV 상태와 SEV-SNP의 CPUID 정보를 준비합니다. 이후 CPUID 실행에 필요한 환경을 먼저 갖추며, 실제 사용 여부는 하드웨어와 게스트 상태를 확인합니다.
movq %cr4, %rax현재 CR4를 rax에 읽습니다. 로더가 켜 둔 기능 중 부팅에 계속 필요한 비트만 남기려는 단계입니다.
andl $(X86_CR4_PAE | X86_CR4_MCE | X86_CR4_LA57), %eaxPAE·MCE·LA57 비트만 남깁니다. 페이지 테이블 형식과 machine check 관련 상태를 유지하면서 다른 CR4 기능은 초기 상태로 정리합니다.
movq %rax, %cr4정리한 CR4 값을 CPU에 적용합니다. 아직 원하는 페이징 단계 수로 전환하는 작업 자체는 수행하지 않습니다.
* configure_5level_paging() updates the number of paging levels using현재 CR4.LA57과 kernel이 원하는 paging level이 다르면 long mode를 잠시 벗어나는 trampoline이 이를 맞춘다.
movq %r15, %rdiconfigure_5level_paging의 첫 인자로 부팅 정보 포인터를 준비합니다.
leaq rva(top_pgtable)(%rbx), %rsi두 번째 인자로 재배치 위치의 최상위 페이지 테이블 주소를 넘깁니다. 낮은 주소의 전환용 trampoline 페이지 테이블을 계속 사용할 필요가 없게 준비하는 값입니다. LEA는 유효 주소를 계산할 뿐 그 주소의 메모리를 읽지 않습니다.
call configure_5level_paging현재 CR4.LA57과 kernel이 원하는 paging level이 다르면 long mode를 잠시 벗어나는 trampoline이 이를 맞춘다.
pushq $0플래그 초기화에 사용할 0을 스택에 넣습니다. 아래 popfq가 이 값을 플래그 레지스터에 적용합니다. PUSHQ는 rsp를 8바이트 낮춘 뒤 새 스택 위치에 피연산자 값을 기록합니다.
popfq스택의 값으로 변경 가능한 RFLAGS 비트를 초기화합니다. 아키텍처상 고정된 예약 비트까지 모두 0으로 만든다는 뜻은 아닙니다.
leaq (_bss-8)(%rip), %rsi복사할 입력의 마지막 8바이트 주소를 rsi에 넣습니다. BSS 직전까지의 압축 이미지 데이터를 뒤에서부터 복사할 준비입니다. LEA는 유효 주소를 계산할 뿐 그 주소의 메모리를 읽지 않습니다.
leaq rva(_bss-8)(%rbx), %rdi새 위치에서 대응하는 마지막 8바이트 주소를 rdi에 계산합니다. rbx가 재배치 시작 기준 주소입니다. LEA는 유효 주소를 계산할 뿐 그 주소의 메모리를 읽지 않습니다.
movl $(_bss - startup_32), %ecx이미지 시작부터 BSS 전까지 복사할 바이트 수를 ecx에 넣습니다. BSS는 파일의 실제 초기 데이터 복사 범위에 포함하지 않습니다.
shrl $3, %ecx바이트 수를 8로 나누어 movsq가 복사할 8바이트 단위 개수를 만듭니다.
stdDF를 켜서 반복 복사의 주소가 매번 감소하게 합니다. 입력과 출력이 겹치는 상황에서 아직 복사하지 않은 데이터를 보호합니다.
rep movsqDF를 세운 상태에서 높은 주소에서 낮은 주소 방향으로 compressed image를 복사한다. 아직 읽지 않은 source를 보호하는 핵심 구간이다.
cld역방향 복사가 끝났으므로 DF를 다시 지웁니다. 이후 C 코드가 기대하는 문자열 명령 방향을 복구합니다.
leaq rva(gdt64)(%rbx), %rax복사된 이미지 쪽 GDT descriptor의 주소를 rax에 구합니다. 원래 GDT는 뒤의 압축 해제 과정에서 덮일 수 있습니다. LEA는 유효 주소를 계산할 뿐 그 주소의 메모리를 읽지 않습니다.
leaq rva(gdt)(%rbx), %rdx새 복사본의 실제 GDT 주소를 rdx에 구합니다. descriptor가 가리킬 대상입니다. LEA는 유효 주소를 계산할 뿐 그 주소의 메모리를 읽지 않습니다.
movq %rdx, 2(%rax)새 descriptor의 base 필드에 새 GDT 주소를 기록합니다.
lgdt (%rax)GDTR를 복사된 GDT로 바꿉니다. 이후 원래 이미지 영역이 덮여도 세그먼트 표를 잃지 않도록 합니다. LGDT는 메모리의 descriptor에서 GDT의 base와 limit를 GDTR에 적재합니다. 표를 메모리에 만든 것만으로 GDTR가 자동 변경되지는 않습니다.
leaq rva(.Lrelocated)(%rbx), %rax재배치된 .Lrelocated의 주소를 rax에 구합니다. 바로 다음 간접 점프는 새 복사본에서 실행을 계속합니다. LEA는 유효 주소를 계산할 뿐 그 주소의 메모리를 읽지 않습니다.
jmp *%rax새 위치의 .Lrelocated로 비복귀 분기한다. GDT도 새 copy를 가리키도록 바꾼 뒤여야 한다.
SYM_CODE_END(startup_64)이 startup_64 코드 심벌의 범위를 끝냅니다. 실행 흐름은 앞의 점프로 이미 재배치 위치로 넘어갔습니다.
SOURCE 03
압축 해제된 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)주요 구문 해설
SYM_CODE_START_NOALIGN(startup_64)압축이 풀린 커널 본체의 startup_64를 정의합니다. 압축 해제기 안의 같은 이름 함수와 구분해야 하며, 이 시점에는 이미 64비트 모드와 초기 identity mapping이 준비되어 있습니다. 이 표기는 빌드 도구에 코드 심벌과 정렬을 알려 주며 그 자체가 런타임 함수 호출은 아닙니다.
UNWIND_HINT_END_OF_STACK스택 추적 도구에 이 부팅 진입점 아래로 일반 호출 프레임이 이어지지 않는다고 알립니다. 실행 중 CPU 레지스터를 바꾸는 명령은 아닙니다.
mov %rsi, %r15물리 주소인 boot_params를 C 호출 사이에 보존한다.
leaq __top_init_kernel_stack(%rip), %rsp검증 C 함수를 호출할 수 있도록 boot CPU의 초기 stack top을 선택한다.
movl $MSR_GS_BASE, %ecxGS base를 설정하는 MSR의 번호를 ecx에 넣습니다. wrmsr는 ecx로 대상 MSR을 선택합니다.
xorl %eax, %eaxMSR에 쓸 64비트 값의 하위 절반 eax를 0으로 만듭니다.
xorl %edx, %edxMSR에 쓸 값의 상위 절반 edx도 0으로 만듭니다. 따라서 EDX:EAX 전체 값이 0이 됩니다.
wrmsrGS base를 0으로 설정합니다. 정상 per-CPU 영역을 마련하기 전 초기 커널 데이터 사용에 맞춘 상태입니다.
call __pi_startup_64_setup_gdt_idt초기 GDT와 IDT를 준비하는 위치 독립 초기화 함수를 호출합니다. 커널의 최종 가상 주소에서 아직 실행하고 있지 않기 때문에 __pi_ 경로를 사용합니다.
pushq $__KERNEL_CS현재 GDT의 커널 코드 세그먼트 선택자를 스택에 넣습니다. 코드 세그먼트와 GDT의 관계를 확실히 맞춥니다. PUSHQ는 rsp를 8바이트 낮춘 뒤 새 스택 위치에 피연산자 값을 기록합니다.
leaq .Lon_kernel_cs(%rip), %raxCS를 바꾼 뒤 이어갈 .Lon_kernel_cs의 현재 주소를 rax에 넣습니다. LEA는 유효 주소를 계산할 뿐 그 주소의 메모리를 읽지 않습니다.
pushq %raxfar return이 읽을 목적지 RIP를 스택에 넣습니다. 그 아래에는 앞서 넣은 CS 선택자가 있습니다. PUSHQ는 rsp를 8바이트 낮춘 뒤 새 스택 위치에 피연산자 값을 기록합니다.
lretq준비한 RIP·CS를 읽어 커널 코드 세그먼트로 실행을 이어갑니다. 일반 사용자 모드 복귀나 인터럽트 복귀는 아닙니다.
.Lon_kernel_cs:코드 세그먼트 전환이 끝난 위치입니다. 이후 암호화 기능과 페이지 테이블을 준비합니다. 레이블은 현재 위치를 가리키는 이름으로, 별도의 CPU 명령을 추가하지 않습니다.
ANNOTATE_NOENDBR이 분기 목적지에 ENDBR 명령이 없어도 되는 위치임을 objtool에 알립니다. 명령을 실행하여 분기 보호를 끄는 동작은 아닙니다.
UNWIND_HINT_END_OF_STACK스택 추적 메타데이터에 부팅 스택의 경계를 다시 표시합니다. far return으로 도착했으므로 일반 호출자가 있다고 추정하지 않게 합니다.
#ifdef CONFIG_AMD_MEM_ENCRYPTAMD SME/SEV 지원 빌드는 boot_params를 전달해 암호화 사용 상태와 필요한 SEV-SNP CPUID 표를 먼저 준비합니다. 이후 초기화에서 CPUID를 사용할 수 있으므로 그보다 앞에 놓은 호출이며, 빌드 지원만으로 암호화가 무조건 활성화되지는 않습니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
movq %r15, %rdi보존해 둔 부팅 정보 포인터를 첫 인자 rdi에 넣습니다. 아래 메모리 암호화 초기화 함수에 전달합니다.
call __pi_sme_enable지원과 설정 조건에 따라 SME·SEV 초기 상태를 준비합니다. SEV-SNP의 CPUID 테이블도 이후 CPUID 명령보다 먼저 사용할 수 있어야 합니다.
#endif86행에서 시작한 선택 구간을 마칩니다. AMD SME/SEV 지원 빌드는 boot_params를 전달해 암호화 사용 상태와 필요한 SEV-SNP CPUID 표를 먼저 준비합니다. 이후 초기화에서 CPUID를 사용할 수 있으므로 그보다 앞에 놓은 호출이며, 빌드 지원만으로 암호화가 무조건 활성화되지는 않습니다.
call verify_cpu커널 실행에 필요한 CPU 기능과 제어 상태를 검사·정리합니다. 초기 페이지 테이블 전환에 앞서 CPU 환경을 맞추는 단계입니다.
leaq common_startup_64(%rip), %rdicommon_startup_64의 현재 실행 위치 기준 주소를 rdi에 구합니다. 다음 줄에서 링크상의 가상 주소를 빼 물리·가상 배치 차이를 계산합니다. LEA는 유효 주소를 계산할 뿐 그 주소의 메모리를 읽지 않습니다.
subq .Lcommon_startup_64(%rip), %rdiruntime RIP와 link-time virtual address 차이로 물리-가상 offset을 계산한다.
movq %r15, %rsi페이지 테이블 보정 함수의 두 번째 인자로 부팅 정보 포인터를 준비합니다. 첫 번째 인자 rdi에는 앞서 계산한 주소 차이가 있습니다.
call __pi___startup_64position-independent C helper가 초기 page table entry를 실제 load address에 맞게 고친다.
leaq early_top_pgt(%rip), %rcx초기 최상위 페이지 테이블 early_top_pgt의 현재 주소를 rcx에 구합니다. 현재 identity mapping으로 접근할 수 있는 물리 기준 주소입니다. LEA는 유효 주소를 계산할 뿐 그 주소의 메모리를 읽지 않습니다.
addq %rcx, %rax초기화 함수가 돌려준 CR3 보정값에 페이지 테이블 주소를 더합니다. SME가 활성화되어 있으면 암호화 비트도 최종 CR3 값에 포함됩니다.
#ifdef CONFIG_AMD_MEM_ENCRYPTAMD 암호화 지원 경로는 SEV 게스트에서 사용할 페이지 표의 C-bit 위치가 올바른지 확인합니다. 잘못된 암호화 비트 때문에 스택을 평문으로 노출하는 등의 문제를 막기 위한 검증이며, 일반 비암호화 빌드에는 없습니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
mov %rax, %rdi설정하려는 CR3 값을 첫 인자로 복사합니다. 다음 함수가 SEV 암호화 C-bit의 유효성을 검사할 때 사용합니다.
call sev_verify_cbitSEV 환경에서 암호화 C-bit 위치가 올바른지 검사합니다. 잘못된 비트로 스택이나 페이지 테이블을 해석하는 일을 방지한 뒤 새 CR3를 적용합니다.
#endif120행에서 시작한 선택 구간을 마칩니다. AMD 암호화 지원 경로는 SEV 게스트에서 사용할 페이지 표의 C-bit 위치가 올바른지 확인합니다. 잘못된 암호화 비트 때문에 스택을 평문으로 노출하는 등의 문제를 막기 위한 검증이며, 일반 비암호화 빌드에는 없습니다.
movq %rax, %cr3고쳐진 early_top_pgt를 활성화한다. 아직 identity mapping을 유지하므로 바로 다음 fetch가 끊기지 않는다.
ANNOTATE_RETPOLINE_SAFE다음 간접 점프가 허용된 부팅 코드 전환임을 objtool에 표시합니다. 이 표시는 실제 점프 목적지를 바꾸거나 CPU 상태를 복원하지 않습니다.
jmp *.Lcommon_startup_64(%rip)이제 link-time kernel virtual address를 사용해 공통 시작 코드로 이동한다.
SYM_CODE_END(startup_64)커널 본체의 startup_64 코드 범위를 끝냅니다. 앞의 점프를 통해 높은 커널 가상 주소의 공통 시작 코드로 진행합니다.
SOURCE 04
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");주요 구문 해설
asmlinkage __visible void *extract_kernel(void *rmode, unsigned char *output)부팅 정보와 압축 해제 목적지를 받아 커널을 추출하는 함수입니다. 초기 출력·메모리 배치를 준비하고, 마지막에는 추출된 커널 진입 주소를 돌려줍니다.
unsigned long virt_addr = LOAD_PHYSICAL_ADDR;가상 배치 오프셋의 초기값을 기본 적재 주소로 둡니다. KASLR 위치 선택 과정에서 이 값이 바뀔 수 있습니다.
memptr heap = (memptr)boot_heap;압축 해제기의 임시 동적 메모리 영역 boot_heap 시작 주소를 heap에 담습니다. 정상 커널 메모리 할당기가 생기기 전 사용하는 공간입니다.
unsigned long needed_size;압축 해제 결과와 커널의 BSS·초기 확장 영역 등을 수용하는 데 필요한 메모리 크기를 담을 변수를 선언합니다.
size_t entry_offset;추출한 이미지 안에서 실행 진입점까지의 오프셋을 담을 변수를 선언합니다. 함수 끝에서 실제 진입 주소를 계산할 때 사용합니다.
boot_params_ptr = rmode;assembly가 %rsi로 보존해 온 zero-page pointer의 소유권을 decompressor C 코드가 넘겨받는다. 이 4KiB가 출력 범위와 겹치면 뒤의 memory map과 command line 해석이 동시에 무너진다.
boot_params_ptr->hdr.loadflags &= ~KASLR_FLAG;부트로더가 남긴 loadflags에서 KASLR 상태 비트를 지웁니다. 이 부팅에서 실제로 무작위 배치를 했는지는 이후 커널 코드가 다시 기록합니다.
parse_mem_encrypt(&boot_params_ptr->hdr);명령줄의 mem_encrypt=on과 mem_encrypt=off 위치를 비교합니다. on이 더 뒤에 있으면 부팅 헤더의 XLF_MEM_ENCRYPTION 비트를 켜서 메모리 암호화 요청을 기록합니다.
sanitize_boot_params(boot_params_ptr);부트로더가 채운 boot_params를 정리합니다. 프로토콜상 유효하지 않거나 초기화되지 않았을 수 있는 필드가 뒤의 처리에 잘못 쓰이지 않게 합니다.
if (boot_params_ptr->screen_info.orig_video_mode == 7) {로더가 보고한 화면 모드가 흑백 텍스트 모드 7인지 검사합니다. 결과에 따라 화면 메모리 주소와 제어 포트가 달라집니다.
vidmem = (char *) 0xb0000;흑백 텍스트 화면의 메모리 주소 0xb0000을 선택합니다. 초기 콘솔이 문자와 속성을 기록할 위치입니다.
vidport = 0x3b4;흑백 화면 제어에 사용하는 포트 0x3b4를 선택합니다.
} else {화면 모드가 흑백 텍스트 모드가 아닌 경우 사용하는 초기 출력 경로입니다.
vidmem = (char *) 0xb8000;컬러 텍스트 화면 메모리 주소 0xb8000을 선택합니다.
vidport = 0x3d4;컬러 텍스트 화면의 제어 포트 0x3d4를 선택합니다.
lines = boot_params_ptr->screen_info.orig_video_lines;부팅 정보에 있는 화면의 행 수를 저장합니다. 초기 텍스트 출력의 줄 이동과 화면 범위를 계산할 때 사용합니다.
cols = boot_params_ptr->screen_info.orig_video_cols;화면의 열 수를 저장합니다. 텍스트 화면에서 다음 문자와 줄의 위치를 계산하는 기준입니다.
init_default_io_ops();초기 포트 I/O 연산을 기본 구현으로 설정합니다. 뒤의 기밀 VM 감지 결과에 따라 특수한 I/O 구현으로 바뀔 수 있습니다.
early_tdx_detect();Intel TDX 게스트인지 초기 감지를 수행합니다. 해당 환경에서는 직접 포트 I/O 대신 게스트용 경로가 필요할 수 있어 콘솔 초기화 전에 처리합니다.
early_sev_detect();SEV-ES 상태이면 화면 행·열 수를 0으로 만들어 비디오 메모리 출력을 막습니다. 이 단계의 #VC 핸들러가 화면 MMIO를 처리하지 못하므로, 특히 kexec가 화면 정보를 넘긴 경우 게스트 종료를 피하기 위한 처리입니다.
console_init();압축 해제 단계에서 메시지를 출력할 초기 콘솔을 준비합니다. 정상 커널의 콘솔 드라이버가 완성되기 전 사용하는 출력입니다.
boot_params_ptr->acpi_rsdp_addr = get_rsdp_addr();ACPI 테이블의 출발점인 RSDP 주소를 찾아 부팅 정보에 저장합니다. 콘솔 이후에 수행하므로 검색 중의 초기 디버그 출력도 볼 수 있습니다.
debug_putstr("early console in extract_kernel\n");압축 해제 함수의 초기 콘솔 단계에 도달했다는 디버그 문자열을 출력합니다. 메모리를 할당하거나 페이지 테이블을 바꾸는 함수는 아닙니다.
free_mem_ptr = heap; /* Heap */임시 힙에서 다음 할당을 시작할 위치를 힙 시작 주소로 설정합니다.
free_mem_end_ptr = heap + BOOT_HEAP_SIZE;힙 시작 주소에 정해진 힙 크기를 더해 사용 가능한 끝 경계를 설정합니다. 할당기는 이 범위를 넘겨 사용할 수 없습니다.
needed_size = max_t(unsigned long, output_len, kernel_total_size);압축 해제된 ELF byte 수만 보지 않고 relocation table, BSS와 brk까지 포함한 두 후보 중 큰 값을 선택한다. KASLR hole 검사는 이 전체 수명을 수용해야 한다.
#ifdef CONFIG_X86_6464비트 커널의 압축 해제 공간을 MIN_KERNEL_ALIGN 배수로 올립니다. 초기 큰 페이지 매핑의 정렬에 맞는 전체 크기를 확보하려는 계산이며 32비트 경로에는 이 추가 정렬이 없습니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
needed_size = ALIGN(needed_size, MIN_KERNEL_ALIGN);x86-64 early mapping이 PMD 단위이므로 마지막 byte가 속한 2MiB frame까지 usable RAM인지 검사하기 위해 범위를 올림한다.
#endif473행에서 시작한 선택 구간을 마칩니다. 64비트 커널의 압축 해제 공간을 MIN_KERNEL_ALIGN 배수로 올립니다. 초기 큰 페이지 매핑의 정렬에 맞는 전체 크기를 확보하려는 계산이며 32비트 경로에는 이 추가 정렬이 없습니다.
debug_putaddr(input_data);압축 입력 데이터의 시작 주소를 디버그 출력합니다. 출력 목적지와의 겹침을 살필 때 필요한 값입니다.
debug_putaddr(input_len);압축 입력의 길이를 디버그 출력합니다. 함수 이름에 addr가 있지만 여기서는 크기 값을 출력합니다.
debug_putaddr(output);압축 해제 결과를 기록할 현재 목적지 주소를 디버그 출력합니다.
debug_putaddr(output_len);압축 해제 결과의 바이트 크기를 디버그 출력합니다.
debug_putaddr(kernel_total_size);BSS와 초기 확장 영역 등을 포함하여 커널 전체에 필요한 크기를 디버그 출력합니다.
debug_putaddr(needed_size);정렬 등을 반영한 실제 메모리 확보 크기 needed_size를 디버그 출력합니다.
#ifdef CONFIG_X86_6464비트 부팅에서 사용한 32비트 전환 trampoline의 주소를 디버그 출력에 포함합니다. 주소를 출력하는 진단 코드이고 이 줄이 trampoline을 실행하는 것은 아닙니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
debug_putaddr(trampoline_32bit);4단계와 5단계 페이징을 전환할 때 쓰는 32비트 trampoline 주소를 출력합니다. 모드 전환용 코드가 낮은 주소에 제대로 놓였는지 확인하는 데 쓰입니다.
#endif485행에서 시작한 선택 구간을 마칩니다. 64비트 부팅에서 사용한 32비트 전환 trampoline의 주소를 디버그 출력에 포함합니다. 주소를 출력하는 진단 코드이고 이 줄이 trampoline을 실행하는 것은 아닙니다.
choose_random_location((unsigned long)input_data, input_len,입력 payload 범위와 필요한 출력 크기를 함께 넘겨 물리 destination과 kernel virtual address를 고른다. 반환된 두 주소는 같은 값일 필요가 없다.
(unsigned long *)&output,목적지 포인터 변수 자체의 주소를 위치 선택 함수에 넘깁니다. 함수가 새 물리 목적지를 골랐을 때 output 값을 바꿀 수 있게 합니다.
needed_size,선택할 위치에서 확보해야 하는 메모리 크기를 인자로 넘깁니다. 시작 주소 한 점만 비어 있는 것이 아니라 이 전체 범위가 사용 가능해야 합니다.
&virt_addr);가상 배치 값의 주소를 넘깁니다. 물리 목적지와 별도로 KASLR의 가상 배치 결과도 호출자에게 돌려받습니다.
if ((unsigned long)output & (MIN_KERNEL_ALIGN - 1))물리 목적지의 하위 비트에 값이 남아 있는지 검사하여 MIN_KERNEL_ALIGN 정렬 위반을 찾습니다. 위반하면 다음 줄에서 부팅 오류로 중단합니다.
error("Destination physical address inappropriately aligned");선택 결과를 신뢰하지 않고 assembly contract와 같은 MIN_KERNEL_ALIGN 제약을 C 경계에서 다시 검증한다.
if (virt_addr & (MIN_KERNEL_ALIGN - 1))선택한 가상 배치도 커널 최소 정렬 단위를 만족하는지 검사합니다. 물리 주소 정렬만 맞아서는 충분하지 않습니다.
error("Destination virtual address inappropriately aligned");가상 배치가 정렬 조건에 맞지 않으면 원인을 출력하고 부팅을 중단합니다. 잘못된 위치로 압축 해제를 계속하지 않습니다.
WORKED EXAMPLES
숫자를 넣어 끝까지 계산한다
compressed image를 16MiB에 적재한 경우의 주소 계산
예를 들어 compressed image runtime base가 0x01000000, link 기준 startup_32 offset이 0x200, 필요한 출력 범위가 0x01734000이라고 가정한다.
- runtime basecall/pop으로 얻은 다음 RIP에서 link-time label offset을 빼
0x01000000을 복원한다. - output hole
ALIGN(0x01734000, 0x00200000) = 0x01800000, 즉 24MiB를 usable RAM으로 확보해야 한다. - destination후보
0x02000000은 2MiB 정렬이고 끝 주소는0x03800000이다. - source protectioncompressed source 끝과
[0x02000000, 0x03800000)이 겹치면 source를 buffer 상단으로 역복사한 뒤 해제한다.
판정GDB에는 %rbp=destination, %rbx=decompression stack/buffer 기준, %r15=boot_params와 destination end를 함께 기록한다. 시작 주소 하나만 맞는 것으로는 overlap 안전성을 증명할 수 없다.
CR3에 넣는 값과 high virtual jump를 따로 검산한다
early page-table root의 physical address가 0x0019a000이면 x86 CR3의 base field는 그대로 0x0019a000이다. 하위 bit에는 PCID 또는 control flag만 들어간다.
- 전환 전 RIP현재 RIP가
0x03801234라면 새 root에도 이 물리 범위를 identity-map한 PTE가 있어야 한다. - 전환 전 RSPRSP가
0x037ff000이면 같은 이유로 stack page도 새 root에서 접근 가능해야 한다. - high alias링크 VA
ffffffff81001234가 같은 physical text frame을 가리켜야 high-address jump가 성공한다. - root self accesspage-table walk가 사용하는
0x0019a000과 하위 table page들은 usable RAM이며 덮어쓰이지 않아야 한다.
판정mov %rax,%cr3 직후 fault는 instruction page만 볼 문제가 아니다. RIP, RSP, page-table page 세 범위를 동시에 walk해야 한다.
DEEP DIVE
코드에서 놓치기 쉬운 경계
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를 사용했는지에 따라 유효한 배치가 달라진다.
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으로 굳었는지 확인해야 한다.
KASLR 위치 선택과 decompression 안전성은 다른 문제다
KASLR은 실행 물리 위치 후보를 고르는 정책이고, source를 buffer 끝으로 옮기는 코드는 chosen destination에서 in-place decompression이 source를 덮지 않게 하는 안전 조건이다. KASLR을 꺼도 후자의 overlap 계산은 남는다.
destination은 alignment와 minimum load address를 만족해야 하고 init_size 전체가 RAM, reserved range, initrd와 겹치지 않아야 한다.
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 전환을 확인할 수 있다.
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까지 같이 적는다.
초기 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다.
FAILURE PATH
멈춘 위치보다 먼저 볼 값
| 조건 | 관찰되는 증상 | 첫 확인 값 |
|---|---|---|
| boot_params 훼손 | memory map이나 command line이 무작위 값으로 보이거나 decompressor가 잘못된 크기를 사용한다. | %rsi와 zero-page 4KiB 범위 |
| destination/source overlap | extract 중 정지하거나 압축 해제 뒤 instruction stream이 손상된다. | %rbp, %rbx, _bss, init_size |
| 잘못된 paging level | configure_5level_paging 또는 직후 #GP/triple fault가 발생한다. | CR4.LA57, CR3, trampoline 주소 |
| early page table fixup 오류 | CR3 write 직후 또는 high-address jump에서 무출력 reset이 난다. | RIP/SP의 identity와 high mapping |
LAB
직접 확인할 실험
- 01QEMU
-S -s로 멈춘 뒤startup_32, 두startup_64,common_startup_64에 breakpoint를 건다.명령을 실행한 시점의 PC, stack, address-space root와 CPU/hart 번호를 함께 기록한다.
- 02각 checkpoint에서 CR0, CR3, CR4, EFER, RIP, RSP, RSI/R15를 기록해 표로 비교한다.
명령을 실행한 시점의 PC, stack, address-space root와 CPU/hart 번호를 함께 기록한다.
- 03
objdump -dr arch/x86/boot/compressed/vmlinux에서rva()reference와 runtime relocation 유무를 확인한다.명령을 실행한 시점의 PC, stack, address-space root와 CPU/hart 번호를 함께 기록한다.
- 04
readelf -lW vmlinux와/proc/iomem을 대조해 실제 load range와 kernel text range를 확인한다.명령을 실행한 시점의 PC, stack, address-space root와 CPU/hart 번호를 함께 기록한다.
PRIMARY REFERENCES