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, 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)
codice:
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 di _exit, ossia il codice di uscita); in r8d viene copiato 0xe7 (il codice della syscall 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:
codice:
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, 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:
codice:
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,
codice:
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:
codice:
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.
codice:
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
codice:
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:
codice:
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:
codice:
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; 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)
codice:
<+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> --------------+