Встроенный ассемблерный код
Если вам нужно работать на самом низком уровне или повысить производительность
программы, то у вас может возникнуть необходимость управлять процессором
напрямую. Rust поддерживает использование встроенного ассемблера и делает это с
помощью с помощью макроса asm!
. Синтаксис примерно соответствует синтаксису
GCC и Clang:
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]
будут опущены.)
Выходные операнды (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}"
);
# } }
Пробелы и отступы также не имеют значения:
# #![feature(asm)]
# #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
# fn main() { unsafe {
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)
}
Однако, если вы захотите использовать реальные операнды (регистры) в этой
позиции, то вам потребуется заключить используемый регистр в фигурные скобки
{}
, и вы должны будете указать конкретный размер операнда. Это полезно для
очень низкоуровневого программирования, когда важны регистры, которые вы
используете:
# #![feature(asm)]
# #[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
# }
Затираемое (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}");
# } }
Если входные и выходные регистры уже заданы в ограничениях, то их не нужно перечислять здесь. В противном случае, любые другие регистры, используемые явно или неявно, должны быть перечислены.
Если ассемблер изменяет регистр кода условия cc
, то он должен быть указан в
качестве одного из затираемых. Точно так же, если ассемблер модифицирует память,
то должно быть указано memory
.
Опции
Последний раздел, options
, специфичен для Rust. Формат представляет собой
разделенные запятыми текстовые строки (т.е. :"foo", "bar", "baz"
). Он
используется для того, чтобы задать некоторые дополнительные данные для
встроенного ассемблера:
На текущий момент разрешены следующие опции:
-
volatile — эта опция аналогична
__asm__ __volatile__ (...)
в gcc/clang; -
alignstack — некоторые инструкции ожидают, что стек был выровнен определенным образом (т.е. SSE), и эта опция указывает компилятору вставить свой обычный код выравнивания стека;
-
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);
# }
Больше информации
Текущая реализация макроса asm!
--- это прямое связывание с
встроенным ассемблером LLVM, поэтому изучите и их
документацию, чтобы лучше понять список затираемого, ограничения и
др.