인라인 어셈블리

인라인 어셈블리 (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)

인라인 어셈블리 템플릿 문자열과 피연산자에는 의미적으로 cfgcfg_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 에 할당된 레지스터를 재사용할 수 있어요(컴파일러가 ininlateout과 같은 초기 값을 가진다는 걸 알 때 일어날 수 있음).
  • 모든 입력이 읽힌 후에만 레지스터에 써야 해요.
#![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! 은 함수 본문 전체를 정의하고 컴파일러가 피연산자를 처리할 추가 코드를 출력할 수 없기 때문에, symconst 피연산자만 쓸 수 있어요. global_asm! 도 함수 밖에 존재하므로 symconst만 쓸 수 있어요.

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에서는 컴파일러가 alah를 따로 할당할 수 있는 반면 reg는 전체 레지스터를 예약하므로, reg_bytereg과 다르게 취급해요.
  • x86-64의 reg_byte 레지스터 클래스에서는 높은 바이트 레지스터(예: ah)를 쓸 수 없어요.
  • "Only clobbers"로 표시된 레지스터 클래스는 입력·출력에 쓸 수 없고, out(<explicit register>) _ 또는 lateout(<explicit register>) _ 형태의 clobber로만 쓸 수 있어요.
  • spe_acc 레지스터는 PowerPC SPE 타깃에서만 쓸 수 있어요.

각 레지스터 클래스는 함께 쓸 수 있는 값 타입에 제약이 있어요. 값이 레지스터에 로드되는 방식이 타입에 달려 있기 때문이에요. 예를 들어 빅엔디언 시스템에서 i32x4i8x16을 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 어셈블리 코드를 포함할 때 주로 유용해요.

컴파일러는 옵션에 대해 몇 가지 추가 검사를 해요.

  • nomemreadonly상호 배타적이에요. 둘 다 지정하면 컴파일 타임 오류예요.
  • 출력이 없거나 버려지는 출력(_)만 있는 asm 블록에 pure 를 지정하는 것은 컴파일 타임 오류예요.
  • 출력이 있고 레이블이 없는 asm 블록에 noreturn 을 지정하는 것은 컴파일 타임 오류예요.
  • 출력이 있는 asm 블록에 어떤 label 블록이 있는 것도 컴파일 타임 오류예요.

naked_asm!att_syntaxraw 옵션만 지원해요. 나머지 옵션은 인라인 어셈블리가 함수 본문 전체를 정의하므로 의미가 없어요. global_asm!att_syntaxraw만 지원해요.

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예요. 이 규칙은 입력이나 출력으로 지정할 수 있는 레지스터에만 적용돼요. lateoutin과 같은 레지스터에 할당되면 이 규칙이 적용되지 않지만, 레지스터 할당 결과에 달려 있으므로 의존하면 안 돼요.
  • 어셈블리 코드 밖으로 실행이 언와인딩(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에서 방향 플래그(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 어셈블러 지시문에 대한 자세한 내용은 Using as - 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)