Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Встроенная ассемблерная вставка (Inline assembly)

Поддержка встроенного ассемблера предоставляется через макросы asm!, naked_asm! и global_asm!. Она может быть использована для вставки написанного вручную ассемблерного кода в ассемблерный вывод, генерируемый компилятором.

Поддержка встроенного ассемблера стабилизирована на следующих архитектурах:

  • x86 и x86-64
  • ARM
  • AArch64 и Arm64EC
  • RISC-V
  • LoongArch
  • s390x

Компилятор выдаст ошибку, если макрос ассемблера используется на неподдерживаемой цели.

Пример

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
use std::arch::asm;

// Умножаем x на 6, используя сдвиги и сложения
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);
}
}

Синтаксис

Следующая грамматика определяет аргументы, которые могут быть переданы макросам asm!, global_asm! и naked_asm!.

Syntax
AsmArgsFormatString ( , FormatString )* ( , AsmOperand )* ,?

FormatStringSTRING_LITERAL | RAW_STRING_LITERAL | MacroInvocation

AsmOperand
      ClobberAbi
    | AsmOptions
    | RegOperand

ClobberAbiclobber_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? }
    )

ParamNameIDENTIFIER_OR_KEYWORD | RAW_IDENTIFIER

DualDirSpecExpression
      Expression
    | Expression => Expression

RegSpecRegisterClass | ExplicitRegister

RegisterClassIDENTIFIER_OR_KEYWORD

ExplicitRegisterSTRING_LITERAL

DirSpec
      in
    | out
    | lateout

DualDirSpec
      inout
    | inlateout

Область видимости

Встроенный ассемблер может быть использован одним из трех способов.

С макросом asm! ассемблерный код излучается в области видимости функции и интегрируется в сгенерированный компилятором ассемблерный код функции. Этот ассемблерный код должен подчиняться строгим правилам, чтобы избежать неопределенного поведения. Обратите внимание, что в некоторых случаях компилятор может выбрать излучение ассемблерного кода как отдельной функции и генерацию вызова к ней.

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
unsafe { core::arch::asm!("/* {} */", in(reg) 0); }
}
}

С макросом naked_asm! ассемблерный код излучается в области видимости функции и составляет полный ассемблерный код функции. Макрос naked_asm! разрешен только в голых функциях (naked functions).

#![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);

Аргументы строки-шаблона

Шаблон ассемблера использует тот же синтаксис, что и строки форматирования (т.е. заполнители указываются фигурными скобками).

Соответствующие аргументы доступны по порядку, по индексу или по имени.

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i64;
let y: i64;
let z: i64;
// Это
unsafe { core::arch::asm!("mov {}, {}", out(reg) x, in(reg) 5); }
// ... это
unsafe { core::arch::asm!("mov {0}, {1}", out(reg) y, in(reg) 5); }
// ... и это
unsafe { core::arch::asm!("mov {out}, {in}", out = out(reg) z, in = in(reg) 5); }
// все имеют одинаковое поведение
assert_eq!(x, y);
assert_eq!(y, z);
}
}

Однако, неявные именованные аргументы (введенные RFC #2795) не поддерживаются.

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x = 5;
// Мы не можем ссылаться на `x` из области видимости напрямую, нам нужен операнд, такой как `in(reg) x`
unsafe { core::arch::asm!("/* {x} */"); } // ОШИБКА: нет аргумента с именем x
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Тест не поддерживается на этой архитектуре");
}

Вызов asm! может иметь один или несколько аргументов строки-шаблона; asm! с несколькими аргументами строки-шаблона обрабатывается так, как если бы все строки были объединены с \n между ними. Ожидается, что каждый аргумент строки-шаблона соответствует строке ассемблерного кода.

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i64;
let y: i64;
// Мы можем разделить несколько строк, как если бы они были написаны вместе
unsafe { core::arch::asm!("mov eax, 5", "mov ecx, eax", out("rax") x, out("rcx") y); }
assert_eq!(x, y);
}
}

Все аргументы строки-шаблона должны появляться перед любыми другими аргументами.

#![allow(unused)]
fn main() {
let x = 5;
#[cfg(target_arch = "x86_64")] {
// Строки-шаблоны должны появляться первыми в вызове asm
unsafe { core::arch::asm!("/* {x} */", x = const 5, "ud2"); } // ОШИБКА: неожиданная лексема
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Тест не поддерживается на этой архитектуре");
}

Как и в случае со строками форматирования, позиционные аргументы должны появляться перед именованными аргументами и явными операндами регистров.

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// Именованные операнды должны идти после позиционных
unsafe { core::arch::asm!("/* {x} {} */", x = const 5, in(reg) 5); }
// ОШИБКА: позиционные аргументы не могут следовать за именованными аргументами или явными аргументами регистров
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Тест не поддерживается на этой архитектуре");
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// Мы также не можем поместить явные регистры перед позиционными операндами
unsafe { core::arch::asm!("/* {} */", in("eax") 0, in(reg) 5); }
// ОШИБКА: позиционные аргументы не могут следовать за именованными аргументами или явными аргументами регистров
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Тест не поддерживается на этой архитектуре");
}

Явные операнды регистров не могут использоваться заполнителями в строке-шаблоне.

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// Явные операнды регистров не подставляются, используйте `eax` явно в строке
unsafe { core::arch::asm!("/* {} */", in("eax") 5); }
// ОШИБКА: неверная ссылка на аргумент по индексу 0
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Тест не поддерживается на этой архитектуре");
}

Все остальные именованные и позиционные операнды должны появляться по крайней мере один раз в строке-шаблоне, иначе генерируется ошибка компилятора.

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// Мы должны назвать все операнды в строке формата
unsafe { core::arch::asm!("", in(reg) 5, x = const 5); }
// ОШИБКА: несколько неиспользуемых аргументов asm
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Тест не поддерживается на этой архитектуре");
}

Точный синтаксис ассемблерного кода зависит от цели и непрозрачен для компилятора, за исключением способа подстановки операндов в строку-шаблон для формирования кода, передаваемого ассемблеру.

В настоящее время все поддерживаемые цели следуют синтаксису ассемблерного кода, используемому внутренним ассемблером LLVM, который обычно соответствует синтаксису ассемблера GNU (GAS). На x86 по умолчанию используется режим .intel_syntax noprefix GAS. На ARM используется режим .syntax unified. Эти цели накладывают дополнительное ограничение на ассемблерный код: любое состояние ассемблера (например, текущая секция, которую можно изменить с помощью .section) должно быть восстановлено до исходного значения в конце строки asm. Ассемблерный код, не соответствующий синтаксису GAS, приведет к поведению, специфичному для ассемблера. Дальнейшие ограничения на директивы, используемые встроенным ассемблером, указаны в разделе Поддержка директив.

Тип операнда

Поддерживается несколько типов операндов:

  • in(<reg>) <expr>
    • <reg> может ссылаться на класс регистров или явный регистр. Выделенное имя регистра подставляется в строку-шаблон asm.
    • Выделенный регистр будет содержать значение <expr> в начале ассемблерного кода.
    • Выделенный регистр должен содержать то же значение в конце ассемблерного кода (за исключением случая, когда lateout выделен в тот же регистр).
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// ``in` может быть использован для передачи значений во встроенный ассемблер...
unsafe { core::arch::asm!("/* {} */", in(reg) 5); }
}
}
  • out(<reg>) <expr>
    • <reg> может ссылаться на класс регистров или явный регистр. Выделенное имя регистра подставляется в строку-шаблон asm.
    • Выделенный регистр будет содержать неопределенное значение в начале ассемблерного кода.
    • <expr> должно быть (возможно, неинициализированным) lvalue выражением, в которое содержимое выделенного регистра записывается в конце ассемблерного кода.
    • Подчеркивание (_) может быть указано вместо выражения, что приведет к отбрасыванию содержимого регистра в конце ассемблерного кода (эффективно действуя как clobber).
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i64;
// и `out` может быть использован для передачи значений обратно в 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` такой же, как `out`
// но компилятор знает, что нам не важно значение любых входов к тому времени,
// когда мы перезаписываем его.
unsafe { core::arch::asm!("mov {}, 5", lateout(reg) x); }
assert_eq!(x, 5)
}
}
  • inout(<reg>) <expr>
    • <reg> может ссылаться на класс регистров или явный регистр. Выделенное имя регистра подставляется в строку-шаблон asm.
    • Выделенный регистр будет содержать значение <expr> в начале ассемблерного кода.
    • <expr> должно быть изменяемым инициализированным lvalue выражением, в которое содержимое выделенного регистра записывается в конце ассемблерного кода.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let mut x: i64 = 4;
// `inout` может быть использован для модификации значений в регистре
unsafe { core::arch::asm!("inc {}", inout(reg) x); }
assert_eq!(x, 5);
}
}
  • inout(<reg>) <in expr> => <out expr>
    • То же, что и inout, за исключением того, что начальное значение регистра берется из значения <in expr>.
    • <out expr> должно быть (возможно, неинициализированным) lvalue выражением, в которое содержимое выделенного регистра записывается в конце ассемблерного кода.
    • Подчеркивание (_) может быть указано вместо выражения для <out expr>, что приведет к отбрасыванию содержимого регистра в конце ассемблерного кода (эффективно действуя как clobber).
    • <in expr> и <out expr> могут иметь разные типы.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i64;
// `inout` также может перемещать значения в разные места
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` - это `inout`, использующий `lateout`
unsafe { core::arch::asm!("inc {}", inlateout(reg) x); }
assert_eq!(x, 5);
}
}
  • sym <path>
    • <path> должен ссылаться на fn или static.
    • Мангированное имя символа, ссылающееся на элемент, подставляется в строку-шаблон asm.
    • Подставленная строка не включает никакие модификаторы (например, GOT, PLT, перемещения и т.д.).
    • <path> может указывать на #[thread_local] static, в этом случае ассемблерный код может комбинировать символ с перемещениями (например, @plt, @TPOFF) для чтения данных из thread-local storage.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
extern "C" fn foo() {
    println!("Hello from inline assembly")
}
// `sym` может быть использован для ссылки на функцию (даже если у нее нет
// внешнего имени, которое мы можем написать напрямую)
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")] {
// перемешивание [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;
// Передаем постоянное значение в инструкцию, которая ожидает непосредственное значение, как `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. Ассемблерный код может перейти по подставленному адресу.
    • Для целей, которые различают прямые и косвенные переходы (например, x86-64 с включенным cf-protection), ассемблерный код не должен переходить по подставленному адресу косвенно.
    • После выполнения блока выражение asm! возвращается.
    • Тип блока должен быть unit или ! (never).
    • Блок начинает новый контекст безопасности; небезопасные операции внутри блока label должны быть обернуты во внутренний блок unsafe, даже если все выражение asm! уже обернуто в 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 получает свое значение от второго выхода, а не от первого
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() {}
// операнды регистров не разрешены, так как мы не в функции
#[cfg(target_arch = "x86_64")]
core::arch::global_asm!("", in(reg) 5);
// ОШИБКА: операнд `in` не может быть использован с `global_asm!`
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Тест не поддерживается на этой архитектуре");
fn main() {}
fn foo() {}

#[cfg(target_arch = "x86_64")]
// `const` и `sym` оба разрешены, однако
core::arch::global_asm!("/* {} {} */", const 0, sym foo);

Операнды регистров

Входные и выходные операнды могут быть указаны либо как явный регистр, либо как класс регистров, из которого распределитель регистров может выбрать регистр. Явные регистры указываются как строковые литералы (например, "eax"), в то время как классы регистров указываются как идентификаторы (например, reg).

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let mut y: i64;
// Мы можем назвать как `reg`, так и явный регистр, такой как `eax`, чтобы получить
// целочисленный регистр
unsafe { core::arch::asm!("mov eax, {:e}", in(reg) 5, lateout("eax") y); }
assert_eq!(y, 5);
}
}

Обратите внимание, что явные регистры рассматривают псевдонимы регистров (например, r14 против lr на ARM) и меньшие представления регистра (например, eax против rax) как эквивалентные базовому регистру.

Ошибка времени компиляции возникает, если один и тот же явный регистр используется для двух входных операндов или двух выходных операндов.

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// Мы не можем назвать eax дважды
unsafe { core::arch::asm!("", in("eax") 5, in("eax") 4); }
// ОШИБКА: регистр `eax` конфликтует с регистром `eax`
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Тест не поддерживается на этой архитектуре");
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// ... даже используя разные псевдонимы
unsafe { core::arch::asm!("", in("ax") 5, in("rax") 4); }
// ОШИБКА: регистр `rax` конфликтует с регистром `ax`
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Тест не поддерживается на этой архитектуре");
}

Кроме того, ошибка времени компиляции также возникает при использовании перекрывающихся регистров (например, ARM VFP) во входных операндах или в выходных операндах.

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// al перекрывается с ax, поэтому мы не можем назвать их обоих.
unsafe { core::arch::asm!("", in("ax") 5, in("al") 4i8); }
// ОШИБКА: регистр `al` конфликтует с регистром `ax`
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Тест не поддерживается на этой архитектуре");
}

Только следующие типы разрешены в качестве операндов для встроенного ассемблера:

  • Целые числа (знаковые и беззнаковые)
  • Числа с плавающей точкой
  • Указатели (только тонкие)
  • Указатели на функции
  • SIMD векторы (структуры, определенные с #[repr(simd)] и которые реализуют Copy). Это включает специфичные для архитектуры векторные типы, определенные в std::arch, такие как __m128 (x86) или int8x16_t (ARM).
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
extern "C" fn foo() {}

// Целые числа разрешены...
let y: i64 = 5;
unsafe { core::arch::asm!("/* {} */", in(reg) y); }

// и указатели...
let py = &raw const y;
unsafe { core::arch::asm!("/* {} */", in(reg) py); }

// числа с плавающей точкой также...
let f = 1.0f32;
unsafe { core::arch::asm!("/* {} */", in(xmm_reg) f); }

// даже указатели на функции и simd векторы.
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); }
}
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
struct Foo;
let x: Foo = Foo;
// Сложные типы, такие как структуры, не разрешены
unsafe { core::arch::asm!("/* {} */", in(reg) x); }
// ОШИБКА: нельзя использовать значение типа `Foo` для встроенного ассемблера
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Тест не поддерживается на этой архитектуре");
}

Вот список currently поддерживаемых классов регистров:

АрхитектураКласс регистраРегистрыКод ограничения LLVM
x86regax, bx, cx, dx, si, di, bp, r[8-15] (только x86-64)r
x86reg_abcdax, bx, cx, dxQ
x86-32reg_byteal, bl, cl, dl, ah, bh, ch, dhq
x86-64reg_byte*al, bl, cl, dl, sil, dil, bpl, r[8-15]bq
x86xmm_regxmm[0-7] (x86) xmm[0-15] (x86-64)x
x86ymm_regymm[0-7] (x86) ymm[0-15] (x86-64)x
x86zmm_regzmm[0-7] (x86) zmm[0-31] (x86-64)v
x86kregk[1-7]Yk
x86kreg0k0Только clobbers
x86x87_regst([0-7])Только clobbers
x86mmx_regmm[0-7]Только clobbers
x86-64tmm_regtmm[0-7]Только clobbers
AArch64regx[0-30]r
AArch64vregv[0-31]w
AArch64vreg_low16v[0-15]x
AArch64pregp[0-15], ffrТолько clobbers
Arm64ECregx[0-12], x[15-22], x[25-27], x30r
Arm64ECvregv[0-15]w
Arm64ECvreg_low16v[0-15]x
ARM (ARM/Thumb2)regr[0-12], r14r
ARM (Thumb1)regr[0-7]r
ARMsregs[0-31]t
ARMsreg_low16s[0-15]x
ARMdregd[0-31]w
ARMdreg_low16d[0-15]t
ARMdreg_low8d[0-8]x
ARMqregq[0-15]w
ARMqreg_low8q[0-7]t
ARMqreg_low4q[0-3]x
RISC-Vregx1, x[5-7], x[9-15], x[16-31] (не-RV32E)r
RISC-Vfregf[0-31]f
RISC-Vvregv[0-31]Только clobbers
LoongArchreg$r1, $r[4-20], $r[23,30]r
LoongArchfreg$f[0-31]f
s390xregr[0-10], r[12-14]r
s390xreg_addrr[1-10], r[12-14]a
s390xfregf[0-15]f
s390xvregv[0-31]Только clobbers
s390xarega[2-15]Только clobbers

Note

  • На x86 мы обрабатываем reg_byte иначе, чем reg, потому что компилятор может выделить al и ah отдельно, тогда как reg резервирует весь регистр.
  • На x86-64 старшие байтовые регистры (например, ah) недоступны в классе регистров reg_byte.
  • Некоторые классы регистров помечены как “Только clobbers”, что означает, что регистры в этих классах не могут быть использованы для входов или выходов, только clobbers вида out(<explicit register>) _ или lateout(<explicit register>) _.

Каждый класс регистров имеет ограничения на то, с какими типами значений они могут быть использованы. Это необходимо потому, что способ загрузки значения в регистр зависит от его типа. Например, в big-endian системах загрузка i32x4 и i8x16 в SIMD регистр может привести к разному содержимому регистра, даже если байтовое представление в памяти обоих значений идентично. Доступность поддерживаемых типов для конкретного класса регистров может зависеть от того, какие целевые функции в настоящее время включены.

АрхитектураКласс регистраЦелевая функцияРазрешенные типы
x86-32regNonei16, i32, f32
x86-64regNonei16, i32, f32, i64, f64
x86reg_byteNonei8
x86xmm_regssei32, f32, i64, f64,
i8x16, i16x8, i32x4, i64x2, f32x4, f64x2
x86ymm_regavxi32, f32, i64, f64,
i8x16, i16x8, i32x4, i64x2, f32x4, f64x2
i8x32, i16x16, i32x8, i64x4, f32x8, f64x4
x86zmm_regavx512fi32, f32, i64, f64,
i8x16, i16x8, i32x4, i64x2, f32x4, f64x2
i8x32, i16x16, i32x8, i64x4, f32x8, f64x4
i8x64, i16x32, i32x16, i64x8, f32x16, f64x8
x86kregavx512fi8, i16
x86kregavx512bwi32, i64
x86mmx_regN/AТолько clobbers
x86x87_regN/AТолько clobbers
x86tmm_regN/AТолько clobbers
AArch64regNonei8, i16, i32, f32, i64, f64
AArch64vregneoni8, i16, i32, f32, i64, f64,
i8x8, i16x4, i32x2, i64x1, f32x2, f64x1,
i8x16, i16x8, i32x4, i64x2, f32x4, f64x2
AArch64pregN/AТолько clobbers
Arm64ECregNonei8, i16, i32, f32, i64, f64
Arm64ECvregneoni8, i16, i32, f32, i64, f64,
i8x8, i16x4, i32x2, i64x1, f32x2, f64x1,
i8x16, i16x8, i32x4, i64x2, f32x4, f64x2
ARMregNonei8, i16, i32, f32
ARMsregvfp2i32, f32
ARMdregvfp2i64, f64, i8x8, i16x4, i32x2, i64x1, f32x2
ARMqregneoni8x16, i16x8, i32x4, i64x2, f32x4
RISC-V32regNonei8, i16, i32, f32
RISC-V64regNonei8, i16, i32, f32, i64, f64
RISC-Vfregff32
RISC-Vfregdf64
RISC-VvregN/AТолько clobbers
LoongArch32regNonei8, i16, i32, f32
LoongArch64regNonei8, i16, i32, i64, f32, f64
LoongArchfregff32
LoongArchfregdf64
s390xreg, reg_addrNonei8, i16, i32, i64
s390xfregNonef32, f64
s390xvregN/AТолько clobbers
s390xaregN/AТолько clobbers

Note

Для целей приведенной выше таблицы указатели, указатели на функции и isize/usize рассматриваются как эквивалентный целочисленный тип (i16/i32/i64 в зависимости от цели).

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x = 5i32;
let y = -1i8;
let z = unsafe { core::arch::x86_64::_mm_set_epi64x(1, 0) };

// reg действителен для `i32`, `reg_byte` действителен для `i8`, и xmm_reg действителен для `__m128i`
// Мы не можем использовать `tmm0` как вход или выход, но мы можем clobber его.
unsafe { core::arch::asm!("/* {} {} {} */", in(reg) x, in(reg_byte) y, in(xmm_reg) z, out("tmm0") _); }
}
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let z = unsafe { core::arch::x86_64::_mm_set_epi64x(1, 0) };
// Мы не можем передать `__m128i` в `reg` вход
unsafe { core::arch::asm!("/* {} */", in(reg) z); }
// ОШИБКА: тип `__m128i` не может быть использован с этим классом регистров
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Тест не поддерживается на этой архитектуре");
}

Если значение имеет меньший размер, чем регистр, в который оно выделено, то старшие биты этого регистра будут иметь неопределенное значение для входов и будут игнорироваться для выходов. Единственное исключение - класс регистров freg на RISC-V, где значения f32 являются NaN-boxed в f64, как того требует архитектура RISC-V.

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let mut x: i64;
// Перемещаем 32-битное значение в 64-битное значение, упс.
#[allow(asm_sub_register)] // rustc предупреждает об этом поведении
unsafe { core::arch::asm!("mov {}, {}", lateout(reg) x, in(reg) 4i32); }
// старшие 32 бита неопределенны
assert_eq!(x, 4); // Это утверждение не гарантированно выполнится
assert_eq!(x & 0xFFFFFFFF, 4); // Однако это выполнится
}
}

Когда для операнда inout указаны отдельные входное и выходное выражения, оба выражения должны иметь одинаковый тип. Единственное исключение - если оба операнда являются указателями или целыми числами, в этом случае от них требуется только иметь одинаковый размер. Это ограничение существует потому, что распределители регистров в LLVM и GCC иногда не могут обрабатывать связанные операнды с разными типами.

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// Указатели и целые числа могут смешиваться (пока они одного размера)
let x: isize = 0;
let y: *mut ();
// Преобразуем `isize` в `*mut ()`, используя магию встроенного ассемблера
unsafe { core::arch::asm!("/*{}*/", inout(reg) x=>y); }
assert!(y.is_null()); // Очень окольный способ создать нулевой указатель
}
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i32 = 0;
let y: f32;
// Но мы не можем переинтерпретировать `i32` в `f32` таким образом
unsafe { core::arch::asm!("/* {} */", inout(reg) x=>y); }
// ОШИБКА: несовместимые типы для аргумента asm inout
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Тест не поддерживается на этой архитектуре");
}

Имена регистров

Некоторые регистры имеют несколько имен. Все они рассматриваются компилятором как идентичные базовому имени регистра. Вот список всех поддерживаемых псевдонимов регистров:

АрхитектураБазовый регистрПсевдонимы
x86axeax, rax
x86bxebx, rbx
x86cxecx, rcx
x86dxedx, rdx
x86siesi, rsi
x86diedi, rdi
x86bpbpl, ebp, rbp
x86spspl, esp, rsp
x86ipeip, rip
x86st(0)st
x86r[8-15]r[8-15]b, r[8-15]w, r[8-15]d
x86xmm[0-31]ymm[0-31], zmm[0-31]
AArch64x[0-30]w[0-30]
AArch64x29fp
AArch64x30lr
AArch64spwsp
AArch64xzrwzr
AArch64v[0-31]b[0-31], h[0-31], s[0-31], d[0-31], q[0-31]
Arm64ECx[0-30]w[0-30]
Arm64ECx29fp
Arm64ECx30lr
Arm64ECspwsp
Arm64ECxzrwzr
Arm64ECv[0-15]b[0-15], h[0-15], s[0-15], d[0-15], q[0-15]
ARMr[0-3]a[1-4]
ARMr[4-9]v[1-6]
ARMr9rfp
ARMr10sl
ARMr11fp
ARMr12ip
ARMr13sp
ARMr14lr
ARMr15pc
RISC-Vx0zero
RISC-Vx1ra
RISC-Vx2sp
RISC-Vx3gp
RISC-Vx4tp
RISC-Vx[5-7]t[0-2]
RISC-Vx8fp, s0
RISC-Vx9s1
RISC-Vx[10-17]a[0-7]
RISC-Vx[18-27]s[2-11]
RISC-Vx[28-31]t[3-6]
RISC-Vf[0-7]ft[0-7]
RISC-Vf[8-9]fs[0-1]
RISC-Vf[10-17]fa[0-7]
RISC-Vf[18-27]fs[2-11]
RISC-Vf[28-31]ft[8-11]
LoongArch$r0$zero
LoongArch$r1$ra
LoongArch$r2$tp
LoongArch$r3$sp
LoongArch$r[4-11]$a[0-7]
LoongArch$r[12-20]$t[0-8]
LoongArch$r21
LoongArch$r22$fp, $s9
LoongArch$r[23-31]$s[0-8]
LoongArch$f[0-7]$fa[0-7]
LoongArch$f[8-23]$ft[0-15]
LoongArch$f[24-31]$fs[0-7]
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let z = 0i64;
// rax - это псевдоним для eax и ax
unsafe { core::arch::asm!("", in("rax") z); }
}
}

Некоторые регистры не могут быть использованы для входных или выходных операндов:

АрхитектураНеподдерживаемый регистрПричина
Всеsp, r15 (s390x)Указатель стека должен быть восстановлен до своего исходного значения в конце ассемблерного кода или перед переходом к блоку label.
Всеbp (x86), x29 (AArch64 и Arm64EC), x8 (RISC-V), $fp (LoongArch), r11 (s390x)Указатель фрейма не может быть использован как вход или выход.
ARMr7 или r11На ARM указатель фрейма может быть либо r7, либо r11 в зависимости от цели. Указатель фрейма не может быть использован как вход или выход.
Всеsi (x86-32), bx (x86-64), r6 (ARM), x19 (AArch64 и Arm64EC), x9 (RISC-V), $s8 (LoongArch)Это используется внутри LLVM как “базовый указатель” для функций со сложными фреймами стека.
x86ipЭто счетчик команд, не настоящий регистр.
AArch64xzrЭто регистр константного нуля, который не может быть изменен.
AArch64x18Это зарезервированный ОС регистр на некоторых целях AArch64.
Arm64ECxzrЭто регистр константного нуля, который не может быть изменен.
Arm64ECx18Это зарезервированный ОС регистр.
Arm64ECx13, x14, x23, x24, x28, v[16-31], p[0-15], ffrЭто регистры AArch64, которые не поддерживаются для Arm64EC.
ARMpcЭто счетчик команд, не настоящий регистр.
ARMr9Это зарезервированный ОС регистр на некоторых целях ARM.
RISC-Vx0Это регистр константного нуля, который не может быть изменен.
RISC-Vgp, tpЭти регистры зарезервированы и не могут быть использованы как входы или выходы.
LoongArch$r0 или $zeroЭто регистр константного нуля, который не может быть изменен.
LoongArch$r2 или $tpЭто зарезервировано для TLS.
LoongArch$r21Это зарезервировано ABI.
s390xc[0-15]Зарезервировано ядром.
s390xa[0-1]Зарезервировано для системного использования.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// bp зарезервирован
unsafe { core::arch::asm!("", in("bp") 5i32); }
// ОШИБКА: неверный регистр `bp`: указатель фрейма не может быть использован как операнд для встроенного asm
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Тест не поддерживается на этой архитектуре");
}

Регистры указателя фрейма и базового указателя зарезервированы для внутреннего использования LLVM. Хотя операторы asm! не могут явно указывать использование зарезервированных регистров, в некоторых случаях LLVM выделит один из этих зарезервированных регистров для операндов reg. Ассемблерный код, использующий зарезервированные регистры, должен быть осторожен, поскольку операнды reg могут использовать те же регистры.

Модификаторы шаблона

Заполнители могут быть дополнены модификаторами, которые указываются после : в фигурных скобках. Эти модификаторы не влияют на распределение регистров, но изменяют способ форматирования операндов при вставке в строку-шаблон.

Только один модификатор разрешен для каждого заполнителя шаблона.

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// Мы не можем указать и `r`, и `e` одновременно.
unsafe { core::arch::asm!("/* {:er}", in(reg) 5i32); }
// ОШИБКА: модификатор шаблона asm должен быть одним символом
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Тест не поддерживается на этой архитектуре");
}

Поддерживаемые модификаторы являются подмножеством модификаторов аргументов шаблона asm LLVM (и GCC), но не используют те же буквенные коды.

АрхитектураКласс регистраМодификаторПример выводаМодификатор LLVM
x86-32regNoneeaxk
x86-64regNoneraxq
x86-32reg_abcdlalb
x86-64reglalb
x86reg_abcdhahh
x86regxaxw
x86regeeaxk
x86-64regrraxq
x86reg_byteNoneal / ahNone
x86xmm_regNonexmm0x
x86ymm_regNoneymm0t
x86zmm_regNonezmm0g
x86*mm_regxxmm0x
x86*mm_regyymm0t
x86*mm_regzzmm0g
x86kregNonek1None
AArch64/Arm64ECregNonex0x
AArch64/Arm64ECregww0w
AArch64/Arm64ECregxx0x
AArch64/Arm64ECvregNonev0None
AArch64/Arm64ECvregvv0None
AArch64/Arm64ECvregbb0b
AArch64/Arm64ECvreghh0h
AArch64/Arm64ECvregss0s
AArch64/Arm64ECvregdd0d
AArch64/Arm64ECvregqq0q
ARMregNoner0None
ARMsregNones0None
ARMdregNoned0P
ARMqregNoneq0q
ARMqrege / fd0 / d1e / f
RISC-VregNonex1None
RISC-VfregNonef0None
LoongArchregNone$r1None
LoongArchfregNone$f0None
s390xregNone%r0None
s390xreg_addrNone%r1None
s390xfregNone%f0None

Note

  • на ARM e / f: это выводит имя регистра младшего или старшего двойного слова NEON квада (128-битного) регистра.
  • на x86: наше поведение для reg без модификаторов отличается от того, что делает GCC. GCC будет выводить модификатор на основе типа значения операнда, тогда как мы по умолчанию используем полный размер регистра.
  • на x86 xmm_reg: модификаторы LLVM x, t и g еще не реализованы в LLVM (они поддерживаются только GCC), но это должно быть простым изменением.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let mut x = 0x10u16;

// u16::swap_bytes используя `xchg`
// младшая половина `{x}` обозначается как `{x:l}`, а старшая половина как `{x:h}`
unsafe { core::arch::asm!("xchg {x:l}, {x:h}", x = inout(reg_abcd) x); }
assert_eq!(x, 0x1000u16);
}
}

Как указано в предыдущем разделе, передача входного значения меньшего, чем ширина регистра, приведет к тому, что старшие биты регистра будут содержать неопределенные значения. Это не проблема, если встроенный asm обращается только к младшим битам регистра, что можно сделать, используя модификатор шаблона для использования имени подрегистра в ассемблерном коде (например, ax вместо rax). Поскольку это распространенная ошибка, компилятор предложит использовать модификатор шаблона, где это уместно, учитывая тип входа. Если все ссылки на операнд уже имеют модификаторы, то предупреждение для этого операнда подавляется.

Clobbers ABI

Ключевое слово clobber_abi может быть использовано для применения набора clobbers по умолчанию к ассемблерному коду. Это автоматически вставит необходимые ограничения clobbers по мере необходимости для вызова функции с определенным соглашением о вызовах: если соглашение о вызовах не полностью сохраняет значение регистра при вызове, то lateout("...") _ неявно добавляется к списку операндов (где ... заменяется на имя регистра).

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
extern "C" fn foo() -> i32 { 0 }

let z: i32;
// Чтобы вызвать функцию, мы должны проинформировать компилятор, что мы портим
// регистры, сохраняемые вызываемой стороной (callee saved registers)
unsafe { core::arch::asm!("call {}", sym foo, out("rax") z, clobber_abi("C")); }
assert_eq!(z, 0);
}
}

clobber_abi может быть указано любое количество раз. Оно вставит clobber для всех уникальных регистров в объединении всех указанных соглашений о вызовах.

#![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;
// Мы можем даже вызывать несколько функций с разными соглашениями и
// разными сохраняемыми регистрами
unsafe {
    core::arch::asm!(
        "call {}",
        "mov ecx, eax",
        "call {}",
        sym foo,
        sym bar,
        out("rax") z,
        clobber_abi("C")
    );
}
assert_eq!(z, 1);
}
}

Общие выходы классов регистров запрещены компилятором при использовании clobber_abi: все выходы должны указывать явный регистр.

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
extern "C" fn foo(x: i32) -> i32 { 0 }

let z: i32;
// должны использоваться явные регистры, чтобы не перекрыть случайно.
unsafe {
    core::arch::asm!(
        "mov eax, {:e}",
        "call {}",
        out(reg) z,
        sym foo,
        clobber_abi("C")
    );
    // ОШИБКА: asm с `clobber_abi` должен указывать явные регистры для выходов
}
assert_eq!(z, 0);
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Тест не поддерживается на этой архитектуре");
}

Явные выходы регистров имеют приоритет над неявными clobbers, вставленными clobber_abi: clobber будет вставлен для регистра только если этот регистр не используется как выход.

Следующие ABI могут быть использованы с clobber_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" (на не-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]

Note

  • На AArch64 x18 включается в список clobbers только если он не считается зарезервированным регистром на цели.
  • На RISC-V x[16-17] и x[28-31] включаются в список clobbers только если они не считаются зарезервированными регистрами на цели.

Список испорченных регистров для каждого ABI обновляется в rustc по мере того, как архитектуры получают новые регистры: это гарантирует, что clobbers asm! продолжат быть корректными, когда LLVM начнет использовать эти новые регистры в своем сгенерированном коде.

Опции

Флаги используются для дальнейшего влияния на поведение встроенного ассемблерного кода. В настоящее время определены следующие опции:

  • pure: Ассемблерный код не имеет побочных эффектов, должен в конечном счете вернуть управление, и его выходы зависят только от его прямых входов (т.е. самих значений, а не того, на что они указывают) или значений, прочитанных из памяти (если опция nomem также не установлена). Это позволяет компилятору выполнять ассемблерный код меньше раз, чем указано в программе (например, вынося его из цикла), или даже полностью устранить его, если выходы не используются. Опция pure должна быть комбинирована либо с опцией nomem, либо с readonly, иначе выдается ошибка времени компиляции.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i32 = 0;
let z: i32;
// pure может быть использована для оптимизации, предполагая, что ассемблер не имеет побочных эффектов
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, либо readonly должны быть удовлетворены, чтобы указать, разрешено ли
// чтение памяти
unsafe { core::arch::asm!("inc {}", inout(reg) x => z, options(pure)); }
// ОШИБКА: опция `pure` должна быть комбинирована либо с `nomem`, либо с `readonly`
assert_eq!(z, 0);
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Тест не поддерживается на этой архитектуре");
}
  • nomem: Ассемблерный код не читает и не пишет в любую память, доступную вне ассемблерного кода. Это позволяет компилятору кэшировать значения измененных глобальных переменных в регистрах во время выполнения ассемблерного кода, поскольку он знает, что они не читаются и не записываются им. Компилятор также предполагает, что ассемблерный код не выполняет никакой синхронизации с другими потоками, например, через барьеры.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let mut x = 0i32;
let z: i32;
// Доступ к внешней памяти из ассемблера, когда указан `nomem`,
// запрещен
unsafe {
    core::arch::asm!("mov {val:e}, dword ptr [{ptr}]",
        ptr = in(reg) &mut x,
        val = lateout(reg) z,
        options(nomem)
    )
}

// Запись во внешнюю память из ассемблера, когда указан `nomem`,
// также является неопределенным поведением
unsafe {
    core::arch::asm!("mov  dword ptr [{ptr}], {val:e}",
        ptr = in(reg) &mut x,
        val = in(reg) z,
        options(nomem)
    )
}
}
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i32 = 0;
let z: i32;
// Если мы выделяем свою собственную память, такую как через `push`, однако,
// мы все еще можем использовать ее
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: Ассемблерный код не пишет в любую память, доступную вне ассемблерного кода. Это позволяет компилятору кэшировать значения неизмененных глобальных переменных в регистрах во время выполнения ассемблерного кода, поскольку он знает, что они не записываются им. Компилятор также предполагает, что этот ассемблерный код не выполняет никакой синхронизации с другими потоками, например, через барьеры.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let mut x = 0;
// Мы не можем модифицировать внешнюю память, когда указан `readonly`
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;
// Мы все еще можем читать из нее, однако
unsafe {
    core::arch::asm!("mov {x}, qword ptr [{x}]",
        x = inout(reg) &x => z,
        options(readonly)
    );
}
assert_eq!(z, 0);
}
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i64 = 0;
let z: i64;
// То же исключение применяется, как и с nomem.
unsafe {
    core::arch::asm!("push {x}", "add qword ptr [rsp], 1", "pop {x}",
        x = inout(reg) x => z,
        options(readonly)
    );
}
assert_eq!(z, 1);
}
}
  • preserves_flags: Ассемблерный код не модифицирует регистр флагов (определенный в правилах ниже). Это позволяет компилятору избежать перевычисления флагов условий после выполнения ассемблерного кода.
  • noreturn: Ассемблерный код не завершается нормально (не проходит дальше); поведение не определено, если это происходит. Он все еще может переходить к блокам label. Если любой блок label возвращает unit, блок asm! вернет unit. Иначе он вернет ! (never). Как и при вызове функции, которая не возвращает управление, локальные переменные в области видимости не уничтожаются перед выполнением ассемблерного кода.
fn main() -> ! {
#[cfg(target_arch = "x86_64")] {
    // Мы можем использовать инструкцию для прерывания выполнения внутри блока noreturn
    unsafe { core::arch::asm!("ud2", options(noreturn)); }
}
#[cfg(not(target_arch = "x86_64"))] panic!("no return");
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// Вы ответственны за то, чтобы не провалиться за конец блока asm noreturn
unsafe { core::arch::asm!("", options(noreturn)); }
}
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")]
let _: () = unsafe {
    // Вы все еще можете переходить к блоку `label`
    core::arch::asm!("jmp {}", label {
        println!();
    }, options(noreturn));
};
}
  • nostack: Ассемблерный код не помещает данные в стек и не пишет в красную зону стека (если поддерживается целью). Если эта опция не используется, то указатель стека гарантированно будет соответственно выровнен (согласно ABI цели) для вызова функции.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// `push` и `pop` являются UB при использовании с nostack
unsafe { core::arch::asm!("push rax", "pop rax", options(nostack)); }
}
}
  • att_syntax: Эта опция действительна только на x86 и заставляет ассемблер использовать режим .att_syntax prefix ассемблера GNU. Операнды регистров подставляются с ведущим %.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i32;
let y = 1i32;
// Нам нужно использовать AT&T Syntax здесь. Порядок операндов: src, dest
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: Это заставляет строку-шаблон разбираться как сырая ассемблерная строка, без специальной обработки { и }. Это в первую очередь полезно при включении сырого ассемблерного кода из внешнего файла с помощью include_str!.

Компилятор выполняет некоторые дополнительные проверки опций:

  • Опции nomem и readonly взаимно исключают друг друга: ошибка времени компиляции возникает, если указаны обе.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// nomem строго сильнее, чем readonly, они не могут быть указаны вместе
unsafe { core::arch::asm!("", options(nomem, readonly)); }
// ОШИБКА: опции `nomem` и `readonly` взаимно исключают друг друга
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Тест не поддерживается на этой архитектуре");
}
  • Ошибка времени компиляции возникает, если указать pure на блок asm без выходов или только с отброшенными выходами (_).
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// pure блоки нуждаются по крайней мере в одном выходе
unsafe { core::arch::asm!("", options(pure)); }
// ОШИБКА: asm с опцией `pure` должен иметь по крайней мере один выход
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Тест не поддерживается на этой архитектуре");
}
  • Ошибка времени компиляции возникает, если указать noreturn на блок asm с выходами и без меток.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let z: i32;
// noreturn не может иметь выходы
unsafe { core::arch::asm!("mov {:e}, 1", out(reg) z, options(noreturn)); }
// ОШИБКА: выходы asm не разрешены с опцией `noreturn`
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Тест не поддерживается на этой архитектуре");
}
  • Ошибка времени компиляции возникает, если есть какие-либо блоки label в блоке asm с выходами.

naked_asm! поддерживает только опции att_syntax и raw. Оставшиеся опции не имеют смысла, потому что встроенный ассемблер определяет все тело функции.

global_asm! поддерживает только опции att_syntax и raw. Оставшиеся опции не имеют смысла для встроенного ассемблера в глобальной области видимости.

fn main() {}
#[cfg(target_arch = "x86_64")]
// nomem бесполезна на global_asm!
core::arch::global_asm!("", options(nomem));
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Тест не поддерживается на этой архитектуре");

Правила для встроенного ассемблера

Чтобы избежать неопределенного поведения, эти правила должны соблюдаться при использовании встроенного ассемблера в области видимости функции (asm!):

  • Любые регистры, не указанные как входы, будут содержать неопределенное значение при входе в ассемблерный код.
    • “Неопределенное значение” в контексте встроенного ассемблера означает, что регистр может (недетерминированно) иметь любое из возможных значений, разрешенных архитектурой. В частности, это не то же самое, что LLVM undef, который может иметь разное значение каждый раз, когда вы его читаете (поскольку такая концепция не существует в ассемблерном коде).
  • Любые регистры, не указанные как выходы, должны иметь то же значение при выходе из ассемблерного кода, что и при входе, иначе поведение не определено.
    • Это применяется только к регистрам, которые могут быть указаны как вход или выход. Другие регистры следуют целеспецифичным правилам.
    • Обратите внимание, что lateout может быть выделен в тот же регистр, что и in, в этом случае это правило не применяется. Однако код не должен полагаться на это, поскольку это зависит от результатов распределения регистров.
  • Поведение не определено, если выполнение раскручивается (unwinds) из ассемблерного кода.
    • Это также применяется, если ассемблерный код вызывает функцию, которая затем раскручивается.
  • Набор мест памяти, которые ассемблерный код может читать и писать, тот же, что и разрешенный для функции FFI.
    • Если установлена опция readonly, то разрешены только чтения из памяти.
    • Если установлена опция nomem, то никакие чтения или записи в память не разрешены.
    • Эти правила не применяются к памяти, которая является приватной для ассемблерного кода, такой как пространство стека, выделенное внутри него.
  • Компилятор не может предполагать, что инструкции в ассемблерном коде - это те, которые будут фактически выполнены.
    • Это effectively означает, что компилятор должен рассматривать ассемблерный код как черный ящик и принимать во внимание только спецификацию интерфейса, а не сами инструкции.
    • Разрешается патчинг кода во время выполнения через целеспецифичные механизмы.
    • Однако нет гарантии, что каждый блок ассемблерного кода в исходнике напрямую соответствует единственному экземпляру инструкций в объектном файле; компилятор свободен дублировать или дедуплицировать ассемблерный код в блоках asm!.
  • Если опция nostack не установлена, ассемблерному коду разрешено использовать пространство стека ниже указателя стека.
    • При входе в ассемблерный код указатель стека гарантированно будет соответственно выровнен (согласно ABI цели) для вызова функции.
    • Вы ответственны за то, чтобы не переполнить стек (например, используйте probing стека, чтобы гарантировать, что вы попадете на guard page).
    • Вы должны ajustить указатель стека при выделении памяти в стеке, как того требует ABI цели.
    • Указатель стека должен быть восстановлен до своего исходного значения перед выходом из ассемблерного кода.
  • Если установлена опция noreturn, то поведение не определено, если выполнение проходит через конец ассемблерного кода.
  • Если установлена опция pure, то поведение не определено, если asm! имеет побочные эффекты, отличные от его прямых выходов. Поведение также не определено, если два выполнения кода asm! с одинаковыми входами приводят к разным выходам.
    • При использовании с опцией 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)
      • Флаг насыщения в CPSR (Q)
      • Флаги “больше или равно” в CPSR (GE).
      • Флаги условий в FPSCR (N, Z, C, V)
      • Флаг насыщения в FPSCR (QC)
      • Флаги исключений с плавающей точкой в FPSCR (IDC, IXC, UFC, OFC, DZC, IOC).
    • AArch64 и Arm64EC
      • Флаги условий (регистр NZCV).
      • Состояние с плавающей точкой (регистр FPSR).
    • RISC-V
      • Флаги исключений с плавающей точкой в fcsr (fflags).
      • Состояние расширения векторов (vtype, vl, vxsat и vxrm).
    • LoongArch
      • Флаги условий с плавающей точкой в $fcc[0-7].
    • s390x
      • Регистр кода условия cc.
  • На x86 флаг направления (DF в EFLAGS) очищен при входе в ассемблерный код и должен быть очищен при выходе.
    • Поведение не определено, если флаг направления установлен при выходе из ассемблерного кода.
  • На x86 стек регистров с плавающей точкой x87 должен оставаться неизменным, если все регистры st([0-7]) не были помечены как испорченные с out("st(0)") _, out("st(1)") _, ....
    • Если все регистры x87 испорчены, то стек регистров x87 гарантированно пуст при входе в ассемблерный код. Ассемблерный код должен гарантировать, что стек регистров x87 также пуст при выходе из ассемблерного кода.
#[cfg(target_arch = "x86_64")]
pub fn fadd(x: f64, y: f64) -> f64 {
  let mut out = 0f64;
  let mut top = 0u16;
  // мы можем делать сложные вещи с x87, если мы портим весь стек x87
  unsafe { core::arch::asm!(
    "fld qword ptr [{x}]",
    "fld qword ptr [{y}])",
    "faddp",
    "fstp qword ptr [{out}]",
    "xor eax, eax",
    "fstsw ax",
    "shl eax, 11",
    x = in(reg) &x,
    y = in(reg) &y,
    out = in(reg) &mut out,
    out("st(0)") _, out("st(1)") _, out("st(2)") _, out("st(3)") _,
    out("st(4)") _, out("st(5)") _, out("st(6)") _, out("st(7)") _,
    out("eax") top
  );}

  assert_eq!(top & 0x7, 0);
  out
}

pub fn main() {
#[cfg(target_arch = "x86_64")]{
  assert_eq!(fadd(1.0, 1.0), 2.0);
}
}
  • Требование восстановления указателя стека и невыходных регистров до их исходного значения применяется только при выходе из ассемблерного кода.
    • Это означает, что ассемблерный код, который не завершается нормально и не переходит к каким-либо блокам label, даже если не помечен noreturn, не нуждается в сохранении этих регистров.
    • При возврате в ассемблерный код другого блока asm!, чем тот, в который вы вошли (например, для переключения контекста), эти регистры должны содержать значение, которое они имели при входе в блок asm!, который вы покидаете.
      • Вы не можете выйти из ассемблерного кода блока asm!, который не был введен. Вы также не можете выйти из ассемблерного кода блока asm!, чей ассемблерный код уже был покинут (без предварительного входа в него снова).
      • Вы ответственны за переключение любого целеспецифичного состояния (например, thread-local storage, границы стека).
      • Вы не можете перейти с адреса в одном блоке asm! на адрес в другом, даже в пределах той же функции или блока, без обработки их контекстов как потенциально разных и требующих переключения контекста. Вы не можете предполагать, что какое-либо конкретное значение в этих контекстах (например, текущий указатель стека или временные значения ниже указателя стека) останется неизменным между двумя блоками asm!.
      • Набор мест памяти, к которым вы можете получить доступ, является пересечением тех, которые разрешены блоками asm!, которые вы ввели и покинули.
  • Вы не можете предполагать, что два блока asm!, смежные в исходном коде, даже без любого другого кода между ними, окажутся в последовательных адресах в бинарном файле без каких-либо других инструкций между ними.
  • Вы не можете предполагать, что блок asm! появится ровно один раз в выходном бинарном файле. Компилятору разрешено создавать несколько копий блока asm!, например, когда содержащая его функция встраивается в нескольких местах.
  • На x86, встроенный ассемблер не должен заканчиваться префиксом инструкции (таким как LOCK), который применялся бы к инструкциям, сгенерированным компилятором.
    • Компилятор в настоящее время не может обнаружить это из-за способа компиляции встроенного ассемблера, но может поймать и отклонить это в будущем.

Note

Как общее правило, флаги, покрываемые preserves_flags, - это те, которые не сохраняются при выполнении вызова функции.

Правила для голого встроенного ассемблера

Чтобы избежать неопределенного поведения, эти правила должны соблюдаться при использовании встроенного ассемблера в области видимости функции в голых функциях (naked_asm!):

  • Любые регистры, не используемые для входов функции согласно соглашению о вызовах и сигнатуре функции, будут содержать неопределенное значение при входе в блок naked_asm!.
    • “Неопределенное значение” в контексте встроенного ассемблера означает, что регистр может (недетерминированно) иметь любое из возможных значений, разрешенных архитектурой. В частности, это не то же самое, что LLVM undef, который может иметь разное значение каждый раз, когда вы его читаете (поскольку такая концепция не существует в ассемблерном коде).
  • Все регистры, сохраняемые вызываемой стороной (callee-saved), должны иметь то же значение при возврате, что и при входе.
  • Регистры, сохраняемые вызывающей стороной (caller-saved), могут использоваться свободно.
  • Поведение не определено, если выполнение проходит за конец ассемблерного кода.
    • Ожидается, что каждый путь через ассемблерный код завершается инструкцией возврата или расходится.
  • Набор мест памяти, которые ассемблерный код может читать и писать, тот же, что и разрешенный для функции FFI.
  • Компилятор не может предполагать, что инструкции в блоке naked_asm! - это те, которые будут фактически выполнены.
    • Это effectively означает, что компилятор должен рассматривать naked_asm! как черный ящик и принимать во внимание только спецификацию интерфейса, а не сами инструкции.
    • Разрешается патчинг кода во время выполнения через целеспецифичные механизмы.
  • Раскрутка (unwinding) из блока naked_asm! разрешена.
    • Для корректного поведения должны использоваться соответствующие ассемблерные директивы, которые излучают метаданные раскрутки.
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
#[unsafe(naked)]
extern "sysv64-unwind" fn unwinding_naked() {
    core::arch::naked_asm!(
        // "CFI" здесь означает "call frame information".
        ".cfi_startproc",
        // CFA (canonical frame address) - это значение `rsp`
        // до `call`, т.е. до того, как обратный адрес, `rip`,
        // был помещен в `rsp`, так что оно на восемь байт выше в памяти,
        // чем `rsp` при входе в функцию (после того, как `rip` был
        // помещен).
        //
        // Это значение по умолчанию, поэтому нам не нужно его писать.
        //".cfi_def_cfa rsp, 8",
        //
        // Традиционно сохранять базовый указатель,
        // так что мы сделаем это.
        "push rbp",
        // Поскольку мы теперь расширили стек вниз на 8 байт в
        // памяти, нам нужно ajustить смещение до CFA от `rsp`
        // еще на 8 байт.
        ".cfi_adjust_cfa_offset 8",
        // Мы также аннотируем, где мы сохранили значение вызывающей стороны
        // `rbp`, относительно CFA, так что при раскрутке в
        // вызывающую сторону мы можем найти его, на случай, если нам нужно вычислить
        // CFA вызывающей стороны относительно него.
        //
        // Здесь мы сохранили `rbp` вызывающей стороны, начиная с 16 байт
        // ниже CFA. Т.е., начиная с CFA, сначала идет
        // `rip` (который начинается на 8 байт ниже CFA и продолжается
        // до него), затем идет `rbp` вызывающей стороны, который мы только что
        // поместили.
        ".cfi_offset rbp, -16",
        // Как традиционно, мы устанавливаем базовый указатель в значение
        // указателя стека. Таким образом, базовый указатель остается
        // тем же самым на протяжении тела функции.
        "mov rbp, rsp",
        // Теперь мы можем отслеживать смещение до CFA от базового
        // указателя. Это означает, что нам не нужно делать дальнейшие
        // ajustments до конца, так как мы не меняем `rbp`.
        ".cfi_def_cfa_register rbp",
        // Теперь мы можем вызвать функцию, которая может паниковать.
        "call {f}",
        // При возврате мы восстанавливаем `rbp` при подготовке к возврату
        // сами.
        "pop rbp",
        // Теперь, когда мы восстановили `rbp`, мы должны снова указать смещение
        // до CFA в терминах `rsp`.
        ".cfi_def_cfa rsp, 8",
        // Теперь мы можем вернуться.
        "ret",
        ".cfi_endproc",
        f = sym may_panic,
    )
}

extern "sysv64-unwind" fn may_panic() {
    panic!("unwind");
}
}
}

Note

Для получения дополнительной информации о директивах ассемблера cfi выше см. эти ресурсы:

Корректность и валидность

В дополнение ко всем предыдущим правилам, строковый аргумент asm! должен в конечном счете стать— после того, как все другие аргументы оценены, форматирование выполнено и операнды переведены— ассемблерным кодом, который является как синтаксически корректным, так и семантически валидным для целевой архитектуры. Правила форматирования позволяют компилятору генерировать ассемблерный код с правильным синтаксисом. Правила, касающиеся операндов, permit валидный перевод операндов Rust в ассемблерный код и из него. Соблюдение этих правил необходимо, но не достаточно, чтобы окончательный расширенный ассемблерный код был как корректным, так и валидным. Например:

  • аргументы могут быть помещены в позиции, которые синтаксически некорректны после форматирования
  • инструкция может быть правильно написана, но иметь архитектурно невалидные операнды
  • архитектурно неспецифицированная инструкция может быть ассемблирована в неспецифицированный код
  • набор инструкций, каждая корректная и валидная, может вызвать неопределенное поведение, если помещены в непосредственной последовательности

Как результат, эти правила неисчерпывающи. Компилятор не обязан проверять корректность и валидность исходной строки ни окончательного сгенерированного ассемблерного кода. Ассемблер может проверять на корректность и валидность, но не обязан делать это. При использовании asm!, опечатка может быть достаточной, чтобы сделать программу некорректной (unsound), и правила для ассемблера могут включать тысячи страниц архитектурных справочных руководств. Программисты должны проявлять соответствующую осторожность, так как вызов этой unsafe возможности сопровождается принятием на себя ответственности за несоблюдение правил как компилятора, так и архитектуры.

Поддержка директив

Встроенный ассемблер поддерживает подмножество директив, поддерживаемых как 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!");
}
}

Поддержка целеспецифичных директив

Раскрутка DWARF

Следующие директивы поддерживаются на целях ELF, которые поддерживают информацию о раскрутке DWARF:

  • .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
Структурированная обработка исключений

На целях со структурированной обработкой исключений, следующие дополнительные директивы гарантированно поддерживаются:

  • .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
  • .thumb_func