인라인 어셈블리
인라인 어셈블리 (Inline assembly)
성능이 정말 중요한 코드에서는 어셈블리를 직접 손으로 써야 할 때가 있어요. Rust는 그럴 때 asm!, naked_asm!, global_asm! 매크로로 인라인 어셈블리(inline assembly) 를 지원해요. 컴파일러가 만든 어셈블리 출력에 손으로 쓴 어셈블리를 끼워 넣을 수 있게 해 주죠.
출처: Rust Reference
본문
지원 아키텍처 (Stable targets)
인라인 어셈블리는 다음 아키텍처에서 stable로 지원돼요.
- x86 및 x86-64
- ARM
- AArch64 및 Arm64EC
- RISC-V
- LoongArch
- s390x
- PowerPC 및 PowerPC64
지원되지 않는 타깃에서 어셈블리 매크로를 쓰면 컴파일러가 오류를 내요.
예시 (Example)
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
use std::arch::asm;
// Multiply x by 6 using shifts and adds
let mut x: u64 = 4;
unsafe {
asm!(
"mov {tmp}, {x}",
"shl {tmp}, 1",
"shl {x}, 2",
"add {x}, {tmp}",
x = inout(reg) x,
tmp = out(reg) _,
);
}
assert_eq!(x, 4 * 6);
}
}
문법 (Syntax)
다음 문법은 asm!, global_asm!, naked_asm! 매크로에 전달할 수 있는 인자를 지정해요.
AsmArgs → AsmAttrFormatString ( , AsmAttrFormatString )* ( , AsmAttrOperand )* ,?
FormatString → STRING_LITERAL | RAW_STRING_LITERAL | MacroInvocation
AsmAttrFormatString → ( OuterAttribute )* FormatString
AsmOperand → ClobberAbi
| AsmOptions
| RegOperand
AsmAttrOperand → ( OuterAttribute )* AsmOperand
ClobberAbi → clobber_abi ( Abi ( , Abi )* ,? )
AsmOptions → options ( ( AsmOption ( , AsmOption )* ,? )? )
AsmOption → pure | nomem | readonly | preserves_flags
| noreturn | nostack | att_syntax | raw
RegOperand → ( ParamName = )?
(
DirSpec ( RegSpec ) Expression
| DualDirSpec ( RegSpec ) DualDirSpecExpression
| sym PathExpression
| const Expression
| label { Statements? }
)
ParamName → IDENTIFIER_OR_KEYWORD | RAW_IDENTIFIER
DualDirSpecExpression → Expression
| Expression => Expression
RegSpec → RegisterClass | ExplicitRegister
RegisterClass → IDENTIFIER_OR_KEYWORD
ExplicitRegister → STRING_LITERAL
DirSpec → in | out | lateout
DualDirSpec → inout | inlateout
범위 (Scope)
인라인 어셈블리는 세 가지 방식으로 쓰일 수 있어요.
asm! 매크로: 어셈블리 코드가 함수 스코프에서 출력되어 함수의 컴파일러 생성 어셈블리 코드에 통합돼요. 이 어셈블리 코드는 undefined behavior를 피하기 위해 엄격한 규칙을 따라야 해요. 어떤 경우에는 컴파일러가 어셈블리 코드를 별도의 함수로 출력하고 호출을 생성하기도 해요.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
unsafe { core::arch::asm!("/* {} */", in(reg) 0); }
}
}
naked_asm! 매크로: 어셈블리 코드가 함수 스코프에서 출력되고 함수의 전체 어셈블리 코드를 구성해요. naked_asm! 매크로는 naked 함수에서만 허용돼요.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
#[unsafe(naked)]
extern "C" fn wrapper() {
core::arch::naked_asm!("/* {} */", const 0);
}
}
}
global_asm! 매크로: 어셈블리 코드가 함수 밖, 전역 스코프에서 출력돼요. 어셈블리 코드로 전체 함수를 손으로 쓰는 데 쓸 수 있고, 일반적으로 임의의 레지스터와 어셈블러 지시문을 훨씬 자유롭게 쓸 수 있어요.
fn main() {}
#[cfg(target_arch = "x86_64")]
core::arch::global_asm!("/* {} */", const 0);
템플릿 문자열 인자 (Template string arguments)
어셈블러 템플릿은 format 문자열과 같은 문법을 써요. 즉 자리 표시자(placeholder)는 중괄호로 지정해요.
대응하는 인자는 순서대로, 인덱스로, 또는 이름으로 접근돼요.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i64;
let y: i64;
let z: i64;
// This
unsafe { core::arch::asm!("mov {}, {}", out(reg) x, in(reg) 5); }
// ... this
unsafe { core::arch::asm!("mov {0}, {1}", out(reg) y, in(reg) 5); }
// ... and this
unsafe { core::arch::asm!("mov {out}, {in}", out = out(reg) z, in = in(reg) 5); }
// all have the same behavior
assert_eq!(x, y);
assert_eq!(y, z);
}
}
다만 RFC #2795에서 도입된 암묵적 이름 인자(implicit named arguments) 는 지원되지 않아요.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x = 5;
// We can't refer to `x` from the scope directly, we need an operand like `in(reg) x`
unsafe { core::arch::asm!("/* {x} */"); } // ERROR: no argument named x
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}
asm! 호출은 하나 이상의 템플릿 문자열 인자를 가질 수 있어요. 여러 템플릿 문자열 인자를 가진 asm! 은 모든 문자열이 \n 으로 연결된 것처럼 취급돼요. 일반적으로 각 템플릿 문자열 인자가 어셈블리 코드 한 줄에 대응하도록 쓰는 게 기대된 사용법이에요.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i64;
let y: i64;
// We can separate multiple strings as if they were written together
unsafe { core::arch::asm!("mov eax, 5", "mov ecx, eax", out("rax") x, out("rcx") y); }
assert_eq!(x, y);
}
}
모든 템플릿 문자열 인자는 다른 인자보다 먼저 나타나야 해요. 또 format 문자열처럼 위치 인자(positional argument)는 이름 인자와 명시적 레지스터 피연산자보다 먼저 나타나야 해요.
#![allow(unused)]
fn main() {
let x = 5;
#[cfg(target_arch = "x86_64")] {
// The template strings need to appear first in the asm invocation
unsafe { core::arch::asm!("/* {x} */", x = const 5, "ud2"); } // ERROR: unexpected token
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}
명시적 레지스터 피연산자는 템플릿 문자열의 자리 표시자로 쓸 수 없어요. 그리고 다른 모든 이름·위치 피연산자는 템플릿 문자열에 적어도 한 번 나타나야 해요. 그렇지 않으면 컴파일 오류가 나요.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// Explicit register operands don't get substituted, use `eax` explicitly in the string
unsafe { core::arch::asm!("/* {} */", in("eax") 5); }
// ERROR: invalid reference to argument at index 0
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}
정확한 어셈블리 코드 문법은 타깃별이고, 컴파일러에게는 피연산자가 템플릿 문자열에 치환되어 어셈블러에 전달되는 코드가 되는 방식 외에는 불투명(opaque) 해요.
현재 모든 지원 타깃은 LLVM 내부 어셈블러의 어셈블리 코드 문법을 따르는데, 이는 보통 GNU 어셈블러(GAS)의 문법과 일치해요. x86에서는 기본적으로 GAS의 .intel_syntax noprefix 모드가, ARM에서는 .syntax unified 모드가 쓰여요. 이 타깃들은 어셈블리 코드에 추가 제약을 두는데, .section 같은 지시문으로 바꿀 수 있는 현재 어셈블러 상태는 asm 문자열 끝에서 원래 값으로 복원되어야 해요. GAS 문법에 맞지 않는 어셈블리 코드는 어셈블러별 동작을 만들 수 있어요.
속성 (Attributes)
인라인 어셈블리 템플릿 문자열과 피연산자에는 의미적으로 cfg와 cfg_attr 속성만 받아들여져요. 다른 속성은 파싱되지만 어셈블리 매크로가 확장되면 거부돼요.
fn main() {}
#[cfg(target_arch = "x86_64")]
core::arch::global_asm!(
#[cfg(not(panic = "abort"))]
".cfi_startproc",
// ...
"ret",
#[cfg(not(panic = "abort"))]
".cfi_endproc",
);
참고:
rustc에서 어셈블리 매크로는 언어에서 비슷한 속성을 다루는 일반 시스템과 분리된 방식으로 이런 속성 처리를 구현해요. 그래서 지원되는 속성 종류가 제한적이고, 동작에 미묘한 차이가 있을 수 있어요.
구문적으로 첫 피연산자 앞에 템플릿 문자열이 적어도 하나 있어야 해요.
피연산자 타입 (Operand type)
여러 종류의 피연산자가 지원돼요.
in(<reg>) <expr>
<reg>는 레지스터 클래스 또는 명시적 레지스터를 가리킬 수 있어요. 할당된 레지스터 이름이 asm 템플릿 문자열에 치환돼요.- 할당된 레지스터는 어셈블리 코드 시작 시
<expr>의 값을 담아요. - 할당된 레지스터는 어셈블리 코드 끝에서도 같은 값이어야 해요(
lateout이 같은 레지스터에 할당된 경우 제외).
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// `in` can be used to pass values into inline assembly...
unsafe { core::arch::asm!("/* {} */", in(reg) 5); }
}
}
참고: 값의 타입이 레지스터보다 작으면 상위 비트의 값은 플랫폼마다 달라요. 어떤 타깃은 상위 비트를 0으로 만들고, 다른 곳은 건드리지 않아요.
out(<reg>) <expr>
<reg>는 레지스터 클래스 또는 명시적 레지스터. 할당된 레지스터 이름이 템플릿 문자열에 치환돼요.- 할당된 레지스터는 어셈블리 코드 시작 시 정의되지 않은 값을 담아요.
<expr>은 (아마도 초기화되지 않은) place 표현식이어야 하고, 어셈블리 코드 끝에 할당된 레지스터의 내용이 그곳에 쓰여요.- 표현식 대신 밑줄(
_)을 지정할 수 있는데, 그러면 어셈블리 코드 끝에 레지스터 내용이 버려져요(clobber로 동작).
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i64;
// and `out` can be used to pass values back to rust.
unsafe { core::arch::asm!("/* {} */", out(reg) x); }
}
}
lateout(<reg>) <expr>
out과 동일하지만, 레지스터 할당자가in에 할당된 레지스터를 재사용할 수 있어요.- 모든 입력이 읽힌 후에만 레지스터에 써야 해요. 그렇지 않으면 입력을 덮어쓸 수 있어요.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i64;
// `lateout` is the same as `out`
// but the compiler knows we don't care about the value of any inputs by the
// time we overwrite it.
unsafe { core::arch::asm!("mov {}, 5", lateout(reg) x); }
assert_eq!(x, 5)
}
}
inout(<reg>) <expr>
<reg>는 레지스터 클래스 또는 명시적 레지스터. 할당된 레지스터 이름이 템플릿 문자열에 치환돼요.- 할당된 레지스터는 어셈블리 코드 시작 시
<expr>의 값을 담아요. <expr>은 가변 초기화된 place 표현식이어야 하고, 어셈블리 코드 끝에 레지스터 내용이 그곳에 쓰여요.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let mut x: i64 = 4;
// `inout` can be used to modify values in-register
unsafe { core::arch::asm!("inc {}", inout(reg) x); }
assert_eq!(x, 5);
}
}
inout(<reg>) <in expr> => <out expr>
inout과 같지만, 레지스터의 초기 값이<in expr>의 값에서 온다는 점이 달라요.<out expr>은 (아마도 초기화되지 않은) place 표현식이어야 하고, 어셈블리 코드 끝에 레지스터 내용이 그곳에 쓰여요.<out expr>대신 밑줄(_)을 지정하면 레지스터 내용이 버려져요(clobber로 동작).<in expr>과<out expr>은 타입이 달라도 돼요.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i64;
// `inout` can also move values to different places
unsafe { core::arch::asm!("inc {}", inout(reg) 4u64=>x); }
assert_eq!(x, 5);
}
}
inlateout(<reg>) <expr> / inlateout(<reg>) <in expr> => <out expr>
inout과 같지만, 레지스터 할당자가in에 할당된 레지스터를 재사용할 수 있어요(컴파일러가in이inlateout과 같은 초기 값을 가진다는 걸 알 때 일어날 수 있음).- 모든 입력이 읽힌 후에만 레지스터에 써야 해요.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let mut x: i64 = 4;
// `inlateout` is `inout` using `lateout`
unsafe { core::arch::asm!("inc {}", inlateout(reg) x); }
assert_eq!(x, 5);
}
}
sym <path>
<path>는fn또는static을 가리켜야 해요.- 아이템을 가리키는 맹글된 심볼 이름이 asm 템플릿 문자열에 치환돼요.
- 치환된 문자열에는 어떤 수정자(modifier)도 포함되지 않아요(예: GOT, PLT, relocations 등).
<path>는#[thread_local]static을 가리킬 수 있는데, 그 경우 어셈블리 코드가 심볼을 relocation(예:@plt,@TPOFF)과 결합해 스레드 로컬 데이터를 읽을 수 있어요.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
extern "C" fn foo() {
println!("Hello from inline assembly")
}
// `sym` can be used to refer to a function (even if it doesn't have an
// external name we can directly write)
unsafe { core::arch::asm!("call {}", sym foo, clobber_abi("C")); }
}
}
const <expr>
<expr>은 정수 상수 표현식이어야 해요. 인라인const블록과 같은 규칙을 따르죠.- 표현식의 타입은 어떤 정수 타입이든 될 수 있지만, 정수 리터럴처럼 기본값은
i32예요. - 표현식의 값은 문자열로 포맷되어 asm 템플릿 문자열에 직접 치환돼요.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// swizzle [0, 1, 2, 3] => [3, 2, 0, 1]
const SHUFFLE: u8 = 0b01_00_10_11;
let x: core::arch::x86_64::__m128 = unsafe { core::mem::transmute([0u32, 1u32, 2u32, 3u32]) };
let y: core::arch::x86_64::__m128;
// Pass a constant value into an instruction that expects an immediate like `pshufd`
unsafe {
core::arch::asm!("pshufd {xmm}, {xmm}, {shuffle}",
xmm = inlateout(xmm_reg) x=>y,
shuffle = const SHUFFLE
);
}
let y: [u32; 4] = unsafe { core::mem::transmute(y) };
assert_eq!(y, [3, 2, 0, 1]);
}
}
label <block>
- 블록의 주소가 asm 템플릿 문자열에 치환돼요. 어셈블리 코드는 그 치환된 주소로 점프할 수 있어요.
- 직접 점프와 간접 점프를 구분하는 타깃(예:
cf-protection이 켜진 x86-64)에서는, 치환된 주소로 간접적으로 점프하면 안 돼요. - 블록 실행 후
asm!표현식이 반환돼요. - 블록의 타입은 unit이거나
!(never)여야 해요. - 블록은 새로운 안전 컨텍스트를 시작해요.
label블록 안의 unsafe 연산은, 전체asm!표현식이 이미unsafe로 감싸여 있어도 내부unsafe블록으로 감싸야 해요.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")]
unsafe {
core::arch::asm!("jmp {}", label {
println!("Hello from inline assembly label");
});
}
}
피연산자 표현식은 함수 호출 인자처럼 왼쪽에서 오른쪽으로 평가돼요. asm! 이 실행된 후 출력은 왼쪽에서 오른쪽 순서로 쓰여요. 두 출력이 같은 곳을 가리킨다면 그곳은 가장 오른쪽 출력의 값을 담게 돼요.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let mut y: i64;
// y gets its value from the second output, rather than the first
unsafe { core::arch::asm!("mov {}, 0", "mov {}, 1", out(reg) y, out(reg) y); }
assert_eq!(y, 1);
}
}
naked_asm! 은 함수 본문 전체를 정의하고 컴파일러가 피연산자를 처리할 추가 코드를 출력할 수 없기 때문에, sym 과 const 피연산자만 쓸 수 있어요. global_asm! 도 함수 밖에 존재하므로 sym 과 const만 쓸 수 있어요.
fn main() {}
// register operands aren't allowed, since we aren't in a function
#[cfg(target_arch = "x86_64")]
core::arch::global_asm!("", in(reg) 5);
// ERROR: the `in` operand cannot be used with `global_asm!`
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
레지스터 피연산자 (Register operands)
입력·출력 피연산자는 명시적 레지스터 또는 레지스터 할당자가 레지스터를 고르는 레지스터 클래스로 지정할 수 있어요. 명시적 레지스터는 문자열 리터럴(예: "eax")로, 레지스터 클래스는 식별자(예: reg)로 지정해요.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let mut y: i64;
// We can name both `reg`, or an explicit register like `eax` to get an
// integer register
unsafe { core::arch::asm!("mov eax, {:e}", in(reg) 5, lateout("eax") y); }
assert_eq!(y, 5);
}
}
명시적 레지스터는 레지스터 별칭(예: ARM의 r14 vs lr)과 더 작은 뷰(예: eax vs rax)를 기본 레지스터와 동등하게 취급해요. 두 입력 피연산자나 두 출력 피연산자에 같은 명시적 레지스터를 쓰는 것은 컴파일 타임 오류예요. 겹치는 레지스터(예: ARM VFP)를 입력·출력 피연산자에 쓰는 것도 컴파일 타임 오류예요.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// We can't name eax twice
unsafe { core::arch::asm!("", in("eax") 5, in("eax") 4); }
// ERROR: register `eax` conflicts with register `eax`
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// al overlaps with ax, so we can't name both of them.
unsafe { core::arch::asm!("", in("ax") 5, in("al") 4i8); }
// ERROR: register `al` conflicts with register `ax`
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}
인라인 어셈블리의 피연산자로 허용되는 타입은 다음과 같아요.
- 정수 (부호 있음 및 없음)
- 부동소수점 숫자
- 포인터 (thin만)
- 함수 포인터
- SIMD 벡터 (
#[repr(simd)]로 정의되고Copy를 구현하는 구조체).std::arch에 정의된__m128(x86)이나int8x16_t(ARM) 같은 아키텍처별 벡터 타입을 포함해요.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
extern "C" fn foo() {}
// Integers are allowed...
let y: i64 = 5;
unsafe { core::arch::asm!("/* {} */", in(reg) y); }
// and pointers...
let py = &raw const y;
unsafe { core::arch::asm!("/* {} */", in(reg) py); }
// floats as well...
let f = 1.0f32;
unsafe { core::arch::asm!("/* {} */", in(xmm_reg) f); }
// even function pointers and simd vectors.
let func: extern "C" fn() = foo;
unsafe { core::arch::asm!("/* {} */", in(reg) func); }
let z = unsafe { core::arch::x86_64::_mm_set_epi64x(1, 0) };
unsafe { core::arch::asm!("/* {} */", in(xmm_reg) z); }
}
}
구조체 같은 복잡한 타입은 허용되지 않아요.
지원되는 레지스터 클래스 (Supported register classes)
현재 지원되는 레지스터 클래스 목록이에요.
| 아키텍처 | 레지스터 클래스 | 레지스터 | LLVM 제약 코드 |
|---|---|---|---|
| x86 | reg |
ax, bx, cx, dx, si, di, bp, r[8-15] (x86-64 only) |
r |
| x86 | reg_abcd |
ax, bx, cx, dx |
Q |
| x86-32 | reg_byte |
al, bl, cl, dl, ah, bh, ch, dh |
q |
| x86-64 | reg_byte* |
al, bl, cl, dl, sil, dil, bpl, r[8-15]b |
q |
| x86 | xmm_reg |
xmm[0-7] (x86) xmm[0-15] (x86-64) |
x |
| x86 | ymm_reg |
ymm[0-7] (x86) ymm[0-15] (x86-64) |
x |
| x86 | zmm_reg |
zmm[0-7] (x86) zmm[0-31] (x86-64) |
v |
| x86 | kreg |
k[1-7] |
Yk |
| x86 | kreg0 |
k0 |
Only clobbers |
| x86 | x87_reg |
st([0-7]) |
Only clobbers |
| x86 | mmx_reg |
mm[0-7] |
Only clobbers |
| x86-64 | tmm_reg |
tmm[0-7] |
Only clobbers |
| AArch64 | reg |
x[0-30] |
r |
| AArch64 | vreg |
v[0-31] |
w |
| AArch64 | vreg_low16 |
v[0-15] |
x |
| AArch64 | preg |
p[0-15], ffr |
Only clobbers |
| Arm64EC | reg |
x[0-12], x[15-22], x[25-27], x30 |
r |
| Arm64EC | vreg |
v[0-15] |
w |
| ARM (ARM/Thumb2) | reg |
r[0-12], r14 |
r |
| ARM (Thumb1) | reg |
r[0-7] |
r |
| ARM | sreg |
s[0-31] |
t |
| ARM | sreg_low16 |
s[0-15] |
x |
| ARM | dreg |
d[0-31] |
w |
| ARM | dreg_low16 |
d[0-15] |
t |
| ARM | dreg_low8 |
d[0-8] |
x |
| ARM | qreg |
q[0-15] |
w |
| ARM | qreg_low8 |
q[0-7] |
t |
| ARM | qreg_low4 |
q[0-3] |
x |
| RISC-V | reg |
x1, x[5-7], x[9-15], x[16-31] (non-RV32E) |
r |
| RISC-V | freg |
f[0-31] |
f |
| RISC-V | vreg |
v[0-31] |
Only clobbers |
| LoongArch | reg |
$r1, $r[4-20], $r[23,30] |
r |
| LoongArch | freg |
$f[0-31] |
f |
| s390x | reg |
r[0-10], r[12-14] |
r |
| s390x | reg_addr |
r[1-10], r[12-14] |
a |
| s390x | freg |
f[0-15] |
f |
| s390x | vreg |
v[0-31] |
v |
| s390x | areg |
a[2-15] |
Only clobbers |
| PowerPC | reg |
r0, r[3-12], r[14-28] |
r |
| PowerPC | reg_nonzero |
r[3-12], r[14-28] |
b |
| PowerPC | spe_acc |
spe_acc |
Only clobbers |
| PowerPC64 | reg |
r0, r[3-12], r[14-29] |
r |
| PowerPC64 | reg_nonzero |
r[3-12], r[14-29] |
b |
| PowerPC/PowerPC64 | freg |
f[0-31] |
f |
| PowerPC/PowerPC64 | vreg |
v[0-31] |
v |
| PowerPC/PowerPC64 | vsreg |
vs[0-63] |
wa |
| PowerPC/PowerPC64 | cr |
cr[0-7], cr |
Only clobbers |
| PowerPC/PowerPC64 | ctr |
ctr |
Only clobbers |
| PowerPC/PowerPC64 | lr |
lr |
Only clobbers |
| PowerPC/PowerPC64 | xer |
xer |
Only clobbers |
참고:
- x86에서는 컴파일러가
al과ah를 따로 할당할 수 있는 반면reg는 전체 레지스터를 예약하므로,reg_byte을reg과 다르게 취급해요.- x86-64의
reg_byte레지스터 클래스에서는 높은 바이트 레지스터(예:ah)를 쓸 수 없어요.- "Only clobbers"로 표시된 레지스터 클래스는 입력·출력에 쓸 수 없고,
out(<explicit register>) _또는lateout(<explicit register>) _형태의 clobber로만 쓸 수 있어요.spe_acc레지스터는 PowerPC SPE 타깃에서만 쓸 수 있어요.
각 레지스터 클래스는 함께 쓸 수 있는 값 타입에 제약이 있어요. 값이 레지스터에 로드되는 방식이 타입에 달려 있기 때문이에요. 예를 들어 빅엔디언 시스템에서 i32x4와 i8x16을 SIMD 레지스터에 로드하면, 두 값의 바이트별 메모리 표현이 동일해도 레지스터 내용이 달라질 수 있어요. 특정 레지스터 클래스에 지원되는 타입은 현재 활성화된 타깃 피처에 따라 달라질 수 있어요.
| 아키텍처 | 레지스터 클래스 | 타깃 피처 | 허용 타입 |
|---|---|---|---|
| x86-32 | reg |
None | i16, i32, f32 |
| x86-64 | reg |
None | i16, i32, f32, i64, f64 |
| x86 | reg_byte |
None | i8 |
| x86 | xmm_reg |
sse |
i32, f32, i64, f64, i8x16, i16x8, i32x4, i64x2, f32x4, f64x2 |
| x86 | ymm_reg |
avx |
i32, f32, i64, f64, i8x16, i16x8, i32x4, i64x2, f32x4, f64x2, i8x32, i16x16, i32x8, i64x4, f32x8, f64x4 |
| x86 | zmm_reg |
avx512f |
i32, f32, i64, f64, i8x16, i16x8, i32x4, i64x2, f32x4, f64x2, i8x32, i16x16, i32x8, i64x4, f32x8, f64x4, i8x64, i16x32, i32x16, i64x8, f32x16, f64x8 |
| x86 | kreg |
avx512f |
i8, i16 |
| x86 | kreg |
avx512bw |
i32, i64 |
| x86 | mmx_reg |
N/A | Only clobbers |
| x86 | x87_reg |
N/A | Only clobbers |
| x86 | tmm_reg |
N/A | Only clobbers |
| AArch64 | reg |
None | i8, i16, i32, f32, i64, f64 |
| AArch64 | vreg |
neon |
i8, i16, i32, f32, i64, f64, i8x8, i16x4, i32x2, i64x1, f32x2, f64x1, i8x16, i16x8, i32x4, i64x2, f32x4, f64x2 |
| AArch64 | preg |
N/A | Only clobbers |
| Arm64EC | reg |
None | i8, i16, i32, f32, i64, f64 |
| Arm64EC | vreg |
neon |
i8, i16, i32, f32, i64, f64, i8x8, i16x4, i32x2, i64x1, f32x2, f64x1, i8x16, i16x8, i32x4, i64x2, f32x4, f64x2 |
| ARM | reg |
None | i8, i16, i32, f32 |
| ARM | sreg |
vfp2 |
i32, f32 |
| ARM | dreg |
vfp2 |
i64, f64, i8x8, i16x4, i32x2, i64x1, f32x2 |
| ARM | qreg |
neon |
i8x16, i16x8, i32x4, i64x2, f32x4 |
| RISC-V32 | reg |
None | i8, i16, i32, f32 |
| RISC-V64 | reg |
None | i8, i16, i32, f32, i64, f64 |
| RISC-V | freg |
f |
f32 |
| RISC-V | freg |
d |
f64 |
| RISC-V | vreg |
N/A | Only clobbers |
| LoongArch32 | reg |
None | i8, i16, i32, f32 |
| LoongArch64 | reg |
None | i8, i16, i32, i64, f32, f64 |
| LoongArch | freg |
f |
f32 |
| LoongArch | freg |
d |
f64 |
| s390x | reg, reg_addr |
None | i8, i16, i32, i64 |
| s390x | freg |
None | f32, f64 |
| s390x | vreg |
vector |
i32, f32, i64, f64, i128, i8x16, i16x8, i32x4, i64x2, f32x4, f64x2 |
| s390x | areg |
N/A | Only clobbers |
| PowerPC | spe_acc |
None | Only clobbers |
| PowerPC/PowerPC64 | reg |
None | i8, i16, i32, i64 (PowerPC64 only) |
| PowerPC/PowerPC64 | reg_nonzero |
None | i8, i16, i32, i64 (PowerPC64 only) |
| PowerPC/PowerPC64 | freg |
None | f32, f64 |
| PowerPC/PowerPC64 | vreg |
altivec |
i8x16, i16x8, i32x4, f32x4 |
| PowerPC/PowerPC64 | vreg |
vsx |
f32, f64, i64x2, f64x2 |
| PowerPC/PowerPC64 | vsreg |
vsx |
vsx와 altivec vreg 타입의 합집합 |
| PowerPC/PowerPC64 | cr / ctr / lr / xer |
None | Only clobbers |
참고: 위 표에서 포인터, 함수 포인터,
isize/usize는 동등한 정수 타입(타깃에 따라i16/i32/i64)으로 취급돼요.
값이 할당된 레지스터보다 작으면, 그 레지스터의 상위 비트는 입력에 대해 정의되지 않은 값을 갖고 출력에 대해 무시돼요. 유일한 예외는 RISC-V의 freg 레지스터 클래스로, RISC-V 아키텍처가 요구하듯 f32 값이 f64에 NaN-boxed 되는 경우예요.
inout 피연산자에 분리된 입력·출력 표현식을 지정하면 두 표현식은 같은 타입이어야 해요. 둘 다 포인터나 정수인 경우에만 같은 크기만 요구돼요. LLVM과 GCC의 레지스터 할당자가 때때로 다른 타입의 연결(tied) 피연산자를 처리하지 못하기 때문이에요.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// Pointers and integers can mix (as long as they are the same size)
let x: isize = 0;
let y: *mut ();
// Transmute an `isize` to a `*mut ()`, using inline assembly magic
unsafe { core::arch::asm!("/*{}*/", inout(reg) x=>y); }
assert!(y.is_null()); // Extremely roundabout way to make a null pointer
}
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i32 = 0;
let y: f32;
// But we can't reinterpret an `i32` to an `f32` like this
unsafe { core::arch::asm!("/* {} */", inout(reg) x=>y); }
// ERROR: incompatible types for asm inout argument
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}
레지스터 이름 (Register names)
어떤 레지스터는 여러 이름을 가져요. 모두 컴파일러가 기본 레지스터 이름과 동일하게 취급해요. 지원되는 레지스터 별칭의 일부는 이래요.
| 아키텍처 | 기본 레지스터 | 별칭 |
|---|---|---|
| x86 | ax |
eax, rax |
| x86 | bx |
ebx, rbx |
| x86 | cx |
ecx, rcx |
| x86 | dx |
edx, rdx |
| x86 | si |
esi, rsi |
| x86 | di |
edi, rdi |
| x86 | bp |
bpl, ebp, rbp |
| x86 | sp |
spl, esp, rsp |
| x86 | ip |
eip, rip |
| x86 | st(0) |
st |
| x86 | r[8-15] |
r[8-15]b, r[8-15]w, r[8-15]d |
| x86 | xmm[0-31] |
ymm[0-31], zmm[0-31] |
| AArch64 | x[0-30] |
w[0-30] |
| AArch64 | x29 |
fp |
| AArch64 | x30 |
lr |
| AArch64 | sp |
wsp |
| AArch64 | xzr |
wzr |
| AArch64 | v[0-31] |
b[0-31], h[0-31], s[0-31], d[0-31], q[0-31] |
| ARM | r[0-3] |
a[1-4] |
| ARM | r[4-9] |
v[1-6] |
| ARM | r11 |
fp |
| ARM | r12 |
ip |
| ARM | r13 |
sp |
| ARM | r14 |
lr |
| ARM | r15 |
pc |
| RISC-V | x0 |
zero |
| RISC-V | x1 |
ra |
| RISC-V | x2 |
sp |
| RISC-V | x3 |
gp |
| RISC-V | x4 |
tp |
| RISC-V | x[5-7] |
t[0-2] |
| RISC-V | x8 |
fp, s0 |
| RISC-V | x9 |
s1 |
| RISC-V | x[10-17] |
a[0-7] |
| RISC-V | x[18-27] |
s[2-11] |
| RISC-V | x[28-31] |
t[3-6] |
| RISC-V | f[0-7] |
ft[0-7] |
| RISC-V | f[10-17] |
fa[0-7] |
| RISC-V | f[18-27] |
fs[2-11] |
| RISC-V | f[28-31] |
ft[8-11] |
| PowerPC/PowerPC64 | r1 |
sp |
| PowerPC/PowerPC64 | r31 |
fp |
| PowerPC/PowerPC64 | r[0-31] |
[0-31] |
| PowerPC/PowerPC64 | f[0-31] |
fr[0-31] |
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let z = 0i64;
// rax is an alias for eax and ax
unsafe { core::arch::asm!("", in("rax") z); }
}
}
어떤 레지스터는 입력·출력 피연산자에 쓸 수 없어요. 스택 포인터(sp 등)는 어셈블리 코드 끝이나 label 블록으로 점프하기 전에 원래 값으로 복원되어야 하고, 프레임 포인터(bp, x29, fp 등)는 입력·출력으로 쓸 수 없으며, 몇몇 레지스터는 LLVM이 "베이스 포인터"로 내부 사용하거나 시스템·OS 예약이라 쓸 수 없어요. 예를 들어 x86의 bp(프레임 포인터)를 쓰면 오류가 나요.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// bp is reserved
unsafe { core::arch::asm!("", in("bp") 5i32); }
// ERROR: invalid register `bp`: the frame pointer cannot be used as an operand for inline asm
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}
프레임 포인터와 베이스 포인터 레지스터는 LLVM의 내부 사용을 위해 예약되어 있어요. asm! 문이 예약된 레지스터의 사용을 명시적으로 지정할 수는 없지만, 어떤 경우 LLVM이 reg 피연산자에 이 예약 레지스터 중 하나를 할당할 수 있어요. 예약 레지스터를 사용하는 어셈블리 코드는 reg 피연산자가 같은 레지스터를 쓸 수 있으므로 주의해야 해요.
템플릿 수정자 (Template modifiers)
자리 표시자는 중괄호 안의 : 뒤에 지정하는 수정자로 보강될 수 있어요. 이 수정자는 레지스터 할당에 영향을 주지 않지만, 템플릿 문자열에 삽입될 때 피연산자가 포맷되는 방식을 바꿔요. 한 템플릿 자리 표시자에는 수정자가 하나만 허용돼요.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// We can't specify both `r` and `e` at the same time.
unsafe { core::arch::asm!("/* {:er}", in(reg) 5i32); }
// ERROR: asm template modifier must be a single character
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}
지원되는 수정자는 LLVM(및 GCC)의 asm 템플릿 인자 수정자의 부분집합이지만, 같은 글자 코드를 쓰진 않아요.
| 아키텍처 | 레지스터 클래스 | 수정자 | 예시 출력 | LLVM 수정자 |
|---|---|---|---|---|
| x86-32 | reg |
None | eax |
k |
| x86-64 | reg |
None | rax |
q |
| x86-32 | reg_abcd |
l |
al |
b |
| x86-64 | reg |
l |
al |
b |
| x86 | reg_abcd |
h |
ah |
h |
| x86 | reg |
x |
ax |
w |
| x86 | reg |
e |
eax |
k |
| x86-64 | reg |
r |
rax |
q |
| x86 | reg_byte |
None | al / ah |
None |
| x86 | xmm_reg |
None | xmm0 |
x |
| x86 | ymm_reg |
None | ymm0 |
t |
| x86 | zmm_reg |
None | zmm0 |
g |
| x86 | *mm_reg |
x / y / z |
xmm0 / ymm0 / zmm0 |
x / t / g |
| x86 | kreg |
None | k1 |
None |
| AArch64/Arm64EC | reg |
None | x0 |
x |
| AArch64/Arm64EC | reg |
w / x |
w0 / x0 |
w / x |
| AArch64/Arm64EC | vreg |
None / v |
v0 |
None |
| AArch64/Arm64EC | vreg |
b / h / s / d / q |
b0 / h0 / s0 / d0 / q0 |
b / h / s / d / q |
| ARM | reg |
None | r0 |
None |
| ARM | sreg |
None | s0 |
None |
| ARM | dreg |
None | d0 |
P |
| ARM | qreg |
None | q0 |
q |
| ARM | qreg |
e / f |
d0 / d1 |
e / f |
| RISC-V | reg |
None | x1 |
None |
| RISC-V | freg |
None | f0 |
None |
| LoongArch | reg |
None | $r1 |
None |
| LoongArch | freg |
None | $f0 |
None |
| s390x | reg |
None | %r0 |
None |
| s390x | reg_addr |
None | %r1 |
None |
| s390x | freg |
None | %f0 |
None |
| s390x | vreg |
None | %v0 |
None |
| PowerPC/PowerPC64 | reg |
None | 0 |
None |
| PowerPC/PowerPC64 | reg_nonzero |
None | 3 |
None |
| PowerPC/PowerPC64 | freg |
None | 0 |
None |
| PowerPC/PowerPC64 | vreg |
None | 0 |
None |
| PowerPC/PowerPC64 | vsreg |
None | 0 |
None |
참고:
- ARM에서
e/f는 NEON 쿼드(128비트) 레지스터의 낮거나 높은 더블워드 레지스터 이름을 출력해요.- x86에서 수정자 없는
reg에 대한 동작은 GCC와 달라요. GCC는 피연산자 값 타입을 기반으로 수정자를 추론하는 반면, 우리는 전체 레지스터 크기를 기본값으로 해요.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let mut x = 0x10u16;
// u16::swap_bytes using `xchg`
// low half of `{x}` is referred to by `{x:l}`, and the high half by `{x:h}`
unsafe { core::arch::asm!("xchg {x:l}, {x:h}", x = inout(reg_abcd) x); }
assert_eq!(x, 0x1000u16);
}
}
입력 값이 레지스터 폭보다 작으면 레지스터 상위 비트에 정의되지 않은 값이 들어가요. 인라인 asm이 레지스터의 하위 비트만 접근한다면 문제가 없는데, 이는 템플릿 수정자로 서브레지스터 이름(예: rax 대신 ax)을 써서 할 수 있어요. 이것이 쉽게 빠지는 함정이라, 컴파일러는 입력 타입에 따라 적절한 템플릿 수정자를 제안해요.
ABI 클로버 (ABI clobbers)
clobber_abi 키워드로 어셈블리 코드에 기본 클로버 집합을 적용할 수 있어요. 특정 호출 규약으로 함수를 호출하는 데 필요한 클로버 제약을 자동으로 넣어 줘요. 호출 규약이 호출을 걸쳐 레지스터 값을 완전히 보존하지 않는다면, 피연산자 목록에 lateout("...") _ 가 암묵적으로 추가돼요(...는 레지스터 이름으로 대체).
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
extern "C" fn foo() -> i32 { 0 }
let z: i32;
// To call a function, we have to inform the compiler that we're clobbering
// callee saved registers
unsafe { core::arch::asm!("call {}", sym foo, out("rax") z, clobber_abi("C")); }
assert_eq!(z, 0);
}
}
clobber_abi 는 몇 번이든 지정할 수 있어요. 지정된 모든 호출 규약의 합집합에 있는 모든 고유 레지스터에 대한 클로버를 삽입해요.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
extern "sysv64" fn foo() -> i32 { 0 }
extern "win64" fn bar(x: i32) -> i32 { x + 1 }
let z: i32;
// We can even call multiple functions with different conventions and
// different saved registers
unsafe {
core::arch::asm!(
"call {}",
"mov ecx, eax",
"call {}",
sym foo,
sym bar,
out("rax") z,
clobber_abi("sysv64"),
clobber_abi("win64"),
);
}
assert_eq!(z, 1);
}
}
clobber_abi 를 쓸 때는 모든 출력이 명시적 레지스터를 지정해야 해요. 제네릭 레지스터 클래스 출력은 허용되지 않아요. 명시적 레지스터 출력은 clobber_abi 가 삽입한 암묵적 클로버보다 우선해요. 즉, 출력으로 쓰이지 않는 레지스터에 대해서만 클로버가 삽입돼요.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
extern "C" fn foo(x: i32) -> i32 { 0 }
let z: i32;
// explicit registers must be used to not accidentally overlap.
unsafe {
core::arch::asm!(
"mov eax, {:e}",
"call {}",
out(reg) z,
sym foo,
clobber_abi("C")
);
// ERROR: asm with `clobber_abi` must specify explicit registers for outputs
}
assert_eq!(z, 0);
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}
clobber_abi 와 함께 쓸 수 있는 ABI는 다음이에요.
| 아키텍처 | ABI 이름 | 클로버되는 레지스터 |
|---|---|---|
| x86-32 | "C", "system", "efiapi", "cdecl", "stdcall", "fastcall" |
ax, cx, dx, xmm[0-7], mm[0-7], k[0-7], st([0-7]) |
| x86-64 | "C", "system" (Windows), "efiapi", "win64" |
ax, cx, dx, r[8-11], xmm[0-31], mm[0-7], k[0-7], st([0-7]), tmm[0-7] |
| x86-64 | "C", "system" (non-Windows), "sysv64" |
ax, cx, dx, si, di, r[8-11], xmm[0-31], mm[0-7], k[0-7], st([0-7]), tmm[0-7] |
| AArch64 | "C", "system", "efiapi" |
x[0-17], x18*, x30, v[0-31], p[0-15], ffr |
| Arm64EC | "C", "system" |
x[0-12], x[15-17], x30, v[0-15] |
| ARM | "C", "system", "efiapi", "aapcs" |
r[0-3], r12, r14, s[0-15], d[0-7], d[16-31] |
| RISC-V | "C", "system", "efiapi" |
x1, x[5-7], x[10-17], x[28-31], f[0-7], f[10-17], f[28-31], v[0-31] |
| LoongArch | "C", "system" |
$r1, $r[4-20], $f[0-23] |
| s390x | "C", "system" |
r[0-5], r14, f[0-7], v[0-31], a[2-15] |
참고: AArch64의
x18은 타깃에서 예약 레지스터로 간주되지 않을 때만 클로버 목록에 포함돼요. RISC-V의x[16-17]과x[28-31]도 예약 레지스터가 아닐 때만 포함돼요. 각 ABI의 클로버 레지스터 목록은 아키텍처가 새 레지스터를 얻을 때 rustc에서 갱신돼요.
옵션 (Options)
플래그로 인라인 어셈블리 코드의 동작을 더 다듬을 수 있어요. 현재 정의된 옵션은 이래요.
pure: 어셈블리 코드에 부작용이 없고, 결국엔 반환하며, 출력이 직접 입력(값 자체)이나 메모리에서 읽은 값에만 의존해요(nomem이 설정된 경우는 제외). 이 옵션은 컴파일러가 어셈블리 코드를 프로그램에 지정된 것보다 적게 실행(예: 루프 밖으로 끌어올림)하거나, 출력이 안 쓰이면 아예 제거하게 해요.pure옵션은nomem이나readonly옵션 중 하나와 반드시 결합해야 해요.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i32 = 0;
let z: i32;
// pure can be used to optimize by assuming the assembly has no side effects
unsafe { core::arch::asm!("inc {}", inout(reg) x => z, options(pure, nomem)); }
assert_eq!(z, 1);
}
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i32 = 0;
let z: i32;
// nomem or readonly must be used with pure
unsafe { core::arch::asm!("inc {}", inout(reg) x => z, options(pure)); }
// ERROR: the `pure` option must be combined with either `nomem` or `readonly`
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}
nomem: 어셈블리 코드가 어셈블리 코드 밖에서 접근 가능한 어떤 메모리도 읽거나 쓰지 않아요. 컴파일러가 수정된 전역 변수의 값을 레지스터에 캐시해도 안전하도록 해 주고, 어셈블리 코드가 펜스 같은 방식으로 다른 스레드와 동기화하지 않는다고 가정해요. 이 옵션을 지정하고 바깥 메모리에 접근하면 undefined behavior예요.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let mut x = 0i32;
let z: i32;
// Accessing outside memory from assembly when `nomem` is
// specified is disallowed
unsafe {
core::arch::asm!("mov {val:e}, dword ptr [{ptr}]",
ptr = in(reg) &mut x,
val = lateout(reg) z,
options(nomem)
)
}
}
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i32 = 0;
let z: i32;
// If we allocate our own memory, such as via `push`, however,
// we can still use it
unsafe {
core::arch::asm!("push {x}", "add qword ptr [rsp], 1", "pop {x}",
x = inout(reg) x => z,
options(nomem)
);
}
assert_eq!(z, 1);
}
}
readonly: 어셈블리 코드가 바깥에서 접근 가능한 어떤 메모리도 쓰지 않아요.nomem보다 약한데, 메모리 읽기는 허용돼요. 이 옵션으로 어셈블리 코드가 바깥 메모리를 수정하면 undefined behavior예요.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let mut x = 0;
// We cannot modify outside memory when `readonly` is specified
unsafe {
core::arch::asm!("mov dword ptr[{}], 1", in(reg) &mut x, options(readonly))
}
}
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i64 = 0;
let z: i64;
// We can still read from it, though
unsafe {
core::arch::asm!("mov {x}, qword ptr [{x}]",
x = inout(reg) &x => z,
options(readonly)
);
}
assert_eq!(z, 0);
}
}
preserves_flags: 어셈블리 코드가 플래그 레지스터를 수정하지 않아요. 컴파일러가 어셈블리 코드 실행 후 조건 플래그를 다시 계산하지 않아도 되게 해요.noreturn: 어셈블리 코드가 끝까지 흘러내려가지 않아요. 흘러내리면 undefined behavior예요.label블록으로는 점프할 수 있어요.label블록이 unit을 반환하면asm!블록도 unit, 그렇지 않으면!(never)를 반환해요. 반환하지 않는 함수 호출처럼, 스코프 안의 지역 변수는 어셈블리 코드 실행 전에 drop되지 않아요.
fn main() -> ! {
#[cfg(target_arch = "x86_64")] {
// We can use an instruction to trap execution inside of a noreturn block
unsafe { core::arch::asm!("ud2", options(noreturn)); }
}
#[cfg(not(target_arch = "x86_64"))] panic!("no return");
}
nostack: 어셈블리 코드가 스택에 데이터를 push하지 않고, (타깃이 지원한다면) 스택 레드존에 쓰지 않아요. 이 옵션을 안 쓰면 어셈블리 코드 시작 시 스택 포인터는 함수 호출에 적합하게 (타깃 ABI에 따라) 정렬되도록 컴파일러가 보장해요.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// `push` and `pop` are UB when used with nostack
unsafe { core::arch::asm!("push rax", "pop rax", options(nostack)); }
}
}
att_syntax: x86에서만 유효한 옵션으로, GNU 어셈블러의.att_syntax prefix모드를 쓰게 해요. 레지스터 피연산자가 앞에%가 붙어 치환돼요.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i32;
let y = 1i32;
// We need to use AT&T Syntax here. src, dest order for operands
unsafe {
core::arch::asm!("mov {y:e}, {x:e}",
x = lateout(reg) x,
y = in(reg) y,
options(att_syntax)
);
}
assert_eq!(x, y);
}
}
raw: 템플릿 문자열을{와}에 대한 특별한 처리가 없는 raw 어셈블리 문자열로 파싱하게 해요.include_str!로 외부 파일의 raw 어셈블리 코드를 포함할 때 주로 유용해요.
컴파일러는 옵션에 대해 몇 가지 추가 검사를 해요.
nomem과readonly는 상호 배타적이에요. 둘 다 지정하면 컴파일 타임 오류예요.- 출력이 없거나 버려지는 출력(
_)만 있는 asm 블록에pure를 지정하는 것은 컴파일 타임 오류예요. - 출력이 있고 레이블이 없는 asm 블록에
noreturn을 지정하는 것은 컴파일 타임 오류예요. - 출력이 있는 asm 블록에 어떤
label블록이 있는 것도 컴파일 타임 오류예요.
naked_asm! 은 att_syntax 와 raw 옵션만 지원해요. 나머지 옵션은 인라인 어셈블리가 함수 본문 전체를 정의하므로 의미가 없어요. global_asm! 도 att_syntax 와 raw만 지원해요.
fn main() {}
#[cfg(target_arch = "x86_64")]
// nomem is useless on global_asm!
core::arch::global_asm!("", options(nomem));
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
인라인 어셈블리 규칙 (Rules for inline assembly)
함수 스코프 인라인 어셈블리(asm!)를 쓸 때 undefined behavior를 피하려면 이 규칙을 따라야 해요.
- 입력으로 지정되지 않은 레지스터는 어셈블리 코드 진입 시 정의되지 않은 값을 담아요. 인라인 어셈블리 맥락의 "정의되지 않은 값"은 레지스터가 (비결정적으로) 아키텍처가 허용하는 값 중 아무거나 가질 수 있다는 뜻이에요. LLVM의
undef(읽을 때마다 값이 달라질 수 있음)와는 같지 않아요. - 출력으로 지정되지 않은 레지스터는 어셈블리 코드를 나갈 때 진입할 때와 같은 값을 가져야 해요. 아니면 undefined behavior예요. 이 규칙은 입력이나 출력으로 지정할 수 있는 레지스터에만 적용돼요.
lateout이in과 같은 레지스터에 할당되면 이 규칙이 적용되지 않지만, 레지스터 할당 결과에 달려 있으므로 의존하면 안 돼요. - 어셈블리 코드 밖으로 실행이 언와인딩(unwind) 되면 undefined behavior예요. 어셈블리 코드가 언와인딩되는 함수를 호출하는 경우에도 적용돼요.
- 어셈블리 코드가 읽고 쓸 수 있는 메모리 위치 집합은 FFI 함수에 허용되는 것과 같아요.
readonly옵션이 설정되면 읽기만 허용되고,nomem이 설정되면 읽기·쓰기 모두 허용되지 않아요. 이 규칙은 어셈블리 코드 내부에서 할당된 스택 공간처럼 어셈블리 코드에 전용인 메모리에는 적용되지 않아요. - 컴파일러는 어셈블리 코드의 명령어가 실제로 실행될 것이라고 가정할 수 없어요. 컴파일러는 어셈블리 코드를 블랙박스로 취급하고 인터페이스 명세만 고려해야 해요. 런타임 코드 패칭은 타깃별 메커니즘으로 허용돼요. 하지만 소스의 각 어셈블리 블록이 오브젝트 파일의 단일 명령어 인스턴스에 직접 대응한다는 보장은 없어요.
nostack옵션이 설정되지 않으면, 어셈블리 코드는 스택 포인터 아래의 스택 공간을 쓸 수 있어요. 진입 시 스택 포인터는 함수 호출에 적합하게 정렬돼요. 스택 오버플로를 만들지 않도록 주의해야 하고(가드 페이지를 밟도록 스택 프로빙을 써야 함), 타깃 ABI가 요구하면 스택 메모리를 할당할 때 스택 포인터를 조정해야 하며, 어셈블리 코드를 떠나기 전에 스택 포인터를 원래 값으로 복원해야 해요.nostack가 설정되지 않으면, 타깃 ABI가 어떤 값을 호출자의 프레임에 저장하도록 요구할 때(예: PowerPC64에서lr저장) 어셈블리 코드가 호출자의 스택 프레임을 수정할 수 있어요.noreturn옵션이 설정되면 어셈블리 코드 끝을 흘러내리면 undefined behavior예요.pure옵션이 설정되면 직접 출력 외에 부작용이 있거나, 같은 입력으로 두 번 실행해 다른 출력이 나오면 undefined behavior예요.nomem과 쓸 때 "입력"은asm!의 직접 입력뿐이고,readonly와 쓸 때는 직접 입력과 읽기 허용 메모리를 포함해요.preserves_flags옵션이 설정되면, 어셈블리 코드를 나갈 때 다음 플래그 레지스터를 복원해야 해요.- x86:
EFLAGS의 상태 플래그(CF, PF, AF, ZF, SF, OF), 부동소수점 상태 워드(전체),MXCSR의 부동소수점 예외 플래그(PE, UE, OE, ZE, DE, IE) - ARM:
CPSR의 조건 플래그(N, Z, C, V)와 포화 플래그(Q), GE 플래그,FPSCR의 조건 플래그(N, Z, C, V)와 포화 플래그(QC), 예외 플래그(IDC, IXC, UFC, OFC, DZC, IOC) - AArch64 및 Arm64EC: 조건 플래그(
NZCV레지스터), 부동소수점 상태(FPSR레지스터) - RISC-V:
fcsr의 부동소수점 예외 플래그(fflags), 벡터 확장 상태(vtype,vl,vxsat,vxrm) - LoongArch:
$fcc[0-7]의 부동소수점 조건 플래그 - PowerPC/PowerPC64:
fpscr의 상태·sticky 비트(DRN, VE, OE, UE, ZE, XE, NI, RN 외),vscr의 상태·sticky 비트(NJ 외), PowerPC SPE에서spefscr의 상태·sticky 비트 - s390x: 조건 코드 레지스터
cc
- x86:
- x86에서 방향 플래그(EFLAGS의 DF)는 진입 시 0이고 나갈 때도 0이어야 해요.
- x86에서 x87 부동소수점 레지스터 스택은 모든
st([0-7])레지스터가 클로버로 표시되지 않는 한 변하지 않아야 해요. 모든 x87 레지스터가 클로버되면 진입 시 x87 스택이 비어 있음이 보장되고, 어셈블리 코드는 나갈 때도 비어 있게 해야 해요. - arm64ec에서 함수를 호출할 때 적절한 썽크가 있는 call checkers가 필수예요.
- 스택 포인터와 non-output 레지스터를 원래 값으로 복원하라는 요구는 어셈블리 코드를 나갈 때만 적용돼요. 흘러내리지 않고 어떤
label블록으로도 점프하지 않는 어셈블리 코드는(심지어noreturn으로 표시되지 않아도) 이 레지스터들을 보존할 필요가 없어요. 들어온 것과 다른asm!블록으로 반환할 때(예: 컨텍스트 스위칭) 레지스터는 나가는 블록에 들어올 때 가졌던 값을 담아야 해요. 들어가지 않은asm!블록을 나갈 수 없고, 이미 나간 코드를 다시 들어오지 않고 나갈 수도 없어요. 타깃별 상태(스레드 로컬 저장소, 스택 경계 등) 전환은 사용자 책임이에요. 한asm!블록의 주소에서 다른 블록의 주소로 점프할 수 없어요(같은 함수 안이라도). 소스에서 인접한 두asm!블록이 이진에서 다른 명령어 없이 연속 주소에 놓인다고 가정할 수 없고,asm!블록이 출력 바이너리에 정확히 한 번 나타난다고도 가정할 수 없어요(컴파일러는 인라인 등으로 여러 복사본을 만들 수 있어요). - x86에서 인라인 어셈블리는 컴파일러가 생성한 명령어에 적용될 명령어 접두어(예:
LOCK)로 끝나면 안 돼요. 컴파일러가 인라인 어셈블리 컴파일 방식 때문에 현재 이걸 감지할 수 없지만, 미래에 잡아서 거부할 수도 있어요.
참고: 일반적으로
preserves_flags가 다루는 플래그는 함수 호출을 수행할 때 보존되지 않는 것들이에요.
Naked 인라인 어셈블리 규칙 (Rules for naked inline assembly)
naked 함수(naked_asm!)에서 함수 스코프 인라인 어셈블리를 쓸 때 undefined behavior를 피하려면 다음 규칙을 따라야 해요.
- 호출 규약과 함수 시그니처에 따라 함수 입력에 쓰이지 않는 레지스터는
naked_asm!블록 진입 시 정의되지 않은 값을 가져요. - 모든 callee-saved 레지스터는 반환 시 진입할 때와 같은 값을 가져야 해요.
- Caller-saved 레지스터는 자유롭게 쓸 수 있어요.
- 어셈블리 코드 끝을 흘러내리면 undefined behavior예요. 코드의 모든 경로는 반환 명령어로 끝나거나 발산할 것으로 기대돼요.
- 읽고 쓸 수 있는 메모리 위치 집합은 FFI 함수에 허용되는 것과 같아요.
- 컴파일러는
naked_asm!블록의 명령어가 실제로 실행될 것이라고 가정할 수 없어요. 블랙박스로 취급해야 해요. 런타임 코드 패칭은 타깃별 메커니즘으로 허용돼요. naked_asm!블록 밖으로 언와인딩하는 것은 허용돼요. 올바른 동작을 위해 언와인딩 메타데이터를 내보내는 적절한 어셈블러 지시문을 써야 해요.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
#[unsafe(naked)]
extern "sysv64-unwind" fn unwinding_naked() {
core::arch::naked_asm!(
// "CFI" here stands for "call frame information".
".cfi_startproc",
// The CFA (canonical frame address) is the value of `rsp`
// before the `call`, i.e. before the return address, `rip`,
// was pushed to `rsp`, so it's eight bytes higher in memory
// than `rsp` upon function entry (after `rip` has been
// pushed).
//
// This is the default, so we don't have to write it.
//".cfi_def_cfa rsp, 8",
//
// The traditional thing to do is to preserve the base
// pointer, so we'll do that.
"push rbp",
".cfi_adjust_cfa_offset 8",
".cfi_offset rbp, -16",
"mov rbp, rsp",
".cfi_def_cfa_register rbp",
// We can now call a function that may panic.
"call {f}",
// Upon return, we restore `rbp` in preparation for returning
// ourselves.
"pop rbp",
".cfi_def_cfa rsp, 8",
// Now we can return.
"ret",
".cfi_endproc",
f = sym may_panic,
)
}
extern "sysv64-unwind" fn may_panic() {
panic!("unwind");
}
}
}
참고: 위
cfi어셈블러 지시문에 대한 자세한 내용은 Usingas- CFI directives, DWARF Debugging Information Format Version 5, ImperialViolet - CFI directives in assembly files를 참고해요.
정확성과 유효성 (Correctness and validity)
앞의 모든 규칙에 더해, asm! 의 문자열 인자는 — 다른 모든 인자가 평가되고, 포맷이 수행되고, 피연산자가 변환된 후 — 타깃 아키텍처에 대해 구문상 정확하고 의미상 유효한 어셈블리가 되어야 해요. 이 규칙들을 지키는 것은 최종 확장 어셈블리가 정확하고 유효하기 위한 필요 조건이지만 충분 조건은 아니에요. 예를 들어:
- 인자가 포맷 후 구문상 틀린 위치에 놓일 수 있어요.
- 명령어가 올바르게 쓰였어도 아키텍처상 유효하지 않은 피연산자를 줄 수 있어요.
- 아키텍처상 명세되지 않은 명령어가 명세되지 않은 코드로 조립될 수 있어요.
- 각각 정확하고 유효한 명령어 집합이 연속 배치되면 undefined behavior를 일으킬 수 있어요.
그 결과 이 규칙들은 비완전(non-exhaustive) 해요. 컴파일러는 초기 문자열이나 생성된 최종 어셈블리의 정확성·유효성을 검사할 의무가 없어요. 어셈블러도 검사할 수 있지만 의무는 아니에요. asm! 을 쓸 때 오타 하나가 프로그램을 unsound하게 만들 수 있고, 어셈블리 규칙은 수천 페이지에 달하는 아키텍처 매뉴얼을 포함할 수 있어요. 프로그램 작성자는 이 unsafe 능력을 호출하면서 컴파일러와 아키텍처 양쪽의 규칙을 어기지 않을 책임을 진다는 걸 명심해야 해요.
지시문 지원 (Directives support)
인라인 어셈블리는 GNU AS와 LLVM 내부 어셈블러가 모두 지원하는 지시문의 부분집합을 지원해요. 그 외 지시문을 쓰면 어셈블러별 동작이 돼요(오류일 수도, 있는 그대로 받아들여질 수도).
인라인 어셈블리가 이후 어셈블리를 처리하는 방식을 바꾸는 "stateful" 지시문을 포함하면, 인라인 어셈블리가 끝나기 전에 그 효과를 되돌려야 해요.
다음 지시문은 어셈블러가 보장 지원해요.
.2byte .4byte .8byte .align .alt_entry .ascii .asciz .balign .balignl .balignw .bss .byte .comm .data .def .double .endef .equ .equiv .eqv .fill .float .global .globl .inst .insn .lcomm .long .octa .option .p2align .popsection .private_extern .pushsection .quad .scl .section .set .short .size .skip .sleb128 .space .string .text .type .uleb128 .word
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let bytes: *const u8;
let len: usize;
unsafe {
core::arch::asm!(
"jmp 3f", "2: .ascii \"Hello World!\"",
"3: lea {bytes}, [2b+rip]",
"mov {len}, 12",
bytes = out(reg) bytes,
len = out(reg) len
);
}
let s = unsafe { core::str::from_utf8_unchecked(core::slice::from_raw_parts(bytes, len)) };
assert_eq!(s, "Hello World!");
}
}
타깃별 지시문 지원 (Target specific directive support)
DWARF 언와인딩: DWARF 언와인딩 정보를 지원하는 ELF 타깃에서는 다음 지시문이 지원돼요.
.cfi_adjust_cfa_offset .cfi_def_cfa .cfi_def_cfa_offset .cfi_def_cfa_register .cfi_endproc .cfi_escape .cfi_lsda .cfi_offset .cfi_personality .cfi_register .cfi_rel_offset .cfi_remember_state .cfi_restore .cfi_restore_state .cfi_return_column .cfi_same_value .cfi_sections .cfi_signal_frame .cfi_startproc .cfi_undefined .cfi_window_save
구조적 예외 처리 (Structured exception handling): 구조적 예외 처리가 있는 타깃에서는 다음 지시문이 추가로 보장 지원돼요.
.seh_endproc .seh_endprologue .seh_proc .seh_pushreg .seh_savereg .seh_setframe .seh_stackalloc
x86 (32비트·64비트): x86 타깃(32·64비트 모두)에서는 다음 지시문이 추가로 보장 지원돼요.
.nops .code16 .code32 .code64
.code16, .code32, .code64 지시문은 상태를 어셈블리 코드를 나가기 전에 기본값으로 되돌려야만 사용할 수 있어요. 32비트 x86은 기본적으로 .code32, x86_64는 기본적으로 .code64 를 써요.
ARM (32비트): ARM에서는 다음 지시문이 추가로 보장 지원돼요.
.even .fnstart .fnend .save .movsp .code .thumb
더 알아보기 (Learn more)
asm!매크로 (std::arch::asm)naked_asm!매크로 (std::arch::naked_asm)global_asm!매크로 (std::arch::global_asm)- naked 함수 —
naked_asm!을 쓸 수 있는 함수 - unsafe 키워드 — 인라인 어셈블리가 unsafe 문맥인 이유