ARM64(AArch64)에서 inline asm은 “문법이 어렵다”보다 컴파일러가 더 엄격하고 최적화가 더 공격적이라서 사고가 난다.
특히 벤더 커널을 Clang으로 옮길 때는 constraint 선택 + clobber + barrier 3개가 대부분의 원인이다.
이 글에서는 ARM64에서 inline asm이 실제로 어떻게 깨지고, 어떻게 안전하게 쓰는지를 정리한다.
0) AArch64 레지스터/상태를 먼저 고정하기
- GPR:
x0 ~ x30(64-bit),w0 ~ w30(하위 32-bit) x31은 “인코딩 상”SP또는XZR로 해석됨(문맥에 따라 다름)- flags:
NZCV(커널 asm에서 조건 플래그 영향이 있으면"cc"clobber 고려)
flowchart LR C[C 코드 변수] -->|constraint| A[Reg Allocator] A --> R[x0..x30 / v0..v31] R -->|template| S[Inline ASM] S -->|outputs| C2[C 변수 갱신] S -->|side effects| M[memory/cc/system state]
핵심: 컴파일러는 asm 텍스트를 “의미를 모르는 블랙박스”로 취급한다.
의미를 아는 건 constraint + clobber뿐이다.
1) ARM64에서 많이 쓰는 constraint 정리 (심화)
1-1. "r": 일반 레지스터 (GPR)
register unsigned long x;
asm volatile("add %0, %0, #1" : "+r"(x));"r"는 보통xN으로 할당된다.- 32-bit 연산이면
wN사용(asm 템플릿에서%w0같은 형태로 지정하는 패턴이 흔함)
커널 스타일 팁: 32-bit로 자르고 싶으면
%w0/%w1를 명시하는 쪽이 안전하다.
1-2. "w": SIMD/FP 레지스터 (NEON v0..v31)
u64 val;
asm volatile(
"fmov %d0, %1\n"
: "=&w"(/* v-reg */) // 예시는 개념용
: "r"(val)
);"w"는 GPR이 아니라 벡터/FP 레지스터로 간다.- 커널에서 FPSIMD를 건드리면 “컨텍스트 저장/복구”와 엮이므로, 일반 코드에서 남용하면 위험하다.
1-3. "I" / "J" / "K" 류: 즉시값(immediate)
ARM64는 즉시값이 “아무 값이나” 되는 게 아니라 인코딩 제약이 있다.
그래서 커널 코드에는 보통 __builtin_constant_p() + 분기 또는 asm 매크로로 제약을 조절한다.
#define my_add_imm(x, imm) \
do { \
if (__builtin_constant_p(imm)) \
asm volatile("add %0, %0, %1" : "+r"(x) : "I"(imm)); \
else \
asm volatile("add %0, %0, %1" : "+r"(x) : "r"(imm)); \
} while (0)- 상수라고 해도
"I"가 받는 범위가 좁으면 깨진다. - 이 패턴은 GCC에서도 유효하지만 Clang에서 더 자주 터진다(즉시값 제약을 더 엄격히 적용하는 편).
1-4. "m" / "Q": 메모리 operand
"m": “메모리”를 의미하지만, 주소형태 제약이 느슨할 수 있다."Q": ARM 계열에서 “단순 베이스 레지스터 주소 형태”를 요구하는 경우가 많다(컴파일러/버전에 따라 의미가 다소 다름).
커널에서는 메모리 operand만으로 순서 보장되지 않는다.
필요하면 "memory" clobber가 같이 가야 한다.
u64 *p = ...;
u64 v;
asm volatile("ldr %0, %1"
: "=r"(v)
: "m"(*p)
: "memory"); // 순서가 중요하면 고려2) %0 vs %x0 vs %w0 (ARM64에서 제일 자주 헷갈리는 부분)
AArch64 inline asm에서는 같은 operand라도 어떤 폭으로 출력할지를 지정할 수 있다.
%x0: 64-bit 레지스터 이름 사용(예:x3)%w0: 32-bit 레지스터 이름 사용(예:w3)
예: 32-bit add를 확실히 하고 싶다면
u32 x;
asm volatile("add %w0, %w0, #1" : "+r"(x));이렇게 쓰는 편이 버그가 적다.
3) tied operand는 +r로 정리하는 게 ARM64에서도 정답
GCC 벤더 코드에 흔한 형태:
asm volatile("eor %0, %0, %1"
: "=r"(x)
: "0"(x), "r"(y));ARM64/Clang 포팅에서는 보통 아래가 더 안전하다:
asm volatile("eor %0, %0, %1"
: "+r"(x)
: "r"(y));"+r"는 read/write를 한 operand로 모델링- 레지스터 묶기(
"0") 실수를 줄임
4) early-clobber (=&r)가 필요한 ARM64 패턴들
ARM64에서는 특히 ldxr/stxr 루프, 멀티 출력, 출력이 입력보다 먼저 덮이는 템플릿에서 early-clobber 누락이 잦다.
4-1) ldxr/stxr 기반 원자적 업데이트 (대표 케이스)
static inline u64 atomic_add_return(u64 i, u64 *p)
{
u64 old, new;
u32 tmp;
asm volatile(
"1: ldxr %0, [%3]\n"
" add %1, %0, %4\n"
" stxr %w2, %1, [%3]\n"
" cbnz %w2, 1b\n"
: "=&r"(old), "=&r"(new), "=&r"(tmp) // early-clobber 자주 필요
: "r"(p), "r"(i)
: "memory");
return new;
}포인트:
%w2처럼 tmp는 32-bit 결과로 쓰는 경우가 많다."memory"없으면 컴파일러가 주변 메모리 접근을 재배치할 수 있다(원자 루프의 의미가 깨질 수 있음).=&r는 “입력과 같은 레지스터 쓰지 마라”를 강제해 Clang의 엄격한 레지스터 모델과 충돌을 피한다.
실전 팁: 이 류는 커널이 이미 검증된 매크로/헬퍼(arch/arm64의 atomic 구현)를 쓰는 편이 낫다. 벤더가 커스텀으로 짠 코드는 Clang에서 잘 깨진다.
5) clobber 심화: "memory"와 "cc"의 정확한 의미
5-1) "memory" clobber
- 컴파일러에게 “이 asm은 메모리에 side-effect가 있다”고 알림
- 결과: 컴파일러가 asm 전후의 메모리 접근을 이동하지 못하게 된다(완전한 barrier에 가까운 효과)
하지만 주의:
"memory"는 **CPU barrier(dmb)**가 아니다.- 즉, 컴파일러 재배치만 막는다. CPU 순서 보장은
dmb ish같은 명령으로 따로 해야 한다.
5-2) "cc" clobber (ARM64에서는 NZCV)
조건 플래그를 바꾸는 asm이라면 "cc"를 넣는다.
asm volatile("subs %0, %0, #1"
: "+r"(x)
:
: "cc");ARM64에서 subs, adds, cmp 같은 건 NZCV에 영향이 있다.
다만 커널 코드에서 플래그를 C 코드가 직접 의존하는 형태가 많지 않아서(대부분 asm 내부에서 분기) "cc"가 빠져도 우연히 동작하는 경우가 있는데, Clang 최적화에서 발목 잡힐 수 있다.
6) barrier(컴파일러/CPU) 구분을 확실히 하기
6-1) 컴파일러 barrier
asm volatile("" ::: "memory");- 명령은 없고, 컴파일러 재배치만 막음
6-2) CPU barrier (ARM64 dmb)
asm volatile("dmb ish" ::: "memory");- CPU의 메모리 순서까지 보장(아키텍처 의미)
"memory"clobber를 같이 넣어야 “컴파일러 재배치 + CPU barrier”가 함께 성립
7) system register 접근 (mrs/msr)에서 자주 깨지는 지점
예: cntvct_el0 읽기
static inline u64 read_cntvct(void)
{
u64 v;
asm volatile("mrs %0, cntvct_el0" : "=r"(v));
return v;
}문제 패턴:
- 결과를 읽었는데 주변 코드가 재배치되면 의미가 바뀌는 경우
- 타이밍/측정 목적이면
"memory"를 넣어 “측정 전후 코드 이동”을 막고 싶을 때가 있다(용도에 따라 다름)
asm volatile("mrs %0, cntvct_el0" : "=r"(v) :: "memory");커널엔 이미
read_sysreg()같은 매크로가 있으니, 벤더 코드가 직접 박아 쓴 asm은 그걸로 치환하는 게 포팅 안정성에 좋다.
8) Clang + IAS(통합 어셈블러)에서만 터지는 ARM64 패턴
벤더 .S 또는 inline asm에 아래가 섞여 있으면 IAS가 민감하게 반응한다.
- GNU as 특화 directive
.arch_extension,.cpu,.fpu처리.inst사용 방식 차이- 매크로/리로케이션 표현 차이
대응 전략:
- 원인 분리: 해당 파일만 따로
clang -c로 재현 - 임시 회피:
make LLVM_IAS=0(외부 GNU as 사용) - 근본 해결: directive/매크로를 “IAS 친화적”으로 수정
9) 디버깅/검증 루틴 (커널 포팅 실전)
9-1) 해당 TU만 asm 확인
clang -S -O2 -target aarch64-linux-gnu -o test.s test.c
9-2) 컴파일러가 뭘 넣었는지 더 보기
-fverbose-asm(가능한 환경에서)objdump -drwC또는llvm-objdump -drwC로 최종 오브젝트 확인
9-3) “asm이 맞는데 동작이 이상하다”면
체크 순서:
"memory"clobber 필요한데 빠졌나?- CPU barrier(dmb/dsb/isb) 필요한데 빠졌나?
+r로 충분한데"0"tied operand로 꼬였나?- early-clobber 필요(
=&r)인데=r로 써서 레지스터 겹쳤나? %w0/%x0폭 지정이 맞나?
10) ARM64 inline asm 작성 체크리스트 (Clang 포팅용)
- 같은 변수 input/output이면
"+r"를 우선 사용 - 출력이 입력보다 먼저 덮이면
"=&r"고려 - 메모리 의미가 있으면
"memory"clobber 검토 - CPU 순서 보장이면
dmb ish등 barrier 명령 +"memory" - flags를 건드리면
"cc"고려 - 레지스터 폭은
%wN/%xN로 의도를 명시 - 즉시값은
"I"같은 constraint 범위 확인, 아니면"r"로 폴백
참고:
- Linux kernel 문서: Clang/LLVM 빌드
https://docs.kernel.org/kbuild/llvm.html - LLVM 문서: Inline Assembly
https://llvm.org/docs/InlineAsm.html - 커널(arch/arm64) atomic/asm 구현은 실제 “검증된 정답” 참고용으로 매우 좋다(벤더 커널이라도 해당 디렉터리 비교 추천)