PDA

Visualizza la versione completa : [asm x86_64]Differenze tra "syscall" e "int 0x80"


pistilloi
17-12-2012, 16:20
Mi trovo ancora negli oscuri anfratti di linux, voglio capire come viene terminato un processo e come restituisce il controllo all'os. Io ho sempre saputo, che a questo scopo si dovrebbe chiamare l'interruzione software "int 0x80" fornendoli come parametro $1 tramite il registro %eax.

Ma quando disassemblo questo codice, linkato staticamente:

#include<stdlib.h>

void main()
{
exit(0);
}



(gdb) disas main
Dump of assembler code for function main:
0x00000000004004d4 <+0>: push %rbp
0x00000000004004d5 <+1>: mov %rsp,%rbp
0x00000000004004d8 <+4>: mov $0x0,%edi
0x00000000004004dd <+9>: callq 0x4010b0 <exit>
End of assembler dump.


dopodiché "(gdb) disassemble exit" mi restituisce un listato bello lungo che non ho esaminato per intero, ma al suo interno vi sono 3 call a subroutine diverse: __exit_funcs, _exit, free.

Approfondisco la sub _exit:

(gdb) disas _exit
Dump of assembler code for function _exit:
0x000000000040d900 <+0>: movslq %edi,%rdx
0x000000000040d903 <+3>: mov $0xffffffffffffffb0,%r9
0x000000000040d90a <+10>: mov $0xe7,%r8d
0x000000000040d910 <+16>: mov $0x3c,%esi
0x000000000040d915 <+21>: jmp 0x40d930 <_exit+48>
0x000000000040d917 <+23>: nopw 0x0(%rax,%rax,1)
0x000000000040d920 <+32>: mov %rdx,%rdi
0x000000000040d923 <+35>: mov %esi,%eax
0x000000000040d925 <+37>: syscall
0x000000000040d927 <+39>: cmp $0xfffffffffffff000,%rax
0x000000000040d92d <+45>: ja 0x40d948 <_exit+72>
0x000000000040d92f <+47>: hlt
0x000000000040d930 <+48>: mov %rdx,%rdi
0x000000000040d933 <+51>: mov %r8d,%eax
0x000000000040d936 <+54>: syscall
0x000000000040d938 <+56>: cmp $0xfffffffffffff000,%rax
0x000000000040d93e <+62>: jbe 0x40d920 <_exit+32>
0x000000000040d940 <+64>: neg %eax
0x000000000040d942 <+66>: mov %eax,%fs:(%r9)
0x000000000040d946 <+70>: jmp 0x40d920 <_exit+32>
0x000000000040d948 <+72>: neg %eax
0x000000000040d94a <+74>: mov %eax,%fs:(%r9)
0x000000000040d94e <+78>: jmp 0x40d92f <_exit+47>
End of assembler dump.


A questo punto, si inizia a vedere aria di uscita! Ho pensato che syscall faccia le veci di int 0x80, ma che parametri passargli per restituire il controllo al sistema? Nel listato di _exit viene chiamata in due modi diversi: prima con $0xe7 in %aex, nella chiamata <_exit+54>; poi in funzione della comparazione in <_exit+56>, viene caricato il valore $0x3c oppure il suo complementare in %aex per poi eseguire syscall.
Sono sulla retta via o completamente fuori strada, mi pongo il dubbio perché tutte guide a riguardo mostrano codice molto più semplice, rispetto ai castelli di software che ho trovato? Inoltre dove posso trovare un quadro delle azioni di syscall in base ai parametri?

MItaly
17-12-2012, 17:30
La funzione di libreria exit() non si limita ad uscire, ma deve prima richiamare tutti gli "exit handler" (registrati con atexit/on_exit); il codice che hai visto è questa roba (http://sourceware.org/git/?p=glibc.git;a=blob;f=stdlib/exit.c;h=1ad548f7a85bb4f81e43dab3abf9c468ef5306d2; hb=HEAD), che di fatto scorre una lista linkata di exit handler e li richiama uno per uno. Alla fine viene richiamata la _exit, che appunto si occupa di uscire direttamente:
(uso la sintassi Intel perché di quella AT&T capisco poco)


0x00007ffff7df2520 <+0>: movsxd rdx,edi
0x00007ffff7df2523 <+3>: mov r8d,0xe7
0x00007ffff7df2529 <+9>: mov esi,0x3c
0x00007ffff7df252e <+14>: jmp 0x7ffff7df2540 <_exit+32>

Viene copiato in rdx il valore di edi (in cui si trova il primo parametro intero (http://en.wikipedia.org/wiki/X86_calling_conventions#System_V_AMD64_ABI) di _exit, ossia il codice di uscita); in r8d viene copiato 0xe7 (il codice della syscall (http://www.acsu.buffalo.edu/~charngda/linux_syscalls_64bit.html) exit_group) e in esi 0x3c (codice della syscall exit).
La differenza tra le due chiamate è che exit_group termina tutti i thread del processo, mentre exit solo il thread corrente. Si salta quindi a _exit+32:


0x00007ffff7df2540 <+32>: mov rdi,rdx
0x00007ffff7df2543 <+35>: mov eax,r8d
0x00007ffff7df2546 <+38>: syscall

qui viene copiato in rdi il codice di uscita (primo parametro per la exit_group) e in eax il codice della syscall exit_group (come da convenzione di chiamata spiegata qui (http://stackoverflow.com/a/2538212/214671), in fondo alla risposta); viene quindi invocata la syscall.

Ora, in teoria exit_group non dovrebbe ritornare, per cui l'esecuzione si ferma qui; ma se per qualche motivo ha fallito (idea a caso: magari questo kernel non supporta la exit_group), l'esecuzione prosegue:


0x00007ffff7df2548 <+40>: cmp rax,0xfffffffffffff000
0x00007ffff7df254e <+46>: jbe 0x7ffff7df2530 <_exit+16>

rax contiene il codice di uscita della syscall, che è da considerarsi "-errno"; 0xfffffffffffff000, interpretato come intero con segno, è -4096; se rax<=-4096 (ovvero, siamo fuori dal range di errno, e quindi non è un errore da riportare in tale maniera), salta direttamente a _exit+16 (su cui torniamo dopo); altrimenti,


0x00007ffff7df2550 <+48>: neg eax
0x00007ffff7df2552 <+50>: mov DWORD PTR [rip+0x20bd5c],eax # 0x7ffff7ffe2b4 <rtld_errno>
0x00007ffff7df2558 <+56>: jmp 0x7ffff7df2530 <_exit+16>

prima copia il codice di errore in errno (dopo averlo cambiato di segno) e quindi salta a _exit+16; suppongo che questo venga fatto per comodità di debugging (se il processo crasha/viene "congelato" da queste parti nel core dump si vede che errno aveva lasciato la syscall fallita).
Andando a _exit+16:


0x00007ffff7df2530 <+16>: mov rdi,rdx
0x00007ffff7df2533 <+19>: mov eax,esi
0x00007ffff7df2535 <+21>: syscall

fa lo stesso mestiere della precedente syscall, ma a questo giro prova a chiamare _exit.


0x00007ffff7df2537 <+23>: cmp rax,0xfffffffffffff000
0x00007ffff7df253d <+29>: ja 0x7ffff7df255a <_exit+58>

stesso gioco di prima: confronta il valore restituito con -4096; se è maggiore (ovvero, se è un errno valido) salta a _exit+58, altrimenti prosegue su


0x00007ffff7df253f <+31>: hlt

hlt di base blocca la CPU (facendola "riposare") finché non ci sono interrupt che la "svegliano"; fa quindi sostanzialmente il lavoro del ciclo idle del sistema. Ora, possono accadere due cose:
- se questo codice sta girando in kernel mode (cosa che non so se possa accadere per la normale glibc), allora fa effettivamente quanto detto - il codice, non sapendo che fare, almeno evita di divorare la CPU;
- se invece siamo in user mode, si verifica un'eccezione hardware (dato che hlt è un'istruzione privilegiata) che suppongo che in qualche maniera vada ad uccidere il processo.
Quanto a _exit+58:


0x00007ffff7df255a <+58>: neg eax
0x00007ffff7df255c <+60>: mov DWORD PTR [rip+0x20bd52],eax # 0x7ffff7ffe2b4 <rtld_errno>
0x00007ffff7df2562 <+66>: jmp 0x7ffff7df253f <_exit+31>

non fa altro che copiare il codice di uscita della syscall (negato) in errno e ritorna a _exit+31, ovvero all'hlt.
Nota che hlt è situata subito prima di _exit+32, per cui, se questo codice sta girando in kernel mode (e quindi hlt non causa un'eccezione) appena il processore si "sveglia" e torna sul thread corrente l'istruzione che viene eseguita è _exit+32, ovvero si ricomincia il ciclo di tentativi exit_group->_exit->hlt.
Per cui:
- prova exit_group e _exit, se per qualche motivo inizialmente falliscono, in user-mode cerca di far crashare il processo, altrimenti riprova ciclicamente;
- altrimenti, se exit_group e _exit non sono supportate (è possibile? boh) e siamo in kernel-mode la _exit di fatto blocca il processo, evitando al contempo che divori la CPU.

In "pseudo-C" sarebbe una cosa del tipo:


void _exit(int status)
{
int ret;
while(1)
{
ret=syscall_exit_group(status);
if(ret>-4096)
errno=-ret;
ret=syscall_exit(status);
if(ret>-4096)
errno=-ret;
hlt();
}
}


Il codice quindi è relativamente complicato perché fa ogni tentativo possibile di uccidere il processo, cercando di salvare il salvabile man mano che i vari metodi falliscono e gestendo in maniera ragionevole anche il caso in cui ci si trovi in kernel mode.

Quanto alla differenza tra int 0x80 e syscall, trovi una spiegazione qui (http://stackoverflow.com/questions/12806584/what-is-better-int-0x80-or-syscall); sostanzialmente, int 0x80 è il metodo "vecchio" per richiamare le syscall, sysenter era il metodo preferito su x86, mentre syscall è un nuovo opcode disponibile su x86_64 che semplifica un po' le cose rispetto a sysenter. Se non ho capito male syscall, tra le altre cose, consente l'uso di metodi per ottimizzare diverse syscall che non hanno davvero bisogno di fare trapping nel kernel, ma possono tranquillamente girare in usermode (a patto di poter leggere dei dati gestiti dal kernel); in ogni caso, nel link riportato ci sono diversi riferimenti che puoi leggere per approfondire.

(riporto qui il disassembly completo con qualche freccina per capire il flusso)


<+0>: movsxd rdx,edi
<+3>: mov r8d,0xe7
<+9>: mov esi,0x3c
<+14>: jmp 0x7ffff7df2540 <_exit+32> ------+
<+16>: mov rdi,rdx <---- | ---+
<+19>: mov eax,esi v |
<+21>: syscall | ^
<+23>: cmp rax,0xfffffffffffff000 v |
<+29>: ja 0x7ffff7df255a <_exit+58> | ^
<+31>: hlt <---- v -- | -+
<+32>: mov rdi,rdx <-----+ ^ |
<+35>: mov eax,r8d | |
<+38>: syscall ^ |
<+40>: cmp rax,0xfffffffffffff000 | |
<+46>: jbe 0x7ffff7df2530 <_exit+16> -----------+ |
<+48>: neg eax ^ |
<+50>: mov DWORD PTR [rip+0x20bd5c],eax | | # 0x7ffff7ffe2b4 <rtld_errno>
<+56>: jmp 0x7ffff7df2530 <_exit+16> -----------+ |
<+58>: neg eax |
<+60>: mov DWORD PTR [rip+0x20bd52],eax | # 0x7ffff7ffe2b4 <rtld_errno>
<+66>: jmp 0x7ffff7df253f <_exit+31> --------------+

MItaly
17-12-2012, 18:06
Ah tra l'altro sono scemo, il sorgente di quella funzione si trovava tranquillamente in glibc (http://sourceware.org/git/?p=glibc.git;a=blob;f=sysdeps/unix/sysv/linux/_exit.c) :D :


void
_exit (status)
int status;
{
while (1)
{
#ifdef __NR_exit_group
INLINE_SYSCALL (exit_group, 1, status);
#endif
INLINE_SYSCALL (exit, 1, status);

#ifdef ABORT_INSTRUCTION
ABORT_INSTRUCTION;
#endif
}
}

che più o meno corrisponde a quanto avevo scritto, dato che si ha (http://sourceware.org/git/?p=glibc.git;a=blob;f=sysdeps/unix/sysv/linux/x86_64/sysdep.h):


# define INLINE_SYSCALL(name, nr, args...) \
({ \
unsigned long int resultvar = INTERNAL_SYSCALL (name, , nr, args); \
if (__builtin_expect (INTERNAL_SYSCALL_ERROR_P (resultvar, ), 0)) \
{ \
__set_errno (INTERNAL_SYSCALL_ERRNO (resultvar, )); \
resultvar = (unsigned long int) -1; \
} \
(long int) resultvar; })

con


# define INTERNAL_SYSCALL_ERROR_P(val, err) \
((unsigned long int) (long int) (val) >= -4095L)
# define INTERNAL_SYSCALL_ERRNO(val, err) (-(val))

e (http://sourceware.org/git/?p=glibc.git;a=blob;f=sysdeps/x86_64/abort-instr.h)


/* An instruction which should crash any program is `hlt'. */
#define ABORT_INSTRUCTION asm ("hlt")

(e questo commento tra l'altro mi dice che non si usa hlt per non bruciare la CPU, ma semplicemente per crashare in user-mode)
:stordita:

pistilloi
17-12-2012, 18:50
Più che esaustivo!

pistilloi
17-12-2012, 19:02
(e questo commento tra l'altro mi dice che non si usa hlt per non bruciare la CPU, ma semplicemente per crashare in user-mode)

Quindi sostanzialmente, se non riesce ad'uscire dal processo, ricorre all'estremo rimedio uccidendolo, bloccando la CPU?

MItaly
17-12-2012, 19:14
Ni, chiama un'istruzione privilegiata (hlt, comunque sostanzialmente innocua), il che, in user mode, genera un'eccezione hardware che fa crashare il processo - per i dettagli esatti di come questo avvenga dovrei riguardarmi un po' di sacri testi :D, comunque l'idea è che l'eccezione fa sì che si faccia trapping in kernel mode, e quindi sono affari del kernel uccidere il processo "dal di fuori".

pistilloi
17-12-2012, 19:29
Ok quindi, se siamo in usermode, il programma va in crash dal momento che hlt è privilegiata; ma se siamo in kernelmode hlt semplicemente congela la cpu sino all'intterupt che la risveglia. Pertanto _exit viene sempre eseguita in usermode...

MItaly
17-12-2012, 19:38
Credo proprio di sì, in generale "quella" _exit è parte di glibc, che non credo proprio venga eseguita in kernel-mode (da non confondere con la syscall exit, invece, che è quella sta in kernel mode e che viene richiamata da qui).

pistilloi
17-12-2012, 19:46
Direi che fila, grazie mille.

MItaly
17-12-2012, 20:10
:ciauz:

Loading