표기 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 c | static 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로 시작하는 임시 라벨은 보통 최종 심볼 테이블에 남기지 않습니다.
표기 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_if | capability에 맞는 명령열 선택·교체 |
| 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의 빌드 결과는 아닙니다.
표기 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 0 | 8바이트 값 출력 | Image offset 및 예약 필드 |
| .long symbol | 4바이트 값 출력 | PE header offset |
| .ascii string | 문자열 바이트 출력, 자동 NUL 없음 | ARM64_IMAGE_MAGIC |
| .balign n | 다음 위치를 n바이트 경계로 정렬 | SYM_* 시작 매크로 |
| .type / .size | ELF 심볼의 타입/크기 기록 | SYM_* 끝과 내부 라벨 |
| .set name, expr | assembler 심볼에 식의 값 지정 | 함수 크기 및 매크로 보조 심볼 |
| .pushsection / .popsection | 다른 섹션을 잠시 사용한 뒤 복원 | BUG table 및 alternative metadata |
AArch64 GAS의 .align 2는 2바이트가 아니라 2의 2제곱, 즉 4바이트 정렬입니다. .balign 4와 단위가 다릅니다. .quad와 .long은 명령이 아니라 데이터 바이트를 내보냅니다. Image의 B primary_entry가 이 데이터를 건너뛰므로 CPU가 크기 필드를 명령으로 실행하지 않습니다.
표기 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,\tsk는 msr 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 전처리와 시점이 다르며 실행 중 분기 명령도 아닙니다.
표기 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비트 포인터와도 구분됩니다.
표기 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 초기화 함수를 호출하는 코드에서 확인해야 합니다.
표기 7. 시스템 레지스터·MMU 매크로
MRS는 시스템 레지스터에서 일반 레지스터로 읽고, MSR는 반대 방향으로 씁니다. mrs x1,CurrentEL과 msr 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, 물리 주소 phys | PA_BITS_52이면 ORR 및 BADDR mask, 아니면 MOV |
| load_ttbr1 pgtbl,tmp1,tmp2 | 루트 PA와 임시 두 개 | phys_to_ttbr → offset_ttbr1 → MSR TTBR1 → ISB |
| set_sctlr_el1 reg | SCTLR 값 | 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 명령으로 세면 코드 크기와 실행 순서를 틀리게 계산합니다.
표기 8. EFI·태스크·EL2·실패 매크로의 구분
| 표기 | 정의·입력 | 실제 효과와 본문의 연결 |
|---|---|---|
| efi_signature_nop | efi-header.S, 인자 없음 | EFI이면 MZ 인코딩을 만드는 CCMP, 아니면 NOP; 본문 02 |
| __EFI_PE_HEADER | efi-header.S, 인자 없음 | CONFIG_EFI에 따른 PE/COFF header 데이터 출력; 함수 호출 아님 |
| init_cpu_task tsk,tmp1,tmp2 | head.S 자체의 .macro | SP_EL0/current, 태스크 스택, 최종 frame, SCS, per-CPU 설치; 본문 06 |
| scs_load_current | scs.h | SHADOW_CALL_STACK이면 current에서 SCS 포인터 로드, 아니면 빈 확장 |
| set_this_cpu_offset src | assembler.h | alternative에 따라 TPIDR_EL1 또는 TPIDR_EL2에 src 기록 |
| ptrauth_keys_init_cpu tsk,tmp1,tmp2,tmp3 | asm_pointer_auth.h | capability/ID 확인, SCTLR 인증 enable 및 태스크 kernel key 설치, ISB |
| init_el2_hcr val | el2_setup.h | HCR 설정과 VHE-only 판별; x0/x1 사용; 본문 09 |
| init_el2_state | el2_setup.h | 여러 EL2 초기화 매크로를 사용 위치에서 펼침; x0/x1/x2의 기존 값 변경; 부록 I |
| update_early_cpu_boot_status status,tmp1,tmp2 | head.S 자체의 .macro | STR → DMB → IVAC; 본문 14 |
| ASM_BUG() | asm-bug.h | BUG 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 명령 접두사가 아닙니다. 각 매크로의 세부 동작은 본문과 부록에서 설명합니다.
표기 9. #상수, 구조체 오프셋, 링크 심볼
명령 인자의 #는 즉시값 표기입니다. #16은 숫자 16이고 #TSK_STACK은 빌드에서 결정된 숫자 상수입니다. 소스 줄 맨 앞의 #include/#if와 같은 용법이 아닙니다. 또한 .quad 0에서 0은 데이터 값이며, adr_l x0,boot_args의 boot_args는 주소를 가진 심볼입니다.
| 종류 | 예 | 값이 정해지는 근거 |
|---|---|---|
| 구조체 필드 오프셋 | TSK_STACK, TSK_TI_CPU, CPU_BOOT_TASK | asm-offsets.c의 offsetof |
| 구조체 크기/위치 | PT_REGS_SIZE, S_STACKFRAME, S_STACKFRAME_TYPE | sizeof(pt_regs), offsetof(pt_regs,...) |
| 빌드 설정 크기 | THREAD_SIZE, PAGE_SIZE, CONFIG_FUNCTION_ALIGNMENT | 설정과 헤더 정의 |
| bit mask | SCTLR_ELx_M, SCTLR_ELx_C | 각각 bit 0, bit 2 |
| bit index | SCTLR_ELx_EE_SHIFT | TBNZ/TBZ에서 몇 번째 비트인지 지정 |
| 상태 조합 | INIT_PSTATE_EL1 | D/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으로 치환하지 않습니다.
표기 10. 이 파일에 나오는 명령과 NZCV
| 명령 | 읽는 법 | 이 파일에서 주의할 점 |
|---|---|---|
| mov dst,src | 값 복사/상수 구성의 alias | src가 주소인지 데이터인지는 문맥으로 결정 |
| add / sub | 덧셈 / 뺄셈 | 여기처럼 S 없는 형식은 NZCV 유지 |
| and / orr / eor / bic | AND / OR / XOR / 비트 지우기 | 이 형식은 NZCV 유지; BIC는 두 번째 mask를 반전하여 AND |
| cmp a,b | a-b 결과의 flags만 남김 | 결과 값을 일반 레지스터에 쓰지 않음 |
| tst a,mask | AND 결과의 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/store | x면 8 B, w면 4 B |
| ldp / stp | 레지스터 2개 load/store | x 두 개면 16 B; 주소 갱신은 별도 문법 |
| b label / br reg | 직접 / 레지스터 간접 분기 | LR을 새로 만들지 않음 |
| bl label / blr reg | 직접 / 레지스터 간접 호출 | x30에 다음 명령 주소 저장 |
| ret | 보통 x30으로 복귀 | SPSR을 복원하지 않음 |
| eret | ELR와 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 pointer | SP_EL0와 언제나 같은 저장소가 아님 |
| lr | x30의 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까지 clean | MMU on 진입 코드 정리 |
| dc civac,addr | clean + invalidate | 부분 cache line 경계 |
| ic iallu | 로컬 I-cache 전체 무효화 | set_sctlr 매크로 |
| tlbi vmalle1 | 해당 번역 체계의 TLB invalidate | __cpu_setup/테이블 교체 |
| wfe | event를 기다릴 수 있는 hint | holding pen |
| wfi | interrupt를 기다릴 수 있는 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는 의도적으로 동기 예외를 일으키는 명령이며 대기와 다릅니다.