Variabili di registro esplicite
https://gcc.gnu.org/onlinedocs/gcc-8.2.0/gcc/Explicit-Register-Variables.html#Explicit-Reg-Vars)
Credo che questo dovrebbe ora essere generalmente l'approccio consigliato rispetto ai vincoli di registro perché:
- può rappresentare tutti i registri, incluso
r8
,r9
er10
che vengono utilizzati per gli argomenti delle chiamate di sistema:come specificare i vincoli di registro sul registro Intel x86_64 da r8 a r15 nell'assembly inline GCC? - è l'unica opzione ottimale per altri ISA oltre a x86 come ARM, che non hanno i nomi dei vincoli di registro magici:come specificare un singolo registro come vincolo nell'assembly in linea ARM GCC? (oltre a utilizzare un registro temporaneo + clobbers + e un'istruzione extra mov)
- Sosterrò che questa sintassi è più leggibile rispetto all'uso di mnemonici a lettera singola come
S -> rsi
Le variabili di registro sono usate per esempio in glibc 2.29, vedi:sysdeps/unix/sysv/linux/x86_64/sysdep.h
.
main_reg.c
#define _XOPEN_SOURCE 700
#include <inttypes.h>
#include <sys/types.h>
ssize_t my_write(int fd, const void *buf, size_t size) {
register int64_t rax __asm__ ("rax") = 1;
register int rdi __asm__ ("rdi") = fd;
register const void *rsi __asm__ ("rsi") = buf;
register size_t rdx __asm__ ("rdx") = size;
__asm__ __volatile__ (
"syscall"
: "+r" (rax)
: "r" (rdi), "r" (rsi), "r" (rdx)
: "rcx", "r11", "memory"
);
return rax;
}
void my_exit(int exit_status) {
register int64_t rax __asm__ ("rax") = 60;
register int rdi __asm__ ("rdi") = exit_status;
__asm__ __volatile__ (
"syscall"
: "+r" (rax)
: "r" (rdi)
: "rcx", "r11", "memory"
);
}
void _start(void) {
char msg[] = "hello world\n";
my_exit(my_write(1, msg, sizeof(msg)) != sizeof(msg));
}
GitHub a monte.
Compila ed esegui:
gcc -O3 -std=c99 -ggdb3 -ffreestanding -nostdlib -Wall -Werror \
-pedantic -o main_reg.out main_reg.c
./main.out
echo $?
Uscita
hello world
0
Per confronto, quanto segue è analogo a How to invoke a system call tramite syscall o sysenter in inline assembly? produce un assembly equivalente:
main_constraint.c
#define _XOPEN_SOURCE 700
#include <inttypes.h>
#include <sys/types.h>
ssize_t my_write(int fd, const void *buf, size_t size) {
ssize_t ret;
__asm__ __volatile__ (
"syscall"
: "=a" (ret)
: "0" (1), "D" (fd), "S" (buf), "d" (size)
: "rcx", "r11", "memory"
);
return ret;
}
void my_exit(int exit_status) {
ssize_t ret;
__asm__ __volatile__ (
"syscall"
: "=a" (ret)
: "0" (60), "D" (exit_status)
: "rcx", "r11", "memory"
);
}
void _start(void) {
char msg[] = "hello world\n";
my_exit(my_write(1, msg, sizeof(msg)) != sizeof(msg));
}
GitHub a monte.
Smontaggio di entrambi con:
objdump -d main_reg.out
è quasi identico, ecco il main_reg.c
uno:
Disassembly of section .text:
0000000000001000 <my_write>:
1000: b8 01 00 00 00 mov $0x1,%eax
1005: 0f 05 syscall
1007: c3 retq
1008: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
100f: 00
0000000000001010 <my_exit>:
1010: b8 3c 00 00 00 mov $0x3c,%eax
1015: 0f 05 syscall
1017: c3 retq
1018: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
101f: 00
0000000000001020 <_start>:
1020: c6 44 24 ff 00 movb $0x0,-0x1(%rsp)
1025: bf 01 00 00 00 mov $0x1,%edi
102a: 48 8d 74 24 f3 lea -0xd(%rsp),%rsi
102f: 48 b8 68 65 6c 6c 6f movabs $0x6f77206f6c6c6568,%rax
1036: 20 77 6f
1039: 48 89 44 24 f3 mov %rax,-0xd(%rsp)
103e: ba 0d 00 00 00 mov $0xd,%edx
1043: b8 01 00 00 00 mov $0x1,%eax
1048: c7 44 24 fb 72 6c 64 movl $0xa646c72,-0x5(%rsp)
104f: 0a
1050: 0f 05 syscall
1052: 31 ff xor %edi,%edi
1054: 48 83 f8 0d cmp $0xd,%rax
1058: b8 3c 00 00 00 mov $0x3c,%eax
105d: 40 0f 95 c7 setne %dil
1061: 0f 05 syscall
1063: c3 retq
Quindi vediamo che GCC ha integrato quelle minuscole funzioni di chiamata di sistema come si desidera.
my_write
e my_exit
sono gli stessi per entrambi, ma _start
in main_constraint.c
è leggermente diverso:
0000000000001020 <_start>:
1020: c6 44 24 ff 00 movb $0x0,-0x1(%rsp)
1025: 48 8d 74 24 f3 lea -0xd(%rsp),%rsi
102a: ba 0d 00 00 00 mov $0xd,%edx
102f: 48 b8 68 65 6c 6c 6f movabs $0x6f77206f6c6c6568,%rax
1036: 20 77 6f
1039: 48 89 44 24 f3 mov %rax,-0xd(%rsp)
103e: b8 01 00 00 00 mov $0x1,%eax
1043: c7 44 24 fb 72 6c 64 movl $0xa646c72,-0x5(%rsp)
104a: 0a
104b: 89 c7 mov %eax,%edi
104d: 0f 05 syscall
104f: 31 ff xor %edi,%edi
1051: 48 83 f8 0d cmp $0xd,%rax
1055: b8 3c 00 00 00 mov $0x3c,%eax
105a: 40 0f 95 c7 setne %dil
105e: 0f 05 syscall
1060: c3 retq
È interessante osservare che in questo caso GCC ha trovato una codifica equivalente leggermente più breve selezionando:
104b: 89 c7 mov %eax,%edi
per impostare il fd
a 1
, che equivale a 1
dal numero syscall, piuttosto che un più diretto:
1025: bf 01 00 00 00 mov $0x1,%edi
Per una discussione approfondita delle convenzioni di chiamata, vedere anche:Quali sono le convenzioni di chiamata per le chiamate di sistema UNIX e Linux (e le funzioni dello spazio utente) su i386 e x86-64
Testato in Ubuntu 18.10, GCC 8.2.0.
Prima di tutto, non puoi tranquillamente usare GNU C Basic asm("");
sintassi per questo (senza vincoli di input/output/clobber). Hai bisogno di Extended asm per comunicare al compilatore i registri che modifichi. Vedere l'asm inline nel manuale GNU C e il wiki tag inline-assembly per collegamenti ad altre guide per dettagli su cose come "D"(1)
significa come parte di un asm()
dichiarazione.
Ti serve anche asm volatile
perché questo non è implicito per Extended asm
istruzioni con 1 o più operandi di output.
Ti mostrerò come eseguire chiamate di sistema scrivendo un programma che scriva Hello World!
allo standard output utilizzando il write()
chiamata di sistema. Ecco il sorgente del programma senza un'implementazione della vera e propria chiamata di sistema :
#include <sys/types.h>
ssize_t my_write(int fd, const void *buf, size_t size);
int main(void)
{
const char hello[] = "Hello world!\n";
my_write(1, hello, sizeof(hello));
return 0;
}
Puoi vedere che ho chiamato la mia funzione di chiamata di sistema personalizzata come my_write
per evitare conflitti di nome con il "normale" write
, fornito da libc. Il resto di questa risposta contiene la fonte di my_write
per i386 e amd64.
i386
Le chiamate di sistema in i386 Linux sono implementate utilizzando il 128esimo vettore di interrupt, ad es. chiamando int 0x80
nel tuo codice assembly, avendo impostato i parametri di conseguenza in anticipo, ovviamente. È possibile fare lo stesso tramite SYSENTER
, ma in realtà l'esecuzione di questa istruzione è ottenuta dal VDSO mappato virtualmente a ciascun processo in esecuzione. Da SYSENTER
non è mai stato inteso come una sostituzione diretta del int 0x80
API, non viene mai eseguito direttamente dalle applicazioni userland - invece, quando un'applicazione ha bisogno di accedere a del codice del kernel, chiama la routine virtualmente mappata nel VDSO (questo è ciò che il call *%gs:0x10
nel tuo codice è for), che contiene tutto il codice che supporta SYSENTER
istruzione. Ce n'è parecchio a causa di come funziona effettivamente l'istruzione.
Se vuoi saperne di più, dai un'occhiata a questo link. Contiene una panoramica abbastanza breve delle tecniche applicate nel kernel e nel VDSO. Vedi anche The Definitive Guide to (x86) Linux System Calls - alcune chiamate di sistema come getpid
e clock_gettime
sono così semplici che il kernel può esportare codice + dati che vengono eseguiti nello spazio utente, quindi il VDSO non ha mai bisogno di entrare nel kernel, rendendolo molto più veloce anche di sysenter
potrebbe essere.
È molto più facile usare il più lento int $0x80
per richiamare l'ABI a 32 bit.
// i386 Linux
#include <asm/unistd.h> // compile with -m32 for 32 bit call numbers
//#define __NR_write 4
ssize_t my_write(int fd, const void *buf, size_t size)
{
ssize_t ret;
asm volatile
(
"int $0x80"
: "=a" (ret)
: "0"(__NR_write), "b"(fd), "c"(buf), "d"(size)
: "memory" // the kernel dereferences pointer args
);
return ret;
}
Come puoi vedere, usando il int 0x80
L'API è relativamente semplice. Il numero della chiamata di sistema va al eax
register, mentre tutti i parametri necessari per la syscall vanno rispettivamente in ebx
, ecx
, edx
, esi
, edi
e ebp
. I numeri di chiamata di sistema possono essere ottenuti leggendo il file /usr/include/asm/unistd_32.h
.
Prototipi e descrizioni delle funzioni sono disponibili nella seconda sezione del manuale, quindi in questo caso write(2)
.
Il kernel salva/ripristina tutti i registri (tranne EAX) in modo che possiamo usarli come operandi di solo input nell'asm inline. Vedi Quali sono le convenzioni di chiamata per le chiamate di sistema UNIX e Linux (e le funzioni dello spazio utente) su i386 e x86-64
Tieni presente che l'elenco dei clobber contiene anche il memory
parametro, il che significa che l'istruzione elencata nell'elenco di istruzioni fa riferimento alla memoria (tramite il buf
parametro). (Un input puntatore a inline asm non implica che anche la memoria puntata sia un input. Vedi Come posso indicare che la memoria *puntata* da un argomento ASM inline può essere usata?)
amd64
Le cose sembrano diverse sull'architettura AMD64 che sfoggia una nuova istruzione chiamata SYSCALL
. È molto diverso dall'originale SYSENTER
istruzione, e sicuramente molto più facile da usare dalle applicazioni userland - assomiglia davvero a un normale CALL
, in realtà, e adattando il vecchio int 0x80
al nuovo SYSCALL
è praticamente banale. (Tranne che utilizza RCX e R11 invece dello stack del kernel per salvare il RIP e RFLAGS dello spazio utente in modo che il kernel sappia dove tornare).
In questo caso, il numero della chiamata di sistema viene comunque passato nel registro rax
, ma i registri utilizzati per contenere gli argomenti ora corrispondono quasi alla convenzione di chiamata della funzione:rdi
, rsi
, rdx
, r10
, r8
e r9
in questo ordine. (syscall
stesso distrugge rcx
quindi r10
viene utilizzato al posto di rcx
, lasciando che le funzioni wrapper di libc usino solo mov r10, rcx
/ syscall
.)
// x86-64 Linux
#include <asm/unistd.h> // compile without -m32 for 64 bit call numbers
// #define __NR_write 1
ssize_t my_write(int fd, const void *buf, size_t size)
{
ssize_t ret;
asm volatile
(
"syscall"
: "=a" (ret)
// EDI RSI RDX
: "0"(__NR_write), "D"(fd), "S"(buf), "d"(size)
: "rcx", "r11", "memory"
);
return ret;
}
(Guardalo compilare su Godbolt)
Nota come praticamente l'unica cosa che doveva essere cambiata erano i nomi dei registri e le effettive istruzioni utilizzate per effettuare la chiamata. Ciò è dovuto principalmente agli elenchi di input/output forniti dalla sintassi di assembly inline estesa di gcc, che fornisce automaticamente le istruzioni di spostamento appropriate necessarie per l'esecuzione dell'elenco di istruzioni.
Il "0"(callnum)
il vincolo di corrispondenza potrebbe essere scritto come "a"
perché l'operando 0 (il "=a"(ret)
output) ha solo un registro tra cui scegliere; sappiamo che sceglierà EAX. Usa quello che trovi più chiaro.
Tieni presente che i sistemi operativi non Linux, come MacOS, utilizzano numeri di chiamata diversi. E persino diverse convenzioni di passaggio di argomenti per 32 bit.