GNU/Linux >> Linux Esercitazione >  >> Linux

Chiamata di sistema Intel x86 vs x64

Parte generale

EDIT:parti irrilevanti di Linux rimosse

Anche se non del tutto sbagliato, restringendo il campo a int 0x80 e syscall semplifica eccessivamente la domanda come con sysenter c'è almeno una terza opzione.

L'uso di 0x80 e eax per il numero di chiamata di sistema, ebx, ecx, edx, esi, edi ed ebp per passare i parametri è solo una delle tante altre possibili scelte per implementare una chiamata di sistema, ma quei registri sono quelli scelti dall'ABI di Linux a 32 bit .

Prima di dare un'occhiata più da vicino alle tecniche coinvolte, va detto che ruotano tutte attorno al problema di sfuggire alla prigione del privilegio in cui si svolge ogni processo.

Un'altra scelta rispetto a quelle qui presentate offerte dall'architettura x86 sarebbe stata l'uso di un gate di chiamata (vedi:http://en.wikipedia.org/wiki/Call_gate)

L'unica altra possibilità presente su tutte le macchine i386 è l'utilizzo di un interrupt software, che consente l'ISR (Interrupt Service Routine o semplicemente un gestore di interrupt ) in modo che venga eseguito a un livello di privilegi diverso rispetto a prima.

(Curiosità:alcuni sistemi operativi i386 hanno utilizzato un'eccezione di istruzione non valida per accedere al kernel per le chiamate di sistema, perché in realtà era più veloce di un int istruzione su CPU 386. Consulta le istruzioni OsDev syscall/sysret e sysenter/sysexit che abilitano per un riepilogo dei possibili meccanismi di chiamata di sistema.)

Interruzione software

Ciò che accade esattamente una volta che viene attivato un interrupt dipende dal fatto che il passaggio all'ISR richieda o meno una modifica dei privilegi:

(Intel® 64 and IA-32 Architectures Software Developer's Manual)

6.4.1 Operazione di chiamata e risposta per le procedure di gestione delle interruzioni o delle eccezioni

...

Se il segmento di codice per la procedura del gestore ha lo stesso livello di privilegio del programma o dell'attività attualmente in esecuzione, la procedura del gestore utilizza lo stack corrente; se il gestore viene eseguito a un livello di privilegio superiore, il processore passa allo stack per il livello di privilegio del gestore.

....

Se si verifica un cambio di stack, il processore esegue quanto segue:

  1. Salva temporaneamente (internamente) il contenuto corrente dei registri SS, ESP, EFLAGS, CS e> EIP.

  2. Carica il selettore di segmento e il puntatore dello stack per il nuovo stack (ovvero lo stack per il livello di privilegio richiamato) dal TSS nei registri SS ed ESP e passa al nuovo stack.

  3. Inserisce i valori SS, ESP, EFLAGS, CS ed EIP temporaneamente salvati per lo stack della procedura interrotta nel nuovo stack.

  4. Inserisce un codice di errore nel nuovo stack (se appropriato).

  5. Carica il selettore di segmento per il nuovo segmento di codice e il nuovo puntatore di istruzione (dall'interrupt gate o dal trap gate) rispettivamente nei registri CS e EIP.

  6. Se la chiamata avviene attraverso un interrupt gate, azzera il flag IF nel registro EFLAGS.

  7. Avvia l'esecuzione della procedura del gestore al nuovo livello di privilegio.

... sospiro, questo sembra essere molto da fare e anche una volta che abbiamo finito non migliora molto:

(estratto tratto dalla stessa fonte sopra citata:Intel® 64 and IA-32 Architectures Software Developer's Manual)

Quando si esegue un ritorno da un gestore di interruzioni o eccezioni da un livello di privilegio diverso rispetto alla procedura interrotta, il processore esegue queste azioni:

  1. Esegue un controllo dei privilegi.

  2. Ripristina i registri CS ed EIP ai valori precedenti all'interruzione o all'eccezione.

  3. Ripristina il registro EFLAGS.

  4. Ripristina i registri SS ed ESP ai valori precedenti all'interruzione o all'eccezione, determinando un ritorno dello stack allo stack della procedura interrotta.

  5. Riprende l'esecuzione della procedura interrotta.

Sistema

Un'altra opzione sulla piattaforma a 32 bit non menzionata affatto nella tua domanda, ma comunque utilizzata dal kernel Linux è sysenter istruzioni.

(Intel® 64 and IA-32 Architectures Software Developer's Manual Volume 2 (2A, 2B &2C):Riferimento al set di istruzioni, A-Z)

Descrizione Esegue una chiamata rapida a una procedura o routine di sistema di livello 0. SYSENTER è un'istruzione complementare a SYSEXIT. L'istruzione è ottimizzata per fornire le massime prestazioni per le chiamate di sistema dal codice utente in esecuzione al livello di privilegio 3 al sistema operativo o alle procedure esecutive in esecuzione al livello di privilegio 0.

Uno svantaggio dell'utilizzo di questa soluzione è che non è presente su tutte le macchine a 32 bit, quindi int 0x80 metodo deve ancora essere fornito nel caso in cui la CPU non lo sappia.

Le istruzioni SYSENTER e SYSEXIT sono state introdotte nell'architettura IA-32 nel processore Pentium II. La disponibilità di queste istruzioni su un processore è indicata con il flag di funzionalità SYSENTER/SYSEXITpresent (SEP) restituito al registro EDX dall'istruzione CPUID. Un sistema operativo che qualifica il flag SEP deve anche qualificare la famiglia e il modello del processore per garantire che le istruzioni SYSTEMENTER/SYSEXIT siano effettivamente presenti

Chiamata di sistema

L'ultima possibilità, il syscall istruzione, consente praticamente la stessa funzionalità del sysenter istruzione. L'esistenza di entrambi è dovuta al fatto che uno (systenter ) è stato introdotto da Intel mentre l'altro (syscall ) è stato introdotto da AMD.

Specifico per Linux

Nel kernel Linux si può scegliere una qualsiasi delle tre possibilità menzionate sopra per realizzare una chiamata di sistema.

Vedi anche La guida definitiva alle chiamate di sistema Linux .

Come già detto sopra, il file int 0x80 Il metodo è l'unica delle 3 implementazioni scelte, che può essere eseguita su qualsiasi CPU i386, quindi questa è l'unica sempre disponibile per lo spazio utente a 32 bit.

(syscall è l'unico sempre disponibile per lo spazio utente a 64 bit e l'unico che dovresti mai usare nel codice a 64 bit; I kernel x86-64 possono essere compilati senza CONFIG_IA32_EMULATION e int 0x80 invoca ancora l'ABI a 32 bit che tronca i puntatori a 32 bit.)

Per consentire il passaggio tra tutte e 3 le scelte, ogni esecuzione del processo ha accesso a uno speciale oggetto condiviso che dà accesso all'implementazione della chiamata di sistema scelta per il sistema in esecuzione. Questo è lo strano linux-gate.so.1 potresti già aver incontrato una libreria irrisolta usando ldd o simili.

(arch/x86/vdso/vdso32-setup.c)

 if (vdso32_syscall()) {                                                                               
        vsyscall = &vdso32_syscall_start;                                                                 
        vsyscall_len = &vdso32_syscall_end - &vdso32_syscall_start;                                       
    } else if (vdso32_sysenter()){                                                                        
        vsyscall = &vdso32_sysenter_start;                                                                
        vsyscall_len = &vdso32_sysenter_end - &vdso32_sysenter_start;                                     
    } else {                                                                                              
        vsyscall = &vdso32_int80_start;                                                                   
        vsyscall_len = &vdso32_int80_end - &vdso32_int80_start;                                           
    }   

Per utilizzarlo non devi fare altro che caricare tutti i tuoi registri system call number in eax, parametri in ebx, ecx, edx, esi, edi come con int 0x80 implementazione della chiamata di sistema e call la routine principale.

Purtroppo non è tutto così facile; per ridurre al minimo il rischio per la sicurezza di un indirizzo fisso predefinito, la posizione in cui il vdso (oggetto condiviso dinamico virtuale ) sarà visibile in un processo randomizzato, quindi dovrai prima capire la posizione corretta.

Questo indirizzo è individuale per ogni processo e viene passato al processo una volta avviato.

Nel caso in cui non lo sapessi, quando viene avviato in Linux, ogni processo riceve puntatori ai parametri passati una volta avviato e puntatori a una descrizione delle variabili d'ambiente in cui è in esecuzione passate nel suo stack, ognuna terminata da NULL.

In aggiunta a questi viene passato un terzo blocco di cosiddetti vettori ausiliari elfi dopo quelli menzionati prima. La posizione corretta è codificata in uno di questi recanti l'identificatore di tipo AT_SYSINFO .

Quindi il layout dello stack è simile a questo (gli indirizzi crescono verso il basso):

  • parametro-0
  • ...
  • parametro-m
  • NULL
  • ambiente-0
  • ....
  • ambiente-n
  • NULL
  • ...
  • vettore elfo ausiliario:AT_SYSINFO
  • ...
  • vettore elfo ausiliario:AT_NULL

Esempio di utilizzo

Per trovare l'indirizzo corretto dovrai prima saltare tutti gli argomenti e tutti i puntatori dell'ambiente e quindi avviare la scansione per AT_SYSINFO come mostrato nell'esempio seguente:

#include <stdio.h>
#include <elf.h>

void putc_1 (char c) {
  __asm__ ("movl $0x04, %%eax\n"
           "movl $0x01, %%ebx\n"
           "movl $0x01, %%edx\n"
           "int $0x80"
           :: "c" (&c)
           : "eax", "ebx", "edx");
}

void putc_2 (char c, void *addr) {
  __asm__ ("movl $0x04, %%eax\n"
           "movl $0x01, %%ebx\n"
           "movl $0x01, %%edx\n"
           "call *%%esi"
           :: "c" (&c), "S" (addr)
           : "eax", "ebx", "edx");
}


int main (int argc, char *argv[]) {

  /* using int 0x80 */
  putc_1 ('1');


  /* rather nasty search for jump address */
  argv += argc + 1;     /* skip args */
  while (*argv != NULL) /* skip env */
    ++argv;            

  Elf32_auxv_t *aux = (Elf32_auxv_t*) ++argv; /* aux vector start */

  while (aux->a_type != AT_SYSINFO) {
    if (aux->a_type == AT_NULL)
      return 1;
    ++aux;
  }

  putc_2 ('2', (void*) aux->a_un.a_val);

  return 0;
}

Come vedrai dando un'occhiata al seguente frammento di /usr/include/asm/unistd_32.h sul mio sistema:

#define __NR_restart_syscall 0
#define __NR_exit            1
#define __NR_fork            2
#define __NR_read            3
#define __NR_write           4
#define __NR_open            5
#define __NR_close           6

La syscall che ho usato è quella numerata 4 (write) come passata nel registro eax. Prendendo filedescriptor (ebx =1), data-pointer (ecx =&c) e size (edx =1) come argomenti, ciascuno passato nel registro corrispondente.

Per farla breve

Confrontando un int 0x80 apparentemente lento chiamata di sistema su any CPU Intel con un'implementazione (si spera) molto più veloce utilizzando il (genuinamente inventato da AMD) syscall l'istruzione sta confrontando le mele con le arance.

IMHO:Molto probabilmente il sysenter istruzione invece di int 0x80 dovrebbe essere alla prova qui.


Ci sono tre cose che devono accadere quando chiami il kernel (facendo una chiamata di sistema):

  1. Il sistema passa dalla "modalità utente" alla "modalità kernel" (anello 0).
  2. Lo stack passa dalla "modalità utente" alla "modalità kernel".
  3. Si fa un salto in una parte adatta del kernel.

Ovviamente, una volta all'interno del kernel, il codice del kernel dovrà sapere cosa vuoi effettivamente che il kernel faccia, quindi inserire qualcosa in EAX, e spesso più cose in altri registri dato che ci sono cose come "nome del file che vuoi aprire " o "buffer in cui leggere i dati da un file" ecc. ecc.

Diversi processori hanno modi diversi per raggiungere i tre passaggi precedenti. In x86, ci sono diverse scelte, ma le due più popolari per asm scritto a mano sono int 0xnn (modalità 32 bit) o ​​syscall (modalità a 64 bit). (C'è anche la modalità a 32 bit sysenter , introdotto da Intel per lo stesso motivo per cui AMD ha introdotto la versione in modalità a 32 bit di syscall :come alternativa più veloce al lento int 0x80 . Glibc a 32 bit utilizza qualsiasi efficiente meccanismo di chiamata di sistema disponibile, utilizzando solo il lento int 0x80 se non è disponibile niente di meglio.)

La versione a 64 bit del syscall l'istruzione è stata introdotta con l'architettura x86-64 come un modo più veloce per inserire una chiamata di sistema. Ha una serie di registri (utilizzando i meccanismi x86 MSR) che contengono l'indirizzo RIP a cui desideriamo saltare, quali valori di selettore caricare in CS e SS e per eseguire la transizione da Ring3 a Ring0. Memorizza anche l'indirizzo di ritorno in ECX/RCX. [Si prega di leggere il manuale del set di istruzioni per tutti i dettagli di questa istruzione - non è del tutto banale!]. Poiché il processore sa che passerà a Ring0, può fare direttamente la cosa giusta.

Uno dei punti chiave è che syscall manipola solo i registri; non esegue alcun caricamento o memorizzazione. (Questo è il motivo per cui sovrascrive RCX con il RIP salvato e R11 con gli RFLAGS salvati). L'accesso alla memoria dipende dalle tabelle delle pagine e le voci della tabella delle pagine hanno un bit che può renderle valide solo per il kernel, non per lo spazio utente, quindi l'accesso alla memoria mentre potrebbe essere necessario attendere la modifica del livello di privilegio rispetto alla semplice scrittura dei registri. Una volta in modalità kernel, il kernel utilizzerà normalmente swapgs o qualche altro modo per trovare lo stack del kernel. (syscall non modificare RSP; punta ancora allo stack dell'utente all'ingresso nel kernel.)

Quando si ritorna utilizzando l'istruzione SYSRET, i valori vengono ripristinati da valori predeterminati nei registri, quindi ancora una volta è veloce, perché il processore deve solo impostare alcuni registri. Il processore sa che cambierà da Ring0 a Ring3, quindi può fare le cose giuste rapidamente.

(Le CPU AMD supportano syscall istruzioni dallo spazio utente a 32 bit; Le CPU Intel no. x86-64 era originariamente AMD64; ecco perché abbiamo syscall in modalità a 64 bit. AMD ha riprogettato il lato kernel di syscall per la modalità a 64 bit, quindi syscall a 64 bit il punto di ingresso del kernel è significativamente diverso dal syscall a 32 bit punto di ingresso nei kernel a 64 bit.)

Il int 0x80 la variante utilizzata in modalità a 32 bit deciderà cosa fare in base al valore nella tabella dei descrittori di interrupt, che significa leggere dalla memoria. Qui trova i nuovi valori CS e EIP/RIP. Il nuovo registro CS determina il nuovo livello "ring", in questo caso Ring0. Utilizzerà quindi il nuovo valore CS per esaminare il Task State Segment (basato sul registro TR) per scoprire quale puntatore dello stack (ESP/RSP e SS), quindi saltare infine al nuovo indirizzo. Poiché questa è una soluzione meno diretta e più generica, è anche più lenta. I vecchi EIP/RIP e CS vengono memorizzati nel nuovo stack, insieme ai vecchi valori di SS e ESP/RSP.

Al ritorno, utilizzando l'istruzione IRET, il processore legge l'indirizzo di ritorno ei valori del puntatore dello stack dallo stack, caricando anche i nuovi valori del segmento dello stack e del segmento di codice dallo stack. Ancora una volta, il processo è generico e richiede alcune letture della memoria. Poiché è generico, il processore dovrà anche controllare "stiamo cambiando modalità da Ring0 a Ring3, in tal caso cambia queste cose".

Quindi, in sintesi, è più veloce perché doveva funzionare in quel modo.

Per il codice a 32 bit, sì, puoi sicuramente usare il lento e compatibile int 0x80 se vuoi.

Per codice a 64 bit, int 0x80 è più lento di syscall e troncerà i tuoi puntatori a 32 bit, quindi non usarlo. Vedere Cosa succede se si utilizza l'ABI Linux 0x80 int a 32 bit nel codice a 64 bit? Inoltre, int 0x80 non è disponibile in modalità a 64 bit su tutti i kernel, quindi non è sicuro nemmeno per un sys_exit che non accetta alcun argomento puntatore:CONFIG_IA32_EMULATION può essere disabilitato, e in particolare è disabilitato su sottosistema Windows per Linux.


Linux
  1. Linux:metodi di chiamata di sistema nel nuovo kernel?

  2. Controllo se errno !=EINTR:cosa significa?

  3. Chiamata di sistema Linux più veloce

  4. Come passare i parametri alla chiamata di sistema Linux?

  5. Perché i numeri di chiamata del sistema Linux in x86 e x86_64 sono diversi?

Come rendere il sistema Linux più veloce su CPU Intel

Modo per scoprire se il sistema supporta Intel Amt?

stampa stack di chiamate in C o C++

Dove posso trovare il codice sorgente della chiamata di sistema?

Qual è la differenza tra chiamata di sistema e chiamata di libreria?

Cosa ha fatto la chiamata di sistema tuxcall?