Per manipolazioni di livello estremamente basso e per ragioni di prestazioni,
qualcuno potrebbe desiderare di controllare direttamente la CPU. Rust consente
di farlo scrivendo codice assembly inline, tramite la macro asm!
.
asm!(template di assembly : operandi di output : operandi di input : clobber : opzioni );
Ogni uso di asm
è una caratteristica attivata da un gate (cioè richiede
#![feature(asm)]
sul crate per consentirla) e naturalmente richiede
un blocco unsafe
.
Nota: qui gli esempi sono dati nell'assembly x86/x86-64, però sono supportate tutte le piattaforme.
Il "template di assembly" è l'unico parametro obbligatorio e deve essere
una stringa letterale (per es. ""
)
#![feature(asm)] #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] fn foo() { unsafe { asm!("NOP"); } } // altre piattaforme #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] fn foo() { /* ... */ } fn main() { // ... foo(); // ... }
(Da qui in avanti, le istruzioni feature(asm)
e #[cfg]
saranno omesse.)
Gli operandi di output, gli operandi di input, i clobber e le opzioni sono
tutti facoltativi, ma si devono sempre mettere i relativi caratteri :
:
asm!("xor %eax, %eax" : : : "eax" );
Anche gli spazi di separazione non hanno importanza:
#![feature(asm)] #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] fn main() { unsafe { asm!("xor %eax, %eax" ::: "eax"); } }asm!("xor %eax, %eax" ::: "eax");
Gli operandi di input e di output hanno lo stesso formato:
: "constraints1"(expr1), "constraints2"(expr2), ..."
.
Le espressioni degli operandi di output devono essere l-value mutabili,
o non ancora assegnati:
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) }
Tuttavia, se si desiderano usare veri operandi in questa posizione,
si devono mettere delle graffe {}
intorno al registro che si desidera, e
si deve mettere la dimensione specifica dell'operando. Questo è utile
per la programmazione a bassissimo livello, in cui è importante
quale registro si usa:
let result: u8; asm!("in %dx, %al" : "={al}"(result) : "{dx}"(port)); result
Alcune istruzioni modificano i valori di alcuni registri, i quali potrebbero altrimenti contenere valori diversi, e quindi si usa la lista dei clobber per indicate al compilatore di non assumere che i valori caricati in quei registri rimangano validi.
#![feature(asm)] #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] fn main() { unsafe { // Metti il valore 0x200 in eax asm!("mov $$0x200, %eax" : /* nessun output */ : /* nessun input */ : "eax"); } }// Metti il valore 0x200 in eax asm!("mov $$0x200, %eax" : /* nessun output */ : /* nessun input */ : "eax");
I registri di input e di output non devono essere elencati dato che tale informazione è già comunicat dai relativi vincoli. Invece, ogni altro registro usato implicitamente o esplicitamente dovrebbe essere elencato.
Se il codice assembly modifica il codice di condizione, il registro cc
dovrebbe essere specificato come uno dei clobber. Similmente, se il codice
assembly modifica la memoria, si dovrebbe specificare anche memory
.
L'ultima sezione, opzioni
è specifica di Rust. Il formato è una sequenza
di stringhe letterali separate da virgole (per es. :"foo", "bar", "baz"
).
Serve a specificare alcune informazioni aggiuntive riguardo l'assembly inline:
Le opzioni attualmente valide sono:
__asm__ __volatile__ (...)
in gcc/clang.let result: i32; unsafe { asm!("mov eax, 2" : "={eax}"(result) : : : "intel") } println!("eax è attualmente {}", result);
L'attuale implementazione della macro asm!
è un legame diretto alle
espressioni assembler inline di LLVM, quindi ci si deve
assicurare di leggere anche la loro documentazione per avere
ulteriori informazioni sui clobbers, i vincoli, ecc.