Visualizzazione dei risultati da 1 a 10 su 10
  1. #1

    [asm AT&T]Differenze tra "syscall" e "int 0x80"

    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:
    codice:
    #include<stdlib.h>
    
    void main()
    {
            exit(0);
    }
    codice:
    (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:
    codice:
    (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?

  2. #2
    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>    --------------+
    Amaro C++, il gusto pieno dell'undefined behavior.

  3. #3
    Ah tra l'altro sono scemo, il sorgente di quella funzione si trovava tranquillamente in glibc :
    codice:
    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:
    codice:
    # 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
    codice:
    # define INTERNAL_SYSCALL_ERROR_P(val, err) \
      ((unsigned long int) (long int) (val) >= -4095L)
    # define INTERNAL_SYSCALL_ERRNO(val, err)	(-(val))
    e
    codice:
    /* 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)
    Amaro C++, il gusto pieno dell'undefined behavior.

  4. #4
    Più che esaustivo!
    Dante

  5. #5
    (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?
    Dante

  6. #6
    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 , 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".
    Amaro C++, il gusto pieno dell'undefined behavior.

  7. #7
    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...
    Dante

  8. #8
    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).
    Amaro C++, il gusto pieno dell'undefined behavior.

  9. #9
    Direi che fila, grazie mille.
    Dante

  10. #10
    Amaro C++, il gusto pieno dell'undefined behavior.

Permessi di invio

  • Non puoi inserire discussioni
  • Non puoi inserire repliche
  • Non puoi inserire allegati
  • Non puoi modificare i tuoi messaggi
  •  
Powered by vBulletin® Version 4.2.1
Copyright © 2025 vBulletin Solutions, Inc. All rights reserved.