DUJINLABS.COM

Linux v6.18.37 · ARM64

ARM64 head.S
물리 주소의 진입점에서 C 커널까지

ARM64 kernel · 다른 .S 파일 해설

부트 CPU·보조 CPU의 전체 경로와, 그 밖으로 연결되는 페이지 테이블·캐시·EL2 초기화 코드를 함께 읽습니다.

원본 기반 분석 · 직접 제작한 도식 · 보드 실행 검증은 별도

01. 분석 기준과 Image의 앞부분

head.S L1–L43 · 원본 대조

arch/arm64/kernel/head.S · L1–L43
1/* SPDX-License-Identifier: GPL-2.0-only */
2/*
3 * Low-level CPU initialisation
4 * Based on arch/arm/kernel/head.S
5 *
6 * Copyright (C) 1994-2002 Russell King
7 * Copyright (C) 2003-2012 ARM Ltd.
8 * Authors:	Catalin Marinas <[email protected]>
9 *		Will Deacon <[email protected]>
10 */
11
12#include <linux/linkage.h>
13#include <linux/init.h>
14#include <linux/pgtable.h>
15
16#include <asm/asm_pointer_auth.h>
17#include <asm/assembler.h>
18#include <asm/boot.h>
19#include <asm/bug.h>
20#include <asm/ptrace.h>
21#include <asm/asm-offsets.h>
22#include <asm/cache.h>
23#include <asm/cputype.h>
24#include <asm/el2_setup.h>
25#include <asm/elf.h>
26#include <asm/image.h>
27#include <asm/kernel-pgtable.h>
28#include <asm/kvm_arm.h>
29#include <asm/memory.h>
30#include <asm/pgtable-hwdef.h>
31#include <asm/page.h>
32#include <asm/scs.h>
33#include <asm/smp.h>
34#include <asm/sysreg.h>
35#include <asm/stacktrace/frame.h>
36#include <asm/thread_info.h>
37#include <asm/virt.h>
38
39#include "efi-header.S"
40
41#if (PAGE_OFFSET & 0x1fffff) != 0
42#error PAGE_OFFSET must be at least 2MB aligned
43#endif

Linux v6.18.37의 ARM64 부팅 코드인 arch/arm64/kernel/head.S를 살펴봅니다. 파일은 523줄이며, 실행 흐름에 따라 구간을 나누었습니다. ARM32의 arch/arm/kernel/head.S는 레지스터와 부팅 규약이 다른 별도 파일입니다.

1–10행은 라이선스와 코드의 작성 이력입니다. .S는 전처리를 거치는 어셈블리 파일입니다. 따라서 12–39행의 헤더와 CONFIG_*에 따라 최종 명령열이 달라집니다. __HEAD·__INIT·.section은 코드가 놓일 ELF 섹션을 선택합니다. SYM_*는 종류와 아키텍처별 정의를 확인해야 합니다. SYM_INNER_LABEL은 심볼 정보를 선언하지만 ARM64의 SYM_FUNC_START 계열은 실제 bti c 명령도 추가합니다. 코드의 매크로 이름을 누르면 정의·확장 설명으로 이동합니다.

asm-offsets.h는 C 구조체의 필드 위치를 어셈블리에서 쓰게 해 줍니다. TSK_STACK, TSK_TI_CPU, CPU_BOOT_TASK는 구조체와 빌드 설정에 따라 값이 정해집니다. assembler.h는 주소 생성·캐시·시스템 레지스터 매크로, el2_setup.h는 EL2 초기화, kernel-pgtable.h는 초기 테이블 크기와 레벨을 제공합니다.

41–43행은 PAGE_OFFSET의 하위 21비트가 0인지 검사합니다. 실패하면 실행 중 예외가 아니라 빌드 오류입니다. 이는 2 MiB 정렬 검사이며, CPU 페이지 크기를 무조건 2 MiB로 고른다는 뜻은 아닙니다.

표기 규칙: PA는 물리 주소, VA는 가상 주소입니다. [start,end)는 끝 주소를 포함하지 않습니다. 도식의 숫자 주소와 4 KiB 페이지·64 B 캐시 라인 예시는 계산을 보여 주기 위한 가정이며, 실제 보드의 측정값이 아닙니다. xN은 64비트, wN은 같은 레지스터의 하위 32비트입니다. wN에 쓰면 상위 32비트는 0이 됩니다.

02. 64바이트 Image 헤더와 실행 전 필요한 조건

head.S L44–L73 · 원본 대조

arch/arm64/kernel/head.S · L44–L73
44
45/*
46 * Kernel startup entry point.
47 * ---------------------------
48 *
49 * The requirements are:
50 *   MMU = off, D-cache = off, I-cache = on or off,
51 *   x0 = physical address to the FDT blob.
52 *
53 * Note that the callee-saved registers are used for storing variables
54 * that are useful before the MMU is enabled. The allocations are described
55 * in the entry routines.
56 */
57	__HEAD
58	/*
59	 * DO NOT MODIFY. Image header expected by Linux boot-loaders.
60	 */
61	efi_signature_nop			// special NOP to identity as PE/COFF executable
62	b	primary_entry			// branch to kernel start, magic
63	.quad	0				// Image load offset from start of RAM, little-endian
64	le64sym	_kernel_size_le			// Effective size of kernel image, little-endian
65	le64sym	_kernel_flags_le		// Informative flags, little-endian
66	.quad	0				// reserved
67	.quad	0				// reserved
68	.quad	0				// reserved
69	.ascii	ARM64_IMAGE_MAGIC		// Magic number
70	.long	.Lpe_header_offset		// Offset to the PE header.
71
72	__EFI_PE_HEADER
73

50–51행에는 커널 실행 전에 갖춰야 할 조건이 적혀 있습니다. MMU와 데이터 캐시는 꺼져 있어야 하며, 명령어 캐시는 켜져 있어도 꺼져 있어도 됩니다. x0에는 FDT의 물리 주소를 전달해야 합니다. 부트 프로토콜에서 x1–x3은 0입니다. 이 버전의 setup.c는 비 EFI 부팅에서 MMU·캐시 on이 기록되면 경고하고 나중에 panic하는 검사까지 포함합니다.

61행의 efi_signature_nop는 EFI 설정에서 ccmp x18,#0,#0xd,pl로 확장됩니다. 명령 인코딩의 앞 두 바이트가 PE의 MZ 서명이 됩니다. 실제 NOP 인코딩은 아니며 조건 플래그에 영향을 줄 수 있지만, 뒤의 부트 코드가 그 플래그에 의존하지 않습니다. EFI를 끄면 일반 nop다.

62행의 b primary_entry는 LR을 갱신하지 않는 분기입니다. CPU는 뒤의 크기·플래그 데이터를 명령으로 실행하지 않습니다. EFI 로더는 PE 헤더의 AddressOfEntryPoint에 있는 EFI stub 진입점을 사용합니다. 일반 Image 진입과 PE 로더 진입은 구별해야 합니다.

Image 시작 기준크기내용
+0x004 BEFI MZ 의사 NOP 또는 NOP
+0x044 Bprimary_entry로 분기
+0x088 B이 소스에서는 text_offset=0
+0x108 B_kernel_size_le
+0x188 B_kernel_flags_le
+0x20,+0x28,+0x30각 8 B예약 필드 0
+0x384 BARM64 Image magic
+0x3c4 BPE 헤더 오프셋

le64sym는 대상 커널의 엔디언과 관계없이 규약의 little-endian 필드를 만듭니다. 실제 크기와 플래그는 kernel/image.h와 링커 심볼에 의해 결정됩니다. 앞의 공통 Image 헤더는 64바이트이며, 72행의 __EFI_PE_HEADER가 만드는 PE 헤더는 그 뒤에 붙는입니다.

02. 64바이트 Image 헤더와 실행 전 필요한 조건
그림을 누르면 원본 크기로 열립니다.

03. primary_entry: 부트 CPU의 실행 순서

head.S L74–L132 · 원본 대조

arch/arm64/kernel/head.S · L74–L132
74	.section ".idmap.text","a"
75
76	/*
77	 * The following callee saved general purpose registers are used on the
78	 * primary lowlevel boot path:
79	 *
80	 *  Register   Scope                      Purpose
81	 *  x19        primary_entry() .. start_kernel()        whether we entered with the MMU on
82	 *  x20        primary_entry() .. __primary_switch()    CPU boot mode
83	 *  x21        primary_entry() .. start_kernel()        FDT pointer passed at boot in x0
84	 */
85SYM_CODE_START(primary_entry)
86	bl	record_mmu_state
87	bl	preserve_boot_args
88
89	adrp	x1, early_init_stack
90	mov	sp, x1
91	mov	x29, xzr
92	adrp	x0, __pi_init_idmap_pg_dir
93	mov	x1, xzr
94	bl	__pi_create_init_idmap
95
96	/*
97	 * If the page tables have been populated with non-cacheable
98	 * accesses (MMU disabled), invalidate those tables again to
99	 * remove any speculatively loaded cache lines.
100	 */
101	cbnz	x19, 0f
102	dmb     sy
103	mov	x1, x0				// end of used region
104	adrp    x0, __pi_init_idmap_pg_dir
105	adr_l	x2, dcache_inval_poc
106	blr	x2
107	b	1f
108
109	/*
110	 * If we entered with the MMU and caches on, clean the ID mapped part
111	 * of the primary boot code to the PoC so we can safely execute it with
112	 * the MMU off.
113	 */
1140:	adrp	x0, __idmap_text_start
115	adr_l	x1, __idmap_text_end
116	adr_l	x2, dcache_clean_poc
117	blr	x2
118
1191:	mov	x0, x19
120	bl	init_kernel_el			// w0=cpu_boot_mode
121	mov	x20, x0
122
123	/*
124	 * The following calls CPU setup code, see arch/arm64/mm/proc.S for
125	 * details.
126	 * On return, the CPU will be ready for the MMU to be turned on and
127	 * the TCR will have been set.
128	 */
129	bl	__cpu_setup			// initialise processor
130	b	__primary_switch
131SYM_CODE_END(primary_entry)
132

이 함수는 초기화 함수들을 필요한 순서대로 호출합니다. 본문에서 호출되는 함수가 파일 뒤쪽에 있더라도 실행은 bl이 있는 위치에서 시작됩니다. 먼저 x19에 진입 상태를 기록하고, x21과 boot_args에 인자를 보존합니다. 이 둘은 스택 없이 수행됩니다. bl은 반환 주소를 x30에 넣으므로, 모든 호출이 처음부터 메모리 스택을 필요로 하는 것은 아닙니다.

입력 → 결과왜 이 순서인가
86–87진입 SCTLR·x0–x3 → x19·x21·boot_args임시 레지스터 재사용 전에 상태를 보존
89–91early_init_stack → sp, x29=0초기 C 함수가 사용할 스택과 프레임 끝 설정
92–94x0=idmap 루트, x1=0 → x0=할당 끝create_init_idmap이 초기 identity mapping 생성
101–107x19=0 → 사용한 테이블 범위 invalidateMMU off 쓰기와 speculative cache line 사이 불일치 방지
114–117x19≠0 → idmap 코드 cleanMMU를 끈 뒤에도 최신 명령을 읽도록 준비
119–121x0=x19 → 부트 모드와 플래그를 x20에 저장예외 레벨과 제어 상태 정리
129–130__cpu_setup → x0=SCTLR on 값준비된 값으로 __primary_switch에 진입

early_init_stack는 링커가 4 KiB를 예약한 뒤 붙이는 상단 심볼입니다. 스택은 낮은 주소 방향으로 늘어납니다. 이 임시 스택은 뒤에서 init_task의 스택으로 교체합니다.

__pi_create_init_idmap의 반환 x0는 매핑한 커널 끝 주소가 아니라 페이지 테이블용으로 사용한 메모리의 끝입니다. 103행에서 이를 x1으로 옮기고 x0를 테이블 시작으로 다시 설정합니다. 따라서 캐시 작업의 대상은 커널 전체 데이터가 아니라 생성한 테이블 범위입니다.

adrp는 PC 상대 페이지 주소를, adr_l는 페이지 주소와 하위 오프셋을 합친 주소를 만듭니다. 페이지 정렬 심볼에는 adrp만으로 충분합니다. MMU off/identity mapping 단계에서는 실행 위치에 따라 물리 위치를 얻고, 최종 가상 주소로 이동한 뒤에는 같은 종류의 주소 생성이 VA를 얻습니다. adrp 자체가 보편적인 VA→PA 변환 명령인 것은 아닙니다.

x19·x20·x21은 함수를 호출한 뒤에도 유지해야 하는 값를 담는 callee-saved 레지스터입니다. x0은 FDT, 테이블 루트, 할당 끝, 상태, SCTLR 값으로 계속 재사용됩니다. x0을 파일 전체에서 FDT라고 읽으면 뒤의 호출을 모두 잘못 해석합니다.

03. primary_entry: 부트 CPU의 실행 순서
그림을 누르면 원본 크기로 열립니다.

04. record_mmu_state: 엔디언, M, C와 NZCV

head.S L133–L166 · 원본 대조

arch/arm64/kernel/head.S · L133–L166
133	__INIT
134SYM_CODE_START_LOCAL(record_mmu_state)
135	mrs	x19, CurrentEL
136	cmp	x19, #CurrentEL_EL2
137	mrs	x19, sctlr_el1
138	b.ne	0f
139	mrs	x19, sctlr_el2
1400:
141CPU_LE( tbnz	x19, #SCTLR_ELx_EE_SHIFT, 1f	)
142CPU_BE( tbz	x19, #SCTLR_ELx_EE_SHIFT, 1f	)
143	tst	x19, #SCTLR_ELx_C		// Z := (C == 0)
144	and	x19, x19, #SCTLR_ELx_M		// isolate M bit
145	csel	x19, xzr, x19, eq		// clear x19 if Z
146	ret
147
148	/*
149	 * Set the correct endianness early so all memory accesses issued
150	 * before init_kernel_el() occur in the correct byte order. Note that
151	 * this means the MMU must be disabled, or the active ID map will end
152	 * up getting interpreted with the wrong byte order.
153	 */
1541:	eor	x19, x19, #SCTLR_ELx_EE
155	bic	x19, x19, #SCTLR_ELx_M
156	b.ne	2f
157	pre_disable_mmu_workaround
158	msr	sctlr_el2, x19
159	b	3f
1602:	pre_disable_mmu_workaround
161	msr	sctlr_el1, x19
1623:	isb
163	mov	x19, xzr
164	ret
165SYM_CODE_END(record_mmu_state)
166

135–139행은 CurrentEL을 읽고, EL2이면 SCTLR_EL2, 아니면 SCTLR_EL1을 x19에 남깁니다. EL2인 경우에도 먼저 SCTLR_EL1을 읽은 뒤 EL2 값으로 덮어씁니다. 136행의 CMP가 만든 Z 플래그는 이 시점의 예외 레벨을 기억합니다.

141–142행은 빌드 엔디언에 따라 한쪽만 남습니다. little-endian 커널은 EE=1일 때, big-endian 커널은 EE=0일 때 1번 라벨로 갑니다. 여기서 TBNZ/TBZ는 NZCV를 바꾸지 않습니다.

정상 엔디언 경로에서 TST는 C 비트가 0이면 Z=1로 만듭니다. 그 다음 and x19,x19,#M은 S 접미사가 없으므로 플래그를 바꾸지 않습니다. csel x19,xzr,x19,eq는 C가 0일 때 x19를 0으로 만들고, C가 1일 때 M 값을 선택합니다. 따라서 결과는 다음과 같습니다.

엔디언 일치MC반환 x19
000
010
100
111
아니오임의임의엔디언 수정 및 M=0 후 0

엔디언 불일치 경로는 TST보다 먼저 갈라집니다. EOR로 EE를 뒤집고 BIC로 M을 지운 뒤 156행의 b.ne는 여전히 136행 CMP의 결과를 사용합니다. EL2이면 EQ라서 SCTLR_EL2에 쓰고, 그 밖의 EL1 경로이면 NE라서 SCTLR_EL1에 씁니다. EOR/BIC도 S가 없으므로 플래그를 보존합니다. 이 연결을 놓치면 156행의 NE가 무엇을 비교하는지 설명할 수 없습니다.

엔디언을 바꾸면서 MMU를 끄는 이유는 활성 페이지 테이블을 새 바이트 순서로 잘못 해석하는 것을 피하기 위해서입니다. pre_disable_mmu_workaround는 해당 Qualcomm erratum 설정에서 SCTLR 쓰기 바로 전에 ISB를 넣습니다. 162행의 ISB는 변경된 실행 제어가 뒤 명령에 반영되게 합니다. 반환 x19=0은 원래 M이 언제나 0이었다는 기록은 아닙니다. 이 값은 엔디언을 조정한 뒤 실행할 경로를 선택하는 데 사용합니다.

04. record_mmu_state: 엔디언, M, C와 NZCV
그림을 누르면 원본 크기로 열립니다.

05. preserve_boot_args: 32바이트 저장과 tail call

head.S L167–L186 · 원본 대조

arch/arm64/kernel/head.S · L167–L186
167/*
168 * Preserve the arguments passed by the bootloader in x0 .. x3
169 */
170SYM_CODE_START_LOCAL(preserve_boot_args)
171	mov	x21, x0				// x21=FDT
172
173	adr_l	x0, boot_args			// record the contents of
174	stp	x21, x1, [x0]			// x0 .. x3 at kernel entry
175	stp	x2, x3, [x0, #16]
176
177	cbnz	x19, 0f				// skip cache invalidation if MMU is on
178	dmb	sy				// needed before dc ivac with
179						// MMU off
180
181	add	x1, x0, #0x20			// 4 x 8 bytes
182	b	dcache_inval_poc		// tail call
1830:	str_l   x19, mmu_enabled_at_boot, x0
184	ret
185SYM_CODE_END(preserve_boot_args)
186

171행은 원래 x0를 x21로 먼저 복사합니다. 173행에서 x0가 boot_args의 주소로 바뀌기 때문입니다. 첫 STP는 원래 x0와 x1을, 두 번째 STP는 x2와 x3을 저장합니다. 64비트 두 개씩이므로 각각 16바이트, 합계 32바이트입니다. FDT 본문을 복사하는 코드가 아닙니다. FDT를 가리키는 8바이트 주소 하나만 저장합니다.

boot_args 오프셋저장 값나중의 의미
+0x00진입 x0, 즉 x21FDT PA
+0x08진입 x1부트 규약 위반 진단
+0x10진입 x2부트 규약 위반 진단
+0x18진입 x3부트 규약 위반 진단

177행에서 x19≠0이면 캐시 무효화를 건너뛰고 mmu_enabled_at_boot에 상태를 씁니다. 이 경로의 x0는 저장 매크로의 임시 주소 레지스터로 또 재사용됩니다. x19=0이면 DMB 뒤에 x1을 boot_args+0x20으로 만들고 [x0,x1)를 invalidate합니다.

182행은 BL이 아닌 B다. 따라서 preserve_boot_args를 불렀을 때 설정된 LR이 그대로 유지됩니다. dcache_inval_poc의 RET가 primary_entry의 88행 다음으로 돌아오는 tail call입니다. 중간 함수가 다시 돌아와 수행할 일이 없으므로 추가 반환 단계를 만들지 않습니다.

DMB는 MMU off 메모리 쓰기와 뒤의 캐시 관리 작업 사이의 순서를 보장합니다. 실제 cache line 작업과 완료 대기는 helper 안의 DC 명령 및 DSB가 담당합니다. 캐시 라인의 일부만 처리할 때 사용하는 CIVAC는 부록 C에서 설명합니다.

05. preserve_boot_args: 32바이트 저장과 tail call
그림을 누르면 원본 크기로 열립니다.

06. init_cpu_task: current, 스택, 프레임 끝, per-CPU

head.S L187–L214 · 원본 대조

arch/arm64/kernel/head.S · L187–L214
187	/*
188	 * Initialize CPU registers with task-specific and cpu-specific context.
189	 *
190	 * Create a final frame record at task_pt_regs(current)->stackframe, so
191	 * that the unwinder can identify the final frame record of any task by
192	 * its location in the task stack. We reserve the entire pt_regs space
193	 * for consistency with user tasks and kthreads.
194	 */
195	.macro	init_cpu_task tsk, tmp1, tmp2
196	msr	sp_el0, \tsk
197
198	ldr	\tmp1, [\tsk, #TSK_STACK]
199	add	sp, \tmp1, #THREAD_SIZE
200	sub	sp, sp, #PT_REGS_SIZE
201
202	stp	xzr, xzr, [sp, #S_STACKFRAME]
203	mov	\tmp1, #FRAME_META_TYPE_FINAL
204	str	\tmp1, [sp, #S_STACKFRAME_TYPE]
205	add	x29, sp, #S_STACKFRAME
206
207	scs_load_current
208
209	adr_l	\tmp1, __per_cpu_offset
210	ldr	w\tmp2, [\tsk, #TSK_TI_CPU]
211	ldr	\tmp1, [\tmp1, \tmp2, lsl #3]
212	set_this_cpu_offset \tmp1
213	.endm
214

이 매크로는 부트 CPU와 보조 CPU가 공유합니다. tsk에는 각 CPU에서 실행할 태스크 주소가 들어옵니다. 196행은 SP_EL0에 그 주소를 기록합니다. 여기서 SP_EL0는 커널이 current를 빠르게 얻는 저장소로 사용됩니다. 지금 커널 SP를 태스크 주소로 바꾸는 명령이 아닙니다.

198–200행은 task->stack을 읽고 THREAD_SIZE를 더한 뒤 PT_REGS_SIZE를 뺍니다. 스택 맨 위에 pt_regs 전체 크기를 예약합니다. 실제 예외가 발생해서 모든 레지스터를 저장했다는 뜻은 아닙니다. 공통 스택 형태를 맞추기 위한 예약입니다.

202–205행은 예약 공간 안의 stackframe 위치에 0 두 개를 쓰고, final frame 메타데이터를 설정하고, x29를 그 프레임으로 가리키게 합니다. 호출 스택을 추적하는 unwinder는 이 메타데이터를 보고 추적을 멈춥니다.

207행의 scs_load_current는 CONFIG_SHADOW_CALL_STACK일 때 current의 TSK_TI_SCS_SP에서 shadow stack 포인터를 읽습니다. 비활성 빌드에서는 빈 매크로입니다. 이는 일반 SP와 별도의 반환 주소 보호 경로입니다.

209–212행은 task의 CPU 번호를 w 레지스터로 읽고, 번호×8 위치의 __per_cpu_offset[cpu]를 읽습니다. 배열 원소가 8바이트이므로 lsl #3입니다. set_this_cpu_offset은 VHE capability에 따라 TPIDR_EL1 또는 TPIDR_EL2에 씁니다. per-CPU 변수 자체를 복사하는 코드가 아니라, 해당 CPU에 할당된 변수에 접근할 때 사용하는 오프셋을 설정합니다.

06. init_cpu_task: current, 스택, 프레임 끝, per-CPU
그림을 누르면 원본 크기로 열립니다.

07. __primary_switched: 최종 가상 주소에서 start_kernel까지

head.S L215–L249 · 원본 대조

arch/arm64/kernel/head.S · L215–L249
215/*
216 * The following fragment of code is executed with the MMU enabled.
217 *
218 *   x0 = __pa(KERNEL_START)
219 */
220SYM_FUNC_START_LOCAL(__primary_switched)
221	adr_l	x4, init_task
222	init_cpu_task x4, x5, x6
223
224	adr_l	x8, vectors			// load VBAR_EL1 with virtual
225	msr	vbar_el1, x8			// vector table address
226	isb
227
228	stp	x29, x30, [sp, #-16]!
229	mov	x29, sp
230
231	str_l	x21, __fdt_pointer, x5		// Save FDT pointer
232
233	adrp	x4, _text			// Save the offset between
234	sub	x4, x4, x0			// the kernel virtual and
235	str_l	x4, kimage_voffset, x5		// physical mappings
236
237	mov	x0, x20
238	bl	set_cpu_boot_mode_flag
239
240#if defined(CONFIG_KASAN_GENERIC) || defined(CONFIG_KASAN_SW_TAGS)
241	bl	kasan_early_init
242#endif
243	mov	x0, x20
244	bl	finalise_el2			// Prefer VHE if possible
245	ldp	x29, x30, [sp], #16
246	bl	start_kernel
247	ASM_BUG()
248SYM_FUNC_END(__primary_switched)
249

이 함수는 파일 앞쪽에 있지만 실제 실행은 맨 끝 __primary_switch에서 분기해 온 뒤입니다. 진입 시 MMU는 켜져 있고 PC는 커널의 최종 VA다. x0는 같은 커널 시작의 PA다. x21에는 아직 FDT PA가 남아 있습니다.

221–222행에서 init_task를 기준으로 현재 태스크, 스택, CPU별 오프셋을 설정합니다. 224–226행은 vectors의 가상 주소를 VBAR_EL1에 넣고 ISB를 실행합니다. 이 시점에 커널 예외 벡터 주소를 사용할 수 있게 되는 것이지, IRQ 마스크를 전부 해제하는 것은 아닙니다.

228–229행의 STP는 SP를 16 줄인 뒤 FP/LR을 저장하는 pre-index 형식입니다. 이후 C 함수 호출을 위한 프레임을 연결합니다. 245행 LDP는 값을 읽고 SP를 16 늘리는 post-index 형식으로 짝을 이룹니다.

231행은 FDT PA를 __fdt_pointer에 씁니다. 233–235행은 kimage_voffset = VA(_text) - PA(KERNEL_START)를 저장합니다. memory.h에서 KERNEL_START는 _text다. 커널 이미지 내 심볼의 PA를 얻을 때 이 오프셋을 뺄 수 있습니다. 이는 전체 RAM의 선형 매핑 오프셋과 구분해야 합니다.

237–244행에서는 부트 모드를 기록하고, 소프트웨어 KASAN을 사용하는 구성의 초기 검사 메모리를 준비한 뒤 EL2의 나머지 설정을 처리합니다. KASAN은 메모리 범위를 벗어난 접근과 해제 후 접근 같은 오류를 찾는 기능입니다. CONFIG_KASAN_GENERIC은 접근 가능 여부를, CONFIG_KASAN_SW_TAGS는 포인터와 메모리의 태그를 별도 shadow 메모리로 검사합니다. 두 설정 중 하나를 사용하는 빌드에서만 kasan_early_init을 호출합니다. 하드웨어 MTE 태그를 사용하는 KASAN은 이 조건에 포함되지 않습니다. finalise_el2는 진입 모드·현재 EL·CPU 기능과 override를 보고 VHE 전환을 시도합니다. VHE 전환 조건을 만족하지 않으면 EL1에서 start_kernel을 실행합니다.

246행에서 일반 커널 초기화인 start_kernel을 호출합니다. 이 함수는 반환하지 않아야 하므로, 예상과 달리 반환하면 ASM_BUG를 실행합니다. 이후 스케줄러·메모리·드라이버 초기화는 start_kernel에서 진행합니다.

이 함수에는 BSS를 지우는 루프가 없습니다. 현재 버전에서는 앞서 실행된 __pi_early_map_kernel의 memset이 BSS와 초기 테이블 영역을 지웁니다. 오래된 head.S 해설의 BSS 순서를 그대로 가져오면 이 버전의 실행 순서와 어긋납니다.

07. __primary_switched: 최종 가상 주소에서 start_kernel까지
그림을 누르면 원본 크기로 열립니다.

08. init_kernel_el의 EL1 경로: RET 대신 ERET

head.S L250–L285 · 원본 대조

arch/arm64/kernel/head.S · L250–L285
250/*
251 * end early head section, begin head code that is also used for
252 * hotplug and needs to have the same protections as the text region
253 */
254	.section ".idmap.text","a"
255
256/*
257 * Starting from EL2 or EL1, configure the CPU to execute at the highest
258 * reachable EL supported by the kernel in a chosen default state. If dropping
259 * from EL2 to EL1, configure EL2 before configuring EL1.
260 *
261 * Since we cannot always rely on ERET synchronizing writes to sysregs (e.g. if
262 * SCTLR_ELx.EOS is clear), we place an ISB prior to ERET.
263 *
264 * Returns either BOOT_CPU_MODE_EL1 or BOOT_CPU_MODE_EL2 in x0 if
265 * booted in EL1 or EL2 respectively, with the top 32 bits containing
266 * potential context flags. These flags are *not* stored in __boot_cpu_mode.
267 *
268 * x0: whether we are being called from the primary boot path with the MMU on
269 */
270SYM_FUNC_START(init_kernel_el)
271	mrs	x1, CurrentEL
272	cmp	x1, #CurrentEL_EL2
273	b.eq	init_el2
274
275SYM_INNER_LABEL(init_el1, SYM_L_LOCAL)
276	mov_q	x0, INIT_SCTLR_EL1_MMU_OFF
277	pre_disable_mmu_workaround
278	msr	sctlr_el1, x0
279	isb
280	mov_q	x0, INIT_PSTATE_EL1
281	msr	spsr_el1, x0
282	msr	elr_el1, lr
283	mov	w0, #BOOT_CPU_MODE_EL1
284	eret
285

250–254행은 hotplug에도 필요한 코드가 .idmap.text에 놓이도록 섹션을 바꿉니다. 모든 head.S 코드가 한 번 부팅 후 버리는 __init 코드인 것은 아닙니다.

271–273행의 CurrentEL 검사로 EL2 경로를 분리합니다. EL1 경로에서는 INIT_SCTLR_EL1_MMU_OFF 값을 쓴 뒤 ISB를 실행합니다. 단순히 기존 SCTLR의 M 비트만 지우는 것과 달리, 커널이 정한 초기 제어값을 설치합니다.

275행의 SYM_INNER_LABEL(init_el1, SYM_L_LOCAL)은 init_kernel_el 내부에 init_el1이라는 local 심볼을 정의합니다. 이 버전에서는 .type init_el1 STT_NOTYPEinit_el1:로 풀리며 별도 정렬·BTI·스택 프레임·반환 명령은 만들지 않습니다. 두 번째 인자 SYM_L_LOCAL은 여기서는 빈 확장이라 .globl을 출력하지 않습니다. 뒤의 init_el2도 같은 방식으로 하나의 함수 안에 붙인 분기 목적지입니다. 표기 사전 1에서 SYM_FUNC_START와의 차이 및 실제 헤더를 볼 수 있습니다.

280–284행은 SPSR_EL1에 원하는 PSTATE, ELR_EL1에 호출자의 LR, w0에 BOOT_CPU_MODE_EL1을 넣고 ERET합니다. ERET는 ELR의 주소와 SPSR의 상태를 복원하므로, 주소만 바꾸는 RET와 역할이 다릅니다. 여기서는 같은 EL1으로 돌아오면서 실행 상태를 커널이 요구하는 값으로 맞춥니다.

INIT_PSTATE_EL1은 ptrace.h의 D/A/I/F 마스크와 EL1h 모드 비트를 OR한 값입니다. debug·SError·IRQ·FIQ를 마스크하고 EL1의 SP를 쓰는 상태로 복귀하게 합니다. 이 경로는 임의의 펌웨어 PSTATE를 그대로 이어받는 보통 함수 반환이 아닙니다. SCTLR 쓰기를 ERET가 반드시 동기화해 준다고 가정하지 않기 때문에 명시적인 ISB가 앞에 있습니다.

08. init_kernel_el의 EL1 경로: RET 대신 ERET
그림을 누르면 원본 크기로 열립니다.

09. init_kernel_el의 EL2 경로: HCR, EL12, boot flags

head.S L286–L332 · 원본 대조

arch/arm64/kernel/head.S · L286–L332
286SYM_INNER_LABEL(init_el2, SYM_L_LOCAL)
287	msr	elr_el2, lr
288
289	// clean all HYP code to the PoC if we booted at EL2 with the MMU on
290	cbz	x0, 0f
291	adrp	x0, __hyp_idmap_text_start
292	adr_l	x1, __hyp_text_end
293	adr_l	x2, dcache_clean_poc
294	blr	x2
295
296	mov_q	x0, INIT_SCTLR_EL2_MMU_OFF
297	pre_disable_mmu_workaround
298	msr	sctlr_el2, x0
299	isb
3000:
301
302	init_el2_hcr	HCR_HOST_NVHE_FLAGS
303	init_el2_state
304
305	/* Hypervisor stub */
306	adr_l	x0, __hyp_stub_vectors
307	msr	vbar_el2, x0
308	isb
309
310	mov_q	x1, INIT_SCTLR_EL1_MMU_OFF
311
312	mrs	x0, hcr_el2
313	and	x0, x0, #HCR_E2H
314	cbz	x0, 2f
315
316	/* Set a sane SCTLR_EL1, the VHE way */
317	msr_s	SYS_SCTLR_EL12, x1
318	mov	x2, #BOOT_CPU_FLAG_E2H
319	b	3f
320
3212:
322	msr	sctlr_el1, x1
323	mov	x2, xzr
3243:
325	mov	x0, #INIT_PSTATE_EL1
326	msr	spsr_el2, x0
327
328	mov	w0, #BOOT_CPU_MODE_EL2
329	orr	x0, x0, x2
330	eret
331SYM_FUNC_END(init_kernel_el)
332

287행은 LR을 ELR_EL2에 먼저 보존합니다. 뒤의 cache helper 호출이 x30을 덮어써도 마지막 ERET의 복귀 주소는 유지됩니다. x0≠0이면 HYP 코드 범위를 PoC까지 clean한 뒤 EL2 MMU off 값을 설치합니다.

302행의 init_el2_hcr는 HCR_HOST_NVHE_FLAGS를 기본으로 설정합니다. 하지만 E2H를 0으로 만들 수 없는 VHE 전용 CPU도 처리합니다. ID_AA64MMFR4_EL1의 E2H0 필드를 검사하고, 필요한 경우 FAR_EL1/FAR_EL2 alias 동작으로 유효한 E2H 상태를 판별합니다. 따라서 이 호출 뒤 HCR.E2H가 반드시 0이라는 가정은 성립하지 않습니다.

303행의 init_el2_state는 타이머, 디버그/PMU, stage-2, GIC 시스템 레지스터, trap, 가상 CPU ID 등을 설정합니다. 하위 EL을 실행시키기 전에 상위 EL의 설정 때문에 제한될 수 있는 레지스터 접근을 준비하는 단계입니다. 기능 레지스터를 보고 없는 기능의 설정을 건너뜁니다. 설정 항목별 역할은 EL2 부록의 표에서 설명합니다.

306–308행은 초기 hypervisor stub의 벡터를 VBAR_EL2에 설치합니다. 뒤에서 EL1 코드가 HVC로 finalise 요청을 할 때 이 경로가 필요합니다.

312–323행은 E2H에 따라 하위 EL1의 SCTLR을 설정하는 방법을 고릅니다. E2H=0이면 SCTLR_EL1을 직접 씁니다. E2H=1이면 EL2에서 일부 EL1 이름이 host EL2 레지스터로 재매핑되므로 SYS_SCTLR_EL12를 써야 원하는 EL1 상태를 설정할 수 있습니다. 이때 x2에 bit 32의 BOOT_CPU_FLAG_E2H를 남깁니다.

325–330행은 SPSR_EL2를 EL1 복귀 상태로 설정하고, x0에 BOOT_CPU_MODE_EL2 | flags를 만든 뒤 ERET합니다. 반환 x0의 EL2는 부팅을 시작한 예외 레벨다. 방금 ERET한 뒤의 현재 EL은 EL1입니다. 상위 32비트 flag를 보존하려고 primary_entry는 x0 전체를 x20으로 복사합니다. w0만 저장하면 상위 비트의 E2H 정보는 저장되지 않습니다.

10. 보조 CPU의 두 진입점과 holding pen

head.S L333–L360 · 원본 대조

arch/arm64/kernel/head.S · L333–L360
333	/*
334	 * This provides a "holding pen" for platforms to hold all secondary
335	 * cores are held until we're ready for them to initialise.
336	 */
337SYM_FUNC_START(secondary_holding_pen)
338	mov	x0, xzr
339	bl	init_kernel_el			// w0=cpu_boot_mode
340	mrs	x2, mpidr_el1
341	mov_q	x1, MPIDR_HWID_BITMASK
342	and	x2, x2, x1
343	adr_l	x3, secondary_holding_pen_release
344pen:	ldr	x4, [x3]
345	cmp	x4, x2
346	b.eq	secondary_startup
347	wfe
348	b	pen
349SYM_FUNC_END(secondary_holding_pen)
350
351	/*
352	 * Secondary entry point that jumps straight into the kernel. Only to
353	 * be used where CPUs are brought online dynamically by the kernel.
354	 */
355SYM_FUNC_START(secondary_entry)
356	mov	x0, xzr
357	bl	init_kernel_el			// w0=cpu_boot_mode
358	b	secondary_startup
359SYM_FUNC_END(secondary_entry)
360

secondary_holding_pen은 기다렸다가 출발하는 경로입니다. x0=0으로 init_kernel_el을 호출하므로 primary의 MMU-on 정리 경로를 요청하지 않습니다. 이어 MPIDR_EL1에서 하드웨어 affinity 식별 비트만 남겨 x2에 보관합니다. 이 값은 커널의 논리 CPU 번호 0,1,2와 같은 개념이 아닙니다.

343–348행은 공유 release 값이 자신의 MPIDR HWID와 같을 때만 secondary_startup으로 갑니다. WFE는 이벤트를 기다릴 뿐 출발 허가를 의미하지 않습니다. 깨어나면 다시 LDR/CMP를 합니다. 다른 CPU를 깨우는 이벤트나 이미 설정된 event 상태 때문에 WFE가 빠르게 끝나더라도 비교를 통과하지 못하면 계속 기다립니다.

이 루프는 x0의 부트 모드를 덮어쓰지 않습니다. 따라서 init_kernel_el의 반환값이 secondary_startup의 x20까지 전달됩니다. 보조 CPU는 이미 초기화된 시스템에서 자신의 부트 모드를 전달받아 시작합니다.

secondary_entry는 holding pen 없이 init_kernel_el에서 secondary_startup으로 직접 갑니다. 시스템 동작 중 CPU를 활성화하는 플랫폼의 코드에서 사용합니다. 어느 진입점을 사용할지는 플랫폼의 CPU 시작 방식이 결정합니다.

10. 보조 CPU의 두 진입점과 holding pen
그림을 누르면 원본 크기로 열립니다.

11. secondary_startup: 기존 테이블로 MMU 켜기

head.S L361–L380 · 원본 대조

arch/arm64/kernel/head.S · L361–L380
361SYM_FUNC_START_LOCAL(secondary_startup)
362	/*
363	 * Common entry point for secondary CPUs.
364	 */
365	mov	x20, x0				// preserve boot mode
366
367#ifdef CONFIG_ARM64_VA_BITS_52
368alternative_if ARM64_HAS_VA52
369	bl	__cpu_secondary_check52bitva
370alternative_else_nop_endif
371#endif
372
373	bl	__cpu_setup			// initialise processor
374	adrp	x1, swapper_pg_dir
375	adrp	x2, idmap_pg_dir
376	bl	__enable_mmu
377	ldr	x8, =__secondary_switched
378	br	x8
379SYM_FUNC_END(secondary_startup)
380

365행은 부트 모드를 x20에 보존합니다. 367–371행은 52비트 VA 빌드와 runtime capability가 활성화된 경우 보조 CPU의 지원 여부를 검사합니다. 부트 CPU가 고른 주소 폭을 이 CPU가 지원하지 않으면 같은 테이블을 사용할 수 없기 때문입니다. 단순히 보조 CPU만 주소 폭을 48비트로 줄여 실행을 계속할 수는 없습니다.

373행의 __cpu_setup은 이 CPU의 TCR·MAIR와 MMU 제어값을 준비합니다. 페이지 테이블은 CPU별로 처음부터 새로 만들지 않습니다. x1에는 swapper_pg_dir, x2에는 idmap_pg_dir를 넣습니다. 부트 CPU의 __pi_init_idmap_pg_dir 및 reserved_pg_dir 조합과 이름이 다릅니다.

376행에서 MMU를 켜도 RET가 향하는 코드는 idmap에 있습니다. 377행의 LDR literal이 최종 __secondary_switched VA를 읽고 378행 BR이 그 주소로 이동합니다. 이 두 단계를 구분해야 MMU 전환 순간 PC가 왜 유효한지 이해할 수 있습니다. BR은 새 LR을 만들지 않습니다.

11. secondary_startup: 기존 테이블로 MMU 켜기
그림을 누르면 원본 크기로 열립니다.

12. __secondary_switched와 시작할 태스크가 없는 보조 CPU

head.S L381–L413 · 원본 대조

arch/arm64/kernel/head.S · L381–L413
381	.text
382SYM_FUNC_START_LOCAL(__secondary_switched)
383	mov	x0, x20
384	bl	set_cpu_boot_mode_flag
385
386	mov	x0, x20
387	bl	finalise_el2
388
389	str_l	xzr, __early_cpu_boot_status, x3
390	adr_l	x5, vectors
391	msr	vbar_el1, x5
392	isb
393
394	adr_l	x0, secondary_data
395	ldr	x2, [x0, #CPU_BOOT_TASK]
396	cbz	x2, __secondary_too_slow
397
398	init_cpu_task x2, x1, x3
399
400#ifdef CONFIG_ARM64_PTR_AUTH
401	ptrauth_keys_init_cpu x2, x3, x4, x5
402#endif
403
404	bl	secondary_start_kernel
405	ASM_BUG()
406SYM_FUNC_END(__secondary_switched)
407
408SYM_FUNC_START_LOCAL(__secondary_too_slow)
409	wfe
410	wfi
411	b	__secondary_too_slow
412SYM_FUNC_END(__secondary_too_slow)
413

383–387행에서 부트 모드를 기록하고 EL2의 나머지 설정을 마칩니다. 389행은 __early_cpu_boot_status를 0으로 만듭니다. 이 한 줄만으로 scheduler까지 온라인 처리가 완료됐다는 뜻은 아닙니다. 이후의 태스크·스택 설정과 secondary_start_kernel이 남아 있습니다.

390–392행에서 가상 벡터 주소를 설치합니다. 394–396행은 secondary_data의 CPU_BOOT_TASK 필드를 읽습니다. x2=0이면 매크로가 잘못된 태스크 주소를 역참조하지 않도록 __secondary_too_slow로 빠집니다. 여기 보이는 분기 조건은 명확히 task 포인터의 NULL 여부이며, 구체적인 타임아웃 정책은 이 파일 밖에 있습니다.

398행은 해당 보조 CPU의 태스크·스택·per-CPU 오프셋을 설정합니다. CONFIG_ARM64_PTR_AUTH이면 ptrauth_keys_init_cpu가 포인터 인증 설정을 수행합니다. 이는 무조건 매 부팅마다 이 자리에서 임의 키를 새로 생성한다는 뜻이 아닙니다. 매크로는 CPU 기능/설정에 맞춰 태스크의 키 설치와 제어 상태를 준비합니다.

404행은 secondary_start_kernel을 호출하고 반환 시 ASM_BUG다. 부트 CPU의 start_kernel을 다시 호출하는 것이 아닙니다. 408–412행의 too_slow 경로는 WFE/WFI 뒤에 자기 자신으로 되돌아가는 대기 루프입니다. 이벤트나 인터럽트가 와서 한 번 깨어났다고 정상 경로로 다시 합류하지 않습니다.

12. __secondary_switched와 시작할 태스크가 없는 보조 CPU
그림을 누르면 원본 크기로 열립니다.

13. set_cpu_boot_mode_flag: 32비트 슬롯 두 개

head.S L414–L426 · 원본 대조

arch/arm64/kernel/head.S · L414–L426
414/*
415 * Sets the __boot_cpu_mode flag depending on the CPU boot mode passed
416 * in w0. See arch/arm64/include/asm/virt.h for more info.
417 */
418SYM_FUNC_START_LOCAL(set_cpu_boot_mode_flag)
419	adr_l	x1, __boot_cpu_mode
420	cmp	w0, #BOOT_CPU_MODE_EL2
421	b.ne	1f
422	add	x1, x1, #4
4231:	str	w0, [x1]			// Save CPU boot mode
424	ret
425SYM_FUNC_END(set_cpu_boot_mode_flag)
426

입력은 w0다. BOOT_CPU_MODE_EL2(0xe12)이면 __boot_cpu_mode+4에 저장하고, 그렇지 않으면 시작 주소에 저장합니다. EL1 표식은 0xe11입니다. 두 슬롯은 CPU 번호로 인덱싱하는 배열이 아닙니다. 부팅 모드의 불일치를 감지하기 위한 두 개의 32비트 값입니다.

str w0,[x1]은 4바이트만 저장합니다. x0 상위 32비트의 BOOT_CPU_FLAG_E2H는 여기 저장되지 않습니다. 따라서 이 함수를 “64비트 부트 상태 전체 저장”이라고 해석할 수 없습니다. virt.h의 is_hyp_mode_available은 두 값이 모두 EL2인지 검사하고, mismatch 검사는 두 슬롯의 차이를 봅니다. 초기 슬롯 값까지 이해하려면 정의가 있는 파일을 함께 확인해야 합니다. 이 구간 자체는 초기값을 설정하는 코드가 아닙니다.

14. MMU off 실패 상태를 외부에 남기는 매크로

head.S L427–L444 · 원본 대조

arch/arm64/kernel/head.S · L427–L444
427/*
428 * The booting CPU updates the failed status @__early_cpu_boot_status,
429 * with MMU turned off.
430 *
431 * update_early_cpu_boot_status tmp, status
432 *  - Corrupts tmp1, tmp2
433 *  - Writes 'status' to __early_cpu_boot_status and makes sure
434 *    it is committed to memory.
435 */
436
437	.macro	update_early_cpu_boot_status status, tmp1, tmp2
438	mov	\tmp2, #\status
439	adr_l	\tmp1, __early_cpu_boot_status
440	str	\tmp2, [\tmp1]
441	dmb	sy
442	dc	ivac, \tmp1			// Invalidate potentially stale cache line
443	.endm
444

437행의 실제 인자 순서는 status,tmp1,tmp2다. 앞의 주석에 적힌 인자 순서와 다릅니다. 438–440행은 상태 상수를 tmp2에, 공유 변수 주소를 tmp1에 넣고 64비트 상태를 씁니다.

441–442행은 DMB SY 후 해당 주소 cache line을 IVAC합니다. 이 코드는 MMU off로 쓴 실패 상태와 오래된 캐시 라인의 불일치를 정리하기 위한 특수한 부트 경로입니다. 정상적인 cacheable 공유 메모리 통신에서 이 순서를 그대로 복사하면 안 됩니다. 특히 DMB가 dirty cache line을 자동 clean한다고 해석해서는 안 됩니다.

계산에 사용하는 두 임시 레지스터의 기존 값은 보존되지 않습니다. 뒤의 두 실패 경로는 이 매크로로 이유를 남긴 뒤 CPU를 계속 대기시킵니다. 원본 매크로에는 DSB가 따로 없으므로 설명에 존재하지 않는 명령을 덧붙이지 않습니다.

15. __enable_mmu: TTBR 설치와 MMU를 켠 뒤에도 실행이 이어지는 이유

head.S L445–L474 · 원본 대조

arch/arm64/kernel/head.S · L445–L474
445/*
446 * Enable the MMU.
447 *
448 *  x0  = SCTLR_EL1 value for turning on the MMU.
449 *  x1  = TTBR1_EL1 value
450 *  x2  = ID map root table address
451 *
452 * Returns to the caller via x30/lr. This requires the caller to be covered
453 * by the .idmap.text section.
454 *
455 * Checks if the selected granule size is supported by the CPU.
456 * If it isn't, park the CPU
457 */
458	.section ".idmap.text","a"
459SYM_FUNC_START(__enable_mmu)
460	mrs	x3, ID_AA64MMFR0_EL1
461	ubfx	x3, x3, #ID_AA64MMFR0_EL1_TGRAN_SHIFT, 4
462	cmp     x3, #ID_AA64MMFR0_EL1_TGRAN_SUPPORTED_MIN
463	b.lt    __no_granule_support
464	cmp     x3, #ID_AA64MMFR0_EL1_TGRAN_SUPPORTED_MAX
465	b.gt    __no_granule_support
466	phys_to_ttbr x2, x2
467	msr	ttbr0_el1, x2			// load TTBR0
468	load_ttbr1 x1, x1, x3
469
470	set_sctlr_el1	x0
471
472	ret
473SYM_FUNC_END(__enable_mmu)
474

입력 x0는 __cpu_setup이 만든 SCTLR_EL1 on 값, x1은 TTBR1 루트 주소, x2는 identity mapping 루트 주소입니다. x3을 임시로 써서 ID_AA64MMFR0_EL1의 설정된 TGRAN 필드를 추출합니다. TGRAN_SHIFT와 허용 범위는 커널의 페이지 크기에 따라 달라집니다. 기능값이 0이면 항상 미지원이라고 가정해서는 안 됩니다.

462–465행은 허용 최솟값과 최댓값을 모두 검사합니다. 실패하면 TTBR/SCTLR 쓰기 전에 __no_granule_support로 이동합니다. 466행의 phys_to_ttbr는 52비트 PA 설정에서 상위 주소 비트를 TTBR 형식으로 재배치하고 마스크합니다. 다른 설정에서는 MOV로 끝납니다. 모든 환경에서 물리 주소 숫자를 그대로 TTBR에 써도 된다는 뜻이 아닙니다.

467행은 TTBR0를 설치합니다. 468행 load_ttbr1은 phys_to_ttbr, 필요 시 48/52비트 fallback 오프셋, MSR TTBR1, ISB로 확장됩니다. 470행 set_sctlr_el1은 MSR·ISB뿐 아니라 IC IALLU·DSB NSH·ISB까지 수행합니다. MMU 활성화와 함께 명령어 캐시도 동기화합니다.

MMU off에서는 PC 값를 물리 위치로 사용하고, on 이후에는 같은 숫자를 VA로 해석합니다. 그 VA가 같은 PA를 가리키도록 TTBR0 idmap을 미리 만들었으므로 다음 명령과 RET 위치를 계속 실행할 수 있습니다. 여기서 CPU가 자동으로 KIMAGE_VADDR를 PC에 더해 주는 일은 없습니다. 최종 VA로의 BR은 호출자 코드에서 별도로 실행합니다.

15. __enable_mmu: TTBR 설치와 MMU를 켠 뒤에도 실행이 이어지는 이유
그림을 누르면 원본 크기로 열립니다.

16. 지원하지 않는 VA 폭·페이지 크기의 실패 경로

head.S L475–L507 · 원본 대조

arch/arm64/kernel/head.S · L475–L507
475#ifdef CONFIG_ARM64_VA_BITS_52
476SYM_FUNC_START(__cpu_secondary_check52bitva)
477#ifndef CONFIG_ARM64_LPA2
478	mrs_s	x0, SYS_ID_AA64MMFR2_EL1
479	and	x0, x0, ID_AA64MMFR2_EL1_VARange_MASK
480	cbnz	x0, 2f
481#else
482	mrs	x0, id_aa64mmfr0_el1
483	sbfx	x0, x0, #ID_AA64MMFR0_EL1_TGRAN_SHIFT, 4
484	cmp	x0, #ID_AA64MMFR0_EL1_TGRAN_LPA2
485	b.ge	2f
486#endif
487
488	update_early_cpu_boot_status \
489		CPU_STUCK_IN_KERNEL | CPU_STUCK_REASON_52_BIT_VA, x0, x1
4901:	wfe
491	wfi
492	b	1b
493
4942:	ret
495SYM_FUNC_END(__cpu_secondary_check52bitva)
496#endif
497
498SYM_FUNC_START_LOCAL(__no_granule_support)
499	/* Indicate that this CPU can't boot and is stuck in the kernel */
500	update_early_cpu_boot_status \
501		CPU_STUCK_IN_KERNEL | CPU_STUCK_REASON_NO_GRAN, x1, x2
5021:
503	wfe
504	wfi
505	b	1b
506SYM_FUNC_END(__no_granule_support)
507

52비트 검사 함수는 CONFIG_ARM64_VA_BITS_52일 때만 포함됩니다. 비 LPA2 경로는 MMFR2.VARange 필드를 보고, LPA2 경로는 MMFR0의 TGRAN 필드를 SBFX로 부호 확장해서 요구값과 비교합니다. 앞의 __enable_mmu에서 UBFX를 쓰는 것과 원본 그대로 구별해야 합니다. 아키텍처 기능 필드에는 부호 있는 인코딩을 사용하는 경우가 있습니다.

성공하면 494행 RET다. 실패하면 CPU_STUCK_IN_KERNEL과 CPU_STUCK_REASON_52_BIT_VA를 OR한 상태를 공유 변수에 쓰고 WFE/WFI 루프로 들어갑니다. __no_granule_support는 같은 구조지만 CPU_STUCK_REASON_NO_GRAN을 씁니다.

이 상태는 “잠시 느리다”와 “이 커널의 주소 변환 구성을 지원하지 못한다”를 구분하는 증거입니다. 두 경로 모두 source 내 복구 분기가 없습니다. 이벤트로 WFE에서 깨어나거나 인터럽트로 WFI가 끝나도 다음 B가 다시 대기 지점으로 보냅니다.

16. 지원하지 않는 VA 폭·페이지 크기의 실패 경로
그림을 누르면 원본 크기로 열립니다.

17. __primary_switch: MMU on에서 최종 VA로 넘어가기

head.S L508–L523 · 원본 대조

arch/arm64/kernel/head.S · L508–L523
508SYM_FUNC_START_LOCAL(__primary_switch)
509	adrp	x1, reserved_pg_dir
510	adrp	x2, __pi_init_idmap_pg_dir
511	bl	__enable_mmu
512
513	adrp	x1, early_init_stack
514	mov	sp, x1
515	mov	x29, xzr
516	mov	x0, x20				// pass the full boot status
517	mov	x1, x21				// pass the FDT
518	bl	__pi_early_map_kernel		// Map and relocate the kernel
519
520	ldr	x8, =__primary_switched
521	adrp	x0, KERNEL_START		// __pa(KERNEL_START)
522	br	x8
523SYM_FUNC_END(__primary_switch)

509–511행은 TTBR1에 reserved_pg_dir, TTBR0에 초기 idmap을 넣어 MMU를 켭니다. 이 단계의 TTBR1은 최종 커널 매핑이 아닙니다. reserved 테이블로 전환 중 불필요한 기존 번역을 차단하고 TTBR0의 identity mapping에서 계속 실행합니다.

513–515행은 초기 스택과 FP를 다시 설정합니다. 516–518행은 전체 boot status를 x0로, FDT PA를 x1로 넘깁니다. __pi_early_map_kernel은 FDT 매핑, BSS 초기화, CPU feature override, KASLR/주소 폭 선택, 최종 커널 매핑과 재배치를 수행합니다. 다음 부록은 이 호출을 실제 C 소스까지 따라갑니다.

520행의 ldr x8,=__primary_switched는 상수/리터럴을 통해 재배치된 최종 VA를 얻습니다. 521행의 adrp x0,KERNEL_START는 아직 identity mapping에서 실행되므로 커널 시작의 물리 위치를 얻습니다. 522행의 BR에서 PC가 최종 VA로 이동합니다. 같은 _text를 가리키더라도 x8 쪽의 분기 대상은 가상 코드 주소이고, x0는 kimage_voffset 계산을 위한 물리 기준입니다.

이 함수는 __primary_switched로 tail branch하며 여기로 돌아오는 정상 경로는 없습니다. 파일 마지막 줄이 실행 종료를 뜻하지도 않습니다. 이제 앞서 220행에 정의된 함수가 실행되고 start_kernel로 이어집니다.

17. __primary_switch: MMU on에서 최종 VA로 넘어가기
그림을 누르면 원본 크기로 열립니다.

A. 초기 C 코드가 MMU 이전에도 실행 가능한 이유

kernel/pi/Makefile는 이 코드를 -fpie, freestanding으로 만들고 stack protector·일반 tracing·SCS·LTO 등의 일반 런타임 의존성을 제거합니다. map_range.o에는 -mstrict-align도 적용합니다. 모든 심볼에 __pi_ 접두사를 붙이는 objcopy 단계가 있으므로, C 소스 이름 create_init_idmap과 head.S의 __pi_create_init_idmap은 서로 다른 알고리즘이 아닙니다.

링커는 init_idmap용 저장 공간을 __initdata_begin 쪽에, BSS 뒤에는 init_pg_dir와 4 KiB early stack을 배치합니다. BSS 초기화 범위는 __bss_start부터 init_pg_end까지입니다. 초기 idmap과 그 시점에 사용하는 early stack은 이 범위와 구분되어 있어, 실행에 필요한 테이블과 스택을 자기 자신이 지우지 않습니다. 소스의 정확한 배치는 동봉한 vmlinux.lds.S에서 확인할 수 있습니다.

초기 C는 메모리 할당기나 일반 커널 서비스가 모두 준비됐다는 뜻이 아닙니다. map_range는 예약된 테이블 영역의 포인터를 증가시키며 하위 테이블을 얻습니다. malloc/kmalloc 호출은 없습니다.

B. create_init_idmap와 map_range: 실제 테이블 생성

create_init_idmap은 ptep=(phys_addr_t)pg_dir+PAGE_SIZE로 시작합니다. 첫 페이지가 root이고 그 뒤가 하위 테이블용 공간입니다. text 보호 속성은 PAGE_KERNEL_ROX, data는 PAGE_KERNEL이며, 인자 clrmask의 비트를 두 속성에서 지웁니다. primary의 첫 호출은 clrmask=0이고 LPA2 재생성에서는 PTE_SHARED를 지웁니다.

두 번의 map_range 호출은 [_stext,__initdata_begin)을 ROX, [__initdata_begin,_end)를 RW 데이터로 identity mapping합니다. 첫 번째 범위는 일반 text만이 아니라 링커 배치상 그 사이의 영역도 포함하는 넓게 묶은 초기 매핑 범위입니다. 최종 TTBR1의 세분화된 보호 속성과 동일하지 않습니다.

map_range의 level은 하드웨어 번역 레벨입니다. lshift=(3-level)*PTDESC_TABLE_SHIFT, lmask=(PAGE_SIZE<<lshift)-1로 현재 엔트리가 담당하는 주소 폭을 계산합니다. 시작 VA와 PA를 페이지 경계로 내리고, 현재 범위의 첫 엔트리로 tbl을 이동합니다. 매 반복의 next는 현재 엔트리의 끝과 PAGE_ALIGN(end) 중 작은 값입니다.

level<2이면 항상 하위 테이블로 내려갑니다. level=2에서도 start·next·pa가 block 경계에 맞지 않으면 level 3으로 내려갑니다. 비어 있는 엔트리일 때만 새 테이블 descriptor를 설치하고 allocator 포인터를 한 테이블 크기만큼 올립니다. 이미 있으면 기존 테이블을 재사용합니다. 이 구현은 level 1에서 1 GiB block을 만들지 않습니다.

4 KiB 페이지 예시에서 한 테이블은 512개×8바이트=4096바이트입니다. level 2의 2 MiB 정렬 구간은 block 하나로 가능합니다. 예를 들어 PA=VA=0x80200000부터 0x80400000까지면 level 2 block 조건을 만족합니다. 반면 0x80201000부터 시작하면 세밀한 level 3 페이지가 필요합니다. 16/64 KiB 빌드에서는 동일 숫자를 그대로 적용하지 않습니다.

하위 테이블을 더 만들지 않는 단계에서는 PA에 protval을 OR하여 엔트리에 씁니다. protval=0인 unmap에서는 descriptor의 valid/type가 없어져 무효가 됩니다. may_use_cont가 참이어도 시작 VA·PA 정렬과 충분한 잔여 범위가 있어야 PTE_CONT를 유지합니다. 연속 물리 메모리라고 해서 자동으로 contiguous 힌트가 붙는 것은 아닙니다. initial idmap은 false로 호출합니다.

반복 끝에 PA와 start에 처리한 범위의 크기를 더하고 tbl을 다음 엔트리로 이동합니다. create_init_idmap은 증가한 ptep를 반환합니다. primary_entry는 이 반환값을 캐시 처리 범위의 끝 주소로 사용합니다.

B. create_init_idmap와 map_range: 실제 테이블 생성
연결된 원본 코드 · arch/arm64/kernel/pi/map_range.c
arch/arm64/kernel/pi/map_range.c · L1–L109
1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright 2023 Google LLC
3// Author: Ard Biesheuvel <[email protected]>
4
5#include <linux/types.h>
6#include <linux/sizes.h>
7
8#include <asm/memory.h>
9#include <asm/pgalloc.h>
10#include <asm/pgtable.h>
11
12#include "pi.h"
13
14/**
15 * map_range - Map a contiguous range of physical pages into virtual memory
16 *
17 * @pte:		Address of physical pointer to array of pages to
18 *			allocate page tables from
19 * @start:		Virtual address of the start of the range
20 * @end:		Virtual address of the end of the range (exclusive)
21 * @pa:			Physical address of the start of the range
22 * @prot:		Access permissions of the range
23 * @level:		Translation level for the mapping
24 * @tbl:		The level @level page table to create the mappings in
25 * @may_use_cont:	Whether the use of the contiguous attribute is allowed
26 * @va_offset:		Offset between a physical page and its current mapping
27 * 			in the VA space
28 */
29void __init map_range(phys_addr_t *pte, u64 start, u64 end, phys_addr_t pa,
30		      pgprot_t prot, int level, pte_t *tbl, bool may_use_cont,
31		      u64 va_offset)
32{
33	u64 cmask = (level == 3) ? CONT_PTE_SIZE - 1 : U64_MAX;
34	ptdesc_t protval = pgprot_val(prot) & ~PTE_TYPE_MASK;
35	int lshift = (3 - level) * PTDESC_TABLE_SHIFT;
36	u64 lmask = (PAGE_SIZE << lshift) - 1;
37
38	start	&= PAGE_MASK;
39	pa	&= PAGE_MASK;
40
41	/* Advance tbl to the entry that covers start */
42	tbl += (start >> (lshift + PAGE_SHIFT)) % PTRS_PER_PTE;
43
44	/*
45	 * Set the right block/page bits for this level unless we are
46	 * clearing the mapping
47	 */
48	if (protval)
49		protval |= (level == 2) ? PMD_TYPE_SECT : PTE_TYPE_PAGE;
50
51	while (start < end) {
52		u64 next = min((start | lmask) + 1, PAGE_ALIGN(end));
53
54		if (level < 2 || (level == 2 && (start | next | pa) & lmask)) {
55			/*
56			 * This chunk needs a finer grained mapping. Create a
57			 * table mapping if necessary and recurse.
58			 */
59			if (pte_none(*tbl)) {
60				*tbl = __pte(__phys_to_pte_val(*pte) |
61					     PMD_TYPE_TABLE | PMD_TABLE_UXN);
62				*pte += PTRS_PER_PTE * sizeof(pte_t);
63			}
64			map_range(pte, start, next, pa, prot, level + 1,
65				  (pte_t *)(__pte_to_phys(*tbl) + va_offset),
66				  may_use_cont, va_offset);
67		} else {
68			/*
69			 * Start a contiguous range if start and pa are
70			 * suitably aligned
71			 */
72			if (((start | pa) & cmask) == 0 && may_use_cont)
73				protval |= PTE_CONT;
74
75			/*
76			 * Clear the contiguous attribute if the remaining
77			 * range does not cover a contiguous block
78			 */
79			if ((end & ~cmask) <= start)
80				protval &= ~PTE_CONT;
81
82			/* Put down a block or page mapping */
83			*tbl = __pte(__phys_to_pte_val(pa) | protval);
84		}
85		pa += next - start;
86		start = next;
87		tbl++;
88	}
89}
90
91asmlinkage phys_addr_t __init create_init_idmap(pgd_t *pg_dir, ptdesc_t clrmask)
92{
93	phys_addr_t ptep = (phys_addr_t)pg_dir + PAGE_SIZE; /* MMU is off */
94	pgprot_t text_prot = PAGE_KERNEL_ROX;
95	pgprot_t data_prot = PAGE_KERNEL;
96
97	pgprot_val(text_prot) &= ~clrmask;
98	pgprot_val(data_prot) &= ~clrmask;
99
100	/* MMU is off; pointer casts to phys_addr_t are safe */
101	map_range(&ptep, (u64)_stext, (u64)__initdata_begin,
102		  (phys_addr_t)_stext, text_prot, IDMAP_ROOT_LEVEL,
103		  (pte_t *)pg_dir, false, 0);
104	map_range(&ptep, (u64)__initdata_begin, (u64)_end,
105		  (phys_addr_t)__initdata_begin, data_prot, IDMAP_ROOT_LEVEL,
106		  (pte_t *)pg_dir, false, 0);
107
108	return ptep;
109}

C. 캐시 helper: IVAC와 CIVAC를 나누는 이유

dcache_inval_poc는 [x0,x1)에 대해 동작합니다. 라인 크기를 얻은 다음 end를 라인 경계로 내립니다. end가 비정렬이면 마지막 부분 라인을 먼저 CIVAC합니다. start도 라인 경계로 내리고, start가 비정렬이면 첫 부분 라인을 CIVAC합니다. 완전히 포함되는 중간 라인은 IVAC하고, 마지막 DSB SY 뒤 RET합니다.

IVAC는 invalidate, CVAC는 clean to PoC, CIVAC는 clean+invalidate다. 부분 라인을 단순 무효화하면 요청 범위 밖의 이웃 데이터 중 dirty 부분을 버릴 수 있으므로, 경계에서는 clean도 합니다. 실제 boot_args는 정렬 속성이 있고 이 그림은 helper의 경계 처리를 설명하기 위한 별도 예시입니다.

64 B 라인에서 요청이 [0x1010,0x1090)이면 0x1080 경계 라인을 먼저 CIVAC, 0x1000 경계 라인을 CIVAC, 완전히 포함된 0x1040 라인을 IVAC합니다. 요청한 바이트 수는 128 B지만 영향을 받는 라인은 세 개입니다. 캐시 작업은 요청한 바이트 범위가 걸쳐 있는 라인 전체에 적용됩니다.

dcache_clean_poc는 dcache_by_line_op cvac,sy 매크로를 씁니다. 시작을 라인 경계로 내리고 end 미만까지 순회하며 마지막 DSB SY를 실행합니다. ARM64_WORKAROUND_CLEAN_CACHE가 적용되면 매크로가 CVAC 대신 CIVAC를 사용할 수 있습니다.

PoC는 관련 관찰자들이 일관된 값을 관찰하는 지점입니다. 이를 모든 시스템에서 반드시 DRAM 칩이라고 단정하지 않습니다. head.S의 MMU-off 테이블 쓰기 후 invalidate 경로와 MMU-on 진입 코드 clean 경로는 해결하려는 불일치의 방향이 서로 다릅니다.

C. 캐시 helper: IVAC와 CIVAC를 나누는 이유
연결된 원본 코드 · arch/arm64/mm/cache.S
arch/arm64/mm/cache.S · L145–L179
145SYM_FUNC_START(__pi_dcache_inval_poc)
146	dcache_line_size x2, x3
147	sub	x3, x2, #1
148	tst	x1, x3				// end cache line aligned?
149	bic	x1, x1, x3
150	b.eq	1f
151	dc	civac, x1			// clean & invalidate D / U line
1521:	tst	x0, x3				// start cache line aligned?
153	bic	x0, x0, x3
154	b.eq	2f
155	dc	civac, x0			// clean & invalidate D / U line
156	b	3f
1572:	dc	ivac, x0			// invalidate D / U line
1583:	add	x0, x0, x2
159	cmp	x0, x1
160	b.lo	2b
161	dsb	sy
162	ret
163SYM_FUNC_END(__pi_dcache_inval_poc)
164SYM_FUNC_ALIAS(dcache_inval_poc, __pi_dcache_inval_poc)
165
166/*
167 *	dcache_clean_poc(start, end)
168 *
169 * 	Ensure that any D-cache lines for the interval [start, end)
170 * 	are cleaned to the PoC.
171 *
172 *	- start   - virtual start address of region
173 *	- end     - virtual end address of region
174 */
175SYM_FUNC_START(__pi_dcache_clean_poc)
176	dcache_by_line_op cvac, sy, x0, x1, x2, x3
177	ret
178SYM_FUNC_END(__pi_dcache_clean_poc)
179SYM_FUNC_ALIAS(dcache_clean_poc, __pi_dcache_clean_poc)

D. __cpu_setup: MMU를 켜기 전 어떤 레지스터를 준비하나

proc.S의 __cpu_setup은 local TLB invalidate와 DSB NSH로 시작합니다. CPACR_EL1을 리셋하고, debug DCC·PMU·AMU의 EL0 접근을 정리합니다. 이어 MAIR·TCR·TCR2 후보를 구성합니다. 이 함수 마지막은 INIT_SCTLR_EL1_MMU_ON 값을 x0에 넣는 RET다. SCTLR을 설치하여 MMU를 켜는 작업은 head.S의 __enable_mmu에 남겨 둡니다.

대상설정의 역할head.S와의 연결
MAIR_EL1Device/Normal/Normal-NC 등 속성 인덱스의 의미descriptor의 AttrIndx를 해석할 표
TCR.T0SZ초기 idmap 주소 폭TTBR0의 번역 범위
TCR.T1SZ커널 VA 폭, 기능에 따라 보정TTBR1의 번역 범위
TCR.TG0/TG14/16/64 KiB granule__enable_mmu의 지원 검사와 일치해야 함
TCR.IRGN/ORGN, SHtable walk의 캐시/공유 속성테이블을 읽는 방식이며 데이터 PTE 속성과 별개
TCR.IPSCPU와 빌드가 허용하는 PA 크기TTBR와 descriptor의 주소 해석
TCR.HA / TCR2.HAFT지원하는 hardware access flag 기능설정과 ID 레지스터 검사를 통과할 때만
PIR/PIRE0 및 TCR2.PIEpermission indirection 지원 시 속성 설정최신 기능 경로도 존재
x0의 SCTLR 후보MMU on 초기 제어값__enable_mmu가 실제 MSR 수행

TCR에는 ASID16, TBI, KASLR·KASAN/MTE 관련 설정도 결합됩니다. 모든 비트가 항상 활성인 것은 아닙니다. 이후 early_map_kernel은 실제 지원 및 override를 보고 T1SZ와 LPA2 구성을 다시 조정할 수 있습니다. “__cpu_setup에서 정한 주소 폭이 이후에도 바뀌지 않는다”는 해석은 피해야 합니다.

연결된 원본 코드 · arch/arm64/mm/proc.S
arch/arm64/mm/proc.S · L473–L560
473SYM_FUNC_START(__cpu_setup)
474	tlbi	vmalle1				// Invalidate local TLB
475	dsb	nsh
476
477	msr	cpacr_el1, xzr			// Reset cpacr_el1
478	mov	x1, MDSCR_EL1_TDCC		// Reset mdscr_el1 and disable
479	msr	mdscr_el1, x1			// access to the DCC from EL0
480	reset_pmuserenr_el0 x1			// Disable PMU access from EL0
481	reset_amuserenr_el0 x1			// Disable AMU access from EL0
482
483	/*
484	 * Default values for VMSA control registers. These will be adjusted
485	 * below depending on detected CPU features.
486	 */
487	mair	.req	x17
488	tcr	.req	x16
489	tcr2	.req	x15
490	mov_q	mair, MAIR_EL1_SET
491	mov_q	tcr, TCR_T0SZ(IDMAP_VA_BITS) | TCR_T1SZ(VA_BITS_MIN) | TCR_CACHE_FLAGS | \
492		     TCR_SHARED | TCR_TG_FLAGS | TCR_KASLR_FLAGS | TCR_ASID16 | \
493		     TCR_TBI0 | TCR_A1 | TCR_KASAN_SW_FLAGS | TCR_MTE_FLAGS
494	mov	tcr2, xzr
495
496	tcr_clear_errata_bits tcr, x9, x5
497
498#ifdef CONFIG_ARM64_VA_BITS_52
499	mov		x9, #64 - VA_BITS
500alternative_if ARM64_HAS_VA52
501	tcr_set_t1sz	tcr, x9
502#ifdef CONFIG_ARM64_LPA2
503	orr		tcr, tcr, #TCR_DS
504#endif
505alternative_else_nop_endif
506#endif
507
508	/*
509	 * Set the IPS bits in TCR_EL1.
510	 */
511	tcr_compute_pa_size tcr, #TCR_IPS_SHIFT, x5, x6
512#ifdef CONFIG_ARM64_HW_AFDBM
513	/*
514	 * Enable hardware update of the Access Flags bit.
515	 * Hardware dirty bit management is enabled later,
516	 * via capabilities.
517	 */
518	mrs	x9, ID_AA64MMFR1_EL1
519	ubfx	x9, x9, ID_AA64MMFR1_EL1_HAFDBS_SHIFT, #4
520	cbz	x9, 1f
521	orr	tcr, tcr, #TCR_HA		// hardware Access flag update
522#ifdef CONFIG_ARM64_HAFT
523	cmp	x9, ID_AA64MMFR1_EL1_HAFDBS_HAFT
524	b.lt	1f
525	orr	tcr2, tcr2, TCR2_EL1_HAFT
526#endif /* CONFIG_ARM64_HAFT */
5271:
528#endif	/* CONFIG_ARM64_HW_AFDBM */
529	msr	mair_el1, mair
530	msr	tcr_el1, tcr
531
532	mrs_s	x1, SYS_ID_AA64MMFR3_EL1
533	ubfx	x1, x1, #ID_AA64MMFR3_EL1_S1PIE_SHIFT, #4
534	cbz	x1, .Lskip_indirection
535
536	mov_q	x0, PIE_E0_ASM
537	msr	REG_PIRE0_EL1, x0
538	mov_q	x0, PIE_E1_ASM
539	msr	REG_PIR_EL1, x0
540
541	orr	tcr2, tcr2, TCR2_EL1_PIE
542
543.Lskip_indirection:
544
545	mrs_s	x1, SYS_ID_AA64MMFR3_EL1
546	ubfx	x1, x1, #ID_AA64MMFR3_EL1_TCRX_SHIFT, #4
547	cbz	x1, 1f
548	msr	REG_TCR2_EL1, tcr2
5491:
550
551	/*
552	 * Prepare SCTLR
553	 */
554	mov_q	x0, INIT_SCTLR_EL1_MMU_ON
555	ret					// return to head.S
556
557	.unreq	mair
558	.unreq	tcr
559	.unreq	tcr2
560SYM_FUNC_END(__cpu_setup)

E. early_map_kernel: FDT → BSS → 주소 폭 → KASLR

이 함수는 이미 MMU on이지만 아직 TTBR0 idmap으로 실행됩니다. pa_base는 현재 &_text의 물리 위치입니다. 가장 먼저 map_fdt가 FDT PA부터 최대 MAX_FDT_SIZE까지 접근할 수 있도록 idmap에 테이블을 추가합니다. FDT가 이미지 앞에 있으면 _text 경계를 넘지 않도록 제한합니다. FDT 파일 전체를 새 버퍼에 복사하는 함수가 아닙니다.

그 다음 memset은 BSS부터 init_pg_end까지 0으로 만듭니다. 이어 /chosen을 찾고 command line의 CPU feature override를 파싱합니다. override 상태가 BSS에 있으므로 먼저 0으로 초기화합니다. x20/x21은 레지스터에 보존되며, 초기 스택과 초기 idmap은 memset 범위 밖에 있습니다.

64 KiB 구성에서 LVA를 지원하지 않으면 va_bits를 VA_BITS_MIN으로 줄입니다. LPA2 구성에서 LPA2가 없으면 va_bits를 줄이고 root_level을 하나 높여 한 단계 적은 테이블을 사용합니다. va_bits가 최소값보다 클 때 TCR.T1SZ를 갱신합니다.

KASLR displacement의 하위 비트는 pa_base % MIN_KIMG_ALIGN로 시작합니다. RANDOMIZE_BASE이면 seed에서 정렬 경계 위의 비트만 취해 OR합니다. 이렇게 하면 물리 주소와 가상 주소를 각각 2 MiB로 나눈 나머지가 같아져 가능한 경우 block mapping을 유지할 수 있습니다. 물리 커널의 위치는 그대로 두고 가상 주소의 오프셋을 계산합니다.

va_base=KIMAGE_VADDR+kaslr_offset, va_offset=va_base-pa_base를 계산하고 map_kernel에 넘깁니다. kaslr_offset은 링크 기준 VA에 대한 displacement이고, va_offset은 현재 PA와 최종 VA 사이의 차입니다. 둘은 서로 다른 값입니다. 예컨대 K=링크 VA, P=물리 base, D=displacement이면 최종 VA=K+D, VA−PA 오프셋=K+D−P다.

KASLR과 KPTI에 따라 non-global mappings가 필요한 경우 Cavium erratum 27456 예외를 검사합니다. 이는 단순 랜덤 주소 생성 외에도 CPU의 하드웨어 제약도 초기 매핑에 반영하는 과정입니다.

E. early_map_kernel: FDT → BSS → 주소 폭 → KASLR
연결된 원본 코드 · arch/arm64/kernel/pi/map_kernel.c
arch/arm64/kernel/pi/map_kernel.c · L241–L289
241asmlinkage void __init early_map_kernel(u64 boot_status, phys_addr_t fdt)
242{
243	static char const chosen_str[] __initconst = "/chosen";
244	u64 va_base, pa_base = (u64)&_text;
245	u64 kaslr_offset = pa_base % MIN_KIMG_ALIGN;
246	int root_level = 4 - CONFIG_PGTABLE_LEVELS;
247	int va_bits = VA_BITS;
248	int chosen;
249	void *fdt_mapped = map_fdt(fdt);
250
251	/* Clear BSS and the initial page tables */
252	memset(__bss_start, 0, (char *)init_pg_end - (char *)__bss_start);
253
254	/* Parse the command line for CPU feature overrides */
255	chosen = fdt_path_offset(fdt_mapped, chosen_str);
256	init_feature_override(boot_status, fdt_mapped, chosen);
257
258	if (IS_ENABLED(CONFIG_ARM64_64K_PAGES) && !cpu_has_lva()) {
259		va_bits = VA_BITS_MIN;
260	} else if (IS_ENABLED(CONFIG_ARM64_LPA2) && !cpu_has_lpa2()) {
261		va_bits = VA_BITS_MIN;
262		root_level++;
263	}
264
265	if (va_bits > VA_BITS_MIN)
266		sysreg_clear_set(tcr_el1, TCR_T1SZ_MASK, TCR_T1SZ(va_bits));
267
268	/*
269	 * The virtual KASLR displacement modulo 2MiB is decided by the
270	 * physical placement of the image, as otherwise, we might not be able
271	 * to create the early kernel mapping using 2 MiB block descriptors. So
272	 * take the low bits of the KASLR offset from the physical address, and
273	 * fill in the high bits from the seed.
274	 */
275	if (IS_ENABLED(CONFIG_RANDOMIZE_BASE)) {
276		u64 kaslr_seed = kaslr_early_init(fdt_mapped, chosen);
277
278		if (kaslr_seed && kaslr_requires_kpti())
279			arm64_use_ng_mappings = ng_mappings_allowed();
280
281		kaslr_offset |= kaslr_seed & ~(MIN_KIMG_ALIGN - 1);
282	}
283
284	if (IS_ENABLED(CONFIG_ARM64_LPA2) && va_bits > VA_BITS_MIN)
285		remap_idmap_for_lpa2();
286
287	va_base = KIMAGE_VADDR + kaslr_offset;
288	map_kernel(kaslr_offset, va_base - pa_base, root_level);
289}

F. map_kernel: 두 번의 권한 설정과 루트 교체

map_segment는 물리 start/end에 va_offset을 더하고 PAGE_OFFSET 부분을 제거하여 테이블 인덱싱에 사용할 VA 범위를 만듭니다. PA는 start 그대로 넘깁니다. map_kernel은 임시 init_pg_dir 아래에 이미지의 구간들을 분리해 매핑합니다.

구간이 초기 단계의 처리
_text.._stext부팅 후 실행하지 않을 헤더 등: data 속성
_stext.._etext일반 text: 필요 시 첫 번째 처리 단계 RW, 최종 text_prot
__start_rodata..__inittext_begin이 단계에서는 data 속성으로 매핑
__inittext_begin..__inittext_endinit text도 text_prot로 전환
__initdata_begin..__initdata_endinit data
_data.._enddata+BSS 등, contiguous 사용 허용

text_prot 기본은 PAGE_KERNEL_ROX다. rodata=off를 명시하면 PAGE_KERNEL_EXEC를 사용합니다. BTI가 설정·지원되면 guarded page 속성을 추가합니다. 동적 SCS는 kernel PAC 또는 BTI와의 관계를 검사하고 필요할 때만 선택합니다.

CONFIG_RELOCATABLE 또는 동적 SCS 패치가 필요하면 매핑을 두 단계로 처리합니다. 첫 번째 처리 단계에서 text의 최종 VA 매핑을 쓰기 가능한 data 속성으로 만들고 init_pg_dir를 TTBR1에 설치합니다. 현재 실행은 TTBR0의 idmap에서 계속되므로 TTBR1 text가 NX여도 이 C 코드 실행이 중단되지 않습니다. 재배치와 코드 패치를 수행한 뒤 일반 text 매핑을 해제하고 barrier/TLBI를 거쳐 text와 inittext를 최종 실행 권한으로 매핑합니다.

원본에서 unmap_segment는 일반 text 범위에 호출되고, inittext도 뒤의 remap 대상에 포함됩니다. 일반 text의 contiguous descriptor 재구성 과정에서 이전 TLB 번역과 충돌하지 않게 하는 것이 주석에 명시된 이유입니다.

마지막 memcpy는 루트 테이블 한 페이지만 swapper_pg_dir의 최종 VA로 복사합니다. 커널 코드 전체도 아니고 하위 테이블 전체도 아닙니다. 복사된 root의 하위 테이블 포인터들이 같은 하위 테이블들을 계속 가리킵니다. DSB 후 TTBR1을 swapper_pg_dir로 교체합니다.

idmap_cpu_replace_ttbr1은 proc.S에서 먼저 reserved_pg_dir를 설치하고 ISB→TLBI VMALLE1→DSB NSH→ISB를 수행한 뒤 새 root를 설치합니다. 테이블 walk의 옛 상태와 새 상태가 섞이지 않도록 빈 테이블을 경유합니다. 이 보조 함수도 TTBR0 매핑을 사용하는 코드에서 실행해야 합니다.

F. map_kernel: 두 번의 권한 설정과 루트 교체
연결된 원본 코드 · arch/arm64/kernel/pi/map_kernel.c
arch/arm64/kernel/pi/map_kernel.c · L39–L140
39static void __init map_kernel(u64 kaslr_offset, u64 va_offset, int root_level)
40{
41	bool enable_scs = IS_ENABLED(CONFIG_UNWIND_PATCH_PAC_INTO_SCS);
42	bool twopass = IS_ENABLED(CONFIG_RELOCATABLE);
43	phys_addr_t pgdp = (phys_addr_t)init_pg_dir + PAGE_SIZE;
44	pgprot_t text_prot = PAGE_KERNEL_ROX;
45	pgprot_t data_prot = PAGE_KERNEL;
46	pgprot_t prot;
47
48	/*
49	 * External debuggers may need to write directly to the text mapping to
50	 * install SW breakpoints. Allow this (only) when explicitly requested
51	 * with rodata=off.
52	 */
53	if (arm64_test_sw_feature_override(ARM64_SW_FEATURE_OVERRIDE_RODATA_OFF))
54		text_prot = PAGE_KERNEL_EXEC;
55
56	/*
57	 * We only enable the shadow call stack dynamically if we are running
58	 * on a system that does not implement PAC or BTI. PAC and SCS provide
59	 * roughly the same level of protection, and BTI relies on the PACIASP
60	 * instructions serving as landing pads, preventing us from patching
61	 * those instructions into something else.
62	 */
63	if (IS_ENABLED(CONFIG_ARM64_PTR_AUTH_KERNEL) && cpu_has_pac())
64		enable_scs = false;
65
66	if (IS_ENABLED(CONFIG_ARM64_BTI_KERNEL) && cpu_has_bti()) {
67		enable_scs = false;
68
69		/*
70		 * If we have a CPU that supports BTI and a kernel built for
71		 * BTI then mark the kernel executable text as guarded pages
72		 * now so we don't have to rewrite the page tables later.
73		 */
74		text_prot = __pgprot_modify(text_prot, PTE_GP, PTE_GP);
75	}
76
77	/* Map all code read-write on the first pass if needed */
78	twopass |= enable_scs;
79	prot = twopass ? data_prot : text_prot;
80
81	/*
82	 * [_stext, _text) isn't executed after boot and contains some
83	 * non-executable, unpredictable data, so map it non-executable.
84	 */
85	map_segment(init_pg_dir, &pgdp, va_offset, _text, _stext, data_prot,
86		    false, root_level);
87	map_segment(init_pg_dir, &pgdp, va_offset, _stext, _etext, prot,
88		    !twopass, root_level);
89	map_segment(init_pg_dir, &pgdp, va_offset, __start_rodata,
90		    __inittext_begin, data_prot, false, root_level);
91	map_segment(init_pg_dir, &pgdp, va_offset, __inittext_begin,
92		    __inittext_end, prot, false, root_level);
93	map_segment(init_pg_dir, &pgdp, va_offset, __initdata_begin,
94		    __initdata_end, data_prot, false, root_level);
95	map_segment(init_pg_dir, &pgdp, va_offset, _data, _end, data_prot,
96		    true, root_level);
97	dsb(ishst);
98
99	idmap_cpu_replace_ttbr1((phys_addr_t)init_pg_dir);
100
101	if (twopass) {
102		if (IS_ENABLED(CONFIG_RELOCATABLE))
103			relocate_kernel(kaslr_offset);
104
105		if (enable_scs) {
106			scs_patch(__eh_frame_start + va_offset,
107				  __eh_frame_end - __eh_frame_start, false);
108			asm("ic ialluis");
109
110			dynamic_scs_is_enabled = true;
111		}
112
113		/*
114		 * Unmap the text region before remapping it, to avoid
115		 * potential TLB conflicts when creating the contiguous
116		 * descriptors.
117		 */
118		unmap_segment(init_pg_dir, va_offset, _stext, _etext,
119			      root_level);
120		dsb(ishst);
121		isb();
122		__tlbi(vmalle1);
123		isb();
124
125		/*
126		 * Remap these segments with different permissions
127		 * No new page table allocations should be needed
128		 */
129		map_segment(init_pg_dir, NULL, va_offset, _stext, _etext,
130			    text_prot, true, root_level);
131		map_segment(init_pg_dir, NULL, va_offset, __inittext_begin,
132			    __inittext_end, text_prot, false, root_level);
133	}
134
135	/* Copy the root page table to its final location */
136	memcpy((void *)swapper_pg_dir + va_offset, init_pg_dir, PAGE_SIZE);
137	dsb(ishst);
138	idmap_cpu_replace_ttbr1((phys_addr_t)swapper_pg_dir);
139}
140

G. relocate_kernel: 주소 보정과 RELR 비트맵

RELA 루프는 R_AARCH64_RELATIVE 항목만 처리합니다. 저장 위치는 r_offset+offset, 저장 값은 r_addend+offset입니다. 여기 offset은 KASLR displacement이며 앞 절의 VA−PA 오프셋이 아닙니다. 항목의 링크 시 VA를 런타임 VA로 보정하는 작업입니다.

예를 들어 링크 기준 저장 위치가 K+0x100, addend가 K+0x800이고 displacement가 D이면, 최종 VA K+D+0x100에 K+D+0x800을 씁니다. 이미 map_kernel이 쓰기 가능한 TTBR1 매핑을 설치했으므로 그 VA를 통해 저장할 수 있습니다. 물리 이미지 전체의 바이트를 새 PA로 복사하는 로더 동작과 다릅니다.

CONFIG_RELR가 아니거나 offset=0이면 RELR 루프를 건너뜁니다. 짝수 항목은 보정할 직접 주소이며 그 위치의 기존 값에 offset을 더하고 place를 u64 하나의 크기인 8바이트만큼 증가시킵니다. 홀수 항목은 최하위 비트가 표식이고 나머지 63비트가 뒤따르는 u64 위치들의 보정 여부를 나타냅니다. 예컨대 표식을 제외한 비트가 0101이면 첫 번째와 세 번째 위치만 보정합니다. 해당 비트맵을 처리한 뒤 place는 항상 u64 63개에 해당하는 504바이트만큼 증가합니다.

이 단계가 끝난 뒤 __primary_switch의 literal load가 최종 __primary_switched VA를 얻고 BR할 수 있습니다. PC 상대 주소는 현재 실행 위치를 기준으로 계산하고, 재배치 대상인 절대 주소는 위 과정에서 보정합니다.

연결된 원본 코드 · arch/arm64/kernel/pi/relocate.c
arch/arm64/kernel/pi/relocate.c · L1–L64
1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright 2023 Google LLC
3// Authors: Ard Biesheuvel <[email protected]>
4//          Peter Collingbourne <[email protected]>
5
6#include <linux/elf.h>
7#include <linux/init.h>
8#include <linux/types.h>
9
10#include "pi.h"
11
12extern const Elf64_Rela rela_start[], rela_end[];
13extern const u64 relr_start[], relr_end[];
14
15void __init relocate_kernel(u64 offset)
16{
17	u64 *place = NULL;
18
19	for (const Elf64_Rela *rela = rela_start; rela < rela_end; rela++) {
20		if (ELF64_R_TYPE(rela->r_info) != R_AARCH64_RELATIVE)
21			continue;
22		*(u64 *)(rela->r_offset + offset) = rela->r_addend + offset;
23	}
24
25	if (!IS_ENABLED(CONFIG_RELR) || !offset)
26		return;
27
28	/*
29	 * Apply RELR relocations.
30	 *
31	 * RELR is a compressed format for storing relative relocations. The
32	 * encoded sequence of entries looks like:
33	 * [ AAAAAAAA BBBBBBB1 BBBBBBB1 ... AAAAAAAA BBBBBB1 ... ]
34	 *
35	 * i.e. start with an address, followed by any number of bitmaps. The
36	 * address entry encodes 1 relocation. The subsequent bitmap entries
37	 * encode up to 63 relocations each, at subsequent offsets following
38	 * the last address entry.
39	 *
40	 * The bitmap entries must have 1 in the least significant bit. The
41	 * assumption here is that an address cannot have 1 in lsb. Odd
42	 * addresses are not supported. Any odd addresses are stored in the
43	 * RELA section, which is handled above.
44	 *
45	 * With the exception of the least significant bit, each bit in the
46	 * bitmap corresponds with a machine word that follows the base address
47	 * word, and the bit value indicates whether or not a relocation needs
48	 * to be applied to it. The second least significant bit represents the
49	 * machine word immediately following the initial address, and each bit
50	 * that follows represents the next word, in linear order. As such, a
51	 * single bitmap can encode up to 63 relocations in a 64-bit object.
52	 */
53	for (const u64 *relr = relr_start; relr < relr_end; relr++) {
54		if ((*relr & 1) == 0) {
55			place = (u64 *)(*relr + offset);
56			*place++ += offset;
57		} else {
58			for (u64 *p = place, r = *relr >> 1; r; p++, r >>= 1)
59				if (r & 1)
60					*p += offset;
61			place += 63;
62		}
63	}
64}

H. LPA2: TTBR0를 사용하는 중에 형식을 바꾸기

LPA2로 DS를 설정하면 descriptor의 일부 shareability 비트가 물리 주소 비트로 해석됩니다. 기존 테이블을 그대로 두고 TCR.DS만 바꾸면 잘못된 PA를 가리킬 수 있습니다. remap_idmap_for_lpa2는 shareability를 나타내는 PTE_SHARED 비트를 제거한 임시 idmap을 init_pg_dir에 먼저 만듭니다.

set_ttbr0_for_lpa2는 .idmap.text에 놓이고 기존 SCTLR을 보관합니다. M을 끄고 ISB, 새 TTBR0와 DS·IPS가 조정된 TCR 설치, ISB, TLBI, DSB NSH, ISB, 원래 SCTLR 복원, ISB 순서로 처리합니다. 그 후 원래 init_idmap 영역을 지우고 새 형식으로 재생성하여 다시 옮겨갑니다. 마지막에 임시 init_pg_dir를 지웁니다.

따라서 init_pg_dir는 이 경로에서 한때 임시 idmap 공간으로 쓰이고 이후 최종 커널 매핑 구축에 다시 사용됩니다. FDT는 이 재생성 시점 이후 더 필요하지 않아 다시 매핑하지 않는다고 원문에 명시되어 있습니다. 실행 중인 테이블을 무작정 제자리에서 지우는 코드가 아닙니다.

연결된 원본 코드 · arch/arm64/kernel/pi/map_kernel.c
arch/arm64/kernel/pi/map_kernel.c · L141–L199
141static void noinline __section(".idmap.text") set_ttbr0_for_lpa2(phys_addr_t ttbr)
142{
143	u64 sctlr = read_sysreg(sctlr_el1);
144	u64 tcr = read_sysreg(tcr_el1) | TCR_DS;
145	u64 mmfr0 = read_sysreg(id_aa64mmfr0_el1);
146	u64 parange = cpuid_feature_extract_unsigned_field(mmfr0,
147							   ID_AA64MMFR0_EL1_PARANGE_SHIFT);
148
149	tcr &= ~TCR_IPS_MASK;
150	tcr |= parange << TCR_IPS_SHIFT;
151
152	asm("	msr	sctlr_el1, %0		;"
153	    "	isb				;"
154	    "   msr     ttbr0_el1, %1		;"
155	    "   msr     tcr_el1, %2		;"
156	    "	isb				;"
157	    "	tlbi    vmalle1			;"
158	    "	dsb     nsh			;"
159	    "	isb				;"
160	    "	msr     sctlr_el1, %3		;"
161	    "	isb				;"
162	    ::	"r"(sctlr & ~SCTLR_ELx_M), "r"(ttbr), "r"(tcr), "r"(sctlr));
163}
164
165static void __init remap_idmap_for_lpa2(void)
166{
167	/* clear the bits that change meaning once LPA2 is turned on */
168	ptdesc_t mask = PTE_SHARED;
169
170	/*
171	 * We have to clear bits [9:8] in all block or page descriptors in the
172	 * initial ID map, as otherwise they will be (mis)interpreted as
173	 * physical address bits once we flick the LPA2 switch (TCR.DS). Since
174	 * we cannot manipulate live descriptors in that way without creating
175	 * potential TLB conflicts, let's create another temporary ID map in a
176	 * LPA2 compatible fashion, and update the initial ID map while running
177	 * from that.
178	 */
179	create_init_idmap(init_pg_dir, mask);
180	dsb(ishst);
181	set_ttbr0_for_lpa2((phys_addr_t)init_pg_dir);
182
183	/*
184	 * Recreate the initial ID map with the same granularity as before.
185	 * Don't bother with the FDT, we no longer need it after this.
186	 */
187	memset(init_idmap_pg_dir, 0,
188	       (char *)init_idmap_pg_end - (char *)init_idmap_pg_dir);
189
190	create_init_idmap(init_idmap_pg_dir, mask);
191	dsb(ishst);
192
193	/* switch back to the updated initial ID map */
194	set_ttbr0_for_lpa2((phys_addr_t)init_idmap_pg_dir);
195
196	/* wipe the temporary ID map from memory */
197	memset(init_pg_dir, 0, (char *)init_pg_end - (char *)init_pg_dir);
198}
199

I. EL2 초기화 함수와 finalise_el2의 역할 구분

init_el2_state의 각 그룹은 기능 유무를 검사하며 하위 EL 실행 조건을 정리합니다. 이 초기 설정과 뒤의 finalise_el2_state는 시점이 다릅니다. 초기에는 하드웨어 기능을 보고 기본 실행 상태를 설정하고, 후반에는 파싱된 feature override를 반영할 수 있습니다.

초기 그룹주요 효과
sctlr / hcrxEL2 MMU off 기본, 지원 시 추가 trap/control 기능
timersEL1 물리 타이머/카운터 접근 허용, CNTVOFF=0; E2H별 비트 위치 대응
debug / brbePMU·SPE·TRBE·branch record 관련 접근과 trap 기본
lor / stage2지원 시 LORegion 제어 리셋, VTTBR_EL2=0
gicv3 / gicv5구현된 GIC system register 접근 허용 및 trap 정리
hstr / nvhe_idregsCP15 trap 정리, MIDR/MPIDR를 가상 ID에 반영
cptrE2H 여부에 따라 coprocessor 접근/trap 초기화
fgt / fgt2구현된 fine-grained trap 레지스터 초기화

finalise_el2의 외부 함수는 w0가 EL2 부트 모드이고 현재 EL이 EL1일 때만 HVC_FINALISE_EL2로 HVC합니다. hyp stub은 finalise_el2_state에서 MPAM/GCS/SVE/SME 등의 후반 상태를 처리합니다. 이어 EL2 MMU가 off인지, VHE가 가능한지, override에서 host VHE를 허용하는지 확인합니다. 실패하면 stub 오류 값을 반환하고 EL1 경로를 유지합니다.

성공하면 HCR_HOST_VHE_FLAGS를 설치하고 EL1 스택·per-CPU 오프셋·벡터·주소 변환 상태를 host EL2 쪽으로 옮깁니다. TCR2 및 permission indirection 상태도 지원 시 복사합니다. SPSR의 복귀 모드를 EL2h로 바꾸고 identity mapping의 enter_vhe에서 EL2 MMU를 켠 뒤 ERET합니다. 이 결과로 커널이 VHE의 EL2에서 계속 실행할 수 있습니다.

실행 순서는 init_kernel_el의 EL2 부팅 기록, EL1에서의 매핑 생성, finalise_el2의 VHE 전환 시도 순입니다. BOOT_CPU_FLAG_E2H는 초기 E2H 상태를 기록하며, 최종 VHE 전환 결과를 나타내지는 않습니다.

연결된 원본 코드 · arch/arm64/kernel/hyp-stub.S
arch/arm64/kernel/hyp-stub.S · L244–L258
244SYM_FUNC_START(finalise_el2)
245	// Need to have booted at EL2
246	cmp	w0, #BOOT_CPU_MODE_EL2
247	b.ne	1f
248
249	// and still be at EL1
250	mrs	x0, CurrentEL
251	cmp	x0, #CurrentEL_EL1
252	b.ne	1f
253
254	mov	x0, #HVC_FINALISE_EL2
255	hvc	#0
2561:
257	ret
258SYM_FUNC_END(finalise_el2)

J. 읽으면서 확인할 상태와 잘못 이해하기 쉬운 부분

확인 위치확인할 상태
preserve_boot_args 직후x21=원래 FDT PA, x0는 더 이상 원래 FDT가 아님
create_init_idmap 반환x0=테이블 할당 끝, 현재 코드/스택이 초기 idmap 범위에 포함
__cpu_setup 반환x0=MMU 활성화에 쓸 SCTLR 값; MMU는 아직 꺼진 상태
__enable_mmu RETTTBR0 idmap으로 다음 명령과 반환 위치가 유효
early_map_kernel 내부MMU on + identity PC; TTBR1 매핑은 아직 구축/수정 중
__primary_switched 진입PC=최종 VA, x0=커널 시작 PA, x21=FDT PA
secondary_startupx20=해당 CPU boot status, 기존 swapper/idmap 테이블 사용

그림의 화살표는 연결 대상에 따라 뜻이 다릅니다. B/BR은 제어 이동, STP/STR은 메모리 쓰기, TTBR은 테이블 루트 선택, descriptor는 주소 번역 관계입니다.

head.S와 관련 함수는 Linux v6.18.37 소스를 기준으로 확인했습니다. 커널 빌드와 실제 보드의 부팅 시험은 수행하지 않았습니다.

표기 1. SYM_INNER_LABEL부터: 이름·범위·ELF 타입

SYM_INNER_LABEL(init_el1, SYM_L_LOCAL)은 init_kernel_el 내부의 현재 위치에 init_el1이라는 이름을 붙입니다. 첫 인자는 이름, 둘째 인자는 심볼을 다른 오브젝트 파일에 공개할지 정하는 매크로입니다. 호출 명령이 아니며, 새 함수 프레임을 만들지도 않습니다. 여기의 LOCAL은 “이 위치로 분기할 수 없다”가 아니라 외부 오브젝트에 전역 심볼로 공개하지 않는다는 의미입니다.

v6.18.37 include/linux/linkage.h의 정의를 따라가면 다음과 같습니다. ASM_NL은 어셈블리 문장을 나누는 구분자이며 여기서는 세미콜론입니다. SYM_L_LOCAL(name)과 SYM_A_NONE은 별도의 어셈블리 코드를 생성하지 않습니다.

/* 원문 매크로의 확장 관계 */
SYM_INNER_LABEL(init_el1, SYM_L_LOCAL)
    -> .type init_el1 STT_NOTYPE
       SYM_ENTRY(init_el1, SYM_L_LOCAL, SYM_A_NONE)
    -> .type init_el1 STT_NOTYPE
       init_el1:

.type은 ELF 심볼 정보를 지정하고 init_el1:은 위치를 정의합니다. 이 둘은 실행 명령 바이트를 추가하지 않습니다. 정렬도 요구하지 않습니다. 그러므로 275행 다음에는 바로 INIT_SCTLR 값을 준비하는 명령이 이어집니다. init_el2도 같은 종류의 내부 라벨이며, 두 경로 모두 init_kernel_el의 시작과 끝 사이에 속합니다.

표기이 버전에서 하는 일하지 않는 일
SYM_CODE_START(name)전역 이름 + CONFIG_FUNCTION_ALIGNMENT 정렬 + label자동 stack frame 생성 없음
SYM_CODE_START_LOCAL(name)비전역 이름 + 정렬 + label외부 공개를 위한 .globl 없음
SYM_CODE_END(name)STT_NOTYPE 및 .size 설정RET를 넣지 않음
SYM_FUNC_START(name)ARM64 override: 전역 이름 + 정렬 + label + bti c레지스터 push나 SP 조정 없음
SYM_FUNC_START_LOCAL(name)비전역 함수 시작 + 정렬 + bti cstatic C 함수 본체를 생성하는 것은 아님
SYM_FUNC_END(name)STT_FUNC 및 .size 설정호출자에게 돌아가는 명령 없음
SYM_INNER_LABEL(name, linkage)함수/코드 범위 안의 STT_NOTYPE label별도 함수 크기나 BTI를 자동 추가하지 않음

특히 SYM_FUNC_START는 include/linux/linkage.h의 일반 정의만 읽으면 불완전합니다. 그보다 먼저 포함되는 arch/arm64/include/asm/linkage.h가 정의를 덮어씁니다. ARM64에서는 다음과 같이 bti c까지 나옵니다. 이 헤더는 ARM64용 정의를 항상 적용합니다. 실제 BTI 보호의 강제 여부는 CPU 기능과 매핑/제어 설정에 따르지만 명령의 삽입 자체와 구분해야 합니다.

/* init_kernel_el 시작 부분의 구조적 확장 */
.globl init_kernel_el
.balign CONFIG_FUNCTION_ALIGNMENT
init_kernel_el:
    bti c
    mrs x1, CurrentEL

/* SYM_FUNC_END(init_kernel_el)의 역할 */
.type init_kernel_el STT_FUNC
.set .L__sym_size_init_kernel_el, .-init_kernel_el
.size init_kernel_el, .L__sym_size_init_kernel_el

위의 CONFIG_FUNCTION_ALIGNMENT는 특정 .config에서 결정되는 바이트 정렬 값입니다. 여기서는 .config를 지정해 빌드하지 않았으므로 정렬 값은 기호로 표기합니다. 점 .은 assembler의 현재 위치이고 .-init_kernel_el은 함수 시작부터 현재 위치까지의 바이트 수입니다. 실제 제어 이동은 코드에 적힌 B, BR, ERET 등의 명령이 담당합니다.

SYM_L_LOCAL은 .local name을 출력하는 구현이 아니라 아무것도 출력하지 않는 구현입니다. 전역 선언이 없는 이 심볼은 local binding을 유지합니다. 이것과 .Lfoo라는 assembler의 임시 라벨 이름 관례는 별개입니다. LOCAL 심볼은 디버깅/심볼 정보에 남을 수 있지만 .L로 시작하는 임시 라벨은 보통 최종 심볼 테이블에 남기지 않습니다.

정의 원문 · include/linux/linkage.h L82–L246
include/linux/linkage.h · L82–L246
82
83/* SYM_T_OBJECT -- type used by assembler to mark data */
84#ifndef SYM_T_OBJECT
85#define SYM_T_OBJECT				STT_OBJECT
86#endif
87
88/* SYM_T_NONE -- type used by assembler to mark entries of unknown type */
89#ifndef SYM_T_NONE
90#define SYM_T_NONE				STT_NOTYPE
91#endif
92
93/* SYM_A_* -- align the symbol? */
94#define SYM_A_ALIGN				ALIGN
95#define SYM_A_NONE				/* nothing */
96
97/* SYM_L_* -- linkage of symbols */
98#define SYM_L_GLOBAL(name)			.globl name
99#define SYM_L_WEAK(name)			.weak name
100#define SYM_L_LOCAL(name)			/* nothing */
101
102#ifndef LINKER_SCRIPT
103#define ALIGN __ALIGN
104#define ALIGN_STR __ALIGN_STR
105
106/* === DEPRECATED annotations === */
107
108#ifndef CONFIG_ARCH_USE_SYM_ANNOTATIONS
109#ifndef GLOBAL
110/* deprecated, use SYM_DATA*, SYM_ENTRY, or similar */
111#define GLOBAL(name) \
112	.globl name ASM_NL \
113	name:
114#endif
115
116#ifndef ENTRY
117/* deprecated, use SYM_FUNC_START */
118#define ENTRY(name) \
119	SYM_FUNC_START(name)
120#endif
121#endif /* CONFIG_ARCH_USE_SYM_ANNOTATIONS */
122#endif /* LINKER_SCRIPT */
123
124#ifndef CONFIG_ARCH_USE_SYM_ANNOTATIONS
125#ifndef WEAK
126/* deprecated, use SYM_FUNC_START_WEAK* */
127#define WEAK(name)	   \
128	SYM_FUNC_START_WEAK(name)
129#endif
130
131#ifndef END
132/* deprecated, use SYM_FUNC_END, SYM_DATA_END, or SYM_END */
133#define END(name) \
134	.size name, .-name
135#endif
136
137#ifndef ENDPROC
138/* deprecated, use SYM_FUNC_END */
139#define ENDPROC(name) \
140	SYM_FUNC_END(name)
141#endif
142#endif /* CONFIG_ARCH_USE_SYM_ANNOTATIONS */
143
144/* === generic annotations === */
145
146/* SYM_ENTRY -- use only if you have to for non-paired symbols */
147#ifndef SYM_ENTRY
148#define SYM_ENTRY(name, linkage, align...)		\
149	linkage(name) ASM_NL				\
150	align ASM_NL					\
151	name:
152#endif
153
154/* SYM_START -- use only if you have to */
155#ifndef SYM_START
156#define SYM_START(name, linkage, align...)		\
157	SYM_ENTRY(name, linkage, align)
158#endif
159
160/* SYM_END -- use only if you have to */
161#ifndef SYM_END
162#define SYM_END(name, sym_type)				\
163	.type name sym_type ASM_NL			\
164	.set .L__sym_size_##name, .-name ASM_NL		\
165	.size name, .L__sym_size_##name
166#endif
167
168/* SYM_ALIAS -- use only if you have to */
169#ifndef SYM_ALIAS
170#define SYM_ALIAS(alias, name, linkage)			\
171	linkage(alias) ASM_NL				\
172	.set alias, name ASM_NL
173#endif
174
175/* === code annotations === */
176
177/*
178 * FUNC -- C-like functions (proper stack frame etc.)
179 * CODE -- non-C code (e.g. irq handlers with different, special stack etc.)
180 *
181 * Objtool validates stack for FUNC, but not for CODE.
182 * Objtool generates debug info for both FUNC & CODE, but needs special
183 * annotations for each CODE's start (to describe the actual stack frame).
184 *
185 * Objtool requires that all code must be contained in an ELF symbol. Symbol
186 * names that have a  .L prefix do not emit symbol table entries. .L
187 * prefixed symbols can be used within a code region, but should be avoided for
188 * denoting a range of code via ``SYM_*_START/END`` annotations.
189 *
190 * ALIAS -- does not generate debug info -- the aliased function will
191 */
192
193/* SYM_INNER_LABEL_ALIGN -- only for labels in the middle of code */
194#ifndef SYM_INNER_LABEL_ALIGN
195#define SYM_INNER_LABEL_ALIGN(name, linkage)	\
196	.type name SYM_T_NONE ASM_NL			\
197	SYM_ENTRY(name, linkage, SYM_A_ALIGN)
198#endif
199
200/* SYM_INNER_LABEL -- only for labels in the middle of code */
201#ifndef SYM_INNER_LABEL
202#define SYM_INNER_LABEL(name, linkage)		\
203	.type name SYM_T_NONE ASM_NL			\
204	SYM_ENTRY(name, linkage, SYM_A_NONE)
205#endif
206
207/* SYM_FUNC_START -- use for global functions */
208#ifndef SYM_FUNC_START
209#define SYM_FUNC_START(name)				\
210	SYM_START(name, SYM_L_GLOBAL, SYM_A_ALIGN)
211#endif
212
213/* SYM_FUNC_START_NOALIGN -- use for global functions, w/o alignment */
214#ifndef SYM_FUNC_START_NOALIGN
215#define SYM_FUNC_START_NOALIGN(name)			\
216	SYM_START(name, SYM_L_GLOBAL, SYM_A_NONE)
217#endif
218
219/* SYM_FUNC_START_LOCAL -- use for local functions */
220#ifndef SYM_FUNC_START_LOCAL
221#define SYM_FUNC_START_LOCAL(name)			\
222	SYM_START(name, SYM_L_LOCAL, SYM_A_ALIGN)
223#endif
224
225/* SYM_FUNC_START_LOCAL_NOALIGN -- use for local functions, w/o alignment */
226#ifndef SYM_FUNC_START_LOCAL_NOALIGN
227#define SYM_FUNC_START_LOCAL_NOALIGN(name)		\
228	SYM_START(name, SYM_L_LOCAL, SYM_A_NONE)
229#endif
230
231/* SYM_FUNC_START_WEAK -- use for weak functions */
232#ifndef SYM_FUNC_START_WEAK
233#define SYM_FUNC_START_WEAK(name)			\
234	SYM_START(name, SYM_L_WEAK, SYM_A_ALIGN)
235#endif
236
237/* SYM_FUNC_START_WEAK_NOALIGN -- use for weak functions, w/o alignment */
238#ifndef SYM_FUNC_START_WEAK_NOALIGN
239#define SYM_FUNC_START_WEAK_NOALIGN(name)		\
240	SYM_START(name, SYM_L_WEAK, SYM_A_NONE)
241#endif
242
243/*
244 * SYM_FUNC_END -- the end of SYM_FUNC_START_LOCAL, SYM_FUNC_START,
245 * SYM_FUNC_START_WEAK, ...
246 */
정의 원문 · arch/arm64/include/asm/linkage.h L1–L48
arch/arm64/include/asm/linkage.h · L1–L48
1#ifndef __ASM_LINKAGE_H
2#define __ASM_LINKAGE_H
3
4#ifdef __ASSEMBLY__
5#include <asm/assembler.h>
6#endif
7
8#define __ALIGN		.balign CONFIG_FUNCTION_ALIGNMENT
9#define __ALIGN_STR	".balign " #CONFIG_FUNCTION_ALIGNMENT
10
11/*
12 * When using in-kernel BTI we need to ensure that PCS-conformant
13 * assembly functions have suitable annotations.  Override
14 * SYM_FUNC_START to insert a BTI landing pad at the start of
15 * everything, the override is done unconditionally so we're more
16 * likely to notice any drift from the overridden definitions.
17 */
18#define SYM_FUNC_START(name)				\
19	SYM_START(name, SYM_L_GLOBAL, SYM_A_ALIGN)	\
20	bti c ;
21
22#define SYM_FUNC_START_NOALIGN(name)			\
23	SYM_START(name, SYM_L_GLOBAL, SYM_A_NONE)	\
24	bti c ;
25
26#define SYM_FUNC_START_LOCAL(name)			\
27	SYM_START(name, SYM_L_LOCAL, SYM_A_ALIGN)	\
28	bti c ;
29
30#define SYM_FUNC_START_LOCAL_NOALIGN(name)		\
31	SYM_START(name, SYM_L_LOCAL, SYM_A_NONE)	\
32	bti c ;
33
34#define SYM_FUNC_START_WEAK(name)			\
35	SYM_START(name, SYM_L_WEAK, SYM_A_ALIGN)	\
36	bti c ;
37
38#define SYM_FUNC_START_WEAK_NOALIGN(name)		\
39	SYM_START(name, SYM_L_WEAK, SYM_A_NONE)		\
40	bti c ;
41
42#define SYM_TYPED_FUNC_START(name)				\
43	SYM_TYPED_START(name, SYM_L_GLOBAL, SYM_A_ALIGN)	\
44	bti c ;
45
46#endif

목차로 돌아가기 · 원래 읽던 위치는 브라우저 뒤로 가기로 돌아갈 수 있습니다.

표기 2. 전처리·어셈블·링크·실행은 서로 다른 단계

head.S의 대문자 S는 C 전처리기를 먼저 거친다는 뜻입니다. #include는 헤더의 정의를 가져오고, #if/#ifdef/#ifndef는 빌드 설정에 따라 소스를 남기거나 제거합니다. #error는 빌드를 중단합니다. CPU가 실행 중에 이 줄을 읽고 판단하는 것이 아닙니다.

단계이 파일에서 보는 예결과
C 전처리SYM_* / CPU_LE / #ifdef텍스트 치환과 조건별 소스 선택
어셈블.macro / .if / adrp / b매크로 전개, 명령 인코딩, relocation 정보
링크_text / early_init_stack / :lo12:섹션 배치와 심볼 주소 확정
부팅 중 패치alternative_ifcapability에 맞는 명령열 선택·교체
CPU 실행mrs / str / b.eq / eret레지스터·메모리·PC 상태 변화

CPU_LE(code...)와 CPU_BE(code...)는 CONFIG_CPU_BIG_ENDIAN에 따라 인자 명령을 남기거나 없앱니다. runtime endian 검사와 이 전처리 선택은 다릅니다. little-endian 빌드에서는 아래 두 줄 중 첫 번째의 TBNZ만 남습니다. 그 TBNZ가 실행될 때 실제 SCTLR.EE를 검사합니다.

CPU_LE( tbnz x19, #SCTLR_ELx_EE_SHIFT, 1f )
CPU_BE( tbz  x19, #SCTLR_ELx_EE_SHIFT, 1f )

/* little-endian 빌드 후 */
tbnz x19, #SCTLR_ELx_EE_SHIFT, 1f

매크로의 확장 결과는 빌드 설정에 따라 달라집니다. 아래 예시는 헤더 정의를 풀어 쓴 것으로, 특정 .config의 빌드 결과는 아닙니다.

정의 원문 · arch/arm64/include/asm/assembler.h L134–L152
arch/arm64/include/asm/assembler.h · L134–L152
134	.endm
135
136/*
137 * Select code when configured for BE.
138 */
139#ifdef CONFIG_CPU_BIG_ENDIAN
140#define CPU_BE(code...) code
141#else
142#define CPU_BE(code...)
143#endif
144
145/*
146 * Select code when configured for LE.
147 */
148#ifdef CONFIG_CPU_BIG_ENDIAN
149#define CPU_LE(code...)
150#else
151#define CPU_LE(code...) code
152#endif

목차로 돌아가기 · 원래 읽던 위치는 브라우저 뒤로 가기로 돌아갈 수 있습니다.

표기 3. __HEAD·__INIT와 데이터 지시문

__HEAD.section ".head.text","ax", __INIT.section ".init.text","ax"로 확장됩니다. section은 이후 출력할 코드·데이터를 어느 입력 섹션에 놓을지 지정합니다. ELF flag에서 a는 메모리에 적재되는 allocatable, x는 executable, w는 writable입니다. 이 문자열이 즉시 SCTLR이나 페이지 테이블 권한을 바꾸지는 않습니다.

head.S의 .section ".idmap.text","a"는 원문 그대로 a만 적혀 있습니다. 링커 스크립트가 이 입력 섹션을 모으고, 초기 페이지 테이블 코드가 해당 주소 범위의 실제 실행 권한을 설정합니다. identity mapping의 VA=PA 관계 역시 .section 한 줄이 만드는 것이 아닙니다.

지시문크기/작용head.S에서의 예
.text일반 text 입력 섹션으로 전환__secondary_switched 앞
.quad 08바이트 값 출력Image offset 및 예약 필드
.long symbol4바이트 값 출력PE header offset
.ascii string문자열 바이트 출력, 자동 NUL 없음ARM64_IMAGE_MAGIC
.balign n다음 위치를 n바이트 경계로 정렬SYM_* 시작 매크로
.type / .sizeELF 심볼의 타입/크기 기록SYM_* 끝과 내부 라벨
.set name, exprassembler 심볼에 식의 값 지정함수 크기 및 매크로 보조 심볼
.pushsection / .popsection다른 섹션을 잠시 사용한 뒤 복원BUG table 및 alternative metadata

AArch64 GAS의 .align 2는 2바이트가 아니라 2의 2제곱, 즉 4바이트 정렬입니다. .balign 4와 단위가 다릅니다. .quad.long은 명령이 아니라 데이터 바이트를 내보냅니다. Image의 B primary_entry가 이 데이터를 건너뛰므로 CPU가 크기 필드를 명령으로 실행하지 않습니다.

정의 원문 · include/linux/init.h L89–L103
include/linux/init.h · L89–L103
89#endif
90
91/* For assembly routines */
92#define __HEAD		.section	".head.text","ax"
93#define __INIT		.section	".init.text","ax"
94#define __FINIT		.previous
95
96#define __INITDATA	.section	".init.data","aw",%progbits
97#define __INITRODATA	.section	".init.rodata","a",%progbits
98#define __FINITDATA	.previous
99
100/* silence warnings when references are OK */
101#define __REF            .section       ".ref.text", "ax"
102#define __REFDATA        .section       ".ref.data", "aw"
103#define __REFCONST       .section       ".ref.rodata", "a"

목차로 돌아가기 · 원래 읽던 위치는 브라우저 뒤로 가기로 돌아갈 수 있습니다.

표기 4. 0f·1b·.macro·역슬래시를 읽는 법

숫자 라벨 1:은 여러 번 사용할 수 있습니다. 1f는 현재 위치에서 앞으로 만나는 가장 가까운 1번 라벨, 1b는 뒤로 거슬러 가장 가까운 1번 라벨입니다. 여기 f/b는 16진수 접미사나 CPU flag 이름이 아닙니다. 반면 pen:은 이름 있는 라벨이고 b pen이 그 위치로 분기합니다.

1:  wfe
    wfi
    b 1b           // 바로 위 1:로 되돌아감

    cbz x0, 2f     // 아래쪽 2:로 이동
    ...
2:  ret

.macro init_cpu_task tsk,tmp1,tmp2는 assembler 매크로 정의입니다. 정의 자체를 호출해서 실행하는 것이 아니라 사용 위치에 본문을 펼칩니다. \tsk는 매크로 인자를 대입합니다. init_cpu_task x4,x5,x6이라면 msr sp_el0,\tskmsr sp_el0,x4가 됩니다. .endm은 정의의 끝입니다. 매크로를 펼치는 행위는 BL도 아니고 LR을 바꾸지도 않습니다.

init_cpu_task 안의 w\tmp2는 눈여겨봐야 합니다. tmp2=x6을 넣으면 문자열은 wx6이 됩니다. assembler.h 앞부분이 .irp n,0,...,30 반복과 wx\n .req w\n으로 wx6을 w6의 alias로 미리 정의합니다. 그래서 실제로는 32비트 CPU 번호를 w6으로 읽고 뒤에서 x6을 인덱스로 사용할 수 있습니다. 단순한 오타가 아닙니다.

매크로 내부의 \@는 매크로 확장마다 고유한 번호를 만들 때 사용합니다. 같은 매크로를 여러 번 펼쳐도 .Lskip_\@ 라벨들이 충돌하지 않습니다. \sym\()_lo32\()는 인자 이름과 뒤 문자열을 구분하는 빈 구분자입니다. sym=_kernel_size_le이면 _kernel_size_le_lo32가 됩니다.

.req는 레지스터 별명을 만들고 .unreq는 그 별명을 해제합니다. 이 파일에서 쓰는 lr은 assembler.h에서 x30으로 정한 별명입니다. .if/.else/.endif는 assembler 단계의 조건부 처리로, #if 계열의 C 전처리와 시점이 다르며 실행 중 분기 명령도 아닙니다.

정의 원문 · arch/arm64/include/asm/assembler.h L29–L37
arch/arm64/include/asm/assembler.h · L29–L37
29	/*
30	 * Provide a wxN alias for each wN register so what we can paste a xN
31	 * reference after a 'w' to obtain the 32-bit version.
32	 */
33	.irp	n,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30
34	wx\n	.req	w\n
35	.endr
36
37	.macro disable_daif
정의 원문 · arch/arm64/include/asm/assembler.h L126–L132
arch/arm64/include/asm/assembler.h · L126–L132
126lr	.req	x30		// link register
127
128/*
129 * Vector entry
130 */
131	 .macro	ventry	label
132	.align	7

목차로 돌아가기 · 원래 읽던 위치는 브라우저 뒤로 가기로 돌아갈 수 있습니다.

표기 5. adr_l·str_l·mov_q·le64sym의 실제 확장

adr_l은 심볼 주소를 만드는 두 개의 명령으로 확장되는 매크로입니다. ADRP는 현재 PC의 4 KiB 경계를 기준으로 대상의 4 KiB page 주소를 만들고 ADD가 하위 12비트를 더합니다. 여기 명령의 4 KiB 단위는 커널의 CONFIG_ARM64_64K_PAGES와 별개입니다. 앞 본문의 “페이지 정렬”은 ADRP 명령의 주소 생성 단위와 실제 MMU granule을 구별해서 읽어야 합니다.

adr_l x0, boot_args
/* 확장 */
adrp x0, boot_args
add  x0, x0, :lo12:boot_args

str_l x21, __fdt_pointer, x5
/* 확장 */
adrp x5, __fdt_pointer
str  x21, [x5, :lo12:__fdt_pointer]

:lo12:는 assembler/linker에 심볼 주소의 하위 12비트를 이 위치의 relocation으로 사용하도록 지시하는 표기입니다. CPU가 실행할 때 문자열을 해석하는 것이 아닙니다. str_l의 세 인자는 저장할 값, 목적 심볼, 주소를 만들 임시 레지스터입니다. x5의 기존 값은 주소 계산 과정에서 바뀌지만, x21의 값은 그대로 유지됩니다. 대상 레지스터가 x이면 8바이트, w이면 4바이트 store다.

ldr x8,=__primary_switched의 등호 표기는 주소 상수를 로드하는 assembler 의사 명령입니다. ldr x8,[x0]처럼 x0가 가리키는 데이터를 읽는 문법과 다릅니다. head.S에서는 최종 분기 대상 주소를 literal로 얻고 BR합니다. ADRP의 PC-relative 계산과 절대 주소 literal의 재배치가 서로 다른 이유입니다.

mov_q는 64비트 상수를 만들기 위해 16비트 조각을 MOVZ/MOVK 계열로 구성합니다. source의 .if가 상수의 크기와 부호를 검사하여 2–4개 wide-immediate 명령을 만듭니다. 메모리에서 8바이트를 읽는 함수가 아닙니다. 다음은 양의 64비트 상수에 대한 의미상 확장 예입니다.

mov_q x0, 0x123456789abcdef0
/* 16비트 조각으로 구성하는 확장 의미 */
movz x0, #0x1234, lsl #48
movk x0, #0x5678, lsl #32
movk x0, #0x9abc, lsl #16
movk x0, #0xdef0

실제 매크로는 :abs_g3:, :abs_g2_nc: 등의 relocation modifier를 사용합니다. g0/g1/g2/g3은 하위부터 16비트 묶음이며, MOVK는 다른 비트를 유지하고 해당 조각만 바꿉니다. 위 예시는 실제 빌드 결과를 캡처한 disassembly가 아니라 이 상수에 대한 확장 의미입니다.

le64sym은 이름 뒤에 _lo32와 _hi32를 붙인 두 심볼을 .long으로 출력합니다. 예를 들어 le64sym _kernel_size_le_kernel_size_le_lo32_kernel_size_le_hi32를 차례로 씁니다. endian 처리는 kernel/image.h의 DEFINE_IMAGE_LE64/DATA_LE32가 링크 시 값에 수행합니다. 그래서 빅 엔디언 커널에서도 Image 규약의 little-endian 필드를 만들 수 있고, 런타임 재배치 대상 64비트 포인터와도 구분됩니다.

정의 원문 · arch/arm64/include/asm/assembler.h L166–L206
arch/arm64/include/asm/assembler.h · L166–L206
166
167/*
168 * Pseudo-ops for PC-relative adr/ldr/str <reg>, <symbol> where
169 * <symbol> is within the range +/- 4 GB of the PC.
170 */
171	/*
172	 * @dst: destination register (64 bit wide)
173	 * @sym: name of the symbol
174	 */
175	.macro	adr_l, dst, sym
176	adrp	\dst, \sym
177	add	\dst, \dst, :lo12:\sym
178	.endm
179
180	/*
181	 * @dst: destination register (32 or 64 bit wide)
182	 * @sym: name of the symbol
183	 * @tmp: optional 64-bit scratch register to be used if <dst> is a
184	 *       32-bit wide register, in which case it cannot be used to hold
185	 *       the address
186	 */
187	.macro	ldr_l, dst, sym, tmp=
188	.ifb	\tmp
189	adrp	\dst, \sym
190	ldr	\dst, [\dst, :lo12:\sym]
191	.else
192	adrp	\tmp, \sym
193	ldr	\dst, [\tmp, :lo12:\sym]
194	.endif
195	.endm
196
197	/*
198	 * @src: source register (32 or 64 bit wide)
199	 * @sym: name of the symbol
200	 * @tmp: mandatory 64-bit scratch register to calculate the address
201	 *       while <src> needs to be preserved.
202	 */
203	.macro	str_l, src, sym, tmp
204	adrp	\tmp, \sym
205	str	\src, [\tmp, :lo12:\sym]
206	.endm
정의 원문 · arch/arm64/include/asm/assembler.h L531–L565
arch/arm64/include/asm/assembler.h · L531–L565
531#define EXPORT_SYMBOL_NOKASAN(name)
532#else
533#define EXPORT_SYMBOL_NOKASAN(name)	EXPORT_SYMBOL(name)
534#endif
535
536	/*
537	 * Emit a 64-bit absolute little endian symbol reference in a way that
538	 * ensures that it will be resolved at build time, even when building a
539	 * PIE binary. This requires cooperation from the linker script, which
540	 * must emit the lo32/hi32 halves individually.
541	 */
542	.macro	le64sym, sym
543	.long	\sym\()_lo32
544	.long	\sym\()_hi32
545	.endm
546
547	/*
548	 * mov_q - move an immediate constant into a 64-bit register using
549	 *         between 2 and 4 movz/movk instructions (depending on the
550	 *         magnitude and sign of the operand)
551	 */
552	.macro	mov_q, reg, val
553	.if (((\val) >> 31) == 0 || ((\val) >> 31) == 0x1ffffffff)
554	movz	\reg, :abs_g1_s:\val
555	.else
556	.if (((\val) >> 47) == 0 || ((\val) >> 47) == 0x1ffff)
557	movz	\reg, :abs_g2_s:\val
558	.else
559	movz	\reg, :abs_g3:\val
560	movk	\reg, :abs_g2_nc:\val
561	.endif
562	movk	\reg, :abs_g1_nc:\val
563	.endif
564	movk	\reg, :abs_g0_nc:\val
565	.endm
정의 원문 · arch/arm64/kernel/image.h L11–L40
arch/arm64/kernel/image.h · L11–L40
11#error This file should only be included in vmlinux.lds.S
12#endif
13
14#include <asm/image.h>
15
16/*
17 * There aren't any ELF relocations we can use to endian-swap values known only
18 * at link time (e.g. the subtraction of two symbol addresses), so we must get
19 * the linker to endian-swap certain values before emitting them.
20 *
21 * Note that, in order for this to work when building the ELF64 PIE executable
22 * (for KASLR), these values should not be referenced via R_AARCH64_ABS64
23 * relocations, since these are fixed up at runtime rather than at build time
24 * when PIE is in effect. So we need to split them up in 32-bit high and low
25 * words.
26 */
27#ifdef CONFIG_CPU_BIG_ENDIAN
28#define DATA_LE32(data)				\
29	((((data) & 0x000000ff) << 24) |	\
30	 (((data) & 0x0000ff00) << 8)  |	\
31	 (((data) & 0x00ff0000) >> 8)  |	\
32	 (((data) & 0xff000000) >> 24))
33#else
34#define DATA_LE32(data) ((data) & 0xffffffff)
35#endif
36
37#define DEFINE_IMAGE_LE64(sym, data)				\
38	sym##_lo32 = DATA_LE32((data) & 0xffffffff);		\
39	sym##_hi32 = DATA_LE32((data) >> 32)
40

목차로 돌아가기 · 원래 읽던 위치는 브라우저 뒤로 가기로 돌아갈 수 있습니다.

표기 6. alternative_if는 매번 실행되는 if문이 아닙니다

alternative 매크로는 기본 명령열, 교체 명령열, 두 위치와 CPU capability를 연결하는 metadata를 만듭니다. 커널의 alternative 패치 단계가 capability에 맞춰 명령을 바꿉니다. 실행할 때마다 이 자리에서 C의 if처럼 기능 레지스터를 검사하는 코드가 자동으로 붙는 것은 아닙니다.

alternative_if ARM64_HAS_VA52
    bl __cpu_secondary_check52bitva
alternative_else_nop_endif

이 경우 VA52가 선택되는 쪽은 BL이고 다른 쪽은 같은 길이의 NOP다. alternative_if는 해당 capability가 있는 쪽의 명령을 먼저 적는 형식이고, alternative_if_not은 없는 쪽의 명령을 먼저 적는 형식입니다. 이름의 if/not을 무시하면 기본 경로와 교체 경로를 반대로 읽게 됩니다.

alternative_else_nop_endif는 내부에서 alternative_else, 앞쪽 길이에 맞춘 nops, alternative_endif를 이어 붙입니다. AArch64 명령 한 개는 4바이트이므로 여기 BL 한 개의 대안은 NOP 한 개입니다. metadata의 orig/alt offset, capability, 길이를 통해 교체 범위를 찾습니다. 외부에서 교체 블록 내부로 분기하는 것은 원본 매크로의 제약에도 금지되어 있습니다.

367행의 #ifdef CONFIG_ARM64_VA_BITS_52는 이 블록 자체를 빌드에 넣는지 결정하고, 안쪽 ARM64_HAS_VA52는 실행할 커널이 선택한 capability와 연결됩니다. 빌드할 때 코드를 포함할지 결정하는 과정과 부팅 중 명령을 교체하는 과정을 구분해야 합니다. 패치가 적용되는 정확한 시점은 alternative 초기화 함수를 호출하는 코드에서 확인해야 합니다.

정의 원문 · arch/arm64/include/asm/alternative-macros.h L115–L217
arch/arm64/include/asm/alternative-macros.h · L115–L217
115
116/*
117 * Alternative sequences
118 *
119 * The code for the case where the capability is not present will be
120 * assembled and linked as normal. There are no restrictions on this
121 * code.
122 *
123 * The code for the case where the capability is present will be
124 * assembled into a special section to be used for dynamic patching.
125 * Code for that case must:
126 *
127 * 1. Be exactly the same length (in bytes) as the default code
128 *    sequence.
129 *
130 * 2. Not contain a branch target that is used outside of the
131 *    alternative sequence it is defined in (branches into an
132 *    alternative sequence are not fixed up).
133 */
134
135/*
136 * Begin an alternative code sequence.
137 */
138.macro alternative_if_not cap
139	.set .Lasm_alt_mode, 0
140	.pushsection .altinstructions, "a"
141	altinstruction_entry 661f, 663f, \cap, 662f-661f, 664f-663f
142	.popsection
143661:
144.endm
145
146.macro alternative_if cap
147	.set .Lasm_alt_mode, 1
148	.pushsection .altinstructions, "a"
149	altinstruction_entry 663f, 661f, \cap, 664f-663f, 662f-661f
150	.popsection
151	.subsection 1
152	.align 2	/* So GAS knows label 661 is suitably aligned */
153661:
154.endm
155
156.macro alternative_cb cap, cb
157	.set .Lasm_alt_mode, 0
158	.pushsection .altinstructions, "a"
159	altinstruction_entry 661f, \cb, (1 << ARM64_CB_SHIFT) | \cap, 662f-661f, 0
160	.popsection
161661:
162.endm
163
164/*
165 * Provide the other half of the alternative code sequence.
166 */
167.macro alternative_else
168662:
169	.if .Lasm_alt_mode==0
170	.subsection 1
171	.else
172	.previous
173	.endif
174663:
175.endm
176
177/*
178 * Complete an alternative code sequence.
179 */
180.macro alternative_endif
181664:
182	.org	. - (664b-663b) + (662b-661b)
183	.org	. - (662b-661b) + (664b-663b)
184	.if .Lasm_alt_mode==0
185	.previous
186	.endif
187.endm
188
189/*
190 * Callback-based alternative epilogue
191 */
192.macro alternative_cb_end
193662:
194.endm
195
196/*
197 * Provides a trivial alternative or default sequence consisting solely
198 * of NOPs. The number of NOPs is chosen automatically to match the
199 * previous case.
200 */
201.macro alternative_else_nop_endif
202alternative_else
203	nops	(662b-661b) / AARCH64_INSN_SIZE
204alternative_endif
205.endm
206
207#define _ALTERNATIVE_CFG(insn1, insn2, cap, cfg, ...)	\
208	alternative_insn insn1, insn2, cap, IS_ENABLED(cfg)
209
210#endif  /*  __ASSEMBLY__  */
211
212/*
213 * Usage: asm(ALTERNATIVE(oldinstr, newinstr, cpucap));
214 *
215 * Usage: asm(ALTERNATIVE(oldinstr, newinstr, cpucap, CONFIG_FOO));
216 * N.B. If CONFIG_FOO is specified, but not selected, the whole block
217 *      will be omitted, including oldinstr.
정의 원문 · arch/arm64/include/asm/alternative.h L10–L22
arch/arm64/include/asm/alternative.h · L10–L22
10#include <linux/types.h>
11#include <linux/stddef.h>
12
13struct alt_instr {
14	s32 orig_offset;	/* offset to original instruction */
15	s32 alt_offset;		/* offset to replacement instruction */
16	u16 cpucap;		/* cpucap bit set for replacement */
17	u8  orig_len;		/* size of original instruction(s) */
18	u8  alt_len;		/* size of new instruction(s), <= orig_len */
19};
20
21typedef void (*alternative_cb_t)(struct alt_instr *alt,
22				 __le32 *origptr, __le32 *updptr, int nr_inst);

목차로 돌아가기 · 원래 읽던 위치는 브라우저 뒤로 가기로 돌아갈 수 있습니다.

표기 7. 시스템 레지스터·MMU 매크로

MRS는 시스템 레지스터에서 일반 레지스터로 읽고, MSR는 반대 방향으로 씁니다. mrs x1,CurrentELmsr sctlr_el1,x0에서 인자 순서가 반대인 이유입니다. 일반 메모리 load/store와 다른 명령이며, 접근 가능한 EL과 trap 설정의 제한을 받습니다.

mrs_s/msr_s는 SYS_*의 수치 인코딩과 일반 레지스터 번호를 조합해 MRS/MSR 명령을 직접 출력하는 assembler 매크로입니다. 도구가 모든 새 시스템 레지스터의 이름을 알지 못해도 인코딩으로 쓸 수 있게 합니다. _s를 플래그 갱신 접미사나 secure 전용 명령으로 해석하지 않습니다. msr_s SYS_SCTLR_EL12,x1 역시 그 레지스터를 대상으로 하는 MSR 한 개의 인코딩입니다.

매크로입력·임시 레지스터확장되는 명령
phys_to_ttbr dst,phys결과 dst, 물리 주소 physPA_BITS_52이면 ORR 및 BADDR mask, 아니면 MOV
load_ttbr1 pgtbl,tmp1,tmp2루트 PA와 임시 두 개phys_to_ttbr → offset_ttbr1 → MSR TTBR1 → ISB
set_sctlr_el1 regSCTLR 값MSR → ISB → IC IALLU → DSB NSH → ISB
pre_disable_mmu_workaround인자 없음해당 Falkor erratum 설정에서 ISB, 아니면 빈 매크로
set_sctlr_el1 x0
/* assembler.h의 확장 */
msr sctlr_el1, x0
isb
ic iallu
dsb nsh
isb

phys_to_ttbr의 52비트 경로는 orr dst,phys,phys,lsr #46 후 TTBR_BADDR_MASK_52로 마스크합니다. 높은 PA 비트를 TTBR 형식에 배치하는 연산입니다. load_ttbr1의 tmp 인자가 원래 pgtbl 레지스터와 같은 경우도 허용하여 head.S는 load_ttbr1 x1,x1,x3을 씁니다. 레지스터 이름이 반복된다고 세 번의 독립적인 주소가 들어온다는 뜻이 아닙니다.

offset_ttbr1의 48/52비트 보정은 VA_BITS_52이면서 비 LPA2인 구성에서 조건부로 들어갑니다. 이런 조건은 실제 빌드 설정에 따라 명령 수를 바꿉니다. 매크로 한 줄을 항상 단일 CPU 명령으로 세면 코드 크기와 실행 순서를 틀리게 계산합니다.

정의 원문 · arch/arm64/include/asm/sysreg.h L1134–L1142
arch/arm64/include/asm/sysreg.h · L1134–L1142
1134#ifdef __ASSEMBLY__
1135
1136	.macro	mrs_s, rt, sreg
1137	 __emit_inst(0xd5200000|(\sreg)|(.L__gpr_num_\rt))
1138	.endm
1139
1140	.macro	msr_s, sreg, rt
1141	__emit_inst(0xd5000000|(\sreg)|(.L__gpr_num_\rt))
1142	.endm
정의 원문 · arch/arm64/include/asm/assembler.h L454–L459
arch/arm64/include/asm/assembler.h · L454–L459
454	.macro		load_ttbr1, pgtbl, tmp1, tmp2
455	phys_to_ttbr	\tmp1, \pgtbl
456	offset_ttbr1 	\tmp1, \tmp2
457	msr		ttbr1_el1, \tmp1
458	isb
459	.endm
정의 원문 · arch/arm64/include/asm/assembler.h L589–L613
arch/arm64/include/asm/assembler.h · L589–L613
589	.macro	offset_ttbr1, ttbr, tmp
590#if defined(CONFIG_ARM64_VA_BITS_52) && !defined(CONFIG_ARM64_LPA2)
591	mrs	\tmp, tcr_el1
592	and	\tmp, \tmp, #TCR_T1SZ_MASK
593	cmp	\tmp, #TCR_T1SZ(VA_BITS_MIN)
594	orr	\tmp, \ttbr, #TTBR1_BADDR_4852_OFFSET
595	csel	\ttbr, \tmp, \ttbr, eq
596#endif
597	.endm
598
599/*
600 * Arrange a physical address in a TTBR register, taking care of 52-bit
601 * addresses.
602 *
603 * 	phys:	physical address, preserved
604 * 	ttbr:	returns the TTBR value
605 */
606	.macro	phys_to_ttbr, ttbr, phys
607#ifdef CONFIG_ARM64_PA_BITS_52
608	orr	\ttbr, \phys, \phys, lsr #46
609	and	\ttbr, \ttbr, #TTBR_BADDR_MASK_52
610#else
611	mov	\ttbr, \phys
612#endif
613	.endm
정의 원문 · arch/arm64/include/asm/assembler.h L643–L651
arch/arm64/include/asm/assembler.h · L643–L651
643/**
644 * Errata workaround prior to disable MMU. Insert an ISB immediately prior
645 * to executing the MSR that will change SCTLR_ELn[M] from a value of 1 to 0.
646 */
647	.macro pre_disable_mmu_workaround
648#ifdef CONFIG_QCOM_FALKOR_ERRATUM_E1041
649	isb
650#endif
651	.endm
정의 원문 · arch/arm64/include/asm/assembler.h L715–L735
arch/arm64/include/asm/assembler.h · L715–L735
715
716/*
717 * Set SCTLR_ELx to the @reg value, and invalidate the local icache
718 * in the process. This is called when setting the MMU on.
719 */
720.macro set_sctlr, sreg, reg
721	msr	\sreg, \reg
722	isb
723	/*
724	 * Invalidate the local I-cache so that any instructions fetched
725	 * speculatively from the PoC are discarded, since they may have
726	 * been dynamically patched at the PoU.
727	 */
728	ic	iallu
729	dsb	nsh
730	isb
731.endm
732
733.macro set_sctlr_el1, reg
734	set_sctlr sctlr_el1, \reg
735.endm

목차로 돌아가기 · 원래 읽던 위치는 브라우저 뒤로 가기로 돌아갈 수 있습니다.

표기 8. EFI·태스크·EL2·실패 매크로의 구분

표기정의·입력실제 효과와 본문의 연결
efi_signature_nopefi-header.S, 인자 없음EFI이면 MZ 인코딩을 만드는 CCMP, 아니면 NOP; 본문 02
__EFI_PE_HEADERefi-header.S, 인자 없음CONFIG_EFI에 따른 PE/COFF header 데이터 출력; 함수 호출 아님
init_cpu_task tsk,tmp1,tmp2head.S 자체의 .macroSP_EL0/current, 태스크 스택, 최종 frame, SCS, per-CPU 설치; 본문 06
scs_load_currentscs.hSHADOW_CALL_STACK이면 current에서 SCS 포인터 로드, 아니면 빈 확장
set_this_cpu_offset srcassembler.halternative에 따라 TPIDR_EL1 또는 TPIDR_EL2에 src 기록
ptrauth_keys_init_cpu tsk,tmp1,tmp2,tmp3asm_pointer_auth.hcapability/ID 확인, SCTLR 인증 enable 및 태스크 kernel key 설치, ISB
init_el2_hcr valel2_setup.hHCR 설정과 VHE-only 판별; x0/x1 사용; 본문 09
init_el2_stateel2_setup.h여러 EL2 초기화 매크로를 사용 위치에서 펼침; x0/x1/x2의 기존 값 변경; 부록 I
update_early_cpu_boot_status status,tmp1,tmp2head.S 자체의 .macroSTR → DMB → IVAC; 본문 14
ASM_BUG()asm-bug.hBUG metadata와 BRK trap; 반환 불가 지점 검사

ASM_BUG()는 ASM_BUG_FLAGS(0)이고 마지막 실행 명령은 brk BUG_BRK_IMM, 이 버전에서 즉시값은 0x800입니다. CONFIG_GENERIC_BUG이면 __bug_table에 상대 위치와 flag를 기록하고, DEBUG_BUGVERBOSE이면 파일·행 정보도 넣습니다. metadata와 BRK는 별개입니다. “함수를 끝내는 장식”이 아니라 도달하면 예외를 일으키는 실제 명령이 포함됩니다.

init_cpu_task는 사용 위치에 코드가 펼쳐지는 매크로입니다. bl __cpu_setup, bl __pi_create_init_idmap, bl finalise_el2는 실제 호출입니다. __pi_는 PI 빌드에서 심볼 이름에 붙인 접두사이지 ISA 명령 접두사가 아닙니다. 각 매크로의 세부 동작은 본문과 부록에서 설명합니다.

정의 원문 · arch/arm64/include/asm/asm-bug.h L10–L61
arch/arm64/include/asm/asm-bug.h · L10–L61
10#ifdef CONFIG_DEBUG_BUGVERBOSE
11#define _BUGVERBOSE_LOCATION(file, line) __BUGVERBOSE_LOCATION(file, line)
12#define __BUGVERBOSE_LOCATION(file, line)			\
13		.pushsection .rodata.str,"aMS",@progbits,1;	\
14	14472:	.string file;					\
15		.popsection;					\
16								\
17		.long 14472b - .;				\
18		.short line;
19#else
20#define _BUGVERBOSE_LOCATION(file, line)
21#endif
22
23#ifdef CONFIG_GENERIC_BUG
24#define __BUG_ENTRY_START				\
25		.pushsection __bug_table,"aw";		\
26		.align 2;				\
27	14470:	.long 14471f - .;			\
28
29#define __BUG_ENTRY_END					\
30		.align 2;				\
31		.popsection;				\
32	14471:
33
34#define __BUG_ENTRY(flags)				\
35		__BUG_ENTRY_START			\
36_BUGVERBOSE_LOCATION(__FILE__, __LINE__)		\
37		.short flags;				\
38		__BUG_ENTRY_END
39#else
40#define __BUG_ENTRY(flags)
41#endif
42
43#define ASM_BUG_FLAGS(flags)				\
44	__BUG_ENTRY(flags)				\
45	brk	BUG_BRK_IMM
46
47#define ASM_BUG()	ASM_BUG_FLAGS(0)
48
49#ifdef CONFIG_DEBUG_BUGVERBOSE
50#define __BUG_LOCATION_STRING(file, line)		\
51		".long " file "- .;"			\
52		".short " line ";"
53#else
54#define __BUG_LOCATION_STRING(file, line)
55#endif
56
57#define __BUG_ENTRY_STRING(file, line, flags)		\
58		__stringify(__BUG_ENTRY_START)		\
59		__BUG_LOCATION_STRING(file, line)	\
60		".short " flags ";"			\
61		__stringify(__BUG_ENTRY_END)
정의 원문 · arch/arm64/include/asm/brk-imm.h L26–L36
arch/arm64/include/asm/brk-imm.h · L26–L36
26#define KPROBES_BRK_SS_IMM		0x006
27#define KRETPROBES_BRK_IMM		0x007
28#define FAULT_BRK_IMM			0x100
29#define KGDB_DYN_DBG_BRK_IMM		0x400
30#define KGDB_COMPILED_DBG_BRK_IMM	0x401
31#define BUG_BRK_IMM			0x800
32#define KASAN_BRK_IMM			0x900
33#define KASAN_BRK_MASK			0x0ff
34#define UBSAN_BRK_IMM			0x5500
35#define UBSAN_BRK_MASK			0x00ff
36
정의 원문 · arch/arm64/include/asm/scs.h L1–L27
arch/arm64/include/asm/scs.h · L1–L27
1/* SPDX-License-Identifier: GPL-2.0 */
2#ifndef _ASM_SCS_H
3#define _ASM_SCS_H
4
5#ifdef __ASSEMBLY__
6
7#include <asm/asm-offsets.h>
8#include <asm/sysreg.h>
9
10#ifdef CONFIG_SHADOW_CALL_STACK
11	scs_sp	.req	x18
12
13	.macro scs_load_current
14	get_current_task scs_sp
15	ldr	scs_sp, [scs_sp, #TSK_TI_SCS_SP]
16	.endm
17
18	.macro scs_save tsk
19	str	scs_sp, [\tsk, #TSK_TI_SCS_SP]
20	.endm
21#else
22	.macro scs_load_current
23	.endm
24
25	.macro scs_save tsk
26	.endm
27#endif /* CONFIG_SHADOW_CALL_STACK */
정의 원문 · arch/arm64/include/asm/asm_pointer_auth.h L60–L83
arch/arm64/include/asm/asm_pointer_auth.h · L60–L83
60	.macro __ptrauth_keys_init_cpu tsk, tmp1, tmp2, tmp3
61	mrs	\tmp1, id_aa64isar1_el1
62	ubfx	\tmp1, \tmp1, #ID_AA64ISAR1_EL1_APA_SHIFT, #8
63	mrs_s	\tmp2, SYS_ID_AA64ISAR2_EL1
64	ubfx	\tmp2, \tmp2, #ID_AA64ISAR2_EL1_APA3_SHIFT, #4
65	orr	\tmp1, \tmp1, \tmp2
66	cbz	\tmp1, .Lno_addr_auth\@
67	mov_q	\tmp1, (SCTLR_ELx_ENIA | SCTLR_ELx_ENIB | \
68			SCTLR_ELx_ENDA | SCTLR_ELx_ENDB)
69	mrs	\tmp2, sctlr_el1
70	orr	\tmp2, \tmp2, \tmp1
71	msr	sctlr_el1, \tmp2
72	__ptrauth_keys_install_kernel_nosync \tsk, \tmp1, \tmp2, \tmp3
73	isb
74.Lno_addr_auth\@:
75	.endm
76
77	.macro ptrauth_keys_init_cpu tsk, tmp1, tmp2, tmp3
78alternative_if_not ARM64_HAS_ADDRESS_AUTH
79	b	.Lno_addr_auth\@
80alternative_else_nop_endif
81	__ptrauth_keys_init_cpu \tsk, \tmp1, \tmp2, \tmp3
82.Lno_addr_auth\@:
83	.endm

목차로 돌아가기 · 원래 읽던 위치는 브라우저 뒤로 가기로 돌아갈 수 있습니다.

표기 9. #상수, 구조체 오프셋, 링크 심볼

명령 인자의 #는 즉시값 표기입니다. #16은 숫자 16이고 #TSK_STACK은 빌드에서 결정된 숫자 상수입니다. 소스 줄 맨 앞의 #include/#if와 같은 용법이 아닙니다. 또한 .quad 0에서 0은 데이터 값이며, adr_l x0,boot_args의 boot_args는 주소를 가진 심볼입니다.

종류값이 정해지는 근거
구조체 필드 오프셋TSK_STACK, TSK_TI_CPU, CPU_BOOT_TASKasm-offsets.c의 offsetof
구조체 크기/위치PT_REGS_SIZE, S_STACKFRAME, S_STACKFRAME_TYPEsizeof(pt_regs), offsetof(pt_regs,...)
빌드 설정 크기THREAD_SIZE, PAGE_SIZE, CONFIG_FUNCTION_ALIGNMENT설정과 헤더 정의
bit maskSCTLR_ELx_M, SCTLR_ELx_C각각 bit 0, bit 2
bit indexSCTLR_ELx_EE_SHIFTTBNZ/TBZ에서 몇 번째 비트인지 지정
상태 조합INIT_PSTATE_EL1D/A/I/F mask와 EL1h 모드
메모리/링크 심볼_text, vectors, init_task, early_init_stack오브젝트와 링커 스크립트의 배치

TSK_STACK은 stack의 크기가 아니라 task_struct 안에서 stack 필드가 놓인 바이트 오프셋입니다. PT_REGS_SIZE는 반대로 구조체 전체 크기입니다. 둘을 같은 종류의 주소로 읽으면 LDR와 SP 계산이 이해되지 않습니다. asm-offsets.c에서 DEFINE(...)으로 C의 레이아웃을 출력하여 생성 헤더에 반영합니다. 특정 .config의 생성된 asm-offsets.h를 빌드하지 않은 상태에서 숫자 오프셋을 고정해 적지 않습니다.

INIT_PSTATE_EL1은 이 버전 ptrace.h에서 PSR_D_BIT + PSR_A_BIT + PSR_I_BIT + PSR_F_BIT + PSR_MODE_EL1h에 해당하는 비트 OR 조합입니다. D/A/I/F는 debug/SError/IRQ/FIQ 마스크입니다. EL1h는 EL1의 SP를 사용하는 모드입니다. SPSR에 이 값을 넣고 ERET하므로 복귀할 때의 실행 상태가 커널이 요구하는 값으로 설정됩니다. 단지 EL 번호만 바꾸는 상수가 아닙니다.

BOOT_CPU_MODE_EL1=0xe11, BOOT_CPU_MODE_EL2=0xe12는 커널의 소프트웨어 표식이며 CurrentEL 시스템 레지스터 인코딩과 동일하지 않습니다. BOOT_CPU_FLAG_E2H는 bit 32이므로 mov x20,x0는 보존하지만 str w0는 저장하지 않습니다. 비트 위치 SHIFT와 이미 이동된 mask를 구분하는 습관도 필요합니다. TBNZ는 비트 번호를 받고, TST는 mask를 받습니다.

CPU_STUCK_IN_KERNEL은 실패 상태의 기본 부분이고 CPU_STUCK_REASON_52_BIT_VA와 CPU_STUCK_REASON_NO_GRAN은 원인 부분입니다. 원본의 OR 연산자로 둘을 합쳐 status 즉시값으로 전달합니다. 이는 CPU 명령이 아니라 커널의 상태 코드 조합입니다. INIT_SCTLR_EL1_MMU_ON/OFF 및 EL2_MMU_OFF도 같은 방식으로 sysreg.h에 정의된 제어 비트 조합입니다. ON에는 M/C/I가 포함되고 OFF는 단순히 정수 0이 아닙니다. 초기 엔디언·필수 제어 비트를 포함하므로 이름만 보고 mov x0,0으로 치환하지 않습니다.

정의 원문 · arch/arm64/kernel/asm-offsets.c L20–L43
arch/arm64/kernel/asm-offsets.c · L20–L43
20#include <asm/thread_info.h>
21#include <asm/memory.h>
22#include <asm/smp_plat.h>
23#include <asm/suspend.h>
24#include <linux/kbuild.h>
25#include <linux/arm-smccc.h>
26
27int main(void)
28{
29  DEFINE(TSK_TI_CPU,		offsetof(struct task_struct, thread_info.cpu));
30  DEFINE(TSK_TI_FLAGS,		offsetof(struct task_struct, thread_info.flags));
31  DEFINE(TSK_TI_PREEMPT,	offsetof(struct task_struct, thread_info.preempt_count));
32#ifdef CONFIG_ARM64_SW_TTBR0_PAN
33  DEFINE(TSK_TI_TTBR0,		offsetof(struct task_struct, thread_info.ttbr0));
34#endif
35#ifdef CONFIG_SHADOW_CALL_STACK
36  DEFINE(TSK_TI_SCS_BASE,	offsetof(struct task_struct, thread_info.scs_base));
37  DEFINE(TSK_TI_SCS_SP,		offsetof(struct task_struct, thread_info.scs_sp));
38#endif
39  DEFINE(TSK_STACK,		offsetof(struct task_struct, stack));
40#ifdef CONFIG_STACKPROTECTOR
41  DEFINE(TSK_STACK_CANARY,	offsetof(struct task_struct, stack_canary));
42#endif
43  BLANK();
정의 원문 · arch/arm64/kernel/asm-offsets.c L72–L100
arch/arm64/kernel/asm-offsets.c · L72–L100
72  DEFINE(S_LR,			offsetof(struct pt_regs, regs[30]));
73  DEFINE(S_SP,			offsetof(struct pt_regs, sp));
74  DEFINE(S_PC,			offsetof(struct pt_regs, pc));
75  DEFINE(S_PSTATE,		offsetof(struct pt_regs, pstate));
76  DEFINE(S_SYSCALLNO,		offsetof(struct pt_regs, syscallno));
77  DEFINE(S_SDEI_TTBR1,		offsetof(struct pt_regs, sdei_ttbr1));
78  DEFINE(S_PMR,			offsetof(struct pt_regs, pmr));
79  DEFINE(S_STACKFRAME,		offsetof(struct pt_regs, stackframe));
80  DEFINE(S_STACKFRAME_TYPE,	offsetof(struct pt_regs, stackframe.type));
81  DEFINE(PT_REGS_SIZE,		sizeof(struct pt_regs));
82  BLANK();
83#ifdef CONFIG_DYNAMIC_FTRACE_WITH_ARGS
84  DEFINE(FREGS_X0,		offsetof(struct __arch_ftrace_regs, regs[0]));
85  DEFINE(FREGS_X2,		offsetof(struct __arch_ftrace_regs, regs[2]));
86  DEFINE(FREGS_X4,		offsetof(struct __arch_ftrace_regs, regs[4]));
87  DEFINE(FREGS_X6,		offsetof(struct __arch_ftrace_regs, regs[6]));
88  DEFINE(FREGS_X8,		offsetof(struct __arch_ftrace_regs, regs[8]));
89  DEFINE(FREGS_FP,		offsetof(struct __arch_ftrace_regs, fp));
90  DEFINE(FREGS_LR,		offsetof(struct __arch_ftrace_regs, lr));
91  DEFINE(FREGS_SP,		offsetof(struct __arch_ftrace_regs, sp));
92  DEFINE(FREGS_PC,		offsetof(struct __arch_ftrace_regs, pc));
93#ifdef CONFIG_DYNAMIC_FTRACE_WITH_DIRECT_CALLS
94  DEFINE(FREGS_DIRECT_TRAMP,	offsetof(struct __arch_ftrace_regs, direct_tramp));
95#endif
96  DEFINE(FREGS_SIZE,		sizeof(struct __arch_ftrace_regs));
97  BLANK();
98#endif
99  DEFINE(CPU_BOOT_TASK,		offsetof(struct secondary_data, task));
100  BLANK();
정의 원문 · arch/arm64/include/asm/ptrace.h L15–L22
arch/arm64/include/asm/ptrace.h · L15–L22
15/* Current Exception Level values, as contained in CurrentEL */
16#define CurrentEL_EL1		(1 << 2)
17#define CurrentEL_EL2		(2 << 2)
18
19#define INIT_PSTATE_EL1 \
20	(PSR_D_BIT | PSR_A_BIT | PSR_I_BIT | PSR_F_BIT | PSR_MODE_EL1h)
21#define INIT_PSTATE_EL2 \
22	(PSR_D_BIT | PSR_A_BIT | PSR_I_BIT | PSR_F_BIT | PSR_MODE_EL2h)
정의 원문 · arch/arm64/include/asm/virt.h L49–L57
arch/arm64/include/asm/virt.h · L49–L57
49#define BOOT_CPU_MODE_EL1	(0xe11)
50#define BOOT_CPU_MODE_EL2	(0xe12)
51
52/*
53 * Flags returned together with the boot mode, but not preserved in
54 * __boot_cpu_mode. Used by the idreg override code to work out the
55 * boot state.
56 */
57#define BOOT_CPU_FLAG_E2H	BIT_ULL(32)
정의 원문 · arch/arm64/include/asm/sysreg.h L832–L878
arch/arm64/include/asm/sysreg.h · L832–L878
832#define SCTLR_ELx_ENDB	 (BIT(13))
833#define SCTLR_ELx_I	 (BIT(12))
834#define SCTLR_ELx_EOS	 (BIT(11))
835#define SCTLR_ELx_SA	 (BIT(3))
836#define SCTLR_ELx_C	 (BIT(2))
837#define SCTLR_ELx_A	 (BIT(1))
838#define SCTLR_ELx_M	 (BIT(0))
839
840/* SCTLR_EL2 specific flags. */
841#define SCTLR_EL2_RES1	((BIT(4))  | (BIT(5))  | (BIT(11)) | (BIT(16)) | \
842			 (BIT(18)) | (BIT(22)) | (BIT(23)) | (BIT(28)) | \
843			 (BIT(29)))
844
845#define SCTLR_EL2_BT	(BIT(36))
846#ifdef CONFIG_CPU_BIG_ENDIAN
847#define ENDIAN_SET_EL2		SCTLR_ELx_EE
848#else
849#define ENDIAN_SET_EL2		0
850#endif
851
852#define INIT_SCTLR_EL2_MMU_ON						\
853	(SCTLR_ELx_M  | SCTLR_ELx_C | SCTLR_ELx_SA | SCTLR_ELx_I |	\
854	 SCTLR_ELx_IESB | SCTLR_ELx_WXN | ENDIAN_SET_EL2 |		\
855	 SCTLR_ELx_ITFSB | SCTLR_EL2_RES1)
856
857#define INIT_SCTLR_EL2_MMU_OFF \
858	(SCTLR_EL2_RES1 | ENDIAN_SET_EL2)
859
860/* SCTLR_EL1 specific flags. */
861#ifdef CONFIG_CPU_BIG_ENDIAN
862#define ENDIAN_SET_EL1		(SCTLR_EL1_E0E | SCTLR_ELx_EE)
863#else
864#define ENDIAN_SET_EL1		0
865#endif
866
867#define INIT_SCTLR_EL1_MMU_OFF \
868	(ENDIAN_SET_EL1 | SCTLR_EL1_LSMAOE | SCTLR_EL1_nTLSMD | \
869	 SCTLR_EL1_EIS  | SCTLR_EL1_TSCXT  | SCTLR_EL1_EOS)
870
871#define INIT_SCTLR_EL1_MMU_ON \
872	(SCTLR_ELx_M      | SCTLR_ELx_C      | SCTLR_ELx_SA    | \
873	 SCTLR_EL1_SA0    | SCTLR_EL1_SED    | SCTLR_ELx_I     | \
874	 SCTLR_EL1_DZE    | SCTLR_EL1_UCT    | SCTLR_EL1_nTWE  | \
875	 SCTLR_ELx_IESB   | SCTLR_EL1_SPAN   | SCTLR_ELx_ITFSB | \
876	 ENDIAN_SET_EL1   | SCTLR_EL1_UCI    | SCTLR_EL1_EPAN  | \
877	 SCTLR_EL1_LSMAOE | SCTLR_EL1_nTLSMD | SCTLR_EL1_EIS   | \
878	 SCTLR_EL1_TSCXT  | SCTLR_EL1_EOS)

목차로 돌아가기 · 원래 읽던 위치는 브라우저 뒤로 가기로 돌아갈 수 있습니다.

표기 10. 이 파일에 나오는 명령과 NZCV

명령읽는 법이 파일에서 주의할 점
mov dst,src값 복사/상수 구성의 aliassrc가 주소인지 데이터인지는 문맥으로 결정
add / sub덧셈 / 뺄셈여기처럼 S 없는 형식은 NZCV 유지
and / orr / eor / bicAND / OR / XOR / 비트 지우기이 형식은 NZCV 유지; BIC는 두 번째 mask를 반전하여 AND
cmp a,ba-b 결과의 flags만 남김결과 값을 일반 레지스터에 쓰지 않음
tst a,maskAND 결과의 flags만 남김record_mmu_state에서 C=0인지 Z로 전달
csel dst,a,b,cond조건 참이면 a, 아니면 b 선택자체는 NZCV를 갱신하지 않음
ubfx dst,src,lsb,width비트 필드를 0 확장하여 추출TGRAN 4비트 추출
sbfx dst,src,lsb,width같은 필드를 부호 확장하여 추출LPA2 기능값 비교에서 부호 의미 유지
ldr / str레지스터 1개 load/storex면 8 B, w면 4 B
ldp / stp레지스터 2개 load/storex 두 개면 16 B; 주소 갱신은 별도 문법
b label / br reg직접 / 레지스터 간접 분기LR을 새로 만들지 않음
bl label / blr reg직접 / 레지스터 간접 호출x30에 다음 명령 주소 저장
ret보통 x30으로 복귀SPSR을 복원하지 않음
eretELR와 SPSR을 사용하는 예외 복귀init_kernel_el의 상태 전환
cbz / cbnz레지스터가 0 / 비0이면 분기NZCV 변경 없음
tbz / tbnz지정 비트가 0 / 1이면 분기NZCV 변경 없음

조건부 B는 기존 NZCV를 검사합니다. EQ는 Z=1, NE는 Z=0입니다. LT/GE는 signed 비교로 각각 N≠V / N=V이고, GT는 Z=0이면서 N=V다. helper에서 보이는 LO는 unsigned lower로 C=0을 검사합니다. CMP 뒤의 LT와 주소 순회 뒤의 LO를 무조건 같은 “작다”로 해석하지 않습니다.

cmp x19, #CurrentEL_EL2   // Z는 EL2 여부
mrs x19, sctlr_el1       // NZCV 유지
b.ne 0f                 // 위 CMP의 결과 사용
...
tst x19, #SCTLR_ELx_C    // 여기 도달하면 Z의 의미가 C==0으로 바뀜
and x19, x19, #SCTLR_ELx_M  // NZCV 유지
csel x19, xzr, x19, eq   // 바로 위 AND가 아니라 TST의 Z를 사용

N은 음수, Z는 zero, C는 carry/unsigned 비교, V는 signed overflow flag다. 플래그를 마지막으로 바꾼 명령이 무엇인지 추적해야 합니다. 코드에서 가까운 줄이 항상 조건 플래그를 설정한 명령인 것은 아닙니다. record_mmu_state의 엔디언 불일치 분기는 TST를 거치지 않아서 더 앞의 CMP를 계속 사용합니다.

목차로 돌아가기 · 원래 읽던 위치는 브라우저 뒤로 가기로 돌아갈 수 있습니다.

표기 11. [주소]·!·LSL·SP·XZR

표기정확한 의미실제 예
[x0]x0 값이 가리키는 메모리stp x21,x1,[x0]
[x0,#16]x0+16에서 접근, x0는 그대로두 번째 boot_args STP
[sp,#-16]!sp를 16 감소시킨 새 주소에서 접근FP/LR 저장의 pre-index
[sp],#16원래 sp에서 접근한 뒤 16 증가FP/LR 복원의 post-index
[base,index,lsl #3]base + index×8__per_cpu_offset[cpu] 읽기
xN / wN같은 레지스터의 64/32비트 이름wN에 쓰면 상위 32비트 0
xzr / wzr읽으면 0, 쓰기는 버림mov x29,xzr, str wzr,...
sp현재 선택된 stack pointerSP_EL0와 언제나 같은 저장소가 아님
lrx30의 assembler 별명BL/BLR의 복귀 주소

STP 자체가 push라는 이름의 명령은 아닙니다. [sp,#-16]!와 조합하여 stack push 효과를 만드는 것입니다. head.S의 boot_args STP는 같은 명령이지만 SP를 전혀 사용하지 않습니다. lsl #3도 이 주소식에서는 별도의 LSL 명령이 아니라 scaled register addressing의 일부입니다.

주소 레지스터를 읽는 것과 그 주소의 메모리를 읽는 것을 나눠야 합니다. mov x21,x0는 주소 값을 복사하고, ldr x2,[x0,#CPU_BOOT_TASK]는 x0+offset의 메모리에서 태스크 포인터를 읽습니다. 둘 다 최종 결과가 포인터여도 접근하는 메모리와 오류가 발생할 수 있는 위치이 다릅니다.

목차로 돌아가기 · 원래 읽던 위치는 브라우저 뒤로 가기로 돌아갈 수 있습니다.

표기 12. DMB·DSB·ISB·DC·대기 명령

명령/인자역할사용 위치
dmb sy지정 범위의 메모리 접근과 캐시 관리 작업 사이의 순서 보장MMU off 쓰기 후 invalidate 전
dsb sy / dsb nsh지정 범위·종류의 선행 작업 완료를 기다리는 동기화캐시 helper 끝, MMU 전환
isb뒤 명령이 갱신된 실행 문맥을 보도록 동기화SCTLR/TCR/VBAR 변경 주변
dc ivac,addr주소의 D-cache line을 PoC까지 invalidate실패 상태 및 캐시 helper
dc cvac,addr주소의 D-cache line을 PoC까지 cleanMMU on 진입 코드 정리
dc civac,addrclean + invalidate부분 cache line 경계
ic iallu로컬 I-cache 전체 무효화set_sctlr 매크로
tlbi vmalle1해당 번역 체계의 TLB invalidate__cpu_setup/테이블 교체
wfeevent를 기다릴 수 있는 hintholding pen
wfiinterrupt를 기다릴 수 있는 hint실패 CPU 대기
bti c간접 호출이 도착해도 되는 위치를 표시ARM64 SYM_FUNC_START 확장

SY는 full-system 범위, NSH는 non-shareable 범위입니다. helper C 코드의 ISHST는 inner-shareable store 범위를 가리킵니다. DMB가 자동으로 dirty data를 clean하지 않고, ISB가 페이지 테이블 데이터를 메모리에 쓰지도 않습니다. DC/TLBI는 캐시·TLB를 관리하고, 배리어는 필요한 실행 순서와 완료 시점을 보장합니다.

WFE/WFI는 다음 명령이 없다는 뜻의 CPU 종료 명령이 아닙니다. event/interrupt나 구현상 허용된 이유로 실행이 이어질 수 있으므로 head.S는 비교를 다시 하거나 B로 대기 루프를 만듭니다. 반대로 ASM_BUG가 내는 BRK는 의도적으로 동기 예외를 일으키는 명령이며 대기와 다릅니다.

목차로 돌아가기 · 원래 읽던 위치는 브라우저 뒤로 가기로 돌아갈 수 있습니다.

동봉한 원본과 출처

모두 v6.18.37 태그에서 내려받았습니다. manifest.json에 파일별 SHA-256과 다운로드 URL을 기록했습니다. 본문에서는 head.S를 여러 줄씩 묶어 설명하고, 관련 함수와 매크로를 추가로 설명합니다. 아래 소스 파일은 원문 확인을 위한 참고 자료이며, 각 파일의 모든 코드를 해설한 것은 아닙니다.

맨 위로 ↑