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, %cr482-170줄 주석
SYM_FUNC_START(startup_32)linker와 assembler가 진입 symbol, section 또는 정렬을 확정하는 지시자다. runtime 명령은 아니지만 entry address와 배치를 결정한다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* 32bit entry is 0 and it is ABI so immutable!offset 0은 Linux x86 boot protocol의 고정 ABI다. 코드를 앞에 삽입해 이 위치를 바꾸면 부트로더는 다른 byte를 entry로 실행한다.
* If we come here directly from a bootloader,원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* kernel(text+data+bss+brk) ramdisk, zero_page, command line원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* all need to be under the 4G limit.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
cld문자열 명령의 방향을 증가 방향으로 고정한다. 펌웨어가 남긴 DF 상태를 C ABI가 기대하는 값으로 되돌리는 줄이다.
cli외부 비동기 진입을 막는다. 이후 코드가 예외 벡터와 stack을 완성하기 전에는 interrupt handler가 실행되어서는 안 된다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* Calculate the delta between where we were compiled to run원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* at and where we were actually loaded at. This can only be done원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* with a short local call on x86. Nothing else will tell us what원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* address we are running at. The reserved chunk of the real-mode원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* data at 0x1e4 (defined as a scratch field) are used as the stack원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* for this calculation. Only 4 bytes are needed.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
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 %ebpstack에서 값을 회수하고 SP를 이동한다. 앞서 쌓인 순서와 폭이 정확히 맞아야 한다.
subl $ rva(1b), %ebp주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Load new GDT with the 64bit segments using 32bit descriptor */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
leal rva(gdt)(%ebp), %eax주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
movl %eax, 2(%eax)주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
lgdt (%eax)descriptor table의 base와 limit를 CPU에 적재한다. 메모리에 표가 존재하는 것과 CPU register가 그 표를 가리키는 것은 별개다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Load segment registers with our descriptors */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
movl $__BOOT_DS, %eax주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
movl %eax, %ds주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
movl %eax, %es주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
movl %eax, %fs주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
movl %eax, %gs주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
movl %eax, %ss주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Setup a stack and load CS from current GDT */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
leal rva(boot_stack_end)(%ebp), %esp주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
pushl $__KERNEL32_CS현재 stack에 값을 쌓는다. 이 줄을 읽을 때는 SP가 가리키는 메모리가 실제로 쓰기 가능한지와 ABI 정렬을 함께 확인한다.
leal rva(1f)(%ebp), %eax주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
pushl %eax현재 stack에 값을 쌓는다. 이 줄을 읽을 때는 SP가 가리키는 메모리가 실제로 쓰기 가능한지와 ABI 정렬을 함께 확인한다.
lretl일반 분기가 아니라 privilege 또는 code segment 상태를 함께 복원하는 제어 이전이다. frame 형식이 틀리면 다음 명령에 도달하지 못한다.
1:분기 대상 label이다. 이 지점에 들어올 수 있는 모든 선행 경로가 같은 register와 stack 조건을 만족하는지 비교한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Setup Exception handling for SEV-ES */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
#ifdef CONFIG_AMD_MEM_ENCRYPT빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
call startup32_load_idt하위 루틴으로 진입한다. 호출 직전 argument register, stack 정렬, 보존 register가 해당 ABI를 만족하는지 확인한다.
#endif빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Make sure cpu supports long mode. */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
call verify_cpu하위 루틴으로 진입한다. 호출 직전 argument register, stack 정렬, 보존 register가 해당 ABI를 만족하는지 확인한다.
testl %eax, %eax이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
jnz .Lno_longmode이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* Compute the delta between where we were compiled to run at원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* and where the code will actually run at.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* %ebp contains the address we are loaded at by the boot loader and %ebx원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* contains the address where we should move the kernel image temporarily원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* for safe in-place decompression.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
#ifdef CONFIG_RELOCATABLE빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
movl %ebp, %ebx주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
movl BP_kernel_alignment(%esi), %eax주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
decl %eax이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
addl %eax, %ebx주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.
notl %eax이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
andl %eax, %ebx주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.
cmpl $LOAD_PHYSICAL_ADDR, %ebx직전에 만든 값이나 flag를 검사해 경로를 나눈다. 실패 분기가 어디에서 멈추는지까지 따라가야 조건의 의미가 완성된다.
jae 1f이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
#endif빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
movl $LOAD_PHYSICAL_ADDR, %ebx주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
1:분기 대상 label이다. 이 지점에 들어올 수 있는 모든 선행 경로가 같은 register와 stack 조건을 만족하는지 비교한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Target address to relocate to for decompression */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
addl BP_init_size(%esi), %ebx주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.
subl $ rva(_end), %ebx주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* Prepare for entering 64 bit mode원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Enable PAE mode */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
movl %cr4, %eax주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
orl $X86_CR4_PAE, %eaxlong mode page translation이 요구하는 PAE bit를 먼저 켠다. 이후 EFER.LME와 CR0.PG 순서가 이어진다.
movl %eax, %cr4주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
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)278-442줄 주석
SYM_CODE_START(startup_64)linker와 assembler가 진입 symbol, section 또는 정렬을 확정하는 지시자다. runtime 명령은 아니지만 entry address와 배치를 결정한다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* 64bit entry is 0x200 and it is ABI so immutable!offset 0x200 역시 외부 bootloader가 직접 사용할 수 있는 고정 entry ABI다.
* We come here either from startup_32 or directly from a원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* 64bit bootloader.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* If we come here from a bootloader, kernel(text+data+bss+brk),원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* ramdisk, zero_page, command line could be above 4G.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* We depend on an identity mapped page table being provided원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* that maps our entire kernel(text+data+bss+brk), zero page원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* and command line.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
cld문자열 명령의 방향을 증가 방향으로 고정한다. 펌웨어가 남긴 DF 상태를 C ABI가 기대하는 값으로 되돌리는 줄이다.
cli외부 비동기 진입을 막는다. 이후 코드가 예외 벡터와 stack을 완성하기 전에는 interrupt handler가 실행되어서는 안 된다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Setup data segments. */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
xorl %eax, %eax이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
movl %eax, %ds주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
movl %eax, %es주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
movl %eax, %ss주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
movl %eax, %fs주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
movl %eax, %gs주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* Compute the decompressed kernel start address. It is where원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* we were loaded at aligned to a 2M boundary. %rbp contains the원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* decompressed kernel start address.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* If it is a relocatable kernel then decompress and run the kernel원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* from load address aligned to 2MB addr, otherwise decompress and원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* run the kernel from LOAD_PHYSICAL_ADDR원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* We cannot rely on the calculation done in 32-bit mode, since we원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* may have been invoked via the 64-bit entry point.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Start with the delta to where the kernel will run at. */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
#ifdef CONFIG_RELOCATABLE빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
leaq startup_32(%rip) /* - $startup_32 */, %rbp주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
movl BP_kernel_alignment(%rsi), %eax주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
decl %eax이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
addq %rax, %rbp주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.
notq %rax이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
andq %rax, %rbp주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.
cmpq $LOAD_PHYSICAL_ADDR, %rbp직전에 만든 값이나 flag를 검사해 경로를 나눈다. 실패 분기가 어디에서 멈추는지까지 따라가야 조건의 의미가 완성된다.
jae 1f이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
#endif빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
movq $LOAD_PHYSICAL_ADDR, %rbp주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
1:분기 대상 label이다. 이 지점에 들어올 수 있는 모든 선행 경로가 같은 register와 stack 조건을 만족하는지 비교한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Target address to relocate to for decompression */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
movl BP_init_size(%rsi), %ebx주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
subl $ rva(_end), %ebx주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.
addq %rbp, %rbx주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Set up the stack */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
leaq rva(boot_stack_end)(%rbx), %rsp주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* At this point we are in long mode with 4-level paging enabled,원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* but we might want to enable 5-level paging or vice versa.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* The problem is that we cannot do it directly. Setting or clearing원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* CR4.LA57 in long mode would trigger #GP. So we need to switch off원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* long mode and paging first.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* We also need a trampoline in lower memory to switch over from원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* 4- to 5-level paging for cases when the bootloader puts the kernel원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* above 4G, but didn't enable 5-level paging for us.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* The same trampoline can be used to switch from 5- to 4-level paging원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* mode, like when starting 4-level paging kernel via kexec() when원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* original kernel worked in 5-level paging mode.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* For the trampoline, we need the top page table to reside in lower원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* memory as we don't have a way to load 64-bit values into CR3 in원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* 32-bit mode.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Make sure we have GDT with 32-bit code segment */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
leaq gdt64(%rip), %rax주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
addq %rax, 2(%rax)주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.
lgdt (%rax)descriptor table의 base와 limit를 CPU에 적재한다. 메모리에 표가 존재하는 것과 CPU register가 그 표를 가리키는 것은 별개다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Reload CS so IRET returns to a CS actually in the GDT */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
pushq $__KERNEL_CS현재 stack에 값을 쌓는다. 이 줄을 읽을 때는 SP가 가리키는 메모리가 실제로 쓰기 가능한지와 ABI 정렬을 함께 확인한다.
leaq .Lon_kernel_cs(%rip), %rax주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
pushq %rax현재 stack에 값을 쌓는다. 이 줄을 읽을 때는 SP가 가리키는 메모리가 실제로 쓰기 가능한지와 ABI 정렬을 함께 확인한다.
lretq일반 분기가 아니라 privilege 또는 code segment 상태를 함께 복원하는 제어 이전이다. frame 형식이 틀리면 다음 명령에 도달하지 못한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
.Lon_kernel_cs:분기 대상 label이다. 이 지점에 들어올 수 있는 모든 선행 경로가 같은 register와 stack 조건을 만족하는지 비교한다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* RSI holds a pointer to a boot_params structure provided by the원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* loader, and this needs to be preserved across C function calls. So원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* move it into a callee saved register.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
movq %rsi, %r15loader가 넘긴 boot_params pointer를 callee-saved %r15에 보존한다. 이후 C 함수를 호출해도 살아 있어야 한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
call load_stage1_idt하위 루틴으로 진입한다. 호출 직전 argument register, stack 정렬, 보존 register가 해당 ABI를 만족하는지 확인한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
#ifdef CONFIG_AMD_MEM_ENCRYPT빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* Now that the stage1 interrupt handlers are set up, #VC exceptions from원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* CPUID instructions can be properly handled for SEV-ES guests.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* For SEV-SNP, the CPUID table also needs to be set up in advance of any원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* CPUID instructions being issued, so go ahead and do that now via원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* sev_enable(), which will also handle the rest of the SEV-related원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* detection/setup to ensure that has been done in advance of any dependent원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* code. Pass the boot_params pointer as the first argument.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
movq %r15, %rdi주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
call sev_enable하위 루틴으로 진입한다. 호출 직전 argument register, stack 정렬, 보존 register가 해당 ABI를 만족하는지 확인한다.
#endif빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Preserve only the CR4 bits that must be preserved, and clear the rest */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
movq %cr4, %rax주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
andl $(X86_CR4_PAE | X86_CR4_MCE | X86_CR4_LA57), %eax주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.
movq %rax, %cr4주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* configure_5level_paging() updates the number of paging levels using현재 CR4.LA57과 kernel이 원하는 paging level이 다르면 long mode를 잠시 벗어나는 trampoline이 이를 맞춘다.
* a trampoline in 32-bit addressable memory if the current number does원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* not match the desired number.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* Pass the boot_params pointer as the first argument. The second원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* argument is the relocated address of the page table to use instead원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* of the page table in trampoline memory (if required).원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
movq %r15, %rdi주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
leaq rva(top_pgtable)(%rbx), %rsi주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
call configure_5level_paging현재 CR4.LA57과 kernel이 원하는 paging level이 다르면 long mode를 잠시 벗어나는 trampoline이 이를 맞춘다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Zero EFLAGS */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
pushq $0현재 stack에 값을 쌓는다. 이 줄을 읽을 때는 SP가 가리키는 메모리가 실제로 쓰기 가능한지와 ABI 정렬을 함께 확인한다.
popfq이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* Copy the compressed kernel to the end of our buffer원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* where decompression in place becomes safe.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
leaq (_bss-8)(%rip), %rsi주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
leaq rva(_bss-8)(%rbx), %rdi주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
movl $(_bss - startup_32), %ecx주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
shrl $3, %ecx이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
std이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
rep movsqDF를 세운 상태에서 높은 주소에서 낮은 주소 방향으로 compressed image를 복사한다. 아직 읽지 않은 source를 보호하는 핵심 구간이다.
cld문자열 명령의 방향을 증가 방향으로 고정한다. 펌웨어가 남긴 DF 상태를 C ABI가 기대하는 값으로 되돌리는 줄이다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* The GDT may get overwritten either during the copy we just did or원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* during extract_kernel below. To avoid any issues, repoint the GDTR원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* to the new copy of the GDT.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
leaq rva(gdt64)(%rbx), %rax주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
leaq rva(gdt)(%rbx), %rdx주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
movq %rdx, 2(%rax)주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
lgdt (%rax)descriptor table의 base와 limit를 CPU에 적재한다. 메모리에 표가 존재하는 것과 CPU register가 그 표를 가리키는 것은 별개다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* Jump to the relocated address.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
leaq rva(.Lrelocated)(%rbx), %rax주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
jmp *%rax새 위치의 .Lrelocated로 비복귀 분기한다. GDT도 새 copy를 가리키도록 바꾼 뒤여야 한다.
SYM_CODE_END(startup_64)linker와 assembler가 진입 symbol, section 또는 정렬을 확정하는 지시자다. runtime 명령은 아니지만 entry address와 배치를 결정한다.
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)38-141줄 주석
SYM_CODE_START_NOALIGN(startup_64)linker와 assembler가 진입 symbol, section 또는 정렬을 확정하는 지시자다. runtime 명령은 아니지만 entry address와 배치를 결정한다.
UNWIND_HINT_END_OF_STACK이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* At this point the CPU runs in 64bit mode CS.L = 1 CS.D = 0,원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* and someone has loaded an identity mapped page table원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* for us. These identity mapped page tables map all of the원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* kernel pages and possibly all of memory.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* %RSI holds the physical address of the boot_params structure원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* provided by the bootloader. Preserve it in %R15 so C function calls원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* will not clobber it.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* We come here either directly from a 64bit bootloader, or from원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* arch/x86/boot/compressed/head_64.S.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* We only come here initially at boot nothing else comes here.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* Since we may be loaded at an address different from what we were원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* compiled to run at we first fixup the physical addresses in our page원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* tables and then reload them.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
mov %rsi, %r15물리 주소인 boot_params를 C 호출 사이에 보존한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Set up the stack for verify_cpu() */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
leaq __top_init_kernel_stack(%rip), %rsp검증 C 함수를 호출할 수 있도록 boot CPU의 초기 stack top을 선택한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* Set up GSBASE.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* Note that on SMP the boot CPU uses the init data section until원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* the per-CPU areas are set up.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
movl $MSR_GS_BASE, %ecx주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
xorl %eax, %eax이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
xorl %edx, %edx이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
wrmsrECX로 선택한 model-specific register에 EDX:EAX 값을 기록한다. MSR 종류와 CPU feature 조건을 함께 확인한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
call __pi_startup_64_setup_gdt_idt하위 루틴으로 진입한다. 호출 직전 argument register, stack 정렬, 보존 register가 해당 ABI를 만족하는지 확인한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Now switch to __KERNEL_CS so IRET works reliably */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
pushq $__KERNEL_CS현재 stack에 값을 쌓는다. 이 줄을 읽을 때는 SP가 가리키는 메모리가 실제로 쓰기 가능한지와 ABI 정렬을 함께 확인한다.
leaq .Lon_kernel_cs(%rip), %rax주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
pushq %rax현재 stack에 값을 쌓는다. 이 줄을 읽을 때는 SP가 가리키는 메모리가 실제로 쓰기 가능한지와 ABI 정렬을 함께 확인한다.
lretq일반 분기가 아니라 privilege 또는 code segment 상태를 함께 복원하는 제어 이전이다. frame 형식이 틀리면 다음 명령에 도달하지 못한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
.Lon_kernel_cs:분기 대상 label이다. 이 지점에 들어올 수 있는 모든 선행 경로가 같은 register와 stack 조건을 만족하는지 비교한다.
ANNOTATE_NOENDBR이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
UNWIND_HINT_END_OF_STACK이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
#ifdef CONFIG_AMD_MEM_ENCRYPT빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* Activate SEV/SME memory encryption if supported/enabled. This needs to원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* be done now, since this also includes setup of the SEV-SNP CPUID table,원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* which needs to be done before any CPUID instructions are executed in원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* subsequent code. Pass the boot_params pointer as the first argument.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
movq %r15, %rdi주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
call __pi_sme_enable하위 루틴으로 진입한다. 호출 직전 argument register, stack 정렬, 보존 register가 해당 ABI를 만족하는지 확인한다.
#endif빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Sanitize CPU configuration */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
call verify_cpu하위 루틴으로 진입한다. 호출 직전 argument register, stack 정렬, 보존 register가 해당 ABI를 만족하는지 확인한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* Derive the kernel's physical-to-virtual offset from the physical and원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* virtual addresses of common_startup_64().원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
leaq common_startup_64(%rip), %rdi주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
subq .Lcommon_startup_64(%rip), %rdiruntime RIP와 link-time virtual address 차이로 물리-가상 offset을 계산한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* Perform pagetable fixups. Additionally, if SME is active, encrypt원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* the kernel and retrieve the modifier (SME encryption mask if SME원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* is active) to be added to the initial pgdir entry that will be원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* programmed into CR3.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
movq %r15, %rsi주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
call __pi___startup_64position-independent C helper가 초기 page table entry를 실제 load address에 맞게 고친다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Form the CR3 value being sure to include the CR3 modifier */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
leaq early_top_pgt(%rip), %rcx주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
addq %rcx, %rax주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
#ifdef CONFIG_AMD_MEM_ENCRYPT빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
mov %rax, %rdi주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* For SEV guests: Verify that the C-bit is correct. A malicious원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* hypervisor could lie about the C-bit position to perform a ROP원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* attack on the guest by writing to the unencrypted stack and wait for원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* the next RET instruction.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
call sev_verify_cbit하위 루틴으로 진입한다. 호출 직전 argument register, stack 정렬, 보존 register가 해당 ABI를 만족하는지 확인한다.
#endif빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* Switch to early_top_pgt which still has the identity mappings원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* present.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
movq %rax, %cr3고쳐진 early_top_pgt를 활성화한다. 아직 identity mapping을 유지하므로 바로 다음 fetch가 끊기지 않는다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Branch to the common startup code at its kernel virtual address */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
ANNOTATE_RETPOLINE_SAFE이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
jmp *.Lcommon_startup_64(%rip)이제 link-time kernel virtual address를 사용해 공통 시작 코드로 이동한다.
SYM_CODE_END(startup_64)linker와 assembler가 진입 symbol, section 또는 정렬을 확정하는 지시자다. runtime 명령은 아니지만 entry address와 배치를 결정한다.
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");407-499줄 주석
asmlinkage __visible void *extract_kernel(void *rmode, unsigned char *output)이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
{이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
unsigned long virt_addr = LOAD_PHYSICAL_ADDR;이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
memptr heap = (memptr)boot_heap;이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
unsigned long needed_size;이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
size_t entry_offset;이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Retain x86 boot parameters pointer passed from startup_32/64. */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
boot_params_ptr = rmode;assembly가 %rsi로 보존해 온 zero-page pointer의 소유권을 decompressor C 코드가 넘겨받는다. 이 4KiB가 출력 범위와 겹치면 뒤의 memory map과 command line 해석이 동시에 무너진다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Clear flags intended for solely in-kernel use. */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
boot_params_ptr->hdr.loadflags &= ~KASLR_FLAG;이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
parse_mem_encrypt(&boot_params_ptr->hdr);이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
sanitize_boot_params(boot_params_ptr);이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
if (boot_params_ptr->screen_info.orig_video_mode == 7) {이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
vidmem = (char *) 0xb0000;이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
vidport = 0x3b4;이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
} else {이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
vidmem = (char *) 0xb8000;이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
vidport = 0x3d4;이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
}이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
lines = boot_params_ptr->screen_info.orig_video_lines;이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
cols = boot_params_ptr->screen_info.orig_video_cols;이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
init_default_io_ops();이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* Detect TDX guest environment.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* It has to be done before console_init() in order to use원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* paravirtualized port I/O operations if needed.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
early_tdx_detect();이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
early_sev_detect();이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
console_init();이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* Save RSDP address for later use. Have this after console_init()원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* so that early debugging output from the RSDP parsing code can be원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* collected.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
boot_params_ptr->acpi_rsdp_addr = get_rsdp_addr();이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
debug_putstr("early console in extract_kernel\n");이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
free_mem_ptr = heap; /* Heap */이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
free_mem_end_ptr = heap + BOOT_HEAP_SIZE;이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* The memory hole needed for the kernel is the larger of either원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* the entire decompressed kernel plus relocation table, or the원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* entire decompressed kernel plus .bss and .brk sections.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* On X86_64, the memory is mapped with PMD pages. Round the원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* size up so that the full extent of PMD pages mapped is원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* included in the check against the valid memory table원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* entries. This ensures the full mapped area is usable RAM원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* and doesn't include any reserved areas.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
needed_size = max_t(unsigned long, output_len, kernel_total_size);압축 해제된 ELF byte 수만 보지 않고 relocation table, BSS와 brk까지 포함한 두 후보 중 큰 값을 선택한다. KASLR hole 검사는 이 전체 수명을 수용해야 한다.
#ifdef CONFIG_X86_64빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
needed_size = ALIGN(needed_size, MIN_KERNEL_ALIGN);x86-64 early mapping이 PMD 단위이므로 마지막 byte가 속한 2MiB frame까지 usable RAM인지 검사하기 위해 범위를 올림한다.
#endif빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Report initial kernel position details. */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
debug_putaddr(input_data);이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
debug_putaddr(input_len);이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
debug_putaddr(output);이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
debug_putaddr(output_len);이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
debug_putaddr(kernel_total_size);이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
debug_putaddr(needed_size);이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
#ifdef CONFIG_X86_64빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
/* Report address of 32-bit trampoline */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
debug_putaddr(trampoline_32bit);이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
#endif빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
choose_random_location((unsigned long)input_data, input_len,입력 payload 범위와 필요한 출력 크기를 함께 넘겨 물리 destination과 kernel virtual address를 고른다. 반환된 두 주소는 같은 값일 필요가 없다.
(unsigned long *)&output,이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
needed_size,이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
&virt_addr);이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Validate memory location choices. */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
if ((unsigned long)output & (MIN_KERNEL_ALIGN - 1))이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
error("Destination physical address inappropriately aligned");선택 결과를 신뢰하지 않고 assembly contract와 같은 MIN_KERNEL_ALIGN 제약을 C 경계에서 다시 검증한다.
if (virt_addr & (MIN_KERNEL_ALIGN - 1))이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
error("Destination virtual address inappropriately aligned");이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
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