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 전체를 다룹니다.

전제 조건: 어셈블리(Assembly) 종합을 먼저 읽으세요. ARM64 명령어 레퍼런스는 RISC 레지스터-레지스터 모델, 조건 코드, Load/Store 아키텍처에 대한 기본 이해가 필요합니다.
일상 비유: ARM64는 레고 블록과 같습니다. 모든 블록(명령어)이 같은 크기(32-bit)로 통일되어 있어 조립(디코딩)이 빠르고 효율적입니다. 복잡한 작업은 여러 단순 블록을 조합해서 구성합니다.

핵심 요약

  • 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 단일 명령어.

단계별 이해

  1. 레지스터 구조 파악
    X0-X30(64-bit), W0-W30(32-bit 하위), XZR/WZR(제로), SP의 관계를 먼저 익힙니다.
  2. Load/Store 모델 이해
    ARM64에서 메모리 접근은 LDR/STR만 사용합니다. 산술은 레지스터 간만 가능합니다.
  3. 예외 레벨과 시스템 레지스터
    EL0-EL3 구조와 MSR/MRS로 접근하는 시스템 레지스터를 이해합니다.
  4. 조건 코드와 분기
    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
ARM64 예외 레벨 (Exception Level) 계층 구조 EL3 — Secure Monitor ATF (ARM Trusted Firmware) EL2 — Hypervisor KVM / Xen (Stage-2 주소 변환) EL1 — OS / Kernel Linux Kernel (TTBR0/TTBR1, VBAR_EL1) EL0 — User Application 유저 프로세스 (가장 낮은 특권) SVC #0 HVC #0 SMC #0 ERET ERET ERET SVC: Supervisor Call (EL0→EL1) HVC: Hypervisor Call (EL1→EL2) SMC: Secure Monitor Call (→EL3) ERET: Exception Return — ELR_ELn→PC, SPSR_ELn→PSTATE (하위 레벨로 복귀)
예외 레벨 전환: ARM64는 네 개의 예외 레벨(EL0~EL3)을 지원합니다. SVC/HVC/SMC 명령으로 상위 레벨에 진입하며, 하드웨어가 자동으로 ELR_ELn(복귀 주소)과 SPSR_ELn(PSTATE 백업)을 저장합니다. ERET로 하위 레벨로 복귀할 때 이 값들이 복원됩니다. 각 EL은 독립적인 SP를 갖고, 시스템 레지스터에 _ELn 접미사를 붙여 레벨별로 분리됩니다.

레지스터 셋

ARM64는 풍부한 레지스터 셋을 제공하여 함수 호출 시 인자 전달과 로컬 변수를 대부분 레지스터에서 처리합니다. x86-64의 16개 범용 레지스터에 비해 31개로 거의 2배이며, 이는 register spilling(스택 임시 저장)을 크게 줄여 성능에 유리합니다:

범용 레지스터

64-bit32-bitAAPCS64 용도
X0-X7W0-W7인자/반환값 (X0=1st 반환, X0-X7=인자)
X8W8간접 결과 레지스터 (XR)
X9-X15W9-W15Caller-saved (임시)
X16W16IP0 (Intra-Procedure scratch)
X17W17IP1 (Intra-Procedure scratch)
X18W18플랫폼 레지스터 (Linux: shadow call stack)
X19-X28W19-W28Callee-saved
X29W29프레임 포인터 (FP)
X30W30링크 레지스터 (LR)
SPWSP스택 포인터 (EL별 독립)
XZRWZR제로 레지스터 (읽기=0, 쓰기=버림)
X 레지스터 (64-bit) / W 레지스터 (32-bit) 관계 63 32 31 0 X0 (64-bit) 상위 32-bit [63:32] W0 (하위 32-bit) [31:0] W 레지스터 쓰기 시 제로 확장: MOV W0, #0x1234 → X0 = 0x0000_0000_0000_1234 (상위 32비트가 자동으로 0이 됨) X 레지스터 쓰기: MOV X0, #0xFFFF_FFFF_0000_1234 → X0 전체 64비트가 설정됨, W0 읽기 시 하위 0x0000_1234만 반환
W 레지스터 쓰기의 제로 확장: ARM64에서 W 레지스터에 값을 쓰면 상위 32비트가 자동으로 0으로 클리어됩니다. 이는 x86-64에서 32-bit 레지스터 쓰기(EAX 등)가 상위 32비트를 클리어하는 것과 동일한 설계입니다. 반면 16-bit/8-bit 하위 접근은 존재하지 않으며, 이를 통해 부분 레지스터 갱신 문제(partial register stall)가 원천적으로 방지됩니다.

PSTATE 조건 플래그

플래그이름설명
NNegative결과의 최상위 비트 (부호)
ZZero결과가 0이면 세트
CCarry부호 없는 올림/빌림
VOverflow부호 있는 오버플로

시스템 레지스터 (주요)

레지스터설명
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/FPV0-V31128-bitBn(8), Hn(16), Sn(32), Dn(64), Qn(128)
SVEZ0-Z31128~2048-bit (가변)구현에 따라 128-bit 단위로 확장
SVE 프레디킷P0-P15가변벡터 요소별 마스크
SVE FFRFFR가변First Fault Register
FP 제어FPCR/FPSR32-bit라운딩 모드, 예외 상태
NEON 레지스터 뷰 및 벡터 레인 구조 스칼라 접근 뷰 (V0 레지스터) 127 0 Q0 128-bit D0 64-bit [63:0] S0 32-bit [31:0] H0 16-bit B0 8b 벡터 레인 뷰 (128-bit 벡터) V0.2D D[1] (64-bit) D[0] (64-bit) V0.4S S[3] S[2] S[1] S[0] V0.8H H[7] H[6] H[5] H[4] H[3] H[2] H[1] H[0] V0.16B B15 B14 B13 B12 B11 B10 B9 B8 B7 B6 B5 B4 B3 B2 B1 B0 V0.2D = 2x64-bit double V0.4S = 4x32-bit single V0.8H = 8x16-bit half V0.16B = 16x8-bit byte 접미사 의미: B=8-bit, H=16-bit, S=32-bit, D=64-bit, Q=128-bit (Q/D/S/H/B는 스칼라 접근 시 레지스터 이름에 사용)
NEON vs SVE: NEON은 고정 128-bit 벡터로 모든 ARM64 구현에서 지원되며, 암호화(AES, SHA), 미디어 코덱 등에 널리 사용됩니다. SVE(Scalable Vector Extension)는 128~2048-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 += immldr 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 vs Post-index 비교:
  • 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 + ADD 패턴 (PC-relative 주소 생성): ARM64에서 즉시값으로 표현할 수 있는 주소 범위가 제한적이므로, 커널 코드에서 심볼 주소를 구할 때 두 명령어의 조합을 사용합니다. 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 값 직접 로드 */
LDP/STP (Load/Store Pair) 이점:
  • 단일 명령어로 두 레지스터를 동시에 로드/저장하여 코드 밀도가 2배 향상됩니다.
  • 128-bit 메모리 버스(Bus)를 완전히 활용하며, 캐시 라인(Cache Line) 접근 효율이 높습니다.
  • 함수 프롤로그/에필로그에서 FP(X29)와 LR(X30)을 한 번에 저장/복원하는 관용 패턴으로 사용됩니다.
  • SP 기반 LDP/STP는 원자적 128-bit 접근을 보장하여 struct 복사에도 유용합니다.

데이터 전송 명령어

명령어문법설명
MOVmov x0, x1레지스터 → 레지스터 이동
MOVZmovz x0, #0x1234, lsl #16즉시값 이동 + 나머지 제로화
MOVNmovn x0, #0즉시값 반전 이동
MOVKmovk x0, #0x567816-bit 즉시값 삽입 (나머지 보존)
LDRldr x0, [x1, #8]메모리 → 레지스터 (64-bit)
LDRB/LDRHldrb w0, [x1]바이트/하프워드 제로 확장 로드
LDRSB/LDRSH/LDRSWldrsw x0, [x1]부호 확장 로드
STRstr x0, [x1, #8]레지스터 → 메모리 (64-bit)
LDPldp x0, x1, [sp]쌍 레지스터 로드 (128-bit)
STPstp x29, x30, [sp, #-16]!쌍 레지스터 저장
ADRPadrp x0, symbolPC 상대 4KB 페이지 주소
ADRadr x0, labelPC 상대 주소 (±1MB)
LDARldar x0, [x1]Acquire 시맨틱 로드
STLRstlr x0, [x1]Release 시맨틱 저장
PRFMprfm pldl1keep, [x0]메모리 프리페치
LDAR/STLR — Acquire/Release 시맨틱과 Linux 메모리 모델:
  • 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]                 /* 실제 값 로드 */
Acquire/Release vs 전체 배리어 비교: LDAR/STLR은 주소별 순서 보장(Ordering)만 하므로, 관련 없는 메모리 접근은 여전히 재배치될 수 있습니다. 전체 순서가 필요한 경우(예: smp_mb())에는 DMB ISH를 사용해야 합니다. Linux 커널에서 일반적인 READ_ONCE()/WRITE_ONCE()는 배리어를 포함하지 않으며, 컴파일러 재배치만 방지합니다.

산술 명령어

명령어문법설명플래그
ADDadd x0, x1, x2덧셈
ADDSadds x0, x1, x2덧셈 + 플래그 세트N, Z, C, V
SUBsub x0, x1, #16뺄셈
SUBSsubs x0, x1, x2뺄셈 + 플래그 세트N, Z, C, V
ADCadc x0, x1, x2캐리 포함 덧셈
SBCsbc x0, x1, x2캐리 포함 뺄셈
NEGneg x0, x1부정 (0 - src)
MULmul x0, x1, x2곱셈 (하위 64-bit)
MADDmadd x0, x1, x2, x3곱셈-덧셈: x1*x2 + x3
MSUBmsub x0, x1, x2, x3곱셈-뺄셈: x3 - x1*x2
SMULLsmull x0, w1, w2부호 있는 32→64 곱셈
UMULLumull x0, w1, w2부호 없는 32→64 곱셈
SMULHsmulh x0, x1, x264×64→128 상위 64-bit (부호)
UMULHumulh x0, x1, x264×64→128 상위 64-bit (부호 없음)
SDIVsdiv x0, x1, x2부호 있는 나눗셈
UDIVudiv x0, x1, x2부호 없는 나눗셈

논리/시프트/비트 조작 명령어

명령어문법설명
ANDand x0, x1, x2비트 AND
ANDSands x0, x1, x2비트 AND + 플래그 세트
ORRorr x0, x1, x2비트 OR
ORNorn x0, x1, x2비트 OR NOT
EOReor x0, x1, x2비트 XOR
EONeon x0, x1, x2비트 XOR NOT
BICbic x0, x1, x2비트 클리어: x1 AND NOT x2
TSTtst x0, #0xFFAND 테스트 (결과 저장 안 함)
MVNmvn x0, x1비트 반전 (NOT)
LSLlsl x0, x1, #4논리 좌측 시프트
LSRlsr x0, x1, #4논리 우측 시프트
ASRasr x0, x1, #4산술 우측 시프트
RORror x0, x1, #4순환 우측 시프트
CLZclz x0, x1선행 제로 카운트
CLScls x0, x1선행 부호 비트 카운트
RBITrbit x0, x1비트 순서 반전
REVrev x0, x1바이트 순서(Byte Order) 반전 (64-bit)
REV16rev16 x0, x1하프워드 내 바이트 반전
REV32rev32 x0, x1워드 내 바이트 반전
EXTRextr x0, x1, x2, #n두 레지스터에서 비트 추출
BFIbfi x0, x1, #lsb, #width비트 필드 삽입
BFXILbfxil x0, x1, #lsb, #width비트 필드 추출 + 삽입
UBFIZ/SBFIZubfiz x0, x1, #lsb, #w부호 없는/있는 비트 필드 제로/부호 확장 삽입

비교/분기 명령어

명령어문법설명
CMPcmp x0, x1SUBS + 결과 버림 (플래그만 세트)
CMNcmn x0, x1ADDS + 결과 버림
TSTtst x0, x1ANDS + 결과 버림
CCMPccmp x0, x1, #nzcv, eq조건부 비교 (조건 미충족 시 nzcv 세트)
CSELcsel x0, x1, x2, eq조건부 선택: eq이면 x1, 아니면 x2
CSINCcsinc x0, x1, x2, ne조건부 선택/증가
CSETcset x0, eq조건 충족 시 1, 아니면 0
Bb label무조건 분기 (±128MB)
B.condb.eq label조건부 분기 (±1MB)
BLbl func함수 호출 (X30 ← PC+4)
BRbr x0간접 분기
BLRblr x0간접 호출 (X30 ← PC+4)
RETret복귀 (BR X30)
CBZcbz x0, label제로이면 분기
CBNZcbnz x0, label비제로이면 분기
TBZtbz x0, #5, label특정 비트가 0이면 분기
TBNZtbnz x0, #5, label특정 비트가 1이면 분기
조건 코드: EQ(같음), NE(다름), GT(부호있는 >), GE(부호있는 >=), LT(부호있는 <), LE(부호있는 <=), HI(부호없는 >), HS(부호없는 >=), LO(부호없는 <), LS(부호없는 <=), MI(음수), PL(양수/0), VS(오버플로), VC(무오버플로).
CCMP (Conditional Compare) — 다중 조건 분기 최적화: CCMP는 이전 조건이 참일 때만 비교를 수행하고, 거짓이면 즉시값으로 NZCV 플래그를 설정합니다. 이를 통해 if (a == 1 && b == 2) 같은 다중 조건을 분기 없이 단일 조건 체인으로 평가할 수 있습니다.
  • x86에서는 CMP + JNE + CMP + JNE로 2개의 분기가 필요하지만, ARM64에서는 CMP + CCMP + B.cond1개의 분기만 사용합니다.
  • 분기 예측(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)으로 복귀 */
AAPCS64 호출 규약(Calling Convention):
  • 인자: 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시스템 레지스터 읽기레지스터별
NOPNo operation모든 레벨
WFI인터럽트 대기 (저전력)EL0+ (트랩 가능)
WFE이벤트 대기EL0+
SEV/SEVL이벤트 전송 (로컬)EL0+
ISB명령어 동기화 배리어모든 레벨
DSB데이터 동기화 배리어모든 레벨
DMB데이터 메모리 배리어(Memory Barrier)모든 레벨
TLBITLB 무효화(Invalidation)EL1+
IC명령어 캐시 유지레벨별
DC데이터 캐시 유지레벨별
AT주소 변환(Address Translation)EL1+
CLREX독점 모니터 초기화모든 레벨
YIELD양보(Yield) 힌트 (SMT)모든 레벨
BRK #imm디버그 브레이크포인트모든 레벨
예외 벡터 테이블 레이아웃 (VBAR_ELn 기준) 4 그룹 x 4 엔트리 = 16 벡터, 각 128 바이트 (0x80), 전체 2048 바이트 (0x800) 오프셋 Synchronous IRQ FIQ SError Current EL with SP_EL0 (SP0) +0x000 Sync (0x80) IRQ (0x80) FIQ (0x80) SError (0x80) Current EL with SP_ELx (SPx) — 커널 예외 처리 +0x200 el1h_sync el1h_irq el1h_fiq el1h_error Lower EL using AArch64 — 유저 → 커널 (시스템 콜) +0x400 el0_sync (SVC) el0_irq el0_fiq el0_error Lower EL using AArch32 — 32-bit 호환 모드 +0x600 el0_32_sync el0_32_irq el0_32_fiq el0_32_error VBAR_EL1 + 0x000: SP0 그룹 | +0x200: SPx 그룹 | +0x400: AArch64 하위 EL | +0x600: AArch32 하위 EL 각 엔트리 = 128바이트 (최대 32개 명령어). ventry 매크로가 정렬 + 분기를 생성합니다. 메모리 배리어 비교: DMB vs DSB vs ISB DMB (Data Memory Barrier) - 메모리 접근 순서만 보장 - 명령어 실행은 계속 진행 - smp_mb() / smp_rmb() 용도: 일반 메모리 순서 보장 DSB (Data Sync Barrier) - 메모리 접근 완료까지 대기 - 후속 명령어 실행 차단 - DMB보다 강력, 비용 높음 용도: TLBI/IC 후, 장치 설정 ISB (Instruction Sync Barrier) - 파이프라인 완전 플러시 - 시스템 레지스터 변경 반영 - 가장 비용이 높은 배리어 용도: MMU/캐시 설정 후 강도 증가: DMB → DSB → ISB (비용도 함께 증가) ISH=Inner Shareable(SMP), OSH=Outer Shareable(클러스터간), SY=System(전체)
배리어 실전 사용 시나리오:
  • 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)

명령어문법설명
LDXRldxr x0, [x1]독점 로드
STXRstxr w2, x0, [x1]독점 저장 (w2=성공 여부: 0=성공)
LDAXRldaxr x0, [x1]독점 로드 + Acquire
STLXRstlxr 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 ISHInner Shareable 전체 배리어
DMB ISHLDInner Shareable 로드 배리어
DMB ISHSTInner 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) vs LSE CAS (ARMv8.1) 성능 비교:
  • 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로 자동 전파 */
독점 모니터 (Exclusive Monitor) 메커니즘:
  • LDXR은 해당 주소를 "독점 상태"로 마킹합니다. 이후 다른 코어가 같은 캐시 라인에 쓰면 독점 상태가 해제됩니다.
  • STXR은 독점 상태가 유지된 경우에만 저장에 성공(W 결과=0)합니다. 해제되었으면 실패(W 결과=1)합니다.
  • 로컬 모니터(코어별)와 글로벌 모니터(시스템 레벨) 두 단계가 존재합니다.
  • 주의: LDXR과 STXR 사이에는 다른 메모리 접근을 최소화해야 합니다. 과도한 명령어는 독점 상태를 불필요하게 해제시킬 수 있습니다.
  • CLREX 명령으로 명시적으로 독점 상태를 해제할 수 있으며, 컨텍스트 스위치 시 커널이 이를 실행합니다.
Linux 커널 대체 패치 (Alternative Patching): 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 */
KPTI (Kernel Page Table Isolation) — TTBR0/TTBR1 전환: Meltdown 유사 공격을 방어하기 위해 Linux 커널은 유저 공간 복귀 시 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 필드)로 명령어 그룹을 결정합니다.

A64 고정 32-bit 명령어 인코딩 31 28 25 24 0 op0 [28:25] 명령어 그룹별 인코딩 필드 op0 값 명령어 그룹 x00x Reserved / Unallocated 100x Data Processing — Immediate 101x Branches, Exception, System x1x0 Loads and Stores x101 Data Processing — Register x111 Data Processing — SIMD & FP

인코딩 예제: ADD X0, X1, X2

ADD X0, X1, X2는 "Data Processing — Register" 그룹(op0=x101)에 속합니다. 구체적으로 Add (shifted register) 형식으로 인코딩됩니다.

ADD X0, X1, X2 인코딩 (Data Processing — Shifted Register) 31 30 29 28:24 23:22 21 20:16 15:10 9:5 4:0 sf op S 01011 shift Rm imm6 Rn Rd 1 0 0 01011 00 00010 000000 00001 00000 각 필드 해석: sf = 1 64-bit 연산 (X 레지스터). 0이면 32-bit (W 레지스터) op = 0 ADD 연산. 1이면 SUB S = 0 플래그 미갱신. 1이면 ADDS (NZCV 플래그 세트) 01011 고정 opcode (Data Processing — Register 그룹 식별) shift = 00 LSL (00=LSL, 01=LSR, 10=ASR). Rm에 적용할 시프트 타입 Rm = 00010 소스 레지스터 2 = X2 (레지스터 번호 2) imm6 = 000000 시프트 양 = 0 (시프트 없음). ADD X0, X1, X2, LSL #3이면 000011 Rn = 00001 소스 레지스터 1 = X1 (레지스터 번호 1) Rd = 00000 대상 레지스터 = X0 (레지스터 번호 0)
최종 바이너리 인코딩: 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를 방어합니다.

ARM64 보안 확장 (ARMv8.3+) PAC (Pointer Authentication) ARMv8.3 — ROP/JOP 방어 PACIA X30, SP → LR 상위 비트에 PAC 삽입 AUTIA X30, SP → PAC 검증, 실패 시 fault 키: APIAKey, APIBKey, APDAKey, APDBKey, APGAKey (EL1 관리) 커널: CONFIG_ARM64_PTR_AUTH 함수 프롤로그/에필로그 자동 삽입 QARMA/PACGA: 64→VA비트 해시 BTI (Branch Target ID) ARMv8.5 — JOP/COP 방어 BTI C → BL(call)의 타겟에만 허용 BTI J → BR(jump)의 타겟에만 허용 BTI JC → BL 또는 BR 모두 허용 SCTLR_EL1.BT1 활성화 커널: CONFIG_ARM64_BTI_KERNEL x86 CET-IBT의 ARM64 대응 MTE (Memory Tagging Ext) ARMv8.5 — UAF/오버플로 탐지 IRG X0, X0 → 랜덤 4비트 태그 생성 STG X0, [X0] → 메모리에 태그 저장 (16B 단위) LDR X1, [X0] → 포인터 태그 ≠ 메모리 태그 시 fault 포인터 bit 56-59에 4비트 태그 커널: CONFIG_ARM64_MTE KASAN HW 모드로 커널 메모리 검사 커널 사용 현황 비교 기능 PAC BTI MTE 대상 위협 ROP (리턴 주소 변조) JOP (간접 분기 하이재킹) UAF, 힙 오버플로 메커니즘 포인터 암호 서명 분기 타겟 표시 명령어 메모리 색상 태깅 성능 비용 ~1% (서명/검증 명령어) ~0% (NOP 크기 명령어) ~3-5% (태그 메모리 접근) 커널 설정 ARM64_PTR_AUTH ARM64_BTI_KERNEL ARM64_MTE + KASAN_HW_TAGS GCC 옵션 -mbranch-protection=pac-ret -mbranch-protection=bti -fsanitize=memtag-stack x86 대응 CET-SS (Shadow Stack) CET-IBT (endbr64) 없음 (ASAN 소프트웨어) 최소 ARMv8 v8.3 (Apple M1+) v8.5 (A78+, X1+) v8.5 (Pixel 8+, A715+)
/* 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 관리가 필수입니다.

ARM64 캐시 & TLB 관리 명령어 데이터 캐시 (DC) 명령어 DC CIVAC, Xt Clean+Invalidate by VA (PoC) DC CVAC, Xt Clean by VA (PoC) — 쓰기 반영 DC CVAU, Xt Clean by VA (PoU) — I$ 일관성 DC IVAC, Xt Invalidate by VA (PoC) DC ZVA, Xt Zero by VA — 캐시 라인 제로 초기화 PoC: Point of Coherency (모든 관찰자) PoU: Point of Unification (I$/D$ 통합점) DMA: dma_map_single() → DC CIVAC clear_page(): DC ZVA 사용 (빠른 제로화) 명령어 캐시 (IC) & TLB (TLBI) IC IALLU 전체 I$ 무효화 (Inner Share) IC IVAU, Xt VA 기반 I$ 무효화 (PoU) TLBI 명령어 TLBI VMALLE1 EL1 전체 TLB 무효화 TLBI VAE1, Xt VA+ASID 기반 무효화 TLBI ASIDE1, Xt ASID 기반 전체 무효화 TLBI VALE1IS, Xt 마지막 레벨+Inner Share IS 접미사: Inner Shareable (모든 코어) 캐시 관리 시퀀스 예시 DMA 버퍼 전송 (CPU→디바이스) 1. DC CVAC — 더티 데이터를 메모리로 flush 2. DSB SY — flush 완료 보장 3. DMA 전송 시작 코드 패칭 (alternatives) 1. 새 명령어 쓰기 (데이터 접근) 2. DC CVAU — D$ clean (PoU까지) 3. DSB ISH — 완료 대기 4. IC IVAU — I$ 무효화 5. DSB ISH + ISB — 파이프라인 동기화 페이지 테이블 수정 1. PTE 수정 (WRITE_ONCE) 2. DSB ISHST — store 완료 보장 3. TLBI VAE1IS — TLB 무효화 4. DSB ISH — TLBI 완료 대기 5. ISB — 파이프라인 flush (선택)
명령어대상동작커널 사용처
DC CIVAC데이터 캐시Clean + Invalidate by VADMA, 비일관 I/O
DC CVAC데이터 캐시Clean by VA (PoC)DMA CPU→디바이스
DC CVAU데이터 캐시Clean by VA (PoU)코드 패칭 전
DC ZVA데이터 캐시Zero by VAclear_page() 최적화
IC IVAU명령어 캐시Invalidate by VA (PoU)코드 패칭 후
IC IALLU명령어 캐시Invalidate All (Inner Share)모듈 로딩
TLBI VMALLE1ISTLBEL1 전체 무효화 (IS)flush_tlb_all()
TLBI VAE1ISTLBVA+ASID 기반 무효화 (IS)flush_tlb_page()
TLBI ASIDE1ISTLBASID 전체 무효화 (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 호환성의 기초입니다.

AAPCS64 레지스터 사용 규약 AAPCS64 레지스터 사용 규약 인자/결과 (Caller-saved) X0-X7 : 함수 인자 / 반환값 X0 : 첫 번째 인자 / 반환값 X1 : 두 번째 인자 / 128-bit 상위 X2-X7 : 3~8번째 인자 X8 : indirect result (struct) → 큰 struct 반환 시 메모리 포인터 → 리눅스 syscall에서 syscall 번호 임시 (Caller-saved) X9-X15 : Temporary 레지스터 X16 (IP0): Intra-procedure call X17 (IP1): Intra-procedure call → 링커 veneer / PLT 사용 → 호출자가 보존 불필요 X18 : Platform Register → Linux 커널: Shadow Call Stack 보존 (Callee-saved) X19-X28 : Callee-saved → 호출된 함수가 반드시 보존 → STP/LDP로 프롤로그/에필로그 X29 (FP): Frame Pointer X30 (LR): Link Register → BL 명령이 자동 설정 특수 레지스터 SP : Stack Pointer → 16바이트 정렬 필수 XZR : Zero Register PC : Program Counter → 직접 접근 불가 (ADR 사용) NZCV : 조건 플래그 → CMP/ADDS 등이 설정 AAPCS64 스택 프레임 레이아웃 (높은 주소 → 낮은 주소) 9번째 이상 인자 (스택 전달) ← 호출자 SP X29(FP), X30(LR) 저장 ← 새 FP 가리킴 Callee-saved (X19-X28) 로컬 변수 호출할 함수 인자 (9번째~) ← 현재 SP FP chain * SP는 항상 16바이트 정렬 | FP chain으로 스택 트레이스 가능 | 8개 초과 인자만 스택 전달

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에 저장 */
커널에서의 차이: Linux 커널은 가변 인자 함수를 printk(), pr_info() 등에서 사용합니다. 커널 빌드 시 -mgeneral-regs-only 옵션으로 SIMD/FP 레지스터 사용을 금지하므로, __vr_top/__vr_offs는 커널 내부에서 사용되지 않습니다. X18은 CONFIG_SHADOW_CALL_STACK 활성화 시 Shadow Call Stack 포인터로 예약되어 일반 용도로 사용할 수 없습니다.
레지스터AAPCS64 역할Caller/CalleeLinux 커널 용도
X0-X7인자 전달 / 반환값Caller-savedsyscall: X0-X5 인자, X0 반환
X8Indirect result locationCaller-savedsyscall 번호 (w8)
X9-X15TemporaryCaller-saved임시 계산
X16 (IP0)Intra-procedure scratchCaller-savedPLT, veneer
X17 (IP1)Intra-procedure scratchCaller-savedPLT, veneer
X18Platform registerCaller-savedShadow Call Stack 포인터
X19-X28Callee-savedCallee-saved함수 간 보존 변수
X29 (FP)Frame PointerCallee-saved스택 트레이스 필수
X30 (LR)Link RegisterCallee-savedBL 반환 주소, PAC 서명 대상

EL0-EL3 전환

ARM64의 예외 레벨(Exception Level) 전환은 하드웨어가 자동으로 레지스터를 뱅킹하고 상태를 보존하는 정교한 메커니즘입니다. 각 EL은 독립적인 시스템 레지스터 세트를 가지며, 예외 진입/복귀 시 하드웨어와 소프트웨어가 협력하여 컨텍스트를 관리합니다.

ARM64 예외 레벨 계층과 레지스터 뱅킹 예외 레벨 전환과 레지스터 뱅킹 EL3 — Secure Monitor SPSR_EL3, ELR_EL3, ESR_EL3 SP_EL3, VBAR_EL3, SCR_EL3 SCTLR_EL3, TCR_EL3, TTBR0_EL3 EL2 — Hypervisor SPSR_EL2, ELR_EL2, ESR_EL2 SP_EL2, VBAR_EL2, HCR_EL2 VTTBR_EL2, VMPIDR_EL2 EL1 — OS Kernel SPSR_EL1, ELR_EL1, ESR_EL1 SP_EL1, VBAR_EL1, SCTLR_EL1 TTBR0_EL1, TTBR1_EL1, TCR_EL1 EL0 — User Application SP_EL0, TPIDR_EL0 (TLS base) 예외 진입 시 하드웨어 동작 1. SPSR_ELn ← PSTATE 저장 2. ELR_ELn ← 복귀 주소 저장 3. ESR_ELn ← 예외 원인 코드 4. PSTATE.DAIF ← 마스크 설정 5. PSTATE.EL ← 대상 EL 6. PC ← VBAR_ELn + offset ERET 복귀 시 하드웨어 동작 1. PC ← ELR_ELn 2. PSTATE ← SPSR_ELn 3. SP 전환 (대상 EL의 SP) VBAR_ELn 오프셋 +0x000: Current EL, SP0 (Sync/IRQ/FIQ/SError) +0x200: Current EL, SPx (Sync/IRQ/FIQ/SError) +0x400: Lower EL, AArch64 (Sync/IRQ/FIQ/SError) +0x600: Lower EL, AArch32 (Sync/IRQ/FIQ/SError) SVC HVC 핵심 포인트: • SPSR_ELn: 이전 PSTATE 전체 (NZCV, DAIF, EL, nRW, SP 선택 비트) 자동 저장 • ELR_ELn: Sync 예외는 트리거 명령어 주소, Async는 다음 실행할 명령어 주소 • ESR_ELn[31:26] EC 필드: 예외 클래스 (SVC=0x15, Data Abort=0x25, Inst Abort=0x21 등) • 범용 레지스터(X0-X30)는 뱅킹 없음 → 소프트웨어가 직접 저장/복원 (kernel_entry 매크로) 예외 진입/복귀 흐름 SVC #0 예외 진입 → 시스템 콜 처리 → ERET 복귀 흐름 EL0 유저 SVC #0 실행 HW 자동 저장 SPSR/ELR/ESR 벡터 테이블 VBAR+0x400 kernel_entry X0-X30 → 스택 el0_svc C 핸들러 호출 sys_call_table X8 → 핸들러 디스패치 ret_to_user 시그널/스케줄 체크 kernel_exit 스택 → X0-X30 ERET → EL0 유저 복귀 ESR_EL1 비트 레이아웃 (예외 원인 식별) [31:26] EC [25] IL [24:0] ISS (명령어 특정 정보) EC=0x15: SVC (AArch64) | EC=0x18: MSR/MRS trap | EC=0x20: Inst Abort (lower EL) EC=0x21: Inst Abort (same EL) | EC=0x24: Data Abort (lower EL) | EC=0x25: Data Abort (same 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)
cpu_switch_to의 핵심: ARM64 컨텍스트 스위치는 callee-saved 레지스터(X19-X28, FP, LR, SP)만 저장/복원합니다. caller-saved 레지스터(X0-X15)는 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
USER() 매크로: 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 재배치가 모두 가능하므로 명시적인 메모리 배리어가 필수적입니다.

ARM64 메모리 타입과 순서 모델 ARM64 메모리 타입과 순서 모델 Normal Memory (캐시 가능) Write-Back (WB) 기본 DRAM 매핑 Inner/Outer Cacheable Write-Through (WT) 즉시 메모리 반영 캐시도 업데이트 Non-Cacheable (NC) — DMA 버퍼, 비일관 I/O 재배치 허용: Load-Load, Load-Store, Store-Store, Store-Load → 모든 종류의 재배치 가능 (약순서 모델) Device Memory (캐시 불가) nGnRnE — 가장 강한 순서 nGnRE — 일반 MMIO nGRE — Gathering 허용 GRE — 가장 약한 순서 nG: non-Gathering | nR: non-Reordering | nE: non-Early write ack MMIO 레지스터 접근: nGnRnE(PCI) 또는 nGnRE(일반) → Device 메모리 간에는 순서 보장, Normal과는 배리어 필요 메모리 배리어 비교 (ARM64 vs Linux Kernel) ARM64 명령어 효과 Linux 매핑 DMB ISH Data Memory Barrier (Inner Shareable) smp_mb() — 전체 배리어 DMB ISHST Store-Store 배리어 smp_wmb() — 쓰기 배리어 DMB ISHLD Load-Load + Load-Store 배리어 smp_rmb() — 읽기 배리어 DSB ISH Data Sync Barrier (이전 접근 완료 보장) mb() — 비SMP에서도 유효 ISB Instruction Sync Barrier (파이프라인 flush) 코드 패칭, 시스템 레지스터 변경 후 LDAR / STLR Load-Acquire / Store-Release (단방향) smp_load_acquire() / smp_store_release()
/* 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 */
ARM64 vs x86 메모리 모델 차이:
  • 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)를 지원합니다.

ARM64 4K granule 4-레벨 페이지 테이블 워크 4K Granule — 48-bit VA 4-레벨 페이지 테이블 워크 가상 주소 비트 분할 (48-bit VA, 4K granule) [63:48] 부호 [47:39] PGD (L0) 9비트 [38:30] PUD (L1) 9비트 [29:21] PMD (L2) 9비트 [20:12] PTE (L3) 9비트 [11:0] 페이지 오프셋 12비트 PGD (L0) 512 엔트리 TTBR0/1_EL1 → 512GB/엔트리 PUD (L1) 512 엔트리 1GB 블록 가능 → 1GB/엔트리 PMD (L2) 512 엔트리 2MB 블록 가능 → 2MB/엔트리 PTE (L3) 512 엔트리 4KB 페이지 → 4KB/엔트리 물리 페이지 Granule별 페이지 테이블 구성 비교 Granule 페이지 크기 레벨 수 (48-bit) 인덱스 비트 블록 매핑 4KB 4,096 bytes 4 (PGD→PUD→PMD→PTE) 9+9+9+9+12 2MB (L2), 1GB (L1) 16KB 16,384 bytes 4 (PGD→PUD→PMD→PTE) 1+11+11+11+14 32MB (L2) 64KB 65,536 bytes 3 (PGD→PMD→PTE) 6+13+13+16 512MB (L2) ARM64 페이지 테이블 엔트리 (PTE) 비트 레이아웃 [63:52] UXN,PXN,sw [51:12] 출력 주소 (OA) — 물리 페이지 프레임 번호 [11:2] 속성/nG/AF/SH [1:0] Valid/Table [0] Valid | [1] Table/Block | [6:2] AttrIndx[2:0]+NS+AP[2:1] | [7] AF(Access Flag) [9:8] SH(Shareability) | [10] nG(not Global) | [11] DBM(Dirty Bit) | [53] PXN | [54] UXN/XN Linux 사용: [55] SW dirty | [56] SW young | [57] SW write | [58] SW special
/* 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 vs TTBR1: ARM64는 두 개의 독립적인 페이지 테이블 베이스 레지스터를 제공합니다. 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 타일 레지스터와 FMOPA 연산 SME: ZA 타일 레지스터와 FMOPA 외적 연산 ZA 타일 레지스터 (SVL x SVL bits) ← ZA 행 0 ← ZA 행 1 SVL=128일 때: 4x4 FP32 | SVL=512일 때: 16x16 FP32 ZA0.S~ZA3.S (타일 슬라이스) FMOPA ZA0.S, P0/M, P1/M, Z0.S, Z1.S a0 a1 a2 a3 Z0 (열) b0 b1 b2 b3 ZA[i][j] += Z0[i]*Z1[j] a0*b0 a0*b1 a0*b2 a0*b3 a1*b0 a1*b1 a1*b2 a1*b3 a2*b0 a2*b1 a2*b2 a2*b3 a3*b0 a3*b1 a3*b2 a3*b3 외적(Outer Product) + 누적 SME PSTATE 상태 관리 Non-Streaming SM=0, ZA=0 (기본) SMSTART Streaming SVE SM=1, ZA=1 SMSTOP SM ZA만 활성 SM=0, ZA=1 SM(Streaming Mode): SVE 명령어가 Streaming SVE VL 사용 | ZA: 타일 레지스터 활성 SMSTART = SM+ZA 활성 | SMSTOP SM = SM만 비활성 (ZA 유지) | SMSTOP = 전체 비활성 주의: SMSTART/SMSTOP 시 Z/P 레지스터 내용이 0으로 초기화됨
/* 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) 로 유저 공간에서 설정 */
SME vs SVE: SVE는 벡터(1차원) 연산 확장이고, SME는 행렬(2차원) 연산 확장입니다. SME의 ZA 레지스터 크기는 SVL(Streaming Vector Length)에 의해 결정되며, SVE의 VL과 독립적입니다. SME2(ARMv9.4)는 다중 벡터 명령어와 ZT0(512-bit 룩업 테이블 레지스터)를 추가하여 양자화된 AI 모델 추론을 더욱 가속합니다.

GIC 시스템 레지스터

GICv3/v4에서 CPU 인터페이스(ICC_*) 접근은 메모리 매핑 MMIO 대신 시스템 레지스터를 통해 이루어집니다. 이는 인터럽트 ACK/EOI 경로의 지연(Latency)을 크게 줄여주며, Linux 커널의 drivers/irqchip/irq-gic-v3.c에서 직접 사용됩니다.

시스템 레지스터용도커널 사용 경로
ICC_IAR1_EL1Interrupt Acknowledge (Group 1)인터럽트 ACK → INTID 반환
ICC_EOIR1_EL1End of Interrupt (Group 1)EOI → 우선순위(Priority) 드롭 + 비활성화
ICC_PMR_EL1Priority Mask Register인터럽트 우선순위 마스킹
ICC_SRE_EL1System Register Enable시스템 레지스터 모드 활성화
ICC_CTLR_EL1Control RegisterEOI 모드, CBPR 설정
ICC_IGRPEN1_EL1Group 1 EnableGroup 1 인터럽트 활성화
ICC_SGI1R_EL1SGI Generation (Group 1)IPI 전송
ICC_DIR_EL1Deactivate InterruptEOImode=1 시 별도 비활성화
ICC_RPR_EL1Running Priority현재 실행 중 인터럽트 우선순위
ICC_BPR1_EL1Binary 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
GICv3 vs GICv2: GICv2는 CPU 인터페이스가 MMIO(GICC_*)로 접근했지만, GICv3는 시스템 레지스터(ICC_*)로 전환되어 접근 지연이 크게 줄었습니다. GICv4는 vPE(virtual PE) 직접 주입을 지원하여 가상 인터럽트가 VM exit 없이 vCPU에 전달됩니다. Linux 커널은 CONFIG_ARM_GIC_V3로 GICv3 드라이버를 활성화하며, GICv4.1은 SGI 직접 주입도 지원합니다.

alternatives 패칭

ARM64 Linux 커널은 부팅 시 CPU 기능(capabilities)을 탐지하고, alternatives 프레임워크를 통해 명령어를 동적으로 패칭합니다. 이를 통해 단일 커널 이미지로 다양한 하드웨어에서 최적 성능을 발휘하며, 보안 완화(erratum workaround)도 적용합니다.

ARM64 alternatives 패칭 흐름 alternatives 패칭 흐름 커널 부팅 start_kernel() CPU 기능 탐지 cpufeature.c: 레지스터 읽기 apply_alternatives 명령어 패칭 수행 캐시/TLB 동기화 DC CVAU + IC IVAU + ISB 패칭 전 (기본 코드) LDXR w0, [x1] /* LL/SC 원자적 연산 */ ADD w0, w0, w2 STXR w3, w0, [x1] /* 실패 시 재시도 */ LSE 지원 패칭 후 (최적화 코드) LDADD w2, w0, [x1] /* 단일 원자적 명령어 */ NOP /* 패딩 */ NOP /* 패딩 */ 주요 alternatives 패칭 대상 ARMv8.1-LSE: LDXR/STXR → LDADD/STADD/CAS/SWP (원자적 단일 명령어) ARMv8.3-PAuth: NOP → PACIASP/AUTIASP (Return Address Signing) Erratum workaround: NOP → 특정 시퀀스 삽입 (Cortex-A53 #843419, Neoverse-N1 #1542419 등) Spectre-BHB: 분기 전 NOP → CLEARBHB 또는 BHB 클리어 루프
/* 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 ISHISB 시퀀스를 실행해야 합니다. 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 필수 */
ARM64 ftrace 컴파일 옵션:
  • -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이 관리하는 상위 주소 영역에 매핑됩니다.

ARM64 커널 가상 주소 레이아웃 ARM64 커널 가상 주소 공간 레이아웃 (48-bit VA) TTBR1_EL1 (커널 공간) 0xFFFF_FFFF_FFFF_FFFF ← Fixmap (최상위) PCI I/O 공간 (16MB) vmemmap (struct page 배열) vmalloc 영역 (동적 매핑) vmalloc, vmap, ioremap 모듈 영역 (128MB) 커널 .text (코드) 커널 .rodata (읽기전용 데이터) 커널 .data (초기화된 데이터) 커널 .bss (제로 초기화 데이터) linear mapping (물리 메모리 전체 1:1) 0xFFFF_0000_0000_0000 ← 커널 시작 비매핑 영역 (주소 홀) 0x0000_FFFF_FFFF_FFFF ← 유저 최대 ARM64 커널 이미지 헤더 (64바이트) 오프셋 0x00: code0 — 분기 명령어 (MZ 또는 B) 오프셋 0x04: code1 — 예약 오프셋 0x08: text_offset — 로드 오프셋 오프셋 0x10: image_size — 이미지 크기 오프셋 0x18: flags — 엔디언, 페이지크기, 물리배치 오프셋 0x20: res2 — 예약 오프셋 0x28: res3 — 예약 오프셋 0x30: res4 — 예약 오프셋 0x38: magic — "ARM\x64" (0x644d5241) 오프셋 0x3C: res5 — PE 헤더 오프셋 (UEFI) KASLR (Kernel ASLR) 동작 1. 부트로더/EFI stub이 난수 생성 (RNDR 명령어 또는 EFI_RNG_PROTOCOL) 2. 커널 물리 배치: 2MB 정렬 랜덤 오프셋 (물리 메모리 내 위치 무작위화) 3. 커널 가상 배치: TTBR1 매핑 랜덤화 (가상 주소 오프셋 무작위화) 4. 모듈 영역: 별도 랜덤 오프셋 (커널과 모듈 간 상대 주소 무작위화) CONFIG_RANDOMIZE_BASE 활성화 필요 nokaslr 커널 파라미터로 비활성화 가능 엔트로피 소스: EFI RNG, RNDR(ARMv8.5), DTB /chosen/kaslr-seed, 카운터 기반 fallback
/* 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);
}
UEFI 부팅: ARM64 커널 이미지는 PE/COFF 헤더를 포함하여 UEFI 펌웨어(Firmware)가 직접 로드할 수 있습니다. 이 경우 _head의 첫 2바이트가 "MZ"(PE 매직)으로 인코딩되며, EFI stub(drivers/firmware/efi/libstub/)이 먼저 실행됩니다. EFI stub은 메모리 맵(Memory Map) 획득, initrd 로딩, KASLR 시드 생성 등을 수행한 후 실제 커널 진입점(Entry Point)으로 분기합니다.

참고 링크

다음 학습: