ARM64 (AArch64) 명령어셋 레퍼런스
ARM64(AArch64) 아키텍처의 명령어를 GAS 문법 기준으로 종합 정리합니다. RISC 설계 철학과 고정 32-bit 명령어 길이, 31개 범용 레지스터(Register)와 예외 레벨(EL0-EL3), NEON(128-bit)/SVE(가변 길이) 벡터 레지스터, 주소 지정 모드, 데이터 전송·산술·논리·분기·스택·시스템·원자적(Atomic)·SIMD 명령어 카테고리 표, A64 인코딩 다이어그램, 커널 핵심 명령어(SVC, ERET, LDXR/STXR, DMB/DSB/ISB) 고급까지 Linux 커널 개발에 필요한 ARM64 ISA 전체를 다룹니다.
핵심 요약
- RISC 아키텍처 — 고정 32-bit 명령어, Load/Store 모델, 레지스터-레지스터 연산.
- 31개 범용 레지스터 — X0-X30(64-bit)/W0-W30(32-bit), XZR=제로, SP=스택, LR=X30.
- 예외 레벨 — EL0(유저), EL1(커널/OS), EL2(하이퍼바이저(Hypervisor)), EL3(시큐어 모니터).
- NEON/SVE — NEON: V0-V31(128-bit 고정), SVE: Z0-Z31(128~2048-bit 가변) + P0-P15 프레디킷.
- 독점 접근 — LDXR/STXR(독점 로드/저장)로 원자적 CAS 구현, ARMv8.1 LSE로 CAS/SWP 단일 명령어.
단계별 이해
- 레지스터 구조 파악
X0-X30(64-bit), W0-W30(32-bit 하위), XZR/WZR(제로), SP의 관계를 먼저 익힙니다. - Load/Store 모델 이해
ARM64에서 메모리 접근은 LDR/STR만 사용합니다. 산술은 레지스터 간만 가능합니다. - 예외 레벨과 시스템 레지스터
EL0-EL3 구조와 MSR/MRS로 접근하는 시스템 레지스터를 이해합니다. - 조건 코드와 분기
PSTATE 플래그(N,Z,C,V)와 B.cond 조건부 분기 패턴을 학습합니다.
아키텍처 개요
ARM64(AArch64)는 ARMv8-A에서 도입된 64-bit RISC 아키텍처입니다. ARMv1(1985) → ARMv7(32-bit A32/T32) → ARMv8(64-bit A64, 2011) → ARMv9(2021, SVE2/MTE/CCA)로 발전했습니다.
| 특성 | ARM64 (AArch64) |
|---|---|
| 설계 철학 | RISC (Reduced Instruction Set Computer) |
| 명령어 길이 | 고정 32-bit (4바이트) |
| 엔디언 | 바이-엔디언 (기본 리틀 엔디언) |
| 범용 레지스터 | 31개 (64-bit) + ZR + SP |
| 주소 공간(Address Space) | 가상 48-bit (256TB) 또는 52-bit (ARMv8.2-LVA) |
| 예외 레벨 | EL0(유저), EL1(OS), EL2(하이퍼바이저), EL3(시큐어) |
| 실행 상태 | AArch64(A64), AArch32(A32/T32) 전환 가능 |
| 페이지(Page) 크기 | 4KB, 16KB, 64KB |
ELR_ELn(복귀 주소)과 SPSR_ELn(PSTATE 백업)을 저장합니다.
ERET로 하위 레벨로 복귀할 때 이 값들이 복원됩니다. 각 EL은 독립적인 SP를 갖고, 시스템 레지스터에 _ELn 접미사를 붙여 레벨별로 분리됩니다.
레지스터 셋
ARM64는 풍부한 레지스터 셋을 제공하여 함수 호출 시 인자 전달과 로컬 변수를 대부분 레지스터에서 처리합니다. x86-64의 16개 범용 레지스터에 비해 31개로 거의 2배이며, 이는 register spilling(스택 임시 저장)을 크게 줄여 성능에 유리합니다:
범용 레지스터
| 64-bit | 32-bit | AAPCS64 용도 |
|---|---|---|
| X0-X7 | W0-W7 | 인자/반환값 (X0=1st 반환, X0-X7=인자) |
| X8 | W8 | 간접 결과 레지스터 (XR) |
| X9-X15 | W9-W15 | Caller-saved (임시) |
| X16 | W16 | IP0 (Intra-Procedure scratch) |
| X17 | W17 | IP1 (Intra-Procedure scratch) |
| X18 | W18 | 플랫폼 레지스터 (Linux: shadow call stack) |
| X19-X28 | W19-W28 | Callee-saved |
| X29 | W29 | 프레임 포인터 (FP) |
| X30 | W30 | 링크 레지스터 (LR) |
| SP | WSP | 스택 포인터 (EL별 독립) |
| XZR | WZR | 제로 레지스터 (읽기=0, 쓰기=버림) |
PSTATE 조건 플래그
| 플래그 | 이름 | 설명 |
|---|---|---|
| N | Negative | 결과의 최상위 비트 (부호) |
| Z | Zero | 결과가 0이면 세트 |
| C | Carry | 부호 없는 올림/빌림 |
| V | Overflow | 부호 있는 오버플로 |
시스템 레지스터 (주요)
| 레지스터 | 설명 |
|---|---|
| SCTLR_EL1 | 시스템 제어 (MMU, 캐시(Cache), 정렬 체크 등) |
| TCR_EL1 | 변환 제어 (페이지 크기, 주소 범위) |
| TTBR0_EL1 / TTBR1_EL1 | 페이지 테이블(Page Table) 베이스 (유저/커널) |
| ESR_EL1 | 예외 신드롬 (예외 원인 정보) |
| FAR_EL1 | 폴트 주소 |
| VBAR_EL1 | 예외 벡터 베이스 주소 |
| ELR_EL1 | 예외 링크 레지스터 (복귀 주소) |
| SPSR_EL1 | 저장된 프로세서 상태 (PSTATE 백업) |
| DAIF | 인터럽트(Interrupt) 마스크 (Debug, SError, IRQ, FIQ) |
| CurrentEL | 현재 예외 레벨 |
SIMD/벡터 레지스터
| 확장 | 레지스터 | 크기 | 접근 방식 |
|---|---|---|---|
| NEON/FP | V0-V31 | 128-bit | Bn(8), Hn(16), Sn(32), Dn(64), Qn(128) |
| SVE | Z0-Z31 | 128~2048-bit (가변) | 구현에 따라 128-bit 단위로 확장 |
| SVE 프레디킷 | P0-P15 | 가변 | 벡터 요소별 마스크 |
| SVE FFR | FFR | 가변 | First Fault Register |
| FP 제어 | FPCR/FPSR | 32-bit | 라운딩 모드, 예외 상태 |
VL(Vector Length)이 실행 시 결정됩니다.
프레디킷 레지스터(P0-P15)를 통해 벡터 요소별 조건부 실행이 가능하여 루프 끝단 처리가 우아합니다.
Linux 커널에서 NEON/SVE를 사용하려면 반드시 kernel_neon_begin() / kernel_neon_end()로 FPU 상태를 보호해야 합니다.
주소 지정 모드
| 모드 | 문법 | 설명 | 예제 |
|---|---|---|---|
| 즉시값 | #imm | 상수 값 | mov x0, #42 |
| 레지스터 | Xn/Wn | 레지스터 값 | mov x0, x1 |
| 베이스 | [Xn] | [Xn] | ldr x0, [x1] |
| 오프셋(Offset) | [Xn, #imm] | [Xn + imm] | ldr x0, [sp, #8] |
| Pre-index | [Xn, #imm]! | Xn += imm; [Xn] | ldr x0, [sp, #-16]! |
| Post-index | [Xn], #imm | [Xn]; Xn += imm | ldr x0, [sp], #16 |
| 레지스터 오프셋 | [Xn, Xm] | [Xn + Xm] | ldr x0, [x1, x2] |
| 확장 오프셋 | [Xn, Xm, LSL #n] | [Xn + Xm << n] | ldr x0, [x1, x2, lsl #3] |
| PC 상대 | label | [PC + offset] | adr x0, label |
| 리터럴 | =value | 리터럴 풀에서 로드 | ldr x0, =0x12345678 |
- Pre-index
[Xn, #imm]!— 먼저 베이스 갱신, 그 다음 접근. 스택 할당(push)에 적합:stp x29, x30, [sp, #-16]!→ SP를 먼저 16 감소, 그 위치에 저장 - Post-index
[Xn], #imm— 먼저 접근, 그 다음 베이스 갱신. 스택 해제(pop)에 적합:ldp x29, x30, [sp], #16→ 현재 SP에서 로드, 그 후 SP를 16 증가
ADRP는 심볼이 있는 4KB 페이지 주소(상위 비트)를 PC 상대로 계산하고, ADD로 페이지 내 오프셋(하위 12-bit)을 더합니다.
이 패턴은 ±4GB 범위의 임의 주소를 단 두 명령어로 생성할 수 있어 매우 효율적입니다.
/* ADRP + ADD: 심볼의 전체 주소 계산 */
adrp x0, my_global /* x0 = my_global이 있는 4KB 페이지 주소 */
add x0, x0, :lo12:my_global /* x0 += 페이지 내 오프셋 (하위 12비트) */
/* 이제 x0 = &my_global 전체 주소 */
/* ADRP + LDR: 심볼에서 직접 값 로드 */
adrp x1, my_global
ldr x1, [x1, :lo12:my_global] /* my_global 값 직접 로드 */
- 단일 명령어로 두 레지스터를 동시에 로드/저장하여 코드 밀도가 2배 향상됩니다.
- 128-bit 메모리 버스(Bus)를 완전히 활용하며, 캐시 라인(Cache Line) 접근 효율이 높습니다.
- 함수 프롤로그/에필로그에서 FP(X29)와 LR(X30)을 한 번에 저장/복원하는 관용 패턴으로 사용됩니다.
- SP 기반 LDP/STP는 원자적 128-bit 접근을 보장하여
struct복사에도 유용합니다.
데이터 전송 명령어
| 명령어 | 문법 | 설명 |
|---|---|---|
| MOV | mov x0, x1 | 레지스터 → 레지스터 이동 |
| MOVZ | movz x0, #0x1234, lsl #16 | 즉시값 이동 + 나머지 제로화 |
| MOVN | movn x0, #0 | 즉시값 반전 이동 |
| MOVK | movk x0, #0x5678 | 16-bit 즉시값 삽입 (나머지 보존) |
| LDR | ldr x0, [x1, #8] | 메모리 → 레지스터 (64-bit) |
| LDRB/LDRH | ldrb w0, [x1] | 바이트/하프워드 제로 확장 로드 |
| LDRSB/LDRSH/LDRSW | ldrsw x0, [x1] | 부호 확장 로드 |
| STR | str x0, [x1, #8] | 레지스터 → 메모리 (64-bit) |
| LDP | ldp x0, x1, [sp] | 쌍 레지스터 로드 (128-bit) |
| STP | stp x29, x30, [sp, #-16]! | 쌍 레지스터 저장 |
| ADRP | adrp x0, symbol | PC 상대 4KB 페이지 주소 |
| ADR | adr x0, label | PC 상대 주소 (±1MB) |
| LDAR | ldar x0, [x1] | Acquire 시맨틱 로드 |
| STLR | stlr x0, [x1] | Release 시맨틱 저장 |
| PRFM | prfm pldl1keep, [x0] | 메모리 프리페치 |
- LDAR (Load-Acquire): 이 로드 이후의 모든 메모리 접근이 이 로드 이전에 재배치(Relocation)되지 않음을 보장합니다. Linux의
smp_load_acquire()에 매핑(Mapping)됩니다. - STLR (Store-Release): 이 저장 이전의 모든 메모리 접근이 이 저장 이후로 재배치되지 않음을 보장합니다. Linux의
smp_store_release()에 매핑됩니다. - DMB 배리어보다 경량이며, 특정 주소에 대한 순서만 강제하므로 성능 영향이 적습니다.
- Acquire-Release 쌍은 단방향 배리어로, 전체 배리어(DMB)보다 파이프라인(Pipeline) 효율이 높습니다.
ADRP + LDR 패턴: 전역 변수 접근
/* C 코드: extern unsigned long jiffies; val = jiffies; */
/* 컴파일러가 생성하는 ADRP + LDR 패턴 */
adrp x0, jiffies /* x0 = jiffies의 4KB 페이지 주소 */
ldr x0, [x0, :lo12:jiffies] /* x0 = *jiffies (페이지 내 오프셋으로 로드) */
/* GOT(Global Offset Table) 경유 패턴 (모듈 코드) */
adrp x0, :got:jiffies /* GOT 엔트리의 페이지 주소 */
ldr x0, [x0, :got_lo12:jiffies] /* GOT에서 실제 주소 로드 */
ldr x0, [x0] /* 실제 값 로드 */
LDAR/STLR은 주소별 순서 보장(Ordering)만 하므로, 관련 없는 메모리 접근은 여전히 재배치될 수 있습니다.
전체 순서가 필요한 경우(예: smp_mb())에는 DMB ISH를 사용해야 합니다.
Linux 커널에서 일반적인 READ_ONCE()/WRITE_ONCE()는 배리어를 포함하지 않으며, 컴파일러 재배치만 방지합니다.
산술 명령어
| 명령어 | 문법 | 설명 | 플래그 |
|---|---|---|---|
| ADD | add x0, x1, x2 | 덧셈 | — |
| ADDS | adds x0, x1, x2 | 덧셈 + 플래그 세트 | N, Z, C, V |
| SUB | sub x0, x1, #16 | 뺄셈 | — |
| SUBS | subs x0, x1, x2 | 뺄셈 + 플래그 세트 | N, Z, C, V |
| ADC | adc x0, x1, x2 | 캐리 포함 덧셈 | — |
| SBC | sbc x0, x1, x2 | 캐리 포함 뺄셈 | — |
| NEG | neg x0, x1 | 부정 (0 - src) | — |
| MUL | mul x0, x1, x2 | 곱셈 (하위 64-bit) | — |
| MADD | madd x0, x1, x2, x3 | 곱셈-덧셈: x1*x2 + x3 | — |
| MSUB | msub x0, x1, x2, x3 | 곱셈-뺄셈: x3 - x1*x2 | — |
| SMULL | smull x0, w1, w2 | 부호 있는 32→64 곱셈 | — |
| UMULL | umull x0, w1, w2 | 부호 없는 32→64 곱셈 | — |
| SMULH | smulh x0, x1, x2 | 64×64→128 상위 64-bit (부호) | — |
| UMULH | umulh x0, x1, x2 | 64×64→128 상위 64-bit (부호 없음) | — |
| SDIV | sdiv x0, x1, x2 | 부호 있는 나눗셈 | — |
| UDIV | udiv x0, x1, x2 | 부호 없는 나눗셈 | — |
논리/시프트/비트 조작 명령어
| 명령어 | 문법 | 설명 |
|---|---|---|
| AND | and x0, x1, x2 | 비트 AND |
| ANDS | ands x0, x1, x2 | 비트 AND + 플래그 세트 |
| ORR | orr x0, x1, x2 | 비트 OR |
| ORN | orn x0, x1, x2 | 비트 OR NOT |
| EOR | eor x0, x1, x2 | 비트 XOR |
| EON | eon x0, x1, x2 | 비트 XOR NOT |
| BIC | bic x0, x1, x2 | 비트 클리어: x1 AND NOT x2 |
| TST | tst x0, #0xFF | AND 테스트 (결과 저장 안 함) |
| MVN | mvn x0, x1 | 비트 반전 (NOT) |
| LSL | lsl x0, x1, #4 | 논리 좌측 시프트 |
| LSR | lsr x0, x1, #4 | 논리 우측 시프트 |
| ASR | asr x0, x1, #4 | 산술 우측 시프트 |
| ROR | ror x0, x1, #4 | 순환 우측 시프트 |
| CLZ | clz x0, x1 | 선행 제로 카운트 |
| CLS | cls x0, x1 | 선행 부호 비트 카운트 |
| RBIT | rbit x0, x1 | 비트 순서 반전 |
| REV | rev x0, x1 | 바이트 순서(Byte Order) 반전 (64-bit) |
| REV16 | rev16 x0, x1 | 하프워드 내 바이트 반전 |
| REV32 | rev32 x0, x1 | 워드 내 바이트 반전 |
| EXTR | extr x0, x1, x2, #n | 두 레지스터에서 비트 추출 |
| BFI | bfi x0, x1, #lsb, #width | 비트 필드 삽입 |
| BFXIL | bfxil x0, x1, #lsb, #width | 비트 필드 추출 + 삽입 |
| UBFIZ/SBFIZ | ubfiz x0, x1, #lsb, #w | 부호 없는/있는 비트 필드 제로/부호 확장 삽입 |
비교/분기 명령어
| 명령어 | 문법 | 설명 |
|---|---|---|
| CMP | cmp x0, x1 | SUBS + 결과 버림 (플래그만 세트) |
| CMN | cmn x0, x1 | ADDS + 결과 버림 |
| TST | tst x0, x1 | ANDS + 결과 버림 |
| CCMP | ccmp x0, x1, #nzcv, eq | 조건부 비교 (조건 미충족 시 nzcv 세트) |
| CSEL | csel x0, x1, x2, eq | 조건부 선택: eq이면 x1, 아니면 x2 |
| CSINC | csinc x0, x1, x2, ne | 조건부 선택/증가 |
| CSET | cset x0, eq | 조건 충족 시 1, 아니면 0 |
| B | b label | 무조건 분기 (±128MB) |
| B.cond | b.eq label | 조건부 분기 (±1MB) |
| BL | bl func | 함수 호출 (X30 ← PC+4) |
| BR | br x0 | 간접 분기 |
| BLR | blr x0 | 간접 호출 (X30 ← PC+4) |
| RET | ret | 복귀 (BR X30) |
| CBZ | cbz x0, label | 제로이면 분기 |
| CBNZ | cbnz x0, label | 비제로이면 분기 |
| TBZ | tbz x0, #5, label | 특정 비트가 0이면 분기 |
| TBNZ | tbnz x0, #5, label | 특정 비트가 1이면 분기 |
CCMP는 이전 조건이 참일 때만 비교를 수행하고, 거짓이면 즉시값으로 NZCV 플래그를 설정합니다.
이를 통해 if (a == 1 && b == 2) 같은 다중 조건을 분기 없이 단일 조건 체인으로 평가할 수 있습니다.
- x86에서는
CMP + JNE + CMP + JNE로 2개의 분기가 필요하지만, ARM64에서는CMP + CCMP + B.cond로 1개의 분기만 사용합니다. - 분기 예측(Branch Prediction) 실패 패널티를 줄이고, 파이프라인 효율을 높이는 ARM64의 핵심 최적화 기법입니다.
- OR 조건(
||)도 nzcv 즉시값을 적절히 설정하여 구현할 수 있습니다.
/* C 코드: if (x == 1 && y == 2) goto target; */
/* x86-64 방식 (분기 2개) */
/* cmp x0, #1 */
/* b.ne skip */
/* cmp x1, #2 */
/* b.eq target */
/* skip: */
/* ARM64 CCMP 방식 (분기 1개) */
cmp x0, #1 /* x == 1? 플래그 세트 */
ccmp x1, #2, #0, eq /* 이전이 EQ이면 y==2 비교, 아니면 NZCV=0(NE) */
b.eq target /* 두 조건 모두 참이면 분기 */
/* C 코드: if (x == 1 || y == 2) goto target; */
cmp x0, #1 /* x == 1? */
ccmp x1, #2, #4, ne /* 이전이 NE이면 y==2 비교, EQ이면 NZCV=4(Z=1,EQ) */
b.eq target /* 둘 중 하나라도 참이면 분기 */
스택/함수 호출 명령어
ARM64에는 PUSH/POP 명령어가 없습니다. STP/LDP로 레지스터 쌍을 저장/복원합니다.
/* 함수 프롤로그/에필로그 패턴 */
my_func:
stp x29, x30, [sp, #-16]! /* FP, LR 저장 (pre-index) */
mov x29, sp /* 프레임 포인터 설정 */
/* ... 함수 본문 ... */
ldp x29, x30, [sp], #16 /* FP, LR 복원 (post-index) */
ret /* X30(LR)으로 복귀 */
- 인자: X0-X7 (최대 8개 정수/포인터)
- 반환값: X0 (+ X1 128-bit)
- Callee-saved: X19-X28, FP(X29), LR(X30)
- Caller-saved: X0-X18 (X16/X17은 링커(Linker) 사용)
- SP는 항상 16-byte 정렬
시스템/특권 명령어
| 명령어 | 설명 | 예외 레벨 |
|---|---|---|
| SVC #imm | 시스템 콜 (EL0 → EL1) | EL0 |
| HVC #imm | 하이퍼바이저 콜 (EL1 → EL2) | EL1 |
| SMC #imm | 시큐어 모니터 콜 (→ EL3) | EL1 |
| ERET | 예외 복귀 (ELR/SPSR 복원) | EL1+ |
| MSR | 시스템 레지스터 쓰기 | 레지스터별 |
| MRS | 시스템 레지스터 읽기 | 레지스터별 |
| NOP | No operation | 모든 레벨 |
| WFI | 인터럽트 대기 (저전력) | EL0+ (트랩 가능) |
| WFE | 이벤트 대기 | EL0+ |
| SEV/SEVL | 이벤트 전송 (로컬) | EL0+ |
| ISB | 명령어 동기화 배리어 | 모든 레벨 |
| DSB | 데이터 동기화 배리어 | 모든 레벨 |
| DMB | 데이터 메모리 배리어(Memory Barrier) | 모든 레벨 |
| TLBI | TLB 무효화(Invalidation) | EL1+ |
| IC | 명령어 캐시 유지 | 레벨별 |
| DC | 데이터 캐시 유지 | 레벨별 |
| AT | 주소 변환(Address Translation) | EL1+ |
| CLREX | 독점 모니터 초기화 | 모든 레벨 |
| YIELD | 양보(Yield) 힌트 (SMT) | 모든 레벨 |
| BRK #imm | 디버그 브레이크포인트 | 모든 레벨 |
- DMB ISH — 스핀락(Spinlock) 획득/해제,
smp_mb()구현. SMP 시스템에서 코어 간 메모리 순서 보장에 가장 많이 사용됩니다. - DSB ISH + ISB — SCTLR_EL1 변경(MMU on/off), 페이지 테이블 변경 후
TLBI+DSB ISH+ISB시퀀스. 시스템 레지스터 변경이 후속 명령어에 반영되어야 할 때 필수입니다. - DSB NSH — Non-Shareable 영역에 한정된 배리어. 단일 코어 장치 레지스터 접근 시 사용합니다.
- DMB ISHLD / DMB ISHST — 각각 로드-로드, 스토어-스토어 순서만 보장하는 경량 배리어로,
smp_rmb()/smp_wmb()에 매핑됩니다.
원자적/동기화 명령어
독점 접근 (ARMv8.0)
| 명령어 | 문법 | 설명 |
|---|---|---|
| LDXR | ldxr x0, [x1] | 독점 로드 |
| STXR | stxr w2, x0, [x1] | 독점 저장 (w2=성공 여부: 0=성공) |
| LDAXR | ldaxr x0, [x1] | 독점 로드 + Acquire |
| STLXR | stlxr w2, x0, [x1] | 독점 저장 + Release |
LSE 원자적 명령어 (ARMv8.1)
| 명령어 | 설명 |
|---|---|
| CAS/CASA/CASL/CASAL | 비교-교환 (Compare And Swap) |
| CASP | 쌍 레지스터 비교-교환 |
| SWP/SWPA/SWPL/SWPAL | 원자적 교환 |
| LDADD/LDCLR/LDEOR/LDSET | 원자적 RMW (Add/Clear/XOR/Set) |
| LDSMAX/LDSMIN/LDUMAX/LDUMIN | 원자적 RMW (Max/Min) |
메모리 배리어
| 명령어 | 설명 |
|---|---|
| DMB ISH | Inner Shareable 전체 배리어 |
| DMB ISHLD | Inner Shareable 로드 배리어 |
| DMB ISHST | Inner Shareable 스토어 배리어 |
| DSB ISH | 데이터 동기화 (배리어 완료 보장) |
| ISB | 명령어 파이프라인 플러시(Flush) |
LDXR/STXR CAS 루프 패턴
/* compare_and_swap(addr, expected, desired) */
cas_loop:
ldaxr x3, [x0] /* 독점 Acquire 로드 */
cmp x3, x1 /* expected와 비교 */
b.ne 1f /* 다르면 실패 */
stlxr w4, x2, [x0] /* 독점 Release 저장 */
cbnz w4, cas_loop /* 저장 실패 시 재시도 */
1:
mov x0, x3 /* 이전 값 반환 */
ret
- LDXR/STXR (독점 접근 루프):
- 모든 ARMv8.0+ 프로세서에서 지원 (범용성 최고)
- 높은 경합(contention) 상황에서 STXR 실패 → 재시도 루프가 반복되어 성능 급격히 저하
- 캐시 라인 바운스(bouncing)로 인한 코히런시 트래픽 증가
- Linux 커널:
CONFIG_ARM64_LSE_ATOMICS가 비활성이면 이 방식 사용
- LSE CAS/SWP/LDADD (ARMv8.1+):
- 단일 명령어로 원자적 RMW 수행 — 재시도 루프 불필요
- 마이크로아키텍처 레벨에서 캐시 컨트롤러가 직접 처리하여 경합 시에도 2~10배 성능 향상
- CAS:
casa x0, x1, [x2](Acquire), CASAL: Acquire+Release 양쪽 - Linux 커널: 부팅 시 LSE 지원 여부를 감지하여 대체 패치(alternative patching)로 런타임 전환
스핀락 구현 예제 (LDAXR/STLXR)
/* arch_spin_lock — 간소화된 스핀락 구현 */
/* x0 = &lock (0=unlocked, 1=locked) */
arch_spin_lock:
mov w2, #1 /* 락 값 준비 */
sevl /* 이벤트 레지스터 세트 (첫 WFE 통과) */
1: wfe /* 이벤트 대기 (전력 절약) */
2: ldaxr w1, [x0] /* Acquire 독점 로드 */
cbnz w1, 1b /* 이미 잠김 → WFE로 대기 */
stxr w1, w2, [x0] /* 독점 저장 시도 */
cbnz w1, 2b /* 저장 실패 → 재시도 (WFE 건너뜀) */
ret /* 락 획득 성공 */
arch_spin_unlock:
stlr wzr, [x0] /* Release 저장: 0(unlock) 기록 */
ret /* SEV는 STLR의 exclusive monitor clear로 자동 전파 */
LDXR은 해당 주소를 "독점 상태"로 마킹합니다. 이후 다른 코어가 같은 캐시 라인에 쓰면 독점 상태가 해제됩니다.STXR은 독점 상태가 유지된 경우에만 저장에 성공(W 결과=0)합니다. 해제되었으면 실패(W 결과=1)합니다.- 로컬 모니터(코어별)와 글로벌 모니터(시스템 레벨) 두 단계가 존재합니다.
- 주의: LDXR과 STXR 사이에는 다른 메모리 접근을 최소화해야 합니다. 과도한 명령어는 독점 상태를 불필요하게 해제시킬 수 있습니다.
CLREX명령으로 명시적으로 독점 상태를 해제할 수 있으며, 컨텍스트 스위치 시 커널이 이를 실행합니다.
arch/arm64/include/asm/atomic_ll_sc.h에 LDXR/STXR 기본 구현이, arch/arm64/include/asm/atomic_lse.h에 LSE 구현이 있습니다.
부팅 시 cpufeature 프레임워크가 ARMv8.1 LSE 지원을 감지하면, .altinstructions 섹션을 통해
LL/SC 루프 코드를 LSE 단일 명령어(CAS, SWP, LDADD 등)로 바이너리 패치합니다.
이를 통해 단일 커널 이미지로 ARMv8.0과 ARMv8.1+ 하드웨어를 모두 최적으로 지원합니다.
SIMD/벡터 명령어
NEON 명령어 (128-bit)
| 명령어 | 설명 |
|---|---|
| LD1 {v0.4s}, [x0] | 벡터 로드 (4×32-bit) |
| ST1 {v0.4s}, [x0] | 벡터 저장 |
| ADD v0.4s, v1.4s, v2.4s | 벡터 정수 덧셈 |
| FADD v0.4s, v1.4s, v2.4s | 벡터 부동소수점 덧셈 |
| FMUL v0.4s, v1.4s, v2.4s | 벡터 부동소수점 곱셈 |
| MUL v0.4s, v1.4s, v2.4s | 벡터 정수 곱셈 |
| AND v0.16b, v1.16b, v2.16b | 벡터 비트 AND |
| CMHI v0.4s, v1.4s, v2.4s | 벡터 부호없는 > 비교 |
| CMEQ v0.4s, v1.4s, v2.4s | 벡터 같음 비교 |
| TBL v0.16b, {v1.16b}, v2.16b | 테이블 룩업 |
| ZIP1/ZIP2 v0.4s, v1.4s, v2.4s | 벡터 인터리브 |
| UZP1/UZP2 | 벡터 디인터리브 |
| TRN1/TRN2 | 벡터 전치 |
| DUP v0.4s, w0 | 스칼라를 모든 요소에 복제 |
| INS v0.s[0], w0 | 스칼라 요소 삽입 |
| UMOV w0, v0.s[0] | 벡터 요소 추출 |
| ADDV s0, v0.4s | 벡터 리덕션 합계 |
SVE 명령어 (가변 길이)
| 명령어 | 설명 |
|---|---|
| LD1 {z0.s}, p0/z, [x0] | 프레디킷 마스크 벡터 로드 |
| ST1 {z0.s}, p0, [x0] | 프레디킷 마스크 벡터 저장 |
| ADD z0.s, p0/m, z0.s, z1.s | 프레디킷 벡터 덧셈 |
| MUL z0.s, p0/m, z0.s, z1.s | 프레디킷 벡터 곱셈 |
| WHILELT p0.s, x0, x1 | 루프 프레디킷 생성 (x0 < x1) |
| PTRUE p0.s | 모든 요소 활성 프레디킷 |
| INDEX z0.s, #0, #1 | 인덱스 벡터 생성 (0,1,2,3,...) |
| COMPACT z0.s, p0, z1.s | 활성 요소 압축 |
| CNTW x0 | 벡터 워드 요소 수 반환 |
커널 핵심 명령어
SVC #0 el0_svc 진입 경로
/* 유저 공간에서 시스템 콜: SVC #0 */
/* ELR_EL1 ← PC+4, SPSR_EL1 ← PSTATE, PSTATE.DAIF 마스크 */
/* PC ← VBAR_EL1 + 0x400 (EL0 64-bit Synchronous) */
/* arch/arm64/kernel/entry.S — 예외 벡터 테이블 */
.align 11 /* 2048-byte 정렬 */
vectors:
/* Current EL with SP0 */
ventry el1_sync_invalid /* Synchronous */
ventry el1_irq_invalid /* IRQ */
ventry el1_fiq_invalid /* FIQ */
ventry el1_error_invalid /* SError */
/* Current EL with SPx */
ventry el1h_sync /* Synchronous */
ventry el1h_irq /* IRQ */
/* ... */
/* Lower EL (EL0) using AArch64 */
ventry el0_sync /* Synchronous (SVC 포함) */
ventry el0_irq /* IRQ */
/* ... */
MSR/MRS 시스템 레지스터 접근
/* SCTLR_EL1 읽기/쓰기 */
mrs x0, sctlr_el1 /* 읽기 */
orr x0, x0, #(1 << 0) /* MMU 활성화 비트 세트 */
msr sctlr_el1, x0 /* 쓰기 */
isb /* 명령어 동기화 */
/* 페이지 테이블 베이스 설정 */
msr ttbr0_el1, x0 /* 유저 페이지 테이블 */
msr ttbr1_el1, x1 /* 커널 페이지 테이블 */
tlbi vmalle1 /* 전체 TLB 무효화 */
dsb ish /* 데이터 동기화 */
isb /* 명령어 동기화 */
ERET 예외 복귀
/* EL1에서 EL0으로 복귀 */
msr elr_el1, x0 /* 복귀 주소 설정 */
msr spsr_el1, x1 /* 프로세서 상태 복원 */
eret /* 예외 복귀: PC←ELR, PSTATE←SPSR */
TTBR0_EL1을 전환하여 커널 매핑을 제거합니다.
- TTBR1_EL1: 커널 주소 공간 (0xFFFF...) — 항상 커널 페이지 테이블을 가리킵니다.
- TTBR0_EL1: 유저 주소 공간 (0x0000...) — EL0 복귀 시 커널 매핑이 제거된 별도 페이지 테이블로 전환됩니다.
- 예외 진입 시(EL0→EL1): trampoline 코드가 TTBR0을 전체 커널 매핑 테이블로 복원합니다.
- 예외 복귀 시(EL1→EL0): TTBR0을 최소 매핑 테이블로 전환한 뒤
ERET합니다.
/* KPTI: 유저 복귀 시 TTBR0 전환 (간소화) */
/* arch/arm64/kernel/entry.S 기반 */
mrs x1, ttbr1_el1 /* 커널 TTBR1 읽기 */
bfi x1, x0, #48, #16 /* ASID 삽입 */
msr ttbr0_el1, x1 /* TTBR0 = 유저 페이지 테이블 */
isb /* 파이프라인 동기화 */
eret /* EL0으로 복귀 */
예외 진입 시 레지스터 저장 (kernel_entry 매크로(Macro))
/* arch/arm64/kernel/entry.S — kernel_entry 매크로 (간소화) */
/* EL0 → EL1 예외 진입 시 전체 레지스터 컨텍스트 저장 */
sub sp, sp, #288 /* pt_regs 구조체 크기만큼 스택 할당 */
stp x0, x1, [sp, #16 * 0] /* X0-X1 저장 */
stp x2, x3, [sp, #16 * 1] /* X2-X3 저장 */
/* ... X4-X27 저장 (STP 쌍으로) ... */
stp x28, x29, [sp, #16 * 14] /* X28-X29 저장 */
mrs x22, elr_el1 /* 예외 복귀 주소 */
mrs x23, spsr_el1 /* 저장된 프로세서 상태 */
mrs x20, sp_el0 /* 유저 SP (EL0 사용 SP) */
stp x30, x20, [sp, #16 * 15] /* LR, SP_EL0 저장 */
stp x22, x23, [sp, #16 * 16] /* ELR_EL1, SPSR_EL1 저장 */
/* 이제 C 함수(el0_sync_handler 등)를 호출할 수 있음 */
mov x0, sp /* pt_regs 포인터를 인자로 전달 */
bl el0_svc_handler /* 시스템 콜 핸들러 호출 */
Linux 커널 GCC 인라인 어셈블리 예제 (ARM64)
/* arch/arm64/include/asm/sysreg.h 기반 */
#define read_sysreg(r) ({ \
u64 __val; \
asm volatile("mrs %0, " __stringify(r) : "=r"(__val)); \
__val; \
})
#define write_sysreg(v, r) do { \
u64 __val = (u64)(v); \
asm volatile("msr " __stringify(r) ", %x0" \
: : "rZ"(__val)); \
} while (0)
/* 사용 예: 인터럽트 비활성화 */
static inline void arch_local_irq_disable(void)
{
asm volatile(
"msr daifset, #3" /* IRQ + FIQ 마스크 */
:
:
: "memory"
);
}
/* atomic_add: 인라인 어셈블리로 원자적 덧셈 */
static inline void arch_atomic_add(int i, atomic_t *v)
{
unsigned long tmp;
int result;
asm volatile(
"1: ldxr %w0, %2\n" /* 독점 로드 */
" add %w0, %w0, %w3\n" /* 덧셈 */
" stxr %w1, %w0, %2\n" /* 독점 저장 */
" cbnz %w1, 1b" /* 실패 시 재시도 */
: "=&r"(result), "=&r"(tmp), "+Q"(v->counter)
: "Ir"(i)
);
}
"=&r"— Early clobber 출력: 입력이 완전히 소비되기 전에 출력이 기록될 수 있음을 알림"+Q"— 메모리 읽기+쓰기 오퍼랜드 (주소가 단일 레지스터로 표현 가능)"Ir"— 즉시값 또는 레지스터 (ADD의 즉시값 범위 내)"rZ"— 레지스터 또는 제로 레지스터 (XZR/WZR 활용)"memory"clobber — 컴파일러가 배리어 앞뒤로 메모리 접근을 재배치하지 않도록 방지
명령어 인코딩
ARM64(A64) 모든 명령어는 고정 32-bit입니다. 상위 비트(op0 필드)로 명령어 그룹을 결정합니다.
인코딩 예제: ADD X0, X1, X2
ADD X0, X1, X2는 "Data Processing — Register" 그룹(op0=x101)에 속합니다. 구체적으로 Add (shifted register) 형식으로 인코딩됩니다.
ADD X0, X1, X2 = 1 0 0 01011 00 0 00010 000000 00001 000002 = 0x8B020020
- 확인:
echo '8B020020' | xxd -r -p | aarch64-linux-gnu-objdump -b binary -m aarch64 -D - - ADDS X0, X1, X2이면 S=1이므로:
0xAB020020(bit[29]이 1로 변경) - SUB X0, X1, X2이면 op=1이므로:
0xCB020020(bit[30]이 1로 변경) - ADD W0, W1, W2이면 sf=0이므로:
0x0B020020(bit[31]이 0으로 변경)
보안 확장 (PAC/BTI/MTE)
ARMv8.3+ 이후 도입된 보안 확장은 커널과 유저 공간 모두에서 메모리 안전성과 제어 흐름 무결성(Integrity)을 하드웨어 수준으로 강화합니다. PAC(Pointer Authentication), BTI(Branch Target Identification), MTE(Memory Tagging Extension)는 각각 ROP/JOP 공격, 간접 분기 하이재킹, use-after-free/buffer-overflow를 방어합니다.
/* PAC 커널 함수 프롤로그/에필로그 (GCC 자동 생성) */
my_function:
paciasp /* LR에 PAC 서명 (SP를 컨텍스트로) */
stp x29, x30, [sp, #-16]!
mov x29, sp
/* ... 함수 본문 ... */
ldp x29, x30, [sp], #16
autiasp /* PAC 검증 (실패 시 fault → SIGILL) */
ret
/* BTI 보호 함수 진입점 */
my_indirect_target:
bti c /* BL로만 도달 가능한 타겟 표시 */
stp x29, x30, [sp, #-16]!
/* ... */
/* MTE 커널 사용 (KASAN HW 모드) */
/* CONFIG_KASAN_HW_TAGS=y → 슬랩 할당 시 자동 태깅 */
/* kmalloc() → kasan_set_tag() → IRG + STG */
/* kfree() → 태그 변경 → 이후 접근 시 Tag Check Fault */
캐시 & TLB 관리 명령어
ARM64는 x86과 달리 소프트웨어가 명시적으로 캐시 유지보수와 TLB 무효화를 수행해야 합니다. 커널의 DMA 매핑, 페이지 테이블 수정, 코드 패칭 등에서 정확한 캐시/TLB 관리가 필수입니다.
| 명령어 | 대상 | 동작 | 커널 사용처 |
|---|---|---|---|
DC CIVAC | 데이터 캐시 | Clean + Invalidate by VA | DMA, 비일관 I/O |
DC CVAC | 데이터 캐시 | Clean by VA (PoC) | DMA CPU→디바이스 |
DC CVAU | 데이터 캐시 | Clean by VA (PoU) | 코드 패칭 전 |
DC ZVA | 데이터 캐시 | Zero by VA | clear_page() 최적화 |
IC IVAU | 명령어 캐시 | Invalidate by VA (PoU) | 코드 패칭 후 |
IC IALLU | 명령어 캐시 | Invalidate All (Inner Share) | 모듈 로딩 |
TLBI VMALLE1IS | TLB | EL1 전체 무효화 (IS) | flush_tlb_all() |
TLBI VAE1IS | TLB | VA+ASID 기반 무효화 (IS) | flush_tlb_page() |
TLBI ASIDE1IS | TLB | ASID 전체 무효화 (IS) | flush_tlb_mm() |
DSB/ISB 필수: ARM64에서 캐시/TLB 유지보수 명령어는 비동기적으로 실행될 수 있습니다. DSB(Data Synchronization Barrier)로 완료를 보장하고, 코드 패칭 시 ISB(Instruction Synchronization Barrier)로 파이프라인을 flush해야 합니다. x86의 invlpg는 동기적이므로 이 문제가 없지만, ARM64에서는 누락하면 stale 캐시/TLB로 인한 크래시가 발생합니다.
AAPCS64 호출 규약
AAPCS64 (ARM Architecture Procedure Call Standard for AArch64)는 ARM64 환경에서 함수 호출 시 레지스터 사용, 스택 프레임(Stack Frame) 구성, 인자 전달 방식을 규정합니다. Linux 커널과 유저 공간 모두 이 규약을 따르며, ABI 호환성의 기초입니다.
C 함수가 어셈블리로 변환될 때 AAPCS64 규약이 적용되는 과정을 확인합니다:
/* C 함수 정의 */
long example_func(long a, long b, long c, long d,
long e, long f, long g, long h,
long i, long j) /* 10개 인자 */
{
return a + b + c + d + e + f + g + h + i + j;
}
/* 컴파일러가 생성하는 어셈블리 (GCC -O2) */
example_func:
/* X0=a, X1=b, X2=c, X3=d, X4=e, X5=f, X6=g, X7=h */
/* i, j는 스택에서 로드 */
ldr x8, [sp] /* i = 9번째 인자 (스택) */
ldr x9, [sp, #8] /* j = 10번째 인자 (스택) */
add x0, x0, x1 /* a + b */
add x0, x0, x2 /* + c */
add x0, x0, x3 /* + d */
add x0, x0, x4 /* + e */
add x0, x0, x5 /* + f */
add x0, x0, x6 /* + g */
add x0, x0, x7 /* + h */
add x0, x0, x8 /* + i */
add x0, x0, x9 /* + j */
ret /* X0에 결과 반환, RET = BR X30 */
/* 스택 프레임 프롤로그/에필로그 (callee-saved 사용 시) */
complex_func:
/* 프롤로그: FP/LR + callee-saved 저장 */
stp x29, x30, [sp, #-64]! /* FP, LR 저장 + SP -= 64 */
mov x29, sp /* FP = 현재 SP (프레임 포인터 설정) */
stp x19, x20, [sp, #16] /* callee-saved X19, X20 저장 */
stp x21, x22, [sp, #32] /* callee-saved X21, X22 저장 */
/* 함수 본문: X19-X22를 자유롭게 사용 */
mov x19, x0 /* 인자 보존 */
bl other_func /* 다른 함수 호출 (X0-X15 파괴 가능) */
add x0, x19, x0 /* 보존된 X19 사용 */
/* 에필로그: 복원 후 반환 */
ldp x21, x22, [sp, #32] /* X21, X22 복원 */
ldp x19, x20, [sp, #16] /* X19, X20 복원 */
ldp x29, x30, [sp], #64 /* FP, LR 복원 + SP += 64 */
ret /* X30(LR)으로 복귀 */
/* 가변 인자 함수 (va_list) — ARM64 구현 */
/* ARM64 va_list는 5개 필드의 구조체 */
typedef struct {
void *__stack; /* 스택 인자 다음 위치 */
void *__gr_top; /* GR save area 끝 */
void *__vr_top; /* VR save area 끝 */
int __gr_offs; /* GR save area 오프셋 (음수) */
int __vr_offs; /* VR save area 오프셋 (음수) */
} va_list;
/* va_arg 확장 (범용 레지스터 인자): */
/* 1. __gr_offs < 0이면 GR save area에서 가져옴 */
/* 2. __gr_offs >= 0이면 __stack에서 가져옴 */
/* 컴파일러가 프롤로그에서 X0-X7을 GR save area에 저장 */
printk(), pr_info() 등에서 사용합니다.
커널 빌드 시 -mgeneral-regs-only 옵션으로 SIMD/FP 레지스터 사용을 금지하므로, __vr_top/__vr_offs는 커널 내부에서 사용되지 않습니다.
X18은 CONFIG_SHADOW_CALL_STACK 활성화 시 Shadow Call Stack 포인터로 예약되어 일반 용도로 사용할 수 없습니다.
| 레지스터 | AAPCS64 역할 | Caller/Callee | Linux 커널 용도 |
|---|---|---|---|
| X0-X7 | 인자 전달 / 반환값 | Caller-saved | syscall: X0-X5 인자, X0 반환 |
| X8 | Indirect result location | Caller-saved | syscall 번호 (w8) |
| X9-X15 | Temporary | Caller-saved | 임시 계산 |
| X16 (IP0) | Intra-procedure scratch | Caller-saved | PLT, veneer |
| X17 (IP1) | Intra-procedure scratch | Caller-saved | PLT, veneer |
| X18 | Platform register | Caller-saved | Shadow Call Stack 포인터 |
| X19-X28 | Callee-saved | Callee-saved | 함수 간 보존 변수 |
| X29 (FP) | Frame Pointer | Callee-saved | 스택 트레이스 필수 |
| X30 (LR) | Link Register | Callee-saved | BL 반환 주소, PAC 서명 대상 |
EL0-EL3 전환
ARM64의 예외 레벨(Exception Level) 전환은 하드웨어가 자동으로 레지스터를 뱅킹하고 상태를 보존하는 정교한 메커니즘입니다. 각 EL은 독립적인 시스템 레지스터 세트를 가지며, 예외 진입/복귀 시 하드웨어와 소프트웨어가 협력하여 컨텍스트를 관리합니다.
/* arch/arm64/kernel/entry.S — SVC 핸들러 진입 */
SYM_CODE_START_LOCAL(el0_sync)
kernel_entry 0 /* EL0에서 진입, 레지스터 저장 */
mrs x25, esr_el1 /* ESR_EL1 읽기 (예외 원인) */
lsr x24, x25, #26 /* EC 필드 추출 [31:26] */
cmp x24, #0x15 /* EC == SVC (AArch64)? */
b.eq el0_svc /* SVC 처리로 분기 */
cmp x24, #0x24 /* EC == Data Abort (lower EL)? */
b.eq el0_da /* 데이터 중단 처리 */
cmp x24, #0x20 /* EC == Instruction Abort? */
b.eq el0_ia /* 명령어 중단 처리 */
/* ... 기타 예외 타입 처리 ... */
b el0_inv /* 알 수 없는 예외 */
SYM_CODE_END(el0_sync)
/* VBAR_EL1 예외 벡터 테이블 구조 */
/* 총 16개 엔트리, 각 128바이트 (32개 명령어) = 2KB 정렬 */
.align 11 /* 2^11 = 2048 바이트 정렬 */
SYM_CODE_START(vectors)
/* ---- Current EL with SP_EL0 (거의 사용 안 함) ---- */
ventry el1t_sync_invalid /* +0x000: Synchronous */
ventry el1t_irq_invalid /* +0x080: IRQ */
ventry el1t_fiq_invalid /* +0x100: FIQ */
ventry el1t_error_invalid /* +0x180: SError */
/* ---- Current EL with SP_ELx (커널 내 예외) ---- */
ventry el1h_sync /* +0x200: Synchronous */
ventry el1h_irq /* +0x280: IRQ */
ventry el1h_fiq /* +0x300: FIQ */
ventry el1h_error /* +0x380: SError */
/* ---- Lower EL using AArch64 (유저→커널) ---- */
ventry el0_sync /* +0x400: Synchronous (SVC) */
ventry el0_irq /* +0x480: IRQ */
ventry el0_fiq /* +0x500: FIQ */
ventry el0_error /* +0x580: SError */
/* ---- Lower EL using AArch32 ---- */
ventry el0_sync_compat /* +0x600: Synchronous */
ventry el0_irq_compat /* +0x680: IRQ */
ventry el0_fiq_compat /* +0x700: FIQ */
ventry el0_error_compat /* +0x780: SError */
SYM_CODE_END(vectors)
/* ventry 매크로: 128바이트 정렬 + 분기 */
.macro ventry label
.align 7 /* 128바이트 정렬 */
b \label /* 핸들러로 분기 */
.endm
예외 벡터 정렬: VBAR_EL1은 하위 11비트가 0이어야 합니다 (2KB 정렬). 각 벡터 엔트리는 128바이트(32개 명령어)로, 이 공간 안에 분기 코드만 넣고 실제 핸들러(Handler)는 별도 위치에 배치합니다. ventry 매크로가 .align 7로 128바이트 경계를 보장합니다.
커널 어셈블리 실전 예제
Linux 커널의 ARM64 아키텍처 의존 코드는 arch/arm64/에 위치하며, 성능 또는 하드웨어 직접 접근이 필요한 경로는 어셈블리로 작성됩니다. 컨텍스트 스위치, 원자적 연산(Atomic Operation), 메모리 복사 등 핵심 경로의 실제 구현을 분석합니다.
/* arch/arm64/kernel/entry.S — 시스템 콜 진입 (간소화) */
SYM_CODE_START_LOCAL(el0_svc)
mov x0, sp /* pt_regs 포인터 (첫 번째 인자) */
mrs x1, esr_el1 /* ESR_EL1 (예외 원인) */
/* 시스템 콜 번호 추출: W8 레지스터 */
ldr x16, [sp, #8 * 8] /* pt_regs->regs[8] = syscall 번호 */
uxtw x16, w16 /* 32→64 제로 확장 */
/* 범위 검사 */
cmp x16, #__NR_syscalls /* 시스템 콜 번호 < 최대값? */
b.hs __sys_ni_syscall /* 초과 시 -ENOSYS */
/* 시스템 콜 테이블 디스패치 */
adr x17, sys_call_table /* 테이블 베이스 주소 */
ldr x17, [x17, x16, lsl #3] /* 함수 포인터 로드 (8바이트 단위) */
blr x17 /* 핸들러 호출 */
/* 반환값 저장 */
str x0, [sp] /* pt_regs->regs[0] = 반환값 */
b ret_to_user /* 유저 복귀 경로 */
SYM_CODE_END(el0_svc)
/* arch/arm64/kernel/process.c → cpu_switch_to (어셈블리) */
/* 컨텍스트 스위치: prev → next 태스크 전환 */
SYM_FUNC_START(cpu_switch_to)
/* prev 태스크의 callee-saved 레지스터 저장 */
mov x10, #THREAD_CPU_CONTEXT /* thread_struct 내 오프셋 */
add x8, x0, x10 /* prev->thread.cpu_context */
stp x19, x20, [x8], #16 /* X19-X20 저장 */
stp x21, x22, [x8], #16 /* X21-X22 저장 */
stp x23, x24, [x8], #16 /* X23-X24 저장 */
stp x25, x26, [x8], #16 /* X25-X26 저장 */
stp x27, x28, [x8], #16 /* X27-X28 저장 */
stp x29, x30, [x8], #16 /* FP, LR 저장 */
mov x9, sp
str x9, [x8] /* SP 저장 */
/* next 태스크의 callee-saved 레지스터 복원 */
add x8, x1, x10 /* next->thread.cpu_context */
ldp x19, x20, [x8], #16 /* X19-X20 복원 */
ldp x21, x22, [x8], #16 /* X21-X22 복원 */
ldp x23, x24, [x8], #16 /* X23-X24 복원 */
ldp x25, x26, [x8], #16 /* X25-X26 복원 */
ldp x27, x28, [x8], #16 /* X27-X28 복원 */
ldp x29, x30, [x8], #16 /* FP, LR 복원 */
ldr x9, [x8]
mov sp, x9 /* SP 복원 */
/* TTBR0 전환 (유저 페이지 테이블) */
msr sp_el0, x1 /* current task pointer (sp_el0) */
ret /* X30(LR)으로 복귀 → next 태스크 실행 */
SYM_FUNC_END(cpu_switch_to)
schedule() 호출 시점에 이미 스택에 저장되어 있거나 더 이상 필요하지 않습니다.
sp_el0에 next 태스크(Task)의 task_struct 포인터를 저장하여 current 매크로가 동작하게 합니다.
/* 원자적 연산: LDXR/STXR 독점 접근 루프 (LL/SC) */
/* arch/arm64/include/asm/atomic_ll_sc.h 기반 */
arch_atomic_add_return:
prfm pstl1strm, [x1] /* L1 캐시에 스토어 예약 프리페치 */
1:
ldaxr w2, [x1] /* Load-Acquire Exclusive (획득 시맨틱) */
add w3, w2, w0 /* 덧셈 */
stlxr w4, w3, [x1] /* Store-Release Exclusive (해제 시맨틱) */
cbnz w4, 1b /* 실패(w4≠0) → 재시도 */
mov w0, w3 /* 새 값 반환 */
ret
/* ARMv8.1 LSE (Large System Extension) 대안 */
/* LDXR/STXR 루프를 단일 명령어로 대체 */
arch_atomic_add_return_lse:
ldaddal w0, w0, [x1] /* Atomic Load-Add, Acquire+Release */
add w0, w0, w2 /* 이전값 + 추가값 = 새 값 */
ret
/* ARM64 spinlock (ticket 기반 → qspinlock 이전) */
/* arch/arm64/include/asm/spinlock.h 역사적 참고 */
arch_spin_lock:
prfm pstl1strm, [x0] /* 프리페치 */
mov w2, #(1 << 16) /* next 필드 증가값 */
1: ldaxr w1, [x0] /* lock 값 독점 로드 */
add w3, w1, w2 /* next++ */
stxr w4, w3, [x0] /* 독점 저장 */
cbnz w4, 1b /* 실패 → 재시도 */
/* 이제 w1의 상위 16비트(next)와 하위 16비트(owner) 비교 */
eor w1, w1, w1, ror #16 /* owner == next? */
cbz w1, 3f /* 같으면 → 잠금 획득 */
2: wfe /* Wait For Event (전력 절약 대기) */
ldaxrh w1, [x0] /* owner 필드만 다시 로드 */
eor w1, w1, w3, lsr #16 /* 내 ticket == owner? */
cbnz w1, 2b /* 아니면 계속 대기 */
3: ret /* 잠금 획득! */
arch_spin_unlock:
ldrh w1, [x0] /* owner 값 로드 */
add w1, w1, #1 /* owner++ (다음 대기자) */
stlrh w1, [x0] /* Release 시맨틱으로 저장 */
ret /* SEV가 암시적으로 발생 */
/* arch/arm64/lib/copy_to_user.S — 유저 공간 메모리 복사 (간소화) */
SYM_FUNC_START(__arch_copy_to_user)
/* X0 = dst (유저), X1 = src (커널), X2 = 크기 */
add x5, x0, x2 /* 끝 주소 계산 */
/* 64바이트 단위 복사 (STP 쌍으로 한 번에 16바이트) */
.Lcopy64:
cmp x2, #64
b.lt .Lcopy_tail
ldp x3, x4, [x1] /* 커널 데이터 로드 */
ldp x5, x6, [x1, #16]
ldp x7, x8, [x1, #32]
ldp x9, x10, [x1, #48]
USER(9998f, stp x3, x4, [x0]) /* 유저 공간 저장 (폴트 가능) */
USER(9998f, stp x5, x6, [x0, #16])
USER(9998f, stp x7, x8, [x0, #32])
USER(9998f, stp x9, x10, [x0, #48])
/* ... 포인터 업데이트, 루프 ... */
9998: /* 유저 폴트 발생 시 여기로 분기 → -EFAULT 반환 */
sub x0, x5, x0 /* 복사 못 한 바이트 수 */
ret
SYM_FUNC_END(__arch_copy_to_user)
/* TLB 무효화 시퀀스 */
/* arch/arm64/include/asm/tlbflush.h 기반 */
/* 단일 페이지 TLB 무효화 */
flush_tlb_page:
dsb ishst /* 이전 페이지 테이블 쓰기 완료 보장 */
lsr x1, x0, #12 /* VA → 페이지 번호 (4KB) */
tlbi vale1is, x1 /* VA + 마지막 레벨 TLB 무효화 (IS) */
dsb ish /* TLB 무효화 완료 대기 */
isb /* 파이프라인 동기화 */
ret
/* ASID 전체 무효화 (mm_struct 전환) */
flush_tlb_mm:
dsb ishst
tlbi aside1is, x0 /* ASID 기반 전체 무효화 */
dsb ish
isb
ret
/* 범위 TLB 무효화 (ARMv8.4-TLBI 확장) */
flush_tlb_range:
dsb ishst
/* TLBI RVALE1IS: 범위 기반 무효화 (start ~ end) */
tlbi rvale1is, x0 /* 범위 지정 무효화 */
dsb ish
isb
ret
copy_to_user에서 유저 공간 접근 명령어는 USER() 매크로로 감쌉니다.
이 매크로는 해당 명령어의 주소를 예외 테이블(__ex_table)에 등록하여, 페이지 폴트(Page Fault) 발생 시 커널 oops 대신 지정된 복구 코드(9998 레이블)로 분기합니다.
이는 x86의 _ASM_EXTABLE과 동일한 메커니즘입니다.
메모리 모델/배리어
ARM64는 약순서(weakly-ordered) 메모리 모델을 사용합니다. x86의 TSO(Total Store Order)와 달리 ARM64에서는 Store-Store 재배치, Load-Load 재배치가 모두 가능하므로 명시적인 메모리 배리어가 필수적입니다.
/* DMB, DSB, ISB 사용 패턴 */
/* 1. 스핀락 해제 (Store-Release 패턴) */
stlr wzr, [x0] /* Store-Release: 이전 쓰기가 모두 완료된 후 저장 */
/* DMB ISHST + STR과 동등하지만 단일 명령어 */
/* 2. 메일박스 폴링 (Load-Acquire 패턴) */
1: ldar w1, [x0] /* Load-Acquire: 로드 후 모든 접근이 이 이후에 실행 */
cbz w1, 1b /* 값이 0이면 계속 폴링 */
/* 이후의 모든 메모리 접근은 ldar 이후에 보임 */
/* 3. DMA 버퍼 쓰기 후 디바이스 트리거 */
str x1, [x2] /* DMA 버퍼에 데이터 쓰기 (Normal) */
dsb sy /* 전체 시스템 데이터 동기화 */
str x3, [x4] /* MMIO 도어벨 레지스터 쓰기 (Device) */
/* 4. 시스템 레지스터 변경 후 동기화 */
msr sctlr_el1, x0 /* 시스템 제어 레지스터 변경 */
isb /* ISB: 변경 효과가 즉시 적용 보장 */
/* Linux 커널 smp_mb/smp_wmb/smp_rmb의 ARM64 구현 */
/* arch/arm64/include/asm/barrier.h */
#define smp_mb() asm volatile("dmb ish" ::: "memory")
#define smp_wmb() asm volatile("dmb ishst" ::: "memory")
#define smp_rmb() asm volatile("dmb ishld" ::: "memory")
/* Acquire/Release — 배리어 명령어 대신 LDAR/STLR 사용 */
#define smp_load_acquire(p) ({ \
typeof(*(p)) ___p; \
asm volatile("ldar %w0, %1" \
: "=r"(___p) : "Q"(*(p)) : "memory"); \
___p; \
})
#define smp_store_release(p, v) do { \
asm volatile("stlr %w1, %0" \
: "=Q"(*(p)) : "r"((u32)(v)) : "memory"); \
} while (0)
/* LDAR/STLR Acquire/Release 시맨틱 상세 */
/* Load-Acquire: 이 로드 이후의 모든 메모리 접근은
이 로드 이후에 관측됨 (단방향 배리어) */
ldar w0, [x1] /* Acquire: ↓ 이후 접근을 위로 이동 금지 */
ldr w2, [x3] /* 이 로드는 반드시 ldar 이후 */
str w4, [x5] /* 이 저장도 반드시 ldar 이후 */
/* Store-Release: 이 저장 이전의 모든 메모리 접근은 */
이 저장 이전에 관측됨 (단방향 배리어) */
ldr w0, [x1] /* 이 로드는 반드시 stlr 이전 */
str w2, [x3] /* 이 저장도 반드시 stlr 이전 */
stlr w4, [x5] /* Release: ↑ 이전 접근을 아래로 이동 금지 */
/* Acquire + Release 조합 = Full Barrier (잠금 acquire/release) */
ldaxr w0, [x1] /* Load-Acquire Exclusive */
/* ... 임계 구역 ... */
stlxr w2, w0, [x1] /* Store-Release Exclusive */
- x86 (TSO): Store-Load만 재배치됨 →
smp_wmb()/smp_rmb()가 컴파일러 배리어(no-op)로 충분 - ARM64 (약순서): 모든 종류의 재배치 가능 →
smp_wmb()는 실제DMB ISHST명령어 필요 - ARM64에서 배리어 누락은 x86에서는 발견되지 않는 미묘한 버그를 만듭니다. 항상 Linux 커널의 메모리 배리어 API를 사용하세요.
페이지 테이블 4K/16K/64K granule
ARM64는 세 가지 페이지 크기(granule)를 지원하며, 가상 주소 공간 크기와 페이지 테이블 레벨 수가 granule에 따라 달라집니다. Linux 커널은 일반적으로 4K granule을 사용하며, 4-레벨 페이지 테이블로 48-bit 가상 주소(256TB)를 지원합니다.
/* arch/arm64/include/asm/pgtable.h — 페이지 테이블 워크 매크로 */
/* PGD (Level 0) — TTBR이 가리킴 */
static inline pgd_t *pgd_offset(struct mm_struct *mm, unsigned long addr)
{
return mm->pgd + pgd_index(addr);
}
#define pgd_index(addr) (((addr) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))
/* PGDIR_SHIFT = 39 (4K), PTRS_PER_PGD = 512 */
/* PUD (Level 1) */
static inline pud_t *pud_offset(p4d_t *p4d, unsigned long addr)
{
return (pud_t *)__va(pud_page_paddr(*p4d)) + pud_index(addr);
}
#define pud_index(addr) (((addr) >> PUD_SHIFT) & (PTRS_PER_PUD - 1))
/* PUD_SHIFT = 30, PTRS_PER_PUD = 512 */
/* PMD (Level 2) */
#define pmd_index(addr) (((addr) >> PMD_SHIFT) & (PTRS_PER_PMD - 1))
/* PMD_SHIFT = 21, 블록 매핑 시 2MB hugepage */
/* PTE (Level 3) */
#define pte_index(addr) (((addr) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))
/* PAGE_SHIFT = 12, PTRS_PER_PTE = 512 */
/* TCR_EL1 구성 — 변환 제어 레지스터 */
/* arch/arm64/include/asm/pgtable-hwdef.h */
/* 주요 TCR_EL1 필드: */
#define TCR_T0SZ(x) (((64) - (x)) << 0) /* TTBR0 VA 크기 */
#define TCR_T1SZ(x) (((64) - (x)) << 16) /* TTBR1 VA 크기 */
#define TCR_TG0_4K (0UL << 14) /* TTBR0 4KB granule */
#define TCR_TG0_16K (2UL << 14) /* TTBR0 16KB granule */
#define TCR_TG0_64K (1UL << 14) /* TTBR0 64KB granule */
#define TCR_SH0_INNER (3UL << 12) /* Inner Shareable */
#define TCR_ORGN0_WBWA (1UL << 10) /* Outer WB Write-Alloc */
#define TCR_IRGN0_WBWA (1UL << 8) /* Inner WB Write-Alloc */
/* Linux 기본 설정 (4K, 48-bit VA): */
/* TCR = T0SZ(16) | T1SZ(16) | TG0_4K | TG1_4K | SH_INNER | ORGN_WBWA | IRGN_WBWA */
/* T0SZ=16 → 64-16=48-bit VA | TTBR0: 유저 0x0000_0000_0000_0000 ~ */
/* T1SZ=16 → 64-16=48-bit VA | TTBR1: 커널 0xFFFF_0000_0000_0000 ~ */
TTBR0_EL1은 유저 공간 주소(VA의 상위 비트가 0), TTBR1_EL1은 커널 공간(Kernel Space) 주소(VA의 상위 비트가 1)를 변환합니다.
컨텍스트 스위치 시 TTBR0만 교체하고 TTBR1은 고정되어, x86처럼 커널 영역을 모든 프로세스(Process)에 중복 매핑할 필요가 없습니다.
ARMv8.2-LVA 확장은 52-bit VA(4PB)를 지원하며 CONFIG_ARM64_VA_BITS_52로 활성화합니다.
SME (Scalable Matrix Extension)
SME(Scalable Matrix Extension)는 ARMv9.2-A에서 도입된 행렬 연산 확장입니다. SVE의 벡터 레지스터를 2차원 타일(tile)로 확장하여 행렬 곱셈, 외적(outer product) 등을 하드웨어에서 직접 가속합니다. AI/ML 워크로드의 GEMM(General Matrix Multiply) 연산에 최적화되어 있습니다.
/* SME 행렬 곱셈 예제 (FP32 GEMM 커널) */
/* C += A * B^T (외적 누적 방식) */
smstart /* Streaming Mode + ZA 활성화 */
zero {za} /* ZA 타일 전체 제로 초기화 */
ptrue p0.s /* 프레디케이트: 모든 FP32 활성 */
ptrue p1.s
/* K 루프: 외적 누적 */
.Lk_loop:
ld1w {z0.s}, p0/z, [x0] /* A의 열 벡터 로드 */
ld1w {z1.s}, p1/z, [x1] /* B의 행 벡터 로드 */
fmopa za0.s, p0/m, p1/m, z0.s, z1.s /* ZA += Z0 ⊗ Z1 (외적) */
/* 포인터 업데이트, K 카운터 감소 */
add x0, x0, x4 /* A 다음 열 */
add x1, x1, x5 /* B 다음 행 */
subs x3, x3, #1 /* K-- */
b.ne .Lk_loop
/* ZA 타일에서 C로 저장 */
mov w12, #0 /* 행 인덱스 */
.Lstore_loop:
st1w {za0h.s[w12, 0]}, p0, [x2] /* ZA 행 → C 저장 */
add x2, x2, x6 /* C 다음 행 */
add w12, w12, #1
cmp w12, w7
b.lt .Lstore_loop
smstop /* Streaming Mode + ZA 비활성 */
/* Linux 커널의 SME 컨텍스트 관리 */
/* arch/arm64/kernel/fpsimd.c */
/* SME 상태 저장 (컨텍스트 스위치 시) */
void sme_save_state(struct user_fpsimd_state *state)
{
/* ZA 타일: SVL x SVL bits → 최대 4KB (512-bit SVL) */
/* Streaming SVE 레지스터 (Z0-Z31, P0-P15) */
/* TPIDR2_EL0 (SME 전용 TLS 레지스터) */
}
/* PSTATE.SM/ZA 확인 */
static inline bool system_supports_sme(void)
{
return cpus_have_const_cap(ARM64_SME);
}
/* SME 벡터 길이(SVL) 조회 */
/* SVL = sme_get_vl() → rdsvl 명령어 사용 */
/* prctl(PR_SME_SET_VL, vl) 로 유저 공간에서 설정 */
GIC 시스템 레지스터
GICv3/v4에서 CPU 인터페이스(ICC_*) 접근은 메모리 매핑 MMIO 대신 시스템 레지스터를 통해 이루어집니다. 이는 인터럽트 ACK/EOI 경로의 지연(Latency)을 크게 줄여주며, Linux 커널의 drivers/irqchip/irq-gic-v3.c에서 직접 사용됩니다.
| 시스템 레지스터 | 용도 | 커널 사용 경로 |
|---|---|---|
ICC_IAR1_EL1 | Interrupt Acknowledge (Group 1) | 인터럽트 ACK → INTID 반환 |
ICC_EOIR1_EL1 | End of Interrupt (Group 1) | EOI → 우선순위(Priority) 드롭 + 비활성화 |
ICC_PMR_EL1 | Priority Mask Register | 인터럽트 우선순위 마스킹 |
ICC_SRE_EL1 | System Register Enable | 시스템 레지스터 모드 활성화 |
ICC_CTLR_EL1 | Control Register | EOI 모드, CBPR 설정 |
ICC_IGRPEN1_EL1 | Group 1 Enable | Group 1 인터럽트 활성화 |
ICC_SGI1R_EL1 | SGI Generation (Group 1) | IPI 전송 |
ICC_DIR_EL1 | Deactivate Interrupt | EOImode=1 시 별도 비활성화 |
ICC_RPR_EL1 | Running Priority | 현재 실행 중 인터럽트 우선순위 |
ICC_BPR1_EL1 | Binary Point Register | 우선순위 그룹핑 |
/* drivers/irqchip/irq-gic-v3.c — GICv3 인터럽트 핸들러 */
/* ICC 시스템 레지스터를 통한 ACK/EOI */
static void gic_handle_irq(struct pt_regs *regs)
{
u32 irqnr;
/* 1. Acknowledge: INTID 읽기 */
irqnr = read_sysreg_s(SYS_ICC_IAR1_EL1);
/* 하드웨어: 가장 높은 우선순위 pending → active */
/* running priority = 해당 인터럽트 우선순위 */
if (likely(irqnr > 15 && irqnr < 1020)) {
/* SPI/PPI 처리 */
handle_domain_irq(gic_data.domain, irqnr, regs);
} else if (irqnr < 16) {
/* SGI (IPI) 처리 */
write_sysreg_s(irqnr, SYS_ICC_EOIR1_EL1); /* EOI 먼저 */
handle_IPI(irqnr, regs);
}
/* irqnr == 1023: Spurious interrupt → 무시 */
}
/* 2. EOI (End of Interrupt) */
static void gic_eoi_irq(struct irq_data *d)
{
write_sysreg_s(d->hwirq, SYS_ICC_EOIR1_EL1);
isb(); /* EOI 완료 보장 */
}
/* IPI 전송: ICC_SGI1R_EL1을 통한 SGI 생성 */
/* arch/arm64/include/asm/smp.h */
static void gic_ipi_send_mask(struct irq_data *d,
const struct cpumask *mask)
{
u64 val;
int cpu;
/* ICC_SGI1R_EL1 레이아웃:
* [55:48] Aff3 | [39:32] Aff2 | [23:16] Aff1
* [15:0] TargetList (비트마스크)
* [27:24] INTID (SGI 번호 0-15)
* [40] IRM (1=모든 PE에 전송)
*/
for_each_cpu(cpu, mask) {
u64 mpidr = cpu_logical_map(cpu);
val = MPIDR_TO_SGI_AFFINITY(mpidr, 3); /* Aff3 */
val |= MPIDR_TO_SGI_AFFINITY(mpidr, 2); /* Aff2 */
val |= MPIDR_TO_SGI_AFFINITY(mpidr, 1); /* Aff1 */
val |= (u64)d->hwirq << 24; /* INTID */
val |= 1UL << (mpidr & 0xf); /* Aff0 → TargetList 비트 */
write_sysreg_s(val, SYS_ICC_SGI1R_EL1);
}
isb(); /* SGI 전송 완료 보장 */
}
/* GICv3 시스템 레지스터 초기화 시퀀스 */
gic_cpu_init:
/* 시스템 레지스터 모드 활성화 */
mrs x0, ICC_SRE_EL1
orr x0, x0, #(1 << 0) /* SRE = 1 (System Register Enable) */
msr ICC_SRE_EL1, x0
isb
/* 우선순위 마스크: 모든 우선순위 허용 */
mov x0, #0xff /* 최저 우선순위 (모두 통과) */
msr ICC_PMR_EL1, x0
/* Group 1 인터럽트 활성화 */
mov x0, #1
msr ICC_IGRPEN1_EL1, x0
isb
ret
CONFIG_ARM_GIC_V3로 GICv3 드라이버를 활성화하며, GICv4.1은 SGI 직접 주입도 지원합니다.
alternatives 패칭
ARM64 Linux 커널은 부팅 시 CPU 기능(capabilities)을 탐지하고, alternatives 프레임워크를 통해 명령어를 동적으로 패칭합니다. 이를 통해 단일 커널 이미지로 다양한 하드웨어에서 최적 성능을 발휘하며, 보안 완화(erratum workaround)도 적용합니다.
/* arch/arm64/include/asm/alternative-macros.h */
/* ALTERNATIVE 매크로 — 조건부 명령어 패칭 */
/* 사용 예: LSE 원자적 명령어 대체 */
asm volatile(
ALTERNATIVE(
"ldxr %w0, %2\n" /* 기본: LL/SC */
"add %w0, %w0, %w3\n"
"stxr %w1, %w0, %2\n"
"cbnz %w1, 1b",
"ldadd %w3, %w0, %2\n" /* 대안: LSE (단일 명령어) */
"nop\n"
"nop\n"
"nop",
ARM64_HAS_LSE_ATOMICS /* 기능 비트 */
)
: "=&r"(result), "=&r"(tmp), "+Q"(v->counter)
: "r"(i)
);
/* ALTERNATIVE 매크로 내부 구조: */
/* 1. 기본 코드를 .text에 배치 */
/* 2. 대안 코드를 .altinstructions 섹션에 저장 */
/* 3. struct alt_instr 메타데이터: 위치, 길이, 기능 비트 */
/* arch/arm64/kernel/cpufeature.c — CPU 기능 탐지 */
/* has_cap: 특정 기능 지원 여부 확인 */
static bool has_lse_atomics(const struct arm64_cpu_capabilities *cap,
int scope)
{
/* ID_AA64ISAR0_EL1.Atomic 필드 확인 */
u64 isar0 = read_sysreg(ID_AA64ISAR0_EL1);
unsigned int val = cpuid_feature_extract_unsigned_field(
isar0, ID_AA64ISAR0_EL1_ATOMIC_SHIFT);
return val >= 2; /* 0x2 = LSE 지원 */
}
/* CPU capability 등록 */
static const struct arm64_cpu_capabilities arm64_features[] = {
{
.desc = "LSE atomic instructions",
.capability = ARM64_HAS_LSE_ATOMICS,
.type = ARM64_CPUCAP_SYSTEM_FEATURE,
.matches = has_lse_atomics,
},
{
.desc = "Pointer authentication",
.capability = ARM64_HAS_ADDRESS_AUTH,
.type = ARM64_CPUCAP_BOOT_CPU_FEATURE,
.matches = has_address_auth_cpucap,
},
/* ... */
};
/* arch/arm64/kernel/alternative.c — 패칭 수행 */
void apply_alternatives_all(void)
{
struct alt_instr *alt;
/* .altinstructions 섹션 순회 */
for (alt = __alt_instructions; alt < __alt_instructions_end; alt++) {
if (!cpus_have_cap(alt->cpufeature))
continue;
/* 기본 코드 → 대안 코드로 교체 */
u32 *origptr = ALT_ORIG_PTR(alt);
u32 *replptr = ALT_REPL_PTR(alt);
int nr_inst = alt->orig_len / AARCH64_INSN_SIZE;
for (int i = 0; i < nr_inst; i++)
origptr[i] = replptr[i];
/* 캐시 동기화: 수정된 코드를 I-cache에 반영 */
__flush_icache_range((unsigned long)origptr,
(unsigned long)origptr + alt->orig_len);
}
}
I-cache 동기화 필수: ARM64에서 코드를 수정한 후에는 반드시 DC CVAU(데이터 캐시 클린) → IC IVAU(명령어 캐시 무효화) → DSB ISH → ISB 시퀀스를 실행해야 합니다. ARM64의 I-cache와 D-cache는 일관성(coherence)이 보장되지 않으므로 (Harvard 아키텍처), 소프트웨어가 명시적으로 동기화해야 합니다.
ftrace ARM64 구현
Linux ftrace는 함수 호출 추적(Call Trace)을 위한 인프라입니다. ARM64에서는 컴파일러가 모든 함수 시작에 BL _mcount 또는 BL ftrace_caller를 삽입하고, ftrace가 비활성화된 상태에서는 NOP로 패칭합니다. 활성화 시 다시 trampoline 호출로 패칭하여 동적 추적이 가능합니다.
/* arch/arm64/kernel/entry-ftrace.S — ftrace trampoline */
/* -fpatchable-function-entry=2 사용 시 */
/* 일반 함수 컴파일 결과: */
some_function:
nop /* ftrace 비활성: NOP */
nop /* 또는: BL ftrace_caller */
/* ... 함수 본문 ... */
/* ftrace 활성화 시 NOP → BL ftrace_caller로 패칭 */
SYM_CODE_START(ftrace_caller)
/* 레지스터 저장 (X0-X30 + LR 보존) */
stp x29, x30, [sp, #-16]!
mov x29, sp
/* ftrace 콜백 인자 구성 */
mov x0, x30 /* ip (추적 대상 함수 주소) */
mov x1, x9 /* parent_ip (호출자 주소) */
ldr x2, [sp, #8] /* pt_regs가 있으면 전달 */
/* 등록된 ftrace 콜백 호출 */
ldr x3, =ftrace_trace_function
ldr x3, [x3]
blr x3 /* ftrace_ops->func() 호출 */
/* 복원 후 원래 함수로 복귀 */
ldp x29, x30, [sp], #16
ret
SYM_CODE_END(ftrace_caller)
/* arch/arm64/kernel/ftrace.c — 동적 패칭 */
int ftrace_make_call(struct dyn_ftrace *rec, unsigned long addr)
{
unsigned long pc = rec->ip;
u32 old = aarch64_insn_gen_nop(); /* NOP */
u32 new = aarch64_insn_gen_branch_imm( /* BL ftrace_caller */
pc, addr, AARCH64_INSN_BRANCH_LINK);
return ftrace_modify_code(pc, old, new, true);
}
int ftrace_make_nop(struct module *mod, struct dyn_ftrace *rec,
unsigned long addr)
{
unsigned long pc = rec->ip;
u32 old = aarch64_insn_gen_branch_imm( /* BL */
pc, addr, AARCH64_INSN_BRANCH_LINK);
u32 new = aarch64_insn_gen_nop(); /* NOP */
return ftrace_modify_code(pc, old, new, true);
}
/* ftrace_modify_code: stop_machine()으로 모든 CPU 동기화 후 패칭 */
/* → aarch64_insn_patch_text() → D-cache clean + I-cache inv + ISB */
/* Function Graph Tracer — 함수 진입/종료 추적 */
/* arch/arm64/kernel/entry-ftrace.S */
/* Graph tracer: LR을 return_to_handler로 교체 */
/* 원래: func() → caller_func()으로 복귀 */
/* 교체: func() → return_to_handler → 추적 기록 → caller_func() */
void prepare_ftrace_return(unsigned long self_addr,
unsigned long *parent,
unsigned long frame_pointer)
{
unsigned long return_hooker = (unsigned long)&return_to_handler;
unsigned long old = *parent; /* 원래 복귀 주소 */
/* 원래 복귀 주소를 ret_stack에 보존 */
if (ftrace_push_return_trace(old, self_addr, frame_pointer))
return;
/* LR을 return_to_handler로 교체 */
*parent = return_hooker;
}
/* BTI 호환: trampoline 진입점에 BTI C 명령어 필요 */
/* BTI(Branch Target Identification)가 활성화된 커널에서는 */
/* 간접 분기 대상에 BTI C/J/JC landing pad 필수 */
-pg: 전통적인 mcount 기반 (함수 시작에BL _mcount삽입)-fpatchable-function-entry=N: GCC 8+, 함수 시작에 N개 NOP 삽입 (mcount보다 낮은 오버헤드(Overhead))- ARM64 Linux 커널은 기본적으로
-fpatchable-function-entry=2사용 - PAC(Pointer Authentication) 활성화 시: ftrace trampoline이
PACIASP/AUTIASP쌍을 포함하여 LR 서명을 보존
커널 이미지 레이아웃
ARM64 Linux 커널 이미지는 특정 헤더 형식을 따르며, 부트로더(U-Boot, UEFI 등)가 이를 파싱하여 메모리에 로드합니다. 커널 가상 주소 공간은 TTBR1_EL1이 관리하는 상위 주소 영역에 매핑됩니다.
/* arch/arm64/kernel/head.S — 커널 이미지 헤더 */
/* Documentation/arch/arm64/booting.rst 참조 */
_head:
/*
* DO NOT MODIFY. Image header expected by Linux boot-loaders.
*/
#ifdef CONFIG_EFI
/* "MZ" EFI stub 매직 (PE/COFF 호환) */
add x13, x18, #0x16 /* code0: "MZ" 인코딩 */
b primary_entry /* code1: 실제 진입점 분기 */
#else
b primary_entry /* code0 */
nop /* code1 */
#endif
.quad _kernel_offset_le /* text_offset (LE) */
.quad _kernel_size_le /* image_size (LE) */
.quad _kernel_flags_le /* flags */
.quad 0 /* res2 */
.quad 0 /* res3 */
.quad 0 /* res4 */
.ascii "ARM\x64" /* magic: 0x644d5241 */
.long pe_header - _head /* PE 헤더 오프셋 */
/* arch/arm64/kernel/kaslr.c — KASLR 무작위화 */
u64 kaslr_early_init(void)
{
u64 seed, range;
/* 엔트로피 소스에서 시드 획득 */
if (efi_nokaslr)
return 0;
/* 1순위: RNDR (ARMv8.5 하드웨어 난수) */
if (__arm64_rndr(&seed))
goto got_seed;
/* 2순위: DTB의 /chosen/kaslr-seed */
seed = get_kaslr_seed_from_fdt(fdt);
if (seed)
goto got_seed;
/* 3순위: 카운터 기반 (약한 엔트로피) */
seed = read_sysreg(cntpct_el0);
got_seed:
/* 가상 오프셋 계산: 2MB 정렬 */
range = BIT(VA_BITS - 2); /* VA 공간의 1/4 범위 */
return (seed % range) & ~(SZ_2M - 1);
}
_head의 첫 2바이트가 "MZ"(PE 매직)으로 인코딩되며, EFI stub(drivers/firmware/efi/libstub/)이 먼저 실행됩니다.
EFI stub은 메모리 맵(Memory Map) 획득, initrd 로딩, KASLR 시드 생성 등을 수행한 후 실제 커널 진입점(Entry Point)으로 분기합니다.
참고 링크
- Arm Architecture Reference Manual for A-profile architecture (ARM ARM) — AArch64 전체 명령어 인코딩, 시스템 레지스터, 예외 모델을 정의하는 공식 레퍼런스입니다
- Arm A64 Instruction Set Architecture Guide — A64 명령어셋을 카테고리별로 정리한 입문용 가이드입니다
- Arm Cortex-A Series Programmer's Guide for ARMv8-A — ARMv8-A 프로그래밍 실무 가이드입니다
- 커널 소스: arch/arm64/kernel/entry.S — ARM64 예외 벡터 테이블, SVC/IRQ/FIQ 진입점 어셈블리입니다
- 커널 소스: arch/arm64/include/asm/assembler.h — 커널 ARM64 어셈블리 매크로 (adr_l, ldr_l 등) 정의입니다
- 커널 소스: arch/arm64/include/asm/barrier.h — DMB/DSB/ISB 메모리 배리어 매크로 및 smp_mb() 구현입니다
- 커널 소스: arch/arm64/include/asm/atomic_lse.h — LSE 원자적 명령어 (LDADD, SWPAL, CAS 등) 구현입니다
- 커널 소스: arch/arm64/include/asm/sysreg.h — MSR/MRS 래퍼 매크로 및 시스템 레지스터 상수 정의입니다
- 커널 문서: Booting AArch64 Linux (Documentation/arch/arm64/booting.rst) — ARM64 리눅스 부팅 프로토콜 문서입니다
- 커널 문서: Memory Layout on AArch64 Linux (Documentation/arch/arm64/memory.rst) — ARM64 가상 메모리 레이아웃 문서입니다
- LWN.net: An introduction to ARM64 Scalable Vector Extension (SVE) — 리눅스 커널의 SVE 지원 구현 분석입니다
- LWN.net: Arm's Scalable Matrix Extension (SME) — SVE2/SME 확장 명령어와 커널 지원 현황입니다
- LWN.net: Large System Extensions for ARM64 — LSE 원자적 명령어의 커널 활용과 성능 이점 분석입니다
- 커널 소스: arch/arm64/include/asm/insn.h — ARM64 명령어 인코딩/디코딩 헬퍼 (kprobes, ftrace용)입니다
관련 문서
- 어셈블리 종합 — GCC 인라인 어셈블리, 호출 규약
- SIMD 명령과 커널 개발 — 커널 NEON/SVE 사용, FPU 컨텍스트
- GNU Assembler (as) — GAS 지시자
- x86_64 명령어셋 (ISA) — x86_64 CISC 비교
- RISC-V 명령어셋 (ISA) — RISC-V 모듈형 ISA
- MIPS 명령어셋 (ISA) — MIPS 전통 RISC