GNU/Linux >> Linux Esercitazione >  >> Linux

Come richiamare una chiamata di sistema tramite syscall o sysenter in assembly inline?

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 e r10 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.


Linux
  1. Come configurare la virtualizzazione su Redhat Linux

  2. Come eseguire il mmap dello stack per la chiamata di sistema clone() su Linux?

  3. Tabella delle chiamate di sistema Linux o cheatsheet per Assembly

  4. Come stampare un numero nell'assemblea NASM?

  5. x86_64 Assembly Linux System Call Confusion

Come installare Cockpit su Debian 10

Come aggiornare Ubuntu 18.04 a Ubuntu 20.04

Come eseguire ed elencare i lavori Cron per un sistema Linux tramite PHP

Come installare Nginx su CentOS 8

Come registrare l'audio in Ubuntu 20.04

Come montare NFS su Debian 11