DOMANDA In Che Modo Vengono Realizzate Le System Call?

Pubblicità
Stato
Discussione chiusa ad ulteriori risposte.

nome non in uso

Nuovo Utente
Messaggi
18
Reazioni
0
Punteggio
22
Mettiamo per esempio che stiamo utilizzando un sistema operativo multi-thread che gestisce i thread a livello kernel secondo il modello uno ad uno, quando il mio programma raggiunge un istruzione che implementa una chiamata di sistema,
in che modo avviene il passaggio dalla modalità utente alla modalità kernel?
in che modo funziona l'eccezione che passa il controllo al sistema operativo?
o meglio il sistema operativo come se ne "accorge" che deve servire una chiamata di sistema?
come avviene il passaggio dei parametri da una funzione a una chiamata di sistema?
- vengono passati nello stack del thread ?
- o in altri modi?

non so se la domanda è ben posta,
grazie a chi mi risponde.
 
Mettiamo per esempio che stiamo utilizzando un sistema operativo multi-thread che gestisce i thread a livello kernel secondo il modello uno ad uno, quando il mio programma raggiunge un istruzione che implementa una chiamata di sistema,
in che modo avviene il passaggio dalla modalità utente alla modalità kernel?
in che modo funziona l'eccezione che passa il controllo al sistema operativo?
o meglio il sistema operativo come se ne "accorge" che deve servire una chiamata di sistema?
come avviene il passaggio dei parametri da una funzione a una chiamata di sistema?
- vengono passati nello stack del thread ?
- o in altri modi?

Prmia di rispondere a questa domanda, occorrono alcune precisazioni. Il discorso è piuttosto lungo e complesso, cercherò di spiegarmi comunque chiaramente.
La differenza c'è anche tra un OS a 32bit ed uno a 64bit.

Prendo come esempio Windows x86 su un Pentium II (o precedente). Windows ha una tabella nella quale mappa dei numeri (entry) a dei valori, degli indirizzi. La tabella è nota come IDT (Interrupt Dispatch Table).
Una di queste voci punta al system service dispatcher. L'entrata in questione è la 0x2E; l'invocazione avviene utilizzando proprio "int 0x2e". La sua esecuzione provoca una transizione da user mode a kernel mode. Vengono usati due registri della CPU per il passaggio di informazioni: EAX, che contiene il numero del servizio che è stato richiesto; e EDX che punta ai parametri passati dal chiamante.

Sui processori x86 superiori al Pentium II la situazione cambia. Windows non utilizza più int 0x2E, ma utilizza l'istruzione sysenter (notare che è la Intel a supportarla). La situazione qui è differente: Windows durante il boot salva l'indirizzo del system service dispatchar all'interno di un registro speciale, chiamato machine specific register, noto come MSR, associato con l'istruzione (sysenter). L'esecuzione di questa istruzione provoca l'entrata nella kernel mode e l'esecuzione del system service dispatcher. Vengono usati EAX ed EDX esattamente come in precedenza, e con gli stessi scopi.

Ora non sono su un 32bit Windows, ma su un 64bit. Comunque la funzione la si può vedere da kernel debugger (abilitando prima il debug).

Su 64bit la situazione è differente. In questo caso l'istruzione utilizzata è syscall. Come sempre, il numero della syscall è in EAX; i primi 4 parametri nei registri disponibili. Se non dovessero essere sufficienti viene utilizzato lo stack.

Nel caso di un 64bit quindi, il codice a seguito ad una chiamata di sistema (come WriteFile, ReadFile etc), sarà il seguente (o qualcosa di simile):
Codice:
mov  r10, rcx
mov  eax, 0x102
syscall
ret

Su un 64bit è bene notare che questo codice è direttamente in NTDLL.DLL. Su un 32bit c'è un passaggio in più (se sei interessato questa sera o domani ti mostro qualche screen preso dal mio "vecchio" 32bit e dall'attuale 64bit).

Su 32bit, la chiamata alla 0x2e o alla sysenter, provoca da parte del kernel una lettura nella system service dispatch table, una tabella simile alla IDT (se vuoi, posso mostrarti anche questa). Questa tabella però, al contrario della IDT, non punta a delle ISR ma a dei servizi di sistema.
Su 64bit invece non ci sono puntatori alle funzioni, ma offset relativi (relativi alla system service dispatch table stessa).

Qui c'è la risposta ad un'altra tua domanda, riguardo allo stack.
Il system service dispatcher è incaricato ad un'operazione molto delicata: copia gli argomenti dallo stack del thread in user-mode al suo in kernel-mode (così non possono essere modificati). A questo punto viene invocato il servizio di sistema.

Tutto ciò avviene nel caso in cui la chiamata di sistema avvenga da user mode, appunto.

Il corso è più o meno questo comunque:
- Chiamata di una funzione della WinAPI (ReadFile, ad esempio);
- viene chiamata ReadFile in Kernelbase.dll (e chiama NtReadFile);
- NtReadFile è in Ntdll.dll, ed esegue sysenter.

A questo punto l'esecuzione passa in kernel-mode:

- KiSystemService in Ntoskrnl.exe che chiama NtReadFile;
- NtReadFile in Ntoskrnl.exe esegue l'operazione, e torna al chiamante (il tutto procede a ritroso).

Questo vale solo per la kernel API; user32.dll e gdi32.dll fanno eccezione e saltano un passaggio (non c'è lo step di Kernelbase.dll).

Piccola nota, una curiosità: la tabella che ho citato prima, su un 32bit, può essere alterata. Può essere alterata da tutto il codice in kernel mode (drivers compresi). C'erano rootkit infatti che patchavano questa tabella cambiando i puntatori. Le versioni di Windows 64bit invece utilizzano una sorta di "guardiano", una protezione, chiamata Kernel Patch Protection, ed in caso di un tentativo di modifica... crasha il sistema (anche qui, era stata trovata una falla).


Se hai altre domande o vuoi chiarimenti, scrivi qui. Vedrò se riesco a fare qualche screen sull'OS a 32bit già questa sera, mettere assieme qualcosa con il 64bit anche, e poi postarli qui.
 
Ultima modifica:
Eccomi qui, come anticipato (ma solo su un 64bit Windows 10).

Questa è la IDT:

Codice:
00:   fffff801b6f66100 nt!KiDivideErrorFault
01:   fffff801b6f66200 nt!KiDebugTrapOrFault
02:   fffff801b6f663c0 nt!KiNmiInterrupt   Stack = 0xFFFFF801B8A97000
03:   fffff801b6f66740 nt!KiBreakpointTrap
04:   fffff801b6f66840 nt!KiOverflowTrap
05:   fffff801b6f66940 nt!KiBoundFault
06:   fffff801b6f66bc0 nt!KiInvalidOpcodeFault
07:   fffff801b6f66e00 nt!KiNpxNotAvailableFault
08:   fffff801b6f66ec0 nt!KiDoubleFaultAbort   Stack = 0xFFFFF801B8A95000
09:   fffff801b6f66f80 nt!KiNpxSegmentOverrunAbort
0a:   fffff801b6f67040 nt!KiInvalidTssFault
0b:   fffff801b6f67100 nt!KiSegmentNotPresentFault
0c:   fffff801b6f67240 nt!KiStackFault
0d:   fffff801b6f67380 nt!KiGeneralProtectionFault
0e:   fffff801b6f67480 nt!KiPageFault
10:   fffff801b6f67840 nt!KiFloatingErrorFault
11:   fffff801b6f679c0 nt!KiAlignmentFault
12:   fffff801b6f67ac0 nt!KiMcheckAbort   Stack = 0xFFFFF801B8A99000
13:   fffff801b6f68140 nt!KiXmmException
1f:   fffff801b6f615a0 nt!KiApcInterrupt
20:   fffff801b6f65780 nt!KiSwInterrupt
29:   fffff801b6f68300 nt!KiRaiseSecurityCheckFailure
2c:   fffff801b6f68400 nt!KiRaiseAssertion
2d:   fffff801b6f68500 nt!KiDebugServiceTrap
2f:   fffff801b6f61870 nt!KiDpcInterrupt
30:   fffff801b6f61aa0 nt!KiHvInterrupt
31:   fffff801b6f61e00 nt!KiVmbusInterrupt0
32:   fffff801b6f62150 nt!KiVmbusInterrupt1
33:   fffff801b6f624a0 nt!KiVmbusInterrupt2
34:   fffff801b6f627f0 nt!KiVmbusInterrupt3
35:   fffff801b6f60428 hal!HalpInterruptCmciService (KINTERRUPT fffff801b76acae0)

50:   fffff801b6f60500 pci!ExpressRootPortMessageRoutine (KINTERRUPT ffffd000759fec80)

51:   fffff801b6f60508 ndis!ndisMiniportMessageIsr (KINTERRUPT ffffd000759fe8c0)

60:   fffff801b6f60580 i8042prt!I8042KeyboardInterruptService (KINTERRUPT ffffd000759fe000)

61:   fffff801b6f60588 *** ERROR: Module load completed but symbols could not be loaded for \SystemRoot\system32\DRIVERS\RtsPer.sys
RtsPer+0x8833c (KINTERRUPT ffffd000759fe640)

62:   fffff801b6f60590 Wdf01000!FxInterrupt::_InterruptThunk (KINTERRUPT ffffd000791f9a00)

71:   fffff801b6f60608 ndis!ndisMiniportMessageIsr (KINTERRUPT ffffd000759fe500)

72:   fffff801b6f60610 Wdf01000!FxInterrupt::_InterruptThunk (KINTERRUPT ffffd000791f9b40)

81:   fffff801b6f60688 Wdf01000!FxInterrupt::_InterruptThunk (KINTERRUPT ffffd000759fe140)

82:   fffff801b6f60690 Wdf01000!FxInterrupt::_InterruptThunk (KINTERRUPT ffffd000791f9c80)

91:   fffff801b6f60708 Wdf01000!FxInterrupt::_InterruptThunk (KINTERRUPT ffffd000759fe280)

92:   fffff801b6f60710 Wdf01000!FxInterrupt::_InterruptThunk (KINTERRUPT ffffd000759fe780)

a1:   fffff801b6f60788 Wdf01000!FxInterrupt::_InterruptThunk (KINTERRUPT ffffd000759fea00)

                    HDAudBus!HdaController::Isr (KINTERRUPT ffffd000759fe3c0)

                    Wdf01000!FxInterrupt::_InterruptThunk (KINTERRUPT ffffd000791f9dc0)

a2:   fffff801b6f60790 dxgkrnl!DpiFdoMessageInterruptRoutine (KINTERRUPT ffffd000791f98c0)

b0:   fffff801b6f60800 ACPI!ACPIInterruptServiceRoutine (KINTERRUPT ffffd000759fedc0)

b1:   fffff801b6f60808 storport!RaidpAdapterMSIInterruptRoutine (KINTERRUPT ffffd000759feb40)

b2:   fffff801b6f60810 dxgkrnl!DpiFdoMessageInterruptRoutine (KINTERRUPT ffffd000791f9780)

cd:   fffff801b6f608e8 hal!HalpInterruptThermalService (KINTERRUPT ffffe000c4589000)

ce:   fffff801b6f608f0 hal!HalpIommuInterruptRoutine (KINTERRUPT fffff801b76ad3e0)

d1:   fffff801b6f60908 hal!HalpTimerClockInterrupt (KINTERRUPT fffff801b76ad2e0)

d2:   fffff801b6f60910 hal!HalpTimerClockIpiRoutine (KINTERRUPT fffff801b76ad1e0)

d7:   fffff801b6f60938 hal!HalpInterruptRebootService (KINTERRUPT fffff801b76acfe0)

d8:   fffff801b6f60940 hal!HalpInterruptStubService (KINTERRUPT fffff801b76acde0)

df:   fffff801b6f60978 hal!HalpInterruptSpuriousService (KINTERRUPT fffff801b76acce0)

e1:   fffff801b6f62b40 nt!KiIpiInterrupt
e2:   fffff801b6f60990 hal!HalpInterruptLocalErrorService (KINTERRUPT fffff801b76acee0)

e3:   fffff801b6f60998 hal!HalpInterruptDeferredRecoveryService (KINTERRUPT fffff801b76acbe0)

fe:   fffff801b6f60a70 hal!HalpPerfInterrupt (KINTERRUPT fffff801b76ad0e0)

Giusto per citare una delle voci: 0x60, i8042prt!I8042KeyboardInterruptService è associato alla tastiera.



Per vedere il System Service Dispatcher (visto che sono su 64bit ci sarà syscall) devo leggere l'MSR:
Codice:
lkd> rdmsr c0000082
msr[c0000082] = fffff801`b6f688c0

Codice:
lkd> ln fffff801`b6f688c0
Browse module
Set bu breakpoint

(fffff801`b6f688c0)   nt!KiSystemCall64   |  (fffff801`b6f689f0)   nt!KiSystemServiceStart
Exact matches:
    nt!KiSystemCall64 (<no parameter info>)

Volendo si potrebbe disassemblare anche quella routine.
Procedendo però con l'esempio citato oggi, ovvero WriteFile:

Codice:
lkd> u ntdll!NtWriteFile
ntdll!NtWriteFile:
00007ff8`aa7835f0 4c8bd1          mov     r10,rcx
00007ff8`aa7835f3 b808000000      mov     eax,8
00007ff8`aa7835f8 0f05            syscall
00007ff8`aa7835fa c3              ret

Come noti già all'interno della Ntdll compare la syscall.

Ci sarebbero tante altre cose da dire... come le funzioni utilizzate dai driver (iniziano con Zw, tipo ZwWriteFile o ZwReadFile). Sono utilizzate in quanto verificano la così detta "previous mode". In pratica, quando viene chiamata una ZwReadFile, ZwCreateFile etc., individua la modalità dalla quale proviene la chiamata (kernel mode) e setta la previous mode a "kernel mode" (una chiamata ad una NtCreateFile fallirebbe in quanto verrebbe settata su user mode; il codice user mode non può passare un indirizzo kernel mode... e quindi la chiamata fallirebbe).
Per completezza e spero per chiarire le cose, ecco come ri presenta una ZwWriteFile:

Codice:
lkd> uf nt!ZwWriteFile
Flow analysis was incomplete, some code may be missing
nt!ZwWriteFile:
fffff801`b6f5ae90 488bc4          mov     rax,rsp
fffff801`b6f5ae93 fa              cli
fffff801`b6f5ae94 4883ec10        sub     rsp,10h
fffff801`b6f5ae98 50              push    rax
fffff801`b6f5ae99 9c              pushfq
fffff801`b6f5ae9a 6a10            push    10h
fffff801`b6f5ae9c 488d05bd640000  lea     rax,[nt!KiServiceLinkage (fffff801`b6f61360)]
fffff801`b6f5aea3 50              push    rax
fffff801`b6f5aea4 b808000000      mov     eax,8
fffff801`b6f5aea9 e992d90000      jmp     nt!KiServiceInternal (fffff801`b6f68840)

Se noti EAX contiene sempre 8.
La particolarità di questa funzione è che simula un'interruzione (di fatto, è come se emulasse un CPU interrupt) ed effettua un JMP direttamente a KiServiceInternals.
A questo punto, il codice interno, andrà a chiamare NtWriteFile, ed ora verrà identificata in maniera corretta (proveniente dalla kernel-mode).

Trattazioni più esaustive meriterebbero un articolo o 2, così da poter sviscerare tanti altri dettagli.
Rimango comunque disponibile a tue domande, o di altri, eventualmente. ;)



E' importante poi precisare un'ultima cosa: i numeri dei servizi possono cambiare non solo da una versione di Windows ad un'altra, ma anche dopo ad un aggiornamento importante (come un Service Pack).
 
Mi permetto di aggiungere alcuni dettagli oltre alla già accurata spiegazione di DispatchCode.
in che modo avviene il passaggio dalla modalità utente alla modalità kernel?
in che modo funziona l'eccezione che passa il controllo al sistema operativo?
o meglio il sistema operativo come se ne "accorge" che deve servire una chiamata di sistema?

Il kernel del SO è un processo trasparente, nel senso che un processo non "sa" di essere sotto il suo controllo ed effettivamente "crede" di essere l'unico in esecuzione, con totale controllo delle risorse dell'elaboratore. Come se fosse l'unico nel PCB (Process Control Block). (passami i predicati tipicamente riservati agli umani)

Tuttavia, per determinate azioni -come la creazione di un thread- un processo ha necessità di invocarlo (molto spesso, indirettamente) poiché queste richiedono il trasferimento del controllo al kernel, il solo in grado di eseguirle. Il meccanismo che si è stabilito attraverso gli anni è quello delle system call. È però necessario, come dici, notificare il SO, indicare la propria disponibilità, ad effettuare la transizione. Il metodo più semplice è quello degli interrupt (https://it.wikipedia.org/wiki/Interrupt); altro non è che una richiesta di attenzione.
Specificamente, si tratta di un interrupt software. Questo perché un interrupt può anche essere di tipo hardware, ossia un reale evento asincrono generato da device collegati alla CPU che ne richiedono attenzione. Con l'istruzione INT num, dove num è appunto l'interrupt richiesto, la CPU riceve questa richiesta ed effettua una serie di preparazioni alla transizione di livello di privilegio, tipicamente dal livello 3 allo 0, il più alto. Ti tralascio i dettagli di questo passaggio perché fuori dal punto della discussione; sappi però che la CPU deve fare in modo di preservare lo stato pre-interrupt e sapere come tornarvi una volta che l'ISR sarà terminato, che coincide con l'istruzione IRET.

Il SO, negli stadi di avvio, predispone quella struttura citata da DispatchCode denominata IDT, o meglio, parte di essa: alcuni interrupt sono predefiniti ed immutabili, come la divisione per zero (DE) o errore generico (GP). Altri, li imposta il kernel: Linux usa(va) lo 0x80, DOS molti di più (come lo 0x21).

Il problema con gli interrupt s. è la loro lentezza: la stessa istruzione INT ha un'elevata latenza. Quindi sono state introdotte delle istruzioni che permettono un veloce ingresso al ring 0, quali sysenter/sysexit e syscall/sysret. Quando vengono invocate, la CPU salta all'indirizzo indicato dal SO nella GDT (un'altra tabella di informazioni per la CPU). Sia che il "salto" avvenga con INT o con sysenter, il SO, a seconda della convenzione di chiamata utilizzata, legge i parametri ed esegue l'handler appropriato.

come avviene il passaggio dei parametri da una funzione a una chiamata di sistema?
Dipende dal SO e quindi dall'ABI (Application Binary Interface). Specifici registri vengono usati e se non sbagli con sysenter non è possibile l'uso dello stack.

Esempio con INT:
Codice:
       movl $4, %eax # sys/write                                                                                       
       movl $1, %ebx # destinazione = stdout                                                                           
       movq $msg, %rcx # puntatore alla stringa                                                                        
       movq $size, %rdx # lunghezza                                                                                    
       int $0x80 # invocazione del kernel  

       movl $1, %eax     # sys/exit
       xorl %ebx, %ebx  # parametro d'uscita
       int $0x80

Esempio con syscall:
Codice:
       movq $1, %rax # sys/write                                                                                       
       movq $1, %rdi # stdout                                                                                          
       leaq msg, %rsi                                                                                                  
       movq $size, %rdx                                                                                                
       syscall                                                                                                         
                                                                                                                       
       movq $60, %rax # exit                                                                                           
       xorq %rdi, %rdi                                                                                                 
       syscall
La sintassi è GAS (ah-ha), però poco conta; nota la differenza dove vengono piazzati gli stessi parametri a parità di syscall e con che valori.
 
Complimenti ragazzi!!!
Vi invidio per la conoscenza che avete della materia.
 
Stato
Discussione chiusa ad ulteriori risposte.
Pubblicità
Pubblicità
Indietro
Top