Inline Assembly per Linux x86 e get_current(void) |
||
Data |
by "Quequero" |
|
16/Oct/2002 |
Published by Quequero |
|
|
|
...Sara' estremamente piu' criptica di quanto l'avrebbe immaginata il piu' pessimista dei pensatori. 2^ Legge di Que |
.... |
|
.... |
Difficoltà |
( )NewBies (X)Intermedio ( )Avanzato ( )Master |
L'ibrido asm/C e' qualcosa di estremamente potente, i programmatori l'hanno capito e hanno creato l'inline assembly... Sebbene sia estremamente ostico c'e' da dire che e' anche molto molto potente, percio' vale la pena di studiarlo
Introduzione |
Tools usati |
Essay |
__asm__ __volatile__ ("cld; rep; movsl;" : : "S" (src), "D" (dest), "c" (count));
Cos'e'? Lo capirete a breve...
Sintassi AT&T vs INTEL
AT&T
|
INTEL
|
movl
%eax,
%ebx
|
mov
ebx,
eax
|
movl $50,
%ecx
|
mov ecx, 50
|
movl
$0x100, %eax
|
mov eax, 100h
|
movb
(%eax), bl
|
mov bl, [eax]
|
movswl
%bx, %edx
|
movsx edx, bx
|
movl 4(%esp),
%eax
|
mov eax, [esp+4]
|
movl (%ebx,%eax,$4),
%edx
|
mov edx, [ebx+eax*4]
|
movb $4, %fs:(%eax)
|
mov fs:eax,
4
|
movl _array(,%eax,$4),
%eax
|
mov eax, [array+eax*4]
|
movw _array(%ebx,%eax,$4),
%cx
|
mov cx, [array+ebx+eax*4]
|
cbtw
|
cbw
|
cwtl
|
cwde
|
cwtd
|
cwd
|
cltd
|
cdq
|
Mi rendo conto che non sia la piu' splendida delle sintassi, specie per chi e' abituato a quella intel, ma bisogna riconoscere che reca alcuni grossi vantaggi (si lo so che potevano risparmiarsi tutti i % e tant'altro, ma non siamo noi a inventare le sintassi purtroppo :) ad ogni modo vi stilo le golden rules della sintassi AT&T in questo modo vi sara' estremamente facile comprenderla:
1.
Le istruzioni seguono quest'ordine: "istruzione
sorgente, destinazione"
al contrario della intel "istruzione destinazione,
sorgente",
1 punto a favore della sintassi AT&T.
2.
I registri sono sempre preceduti dal segno % le
costanti dal segno $ e per indicare
un numero esadecimale si usa la notazione $0x1234, nella sintassi
intel i registri si chiamano per nome, le costanti sono numeri e basta e possiamo
avvalerci sia della notazione 0x1234 che di quella 1234h, 1 punto a favore della
sintassi Intel.
3. Ad ogni istruzione va accodata:
una l se parliamo di dword (32-bit) (movl, addl,
subl),
una w se parliamo di word (16-bit) (movw, addw,
subw),
una b se parliamo di byte (8-bit) (movb, addb,
subb),
quindi se dobbiamo muovere una dword da eax in ebx scriveremo: "movl %eax,
%ebx", se dobbiamo muovere un byte semplicemente:
"movb %al, %bl,
questo logicamente vale anche per le referenze alla memoria, quindi se dobbiamo
muovere un byte puntato da eax in ebx faremo cosi:
"movb (%eax), %ebx"
come vedete per indicare i valori che si trovano in memoria utilizziamo le parentesi
tonde invece che le quadre. In questo caso e' difficile dire quale delle due
notazioni sia migliore, da una parte ci evitiamo di scrivere "dword ptr,
word ptr, o semplicemente: word, dword" quando serve, ma dall'altra ci
tocca assegnare una letterina a quasi tutti gli operatori, questo sarebbe poco
problematico fino a che non vengono usate istruzioni come movsx movzx, oppure
con tutte le istruzioni come loop, loopnz, loopne, loopz etc... Perche' in questo
caso non va accodato nulla all'istruzione, il connubbio migliore secondo me
e' quello che usa il nasm (vero albe? :p) dal momento che vuole solo sapere
se dobbiamo muovere byte, word o dword senza aggiungere ptr e cavoli vari (come
nel Tasm) percio' direi che e' un punto a favore della sintassi Intel.
4. Se utilizziamo istruzioni come movsx, movzx...
Non possiamo farlo in maniera _nativa_ dobbiamo utilizzare una notazione differente,
la prima parte dell'istruzione resta invariata, percio' movs resta movs e movz
resta movz, la seconda parte cambia perche' al compilatore serve sapere la grandezza
del Sorgente e quella della Destinazione
(movsSD, movzSD)
quindi:
"movsx edx, bx" diventa
"movswl %bx,
%edx"
indubbiamente un punto a favore della sintassi Intel che almeno ci consente
di utilizzare le istruzione per quello che realmente sono.
5. Se per spostare un valore puntato da un registro
da qualche parte basta utilizzare le parentesi tonde, non e' altrettanto banale
muovere un valore puntato + una costante, se nella sintassi Intel si usa semplicemente:
"mov eax, dword [ebx+4]"
nella sintassi AT&T si fa cosi:
"movl 4(%ebx), %eax"
E se dovessimo fare una cosa del tipo:
"mov
eax, dword [ebx*4]"
Dovremmo fare niente di meno intuitivo di:
"movl (%ebx, $4), %eax"
proviamo addirittura a combinare le due cose e traduciamo:
"mov ecx, dword [ebp*4+eax]"
diventa:
"movl (%eax, %ebp, $4), %ecx"
Ma l'apice del masochismo lo raggiungeremmo se dovessimo fare:
"mov cx, [array+ebx+eax*4]"
che diventerebbe:
"movw _array(%ebx,%eax,$4), %cx"
Gia', non sono un grande fan della sintassi AT&T, anzi la reputo davvero
qualcosa di tremendamente criptico e sinceramente non capisco perche' sia stata
concepita in questa maniera (forse solo per attenersi alla seconda legge di
Que), ma visto che c'e' ed e' piuttosto utilizzata sotto ogni *nix, ci tocca
conoscerla :)
Un po` di Inline Assembly
Adesso che siete dei guru della splendida sintassi AT&T (erm...) possiamo
spiegare come si utilizza l'inline assembly nei nostri sorgenti: prendiamo un
listato di questo tipo:
inline.c:
main(){
int a=10, b;
a += 5;
b = a;
printf("b vale %d\n", b);
}
compiliamolo e vediamo
cosa succede:
quequero@panther:~$ gcc inline.c -o inline && ./inline
b vale: 15
bene, sembra funzionare!
;p immaginiamo adesso di voler fare quel calcolo in asm, la prima cosa che ci
interessa sapere e' la sintassi dell'inline assembly, che si presenta cosi:
__asm__ ( assembler template
: output operands (optional)
: input operands (optional)
: list of clobbered registers (optional)
);
Ok, il motivo per
cui si usa l'inline asm e' che altrimenti sarebbe impossibile far interagire
le variabili C con il nostro codice assembler, e se alle volte ci servisse di
utilizzare l'asm in routine critiche, ci sarebbe impossibile farlo, percio'
traduciamo in pseudo-asm le operazioni di sopra e mettiamole nel sorgente:
addl
a, 5 ;
a = 15
movl a, %ebx ; ebx
= a
movl %ebx, b ; il
risultato in b
il problema principale
e' che non ci e' consentito di fare il primo, secondo e terzo passaggio per
ovvi motivi ;) cosi ricorriamo all'inline, il primo parametro "assembler
template" non e' altro che il nostro set di istruzioni, "output operands"
sono gli operandi nei quali finiranno i risultati delle operazioni fatte nell'assembler
template (e' opzionale perche' non e' detto che dobbiamo per forza andare a
mettere il risultato in qualche posto, un nop ad esempio non ha output) l'altro
parametro sono gli "input operands" (opzionale per lo stesso motivo)
e il terzo sono i "clobbered registers" ovvero i registri modificati,
anche questo e' opzionale dal momento che non e' detto che andremo a modificare
qualcosa, oppure possiamo salvare lo stato dei registri con push e pop e quindi
di fatto nulla verrebbe modificato. Adesso quindi possiamo tradurre in inline
asm le nostre operazioni:
__asm__ ("
movl %1, %%eax;
addl $5, %%eax;
movl %%eax, %0;
"
: "=r" (b)
: "r" (a)
: "%eax"
);
Non vi spaventate e' tutto
molto semplice, facciamo un paio di precisazioni, all'interno dell'inline i
registri vanno chiamati con due %% invece che con uno, questo perche con un
solo % vengono indicate le nostre variabili. Vi ho colorato il listato per rendervene
piu' semplice la comprensione, dunque, in rosso troviamo il nostro "assembler
template", in blu "output register"
in verde "input register" in giallo i
"clobbered registers". La prima perplessita'
dovrebbe esser rappresentata da quegli %1 e %0, per capire cosa sono dovete
andare nell'input register, e vedete che c'e' (b), quella e' la variabile %0,
una riga sotto troviamo (a) e quella e' la variabile %1, logicamente (b) e'
l'int `b` e (a) l'int `a`. Eax e' invece il registro che alla fine della routine
sara' stato modificato, questo serve al gcc in fase di compilazione per consentirgli
o di non usare eax se possibile, oppure di salvare il risultato di eax prima
di entrare nella routine e recuperarlo dopo, se non specificassimo che eax e'
un registro modificato potremmo avere seri problemi, infatti eax potrebbe contenere
un valore importante che di conseguenza andrebbe perso.
Esaminiamo ora riga per riga quello che e' successo:
movl
%1, %%eax; <- la variabile %1 ovvero `a` viene messa
in eax
addl $5, %%eax;
<- quindi le viene sommato
5 (a+5)
movl %%eax, %0;
<- e viene spostata dentro la variabile %0 ovvero `b`
Questo e'
abbastanza semplice, passiamo agli output registers:
"=r"
(b)
tra
le virgolette il parametro `=` significa che il registro che stiamo usando e'
un registro di OUTPUT, e' fondamentale mettere il simboletto `=` li, `r` sta
ad indicare che alla variabile `b` possiamo associare un qualunque registro
(eax, ebx, ecx, edx), `(b)` e' ovviamente la variabile alla quale assegneremo
il registro ed essendo la prima specificata sara' anche la variabile %0. Riassumento
quella riga vuol dire: assegna all'intero `b` un registro qualunque e ricordati
di contrassegnare quel registro come registro di uscita.
"r"
(a)
non
essendo un output register, non abbiamo bisogno del simbolo `=`, questa riga
vuol dire: assegna all'intero `a` un registro a piacere, logicamente essendo
`a` la seconda variabile definita dentro il nostro inline, sara' anche %1.
"%eax"
questa
vuol dire: gcc mentre compili ricordati che in eax non sai cosa ci troverai
dal momento che viene usato dall'utente, percio' attento (nella lista dei clobbered
register non sono necessari due %% per indicare i registri ma ne basta uno).
Trasferiamo quindi il nostro codice all'interno del sorgente e compiliamo:
inline.c
main(){
int a=10, b;
__asm__("movl %1, %%eax; addl $5, %%eax; movl %%eax, %0;" : "=r"
(b) : "r" (a) : "%eax");
printf("b vale: %d\n", b);
}
vediamo cosa succede:
quequero@panther:~$ gcc inline.c -o inline &&
./inline
b vale: 15
funziona,
quindi vediamo come l'ha compilato il gcc (gcc inline.c -o inline -S):
main:
pushl %ebp
movl %esp, %ebp
pushl %ebx
subl $20, %esp
andl $-16, %esp
movl $0, %eax
subl %eax, %esp
movl $10, -8(%ebp)
movl -8(%ebp), %edx
#APP
movl %edx, %eax; addl $5, %eax; movl %eax, %edx;
#NO_APP
movl %edx, %eax ; un pochino
ridondante ;p
movl %eax, -12(%ebp) ; gia :)
subl $8, %esp
pushl -12(%ebp)
pushl $.LC0
call printf
addl $16, %esp
movl -4(%ebp), %ebx
leave
ret
come potete vedere,
il gcc non ha utilizzato eax perche' l'ha assegnato alla nostra variabile, ho
contrassegnato un paio di righe come ridondanti ma quelle spariscono se compiliamo
come -O2/-O3, ok prima di complicarci la vita ulteriormente vorrei dirvi che
all'interno dell'assembler template potete separare ogni istruzione sia con
un ; che con uno \n e se volete studiare il listato asm potete accodare allo
\n anche uno \t quindi fare questo "movl %eax, %ebx; nop;" e' identico
a: "movl %eax, %ebx\n\t nop\n\t" a questo punto complichiamoci un
po' la vita :)
Alle volte puo' non bastarci utilizzare il parametro "r" o "=r"
perche' dobbiamo utilizzare un registro specifico (prendete come esempio le
istruzioni stosw, stosb, loop...) possiamo quindi avvalerci di ulteriori parametri
che saranno spesso fondamentali, un breve elenco e' questo (non sono tutti elencati,
`man gcc` se vi interessa conoscerli uno per uno):
Parametro
|
Registro
|
a
|
eax
|
b
|
ebx
|
c
|
ecx
|
d
|
edx
|
S
|
esi
|
D
|
edi
|
q
|
eax,
ebx, ecx, edx
|
r
|
eax,
ebx, ecx, edx, esi, edi
|
g
|
eax,
ebx, ecx, edx o una variabile in memoria
|
A
|
eax
viene esteso a edx diventando un registro a 64-bit
|
m
|
una
variabile in memoria
|
&x
(&r...)
|
un registro diverso da ... (guardate gli esempi sotto)
|
0...n
|
non
importa quale registro purche' sia lo stesso della variabile %0, %1....%n
|
facciamo qualche esempio, traduciamo questo: `for (int i=0; i<50; i++)` in inline asm:
main(){
int loop=50, i=0;
__asm__("label: inc %0; loop label" : "=a" (i) : "c"
(loop));
printf("i vale: %d\n", i);
}
con allegria abbiamo
dato in pasto al gcc il nostro assembler template, riservandoci di dirgli che
volevamo `i` dentro eax e `loop` dentro ecx, questo era obbligatorio dal momento
che l'istruzione loop funziona su ecx, compiliamo e vediamo se effettivamente
ha fatto cosi:
movl $50, %ecx
#APP
label: inc %eax; loop label
#NO_APP
pushl %eax
come potete vedere
ha assegnato ecx a `loop` eax a `i`, ma non manca nulla? Gia i clobbered registers,
non ce n'e' bisogno dal momento che abbiamo chiesto esplicitamente eax ed ecx
nell'input/output, se avessimo invece chiesto un registro generico, allora avremmo
dovuto specificare quale avevamo utilizzato nella nostra clobber list. Facciamo
un altro esperimento:
main(){
int loop=50, i=0;
__asm__("label: movl %0, %%eax; inc %%eax; movl %%eax, %0; loop label"
: "=m" (i) : "c" (loop): "%eax");
printf("i vale: %d\n", i);
}
Come vedete qui
ho scelto di infilare `i` dentro una variabile in memoria, quindi non posso
fare `inc variabile` ma la devo estrarre, incrementare e quindi rimettere in
memoria. Vediamo il listato asm:
movl $50, %ecx
#APP
label: movl -4(%ebp), %eax; inc %eax; movl %eax, -4(%ebp); loop label
#NO_APP
pushl -4(%ebp)
come possiamo felicemente constatare, `i` e' stata messa dentro [ebp-4], quindi
funziona :), ovviamente ho dovuto aggiungere eax alla lista dei registri utilizzati
visto che lo modifichiamo all'interno dell'asm template. Altra prova:
main(){
int i=0;
asm ("incl %0" :"=a"(i):"0"(i));
printf("i vale: %d\n", i);
}
abbiamo
chiesto al gcc di assegnare eax a `i`, ma quello "0" cos'e'? Semplicissimo:
`i` e' la variabile %0, quindi passando al gcc il parametro "0" nell'input
register gli diciamo: gcc dammi come input register lo stesso che utilizzi per
la variabile %0, nel nostro caso eax, ma se avessimo usato "=r" come
output e come input "0" cmq il registro di input sarebbe stato lo
stesso di quello dell'output, semplice vero? :) Verifichiamo:
xorl %eax, %eax
#APP
incl %eax
#NO_APP
pushl %eax
Logicamente funziona ma se per esempio ci servisse di avere in un registro qualunque
una variabile e in un'altro la stessa?
main(){
int x=10, y;
asm ("movl %1, %%ecx; movl %%ecx, %0" :"=&r"(y):"r"(x):
"%ecx");
printf("y vale: %d\n", y);
}
in questo caso diciamo al
gcc che per la variabile `y` vogliamo un registro qualunque purche' sia differente
da quello utilizzato per `x`, percio' utilizzando l'operatore & costringiamo
il gcc ad utilizzare un altro registro, verifichiamo anche stavolta se e' stato
cosi:
movl
$10, %eax
subl $16, %esp
#APP
movl %eax, %ecx; movl %ecx, %edx
#NO_APP
pushl %edx
e infatti come volevasi
dimostrare, e' stato assegnato eax a `x` ed edx a `y` :)
get_current(void)?
[18:45:17] <buffer> Quequero: io l'ho conosciuto a padova mentre interpetavo
la get_current() del kernel....però manco lui ci capiva nulla! ;))))
[18:45:28] <Quequero> rotfl
[18:45:52] <buffer> è una sola riga di asm inline alquanto criptica.....
[18:46:57] <buffer> no poi l'ho capita ma al momento mi lasciò
perplesso
[18:47:12] <Quequero> ;pp
[18:47:55] <buffer> static inline struct task_struct * get_current(void)
[18:47:56] <buffer> {
[18:47:56] <buffer> struct task_struct *current;
[18:47:56] <buffer> __asm__("andl %esp,%0; ":"=r"
(current) : "0" (~8191UL));
[18:47:56] <buffer> return current;
[18:47:56] <buffer> }
[18:48:01] <buffer> per la cronaca è questa
apriamo: /usr/src/linux/include/asm-i386/current.h
static inline struct task_struct * get_current(void)
{
struct task_struct *current;
__asm__("andl %esp,%0; ":"=r" (current)
: "0" (~8191UL));
return current;
}
NotaBene:
(~8191UL) NON vuol dire che si cerca di fare il NOT bit a bit, bensi che si
fa il NOT del valore in modo da renderlo NEGATIVO!
Ebbene si, buffer
mi ha parecchio invogliato a studiare l'inline, senza dire che il get_current
mi lascio' un attimino perplesso anche dopo aver capito a fondo l'inline (ovvero
ieri ;p). Cosa fa quella piccola funzione? (Logicamente oltre a restituire il
descrittore del processo corrente...) Si sa che le funzioni piu' piccole sono
anche le piu' infime, per prima cosa creiamo un programmino che utilizzi la
get_current:
#include <asm/current.h>
main(){
get_current();
}
bene, a questo punto
compiliamolo con -S e vediamo cosa otteniamo (ovviamente dovrebbe girare a livello
kernel per leggere lo stack kernel mode):
get_current:
pushl %ebp ;
..
movl %esp, %ebp ; ..
subl $4, %esp ; sistema
lo stack
movl $-8192, %eax ; tenete d'occhio -8192 (e' 8192
invece che 8191 per via del complemento a due, grazie buff)
#APP
andl %esp,%eax; ; fa l'and dell'esp
con -8192
#NO_APP
movl %eax, -4(%ebp) ; un po ridondante anche con -O2
movl -4(%ebp), %eax ;
leave
ret
Cosa fa di particolare
questa get_current() in una sola riga di asm? Fa l'AND dell'esp corrente con
un numero, -8192 ma perche'? Per capirlo dobbiamo andare un pochino piu' a fondo
nei meandri del kernel... Noi sappiamo che ogni processo ha due stack, uno usermode
e uno logicamente kernelmode che e' grande 8kb (due pagine contigue da 4kb),
attenzione... Alla cima di questo stack (per come cresce lo stack sugli intel
per cima intendo l'inizio dello stack+8kb) sta il descrittore del nostro processo
(la task_struct) e sotto c'e' lo stack kernel mode, in pratica:
0xFFFFFFFF
+------------------------------+
+ ...................
+
| + Stack
kernel mode + <-
Inizio stack Kernel Mode
V + ... + |
+ ..... +
| Questi sono 8kb
| + ....... +
|
V + struct
task_struct + <-
Alla fine dello stack Kernel Mode c'e' la nostra task_struct
+ ....... +
| + ..... +
V +
... +
+ Fine
memoria +
0x00000000 +------------------------------+
bisogna capire cosa sia ora il numero -8192, in realta' e' semplice, fate 1024*8
= 8192, percio prendete il programma, stampate lo stack a skermo e fate due
conti:
#include <asm/current.h>
main(){
int esp;
__asm__("mov %%esp, %0": "=r" (esp));
printf("esp vale 0x%x\n", esp);
get_current();
}
vediamo
cosa stampa:
quequero@panther:~$ ./inline
esp vale 0xbffff870
quindi 0xbffff870
AND -8192 = 0xBFFFE000, come e' chiaro succede una cosa, gli ultimi 13 bit di
esp vengono azzerati (infatti gli ultimi 13 bit di -8192 in binario valgono:
0000000000000, quindi con l'and azzeriamo gli ultimi 13bit) facendo questo non
facciamo altro che arrotondare il valore dello stack agli 8kb inferiori, e guarda
caso proprio li si trova il primo byte della nostra task_struct, una maniera
quindi estremamente efficiente per ottenere l'indirizzo della struttura. Io
ho impiegato un pochino a capire di preciso come funzionasse ma vi assicuro
che diventera' piuttosto semplice dopo che l'avrete riflettuta :)) buon divertimento.
-=Quequero=-
Note finali |
Al mio amore :*.
Ringrazio poi: awgn,
twiz e buffer perche' se ho capito questa roba e' soltanto grazie loro, saluto
poi tutta #crack-it, #phrack.it, #spippolatori, #ahccc, #fogna, #cisco, sgrakkyu,
il mio spasimante maranik :*, alor che mi ha aiutato con le prime ebuild ;p
(trema, io e awgn stiamo preparando uno shellcode talmente elite che ti verra'
un infarto quando lo vedrai :), marella (la piu` bella :*), artemis :*, Yado,
andregay, CAMILLOOOOOOO, il mio fratellone N0bodY88, l'altro mio fratellone
Vinciccio, tutta la STRAMITICA crew degli Spippolatori... Beh, se ho salutato
i canali era per non menzionarvi uno a uno o sbaglio? ;ppp ciauz gente!
Disclaimer |
Non garantisco nulla, inclusa la veridicita' delle informazioni, il tutorial viene pubblicato sotto licenza GPL versione 2, potete fare quello che volete a patto di menzionare sempre l'autore (IO), il sito dal quale e' stato prelevato (www.quequero.tk, www.scared.tk, www.quequero.cjb.net ...) ed eventuali modifiche apportate da terzi se ne dovessero venir fatte.