Come funziona Inotify – parte 2 – Come viene chiamata la system call

In questo blog post cercheremo di capire come le chiamate di Inotify arrivano a livello kernel, passando per la libreria di interfaccia del sistema (libc).

Se effettuiamo strace del nostro programma vediamo le chiamate effettuate dal nostro processo.

Strace e’ un comando che registra le syscall fatte dal nostro processo in user space verso il kernel in kernel space, ci ritorna come output la system call, i parametri che passiamo il valore di ritorno. Lo eseguiamo sul programma che abbiamo creato nel precedente articolo http://www.spaghettiml.com/2019/02/01/come-funziona-inotify-parte-1/

Compiliamo il programma ed eseguiamo quindi strace:

gcc inotify_example.c && strace ./a.out 

Ora, a noi interessa capire come venga chiamata la funzione: inotify_add_watch

...
inotify_add_watch(3, "/home/joxer/code", IN_ACCESS|IN_MODIFY|IN_ATTRIB|IN_CLOSE_WRITE|IN_CLOSE_NOWRITE|IN_OPEN|IN_MOVED_FROM|IN_MOVED_TO|IN_CREATE|IN_DELETE|IN_DELETE_SELF|IN_MOVE_SELF) = 1
...

Questa e’ la system call che viene chiamata quando nel nostro codice effettuiamo inotify_add_watch( inotifyFd, bufPtr, IN_ALL_EVENTS );
Si vede qui, come la costante IN_ALL_EVENTS venga espansa e 3 indichi il file descriptor sul quale stiamo agendo.

Ora, dove possiamo trovare questa system call? Ci basta cercare nel source code del kernel Linux e troviamo il file inotify_user.c che la contiene
https://github.com/torvalds/linux/blob/master/fs/notify/inotify/inotify_user.c#L696

In un successivo articolo trattero’ del corpo della funzione, per ora mi limitero’ a illustrare come viene effettuata la chiamata dallo user space in kernel space.

Ora, come viene invocata una system call da libc nel kernel?
Questo dipende dall’architettura e da come vengono passati i parametri al kernel

Sia su sistemi x86 che su sistemi x86_64, Linux effettua una system call chiamando l’interrupt 0x80, ovvero usando l’istruzioneint $0x80
Sui sistemi x86_64 e’ disponibile, ed e’ definita come predefinita, l’istruzione syscall, al posto dell’interrupt 0x80.
I parametri vengono passati settando i registri nel seguente modo:

I valori associati a ogni system call, cioe’ il valore che dobbiamo mettere nel registro eax per chiamare le syscall vengono generati nel file arch/x86/include/generated/uapi/asm/unistd_32.h o usr/include/asm/unistd_32.h

Se vogliamo chiamare la system call dallo user space, il metodo e’ usare <sys/syscall.h> con la funzione long syscall(long number, …)
Questa funzione effettua le seguenti azioni:

  • Copia gli argomenti e setta il numero della system call nei registri del processore dove il kernel si aspetta di trovarli
  • Viene effettuata la trap in kernel mode, si passa in kernel mode e ora e’ il kernel ad avere il comando ed effettuare il codice
  • Se la system call ritorna un errore, viene settato errno e la cpu torna in user mode

Libc effettua del wrapping attorno le system call del kernel o le chiama direttamente. Ulteriori informazioni su questo si possono trovare alla pagina: https://sourceware.org/glibc/wiki/SyscallWrappers

Se visualizziamo il codice assembly per il nostro eseguibile possiamo vedere come viene chiamata la nostra funzione inotify_add_watch:

 gcc -g file.c
objdump -d -S a.out

Qui possiamo vedere le seguenti linee:

int wd = inotify_add_watch( inotifyFd, bufPtr, IN_ALL_EVENTS );
a02: 48 8b 8d 20 f5 ff ff mov -0xae0(%rbp),%rcx
a09: 8b 85 04 f5 ff ff mov -0xafc(%rbp),%eax
a0f: ba ff 0f 00 00 mov $0xfff,%edx
a14: 48 89 ce mov %rcx,%rsi
a17: 89 c7 mov %eax,%edi
a19: e8 62 fd ff ff callq 780 <inotify_add_watch@plt>
a1e: 89 85 08 f5 ff ff mov %eax,-0xaf8(%rbp)

Quindi vediamo la riga: callq 780 <inotify_add_watch@plt> che chiama la funzione

0000000000000780 <inotify_add_watch@plt>:
780: ff 25 42 18 20 00 jmpq *0x201842(%rip) # 201fc8 <inotify_add_watch@GLIBC_2.4>
786: 68 07 00 00 00 pushq $0x7
78b: e9 70 ff ff ff jmpq 700 <.plt>

Si vede una jmpq alla riga 780 verso la funzione di glibc linkata dal mio programma. Questa e’ scritta come un riferimento a @plt

PLT e’ la Procedure Linkage Table, una tabella inclusa nel nostro eseguibile contenente gli indirizzi delle funzioni la quale risoluzione non e’ conosciuta in tempo di linking ed e’ lasciata come compito per il dynamic linker quando il programma viene eseguito.

GOT e’ la Global Offsets Table, ed e’ usata in modo simile per risolvere gli indirizzi. Tuttavia la GOT contiene una tabella fissa di questi indirizzi all’interno dell’eseguibile, in questo modo la PLT e’ dinamica ogni volta che si esegue il processo, invece la GOT e’ statica e il nostro programma conosce sempre in quale punto possono essere trovati i riferimenti alle funzioni. La GOT viene aggiornata in tempo di esecuzione del processo da parte del linker.
Usiamo il comando readelf sul nostro eseguibile, la flag –relocs mostra la sezione relocations del nostro eseguibile.

$ readelf –relocs a.out

Relocation section '.rela.plt' at offset 0x608 contains 9 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000201f90 000200000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0
000000201f98 000300000007 R_X86_64_JUMP_SLO 0000000000000000 pathconf@GLIBC_2.2.5 + 0
000000201fa0 000400000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
000000201fa8 000500000007 R_X86_64_JUMP_SLO 0000000000000000 getcwd@GLIBC_2.2.5 + 0
000000201fb0 000600000007 R_X86_64_JUMP_SLO 0000000000000000 read@GLIBC_2.2.5 + 0
000000201fb8 000900000007 R_X86_64_JUMP_SLO 0000000000000000 inotify_init@GLIBC_2.4 + 0
000000201fc0 000a00000007 R_X86_64_JUMP_SLO 0000000000000000 malloc@GLIBC_2.2.5 + 0
000000201fc8 000b00000007 R_X86_64_JUMP_SLO 0000000000000000 inotify_add_watch@GLIBC_2.4 + 0
000000201fd0 000c00000007 R_X86_64_JUMP_SLO 0000000000000000 exit@GLIBC_2.2.5 + 0

In grassetto possiamo vedere l’offset della nostra funzione. I campi del comando readelf –relocs sono i seguenti:

  • Offset: e’ l’offset che il nostro simbolo avra’
  • Info: Ci indica l’indice del simbolo nella symbol table
  • Type: Ci indica il simbolo secondo la ABI (Application binary interface)
  • Sym value: e’ il padding che dobbiamo aggiungere per il symbol resolution
  • Sym name and addend: indica il nome del simbolo piu’ il padding

Se vogliamo vedere la sezione GOT del nostro eseguibile possiamo eseguire il comando che ci da’ il dump della sezione .got: objdump -j .got -s a.out

a.out: file format elf64-x86-64

Contents of section .got:
201f78 881d2000 00000000 00000000 00000000 .. ………….
201f88 00000000 00000000 16070000 00000000 …………….
201f98 26070000 00000000 36070000 00000000 &…….6…….
201fa8 46070000 00000000 56070000 00000000 F…….V…….
201fb8 66070000 00000000 76070000 00000000 f…….v…….
201fc8 86070000 00000000 96070000 00000000 …………….
201fd8 00000000 00000000 00000000 00000000 …………….
201fe8 00000000 00000000 00000000 00000000 …………….
201ff8 00000000 00000000 ……..

ed eseguendo il comando objdump -R a.out, otteniamo i relocation records della nostra GOT quando il programma viene lanciato.

a.out: file format elf64-x86-64

DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
0000000000201d78 R_X86_64_RELATIVE ABS+0x00000000000008b0
0000000000201d80 R_X86_64_RELATIVE ABS+0x0000000000000870
0000000000202008 R_X86_64_RELATIVE ABS+0x0000000000202008
0000000000201fd8 R_X86_64_GLOB_DAT _ITM_deregisterTMCloneTable
0000000000201fe0 R_X86_64_GLOB_DAT
libc_start_main@GLIBC_2.2.5 0000000000201fe8 R_X86_64_GLOB_DAT __gmon_start
0000000000201ff0 R_X86_64_GLOB_DAT _ITM_registerTMCloneTable
0000000000201ff8 R_X86_64_GLOB_DAT __cxa_finalize@GLIBC_2.2.5
0000000000201f90 R_X86_64_JUMP_SLOT puts@GLIBC_2.2.5
0000000000201f98 R_X86_64_JUMP_SLOT pathconf@GLIBC_2.2.5
0000000000201fa0 R_X86_64_JUMP_SLOT printf@GLIBC_2.2.5
0000000000201fa8 R_X86_64_JUMP_SLOT getcwd@GLIBC_2.2.5
0000000000201fb0 R_X86_64_JUMP_SLOT read@GLIBC_2.2.5
0000000000201fb8 R_X86_64_JUMP_SLOT inotify_init@GLIBC_2.4
0000000000201fc0 R_X86_64_JUMP_SLOT malloc@GLIBC_2.2.5
0000000000201fc8 R_X86_64_JUMP_SLOT inotify_add_watch@GLIBC_2
.4
0000000000201fd0 R_X86_64_JUMP_SLOT exit@GLIBC_2.2.5

Vediamo come gli indirizzi presenti nell’offset sono esattamente quelli presenti quando vediamo il contenuto della sezione GOT. Inoltre, se vediamo la riga:

780: ff 25 42 18 20 00 jmpq *0x201842(%rip) # 201fc8 <inotify_add_watch@GLIBC_2.4>

Vediamo che il valore 201fc8 e’ quello dove, nella nostra GOT, troviamo il punto dove ci si aspetta di trovare l’indirizzo della funzione inotify_add_watch della libreria GLIBC.

Quindi i nostri programmi vengono linkati verso le funzioni esterne di glibc per poter chiamare la syscall.
Se lanciamo objdump per vedere il contenuto della nostra libc avremo il seguente output:

objdump -d -S /lib/x86_64-linux-gnu/libc.so.6 | grep inotify_add_watch -A 10

00000000001222d0 :
1222d0: b8 fe 00 00 00 mov $0xfe,%eax
1222d5: 0f 05 syscall
1222d7: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax
1222dd: 73 01 jae 1222e0
1222df: c3 retq
1222e0: 48 8b 0d 81 8b 2c 00 mov 0x2c8b81(%rip),%rcx # 3eae68
1222e7: f7 d8 neg %eax
1222e9: 64 89 01 mov %eax,%fs:(%rcx)
1222ec: 48 83 c8 ff or $0xffffffffffffffff,%rax
1222f0: c3 retq
1222f1: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
1222f8: 00 00 00
1222fb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)

Abbiamo quindi capito come i nostri programmi possono accedere al kernel space per poter eseguire le system call e come vengono linkati tramite il linker in tempo di esecuzione verso le funzioni che non sono ancora conosciute.
Nel prossimo articolo trattero’ il sorgente del kernel cercando di capire come funziona inotify e come vengono create veramente le system call dentro Linux.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *