Встроенный ассемблерный код

Если вам нужно работать на самом низком уровне или повысить производительность программы, то у вас может возникнуть необходимость управлять процессором напрямую. Rust поддерживает использование встроенного ассемблера и делает это с помощью с помощью макроса asm!. Синтаксис примерно соответствует синтаксису GCC и Clang:

fn main() { asm!(assembly template : output operands : input operands : clobbers : options ); }
asm!(assembly template
   : output operands
   : input operands
   : clobbers
   : options
   );

Использование asm является закрытой возможностью (требуется указать #![feature(asm)] для контейнера, чтобы разрешить ее использование) и, конечно же, требует unsafe блока.

Примечание: здесь примеры приведены для x86/x86-64 ассемблера, но поддерживаются все платформы.

Шаблон инструкции ассемблера

Шаблон инструкции ассемблера (assembly template) является единственным обязательным параметром, и он должен быть представлен строкой символов (т.е. "")

#![feature(asm)] #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] fn foo() { unsafe { asm!("NOP"); } } // other platforms #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] fn foo() { /* ... */ } fn main() { // ... foo(); // ... }
#![feature(asm)]

#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
fn foo() {
    unsafe {
        asm!("NOP");
    }
}

// other platforms
#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
fn foo() { /* ... */ }

fn main() {
    // ...
    foo();
    // ...
}

(Далее атрибуты feature(asm) и #[cfg] будут опущены.)

Выходные операнды (output operands), входные операнды (input operands), затираемое (clobbers) и опции (options) не являются обязательными, но вы должны будете добавить соответствующее количество : если хотите пропустить их:

#![feature(asm)] #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] fn main() { unsafe { asm!("xor %eax, %eax" : : : "{eax}" ); } }
asm!("xor %eax, %eax"
    :
    :
    : "{eax}"
   );

Пробелы и отступы также не имеют значения:

#![feature(asm)] #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] fn main() { unsafe { asm!("xor %eax, %eax" ::: "{eax}"); } }
asm!("xor %eax, %eax" ::: "{eax}");

Операнды

Входные и выходные операнды имеют одинаковый формат: :"ограничение1"(выражение1), "ограничение2"(выражение2), ...". Выражения для выходных операндов должны быть либо изменяемыми, либо неизменяемыми, но еще не иницилиализированными, L-значениями:

#![feature(asm)] #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] fn add(a: i32, b: i32) -> i32 { let c: i32; unsafe { asm!("add $2, $0" : "=r"(c) : "0"(a), "r"(b) ); } c } #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] fn add(a: i32, b: i32) -> i32 { a + b } fn main() { assert_eq!(add(3, 14159), 14162) }
fn add(a: i32, b: i32) -> i32 {
    let c: i32;
    unsafe {
        asm!("add $2, $0"
             : "=r"(c)
             : "0"(a), "r"(b)
             );
    }
    c
}

fn main() {
    assert_eq!(add(3, 14159), 14162)
}

Однако, если вы захотите использовать реальные операнды (регистры) в этой позиции, то вам потребуется заключить используемый регистр в фигурные скобки {}, и вы должны будете указать конкретный размер операнда. Это полезно для очень низкоуровневого программирования, когда важны регистры, которые вы используете:

#![feature(asm)] fn main() { #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] unsafe fn read_byte_in(port: u16) -> u8 { let result: u8; asm!("in %dx, %al" : "={al}"(result) : "{dx}"(port)); result } }
let result: u8;
asm!("in %dx, %al" : "={al}"(result) : "{dx}"(port));
result

Затираемое (Clobbers)

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

#![feature(asm)] #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] fn main() { unsafe { // Put the value 0x200 in eax asm!("mov $$0x200, %eax" : /* no outputs */ : /* no inputs */ : "{eax}"); } }
// Put the value 0x200 in eax
asm!("mov $$0x200, %eax" : /* no outputs */ : /* no inputs */ : "{eax}");

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

Если ассемблер изменяет регистр кода условия cc, то он должен быть указан в качестве одного из затираемых. Точно так же, если ассемблер модифицирует память, то должно быть указано memory.

Опции

Последний раздел, options, специфичен для Rust. Формат представляет собой разделенные запятыми текстовые строки (т.е. :"foo", "bar", "baz"). Он используется для того, чтобы задать некоторые дополнительные данные для встроенного ассемблера:

На текущий момент разрешены следующие опции:

  1. volatile — эта опция аналогична __asm__ __volatile__ (...) в gcc/clang;

  2. alignstack — некоторые инструкции ожидают, что стек был выровнен определенным образом (т.е. SSE), и эта опция указывает компилятору вставить свой обычный код выравнивания стека;

  3. intel — эта опция указывает использовать синтаксис Intel вместо используемого по умолчанию синтаксиса AT&T.

#![feature(asm)] #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] fn main() { let result: i32; unsafe { asm!("mov eax, 2" : "={eax}"(result) : : : "intel") } println!("eax is currently {}", result); }
let result: i32;
unsafe {
   asm!("mov eax, 2" : "={eax}"(result) : : : "intel")
}
println!("eax is currently {}", result);

Больше информации

Текущая реализация макроса asm! --- это прямое связывание с встроенным ассемблером LLVM, поэтому изучите и их документацию, чтобы лучше понять список затираемого, ограничения и др.