Bune practici

Mai jos găsiți un ghid bune practici, recomandări și greșeli frecvente care apar în momentul în care lucrați în limbajul de asamblare. Să țineți cont, vă rugăm, de acestea în momentul în care lucrați în laboratoare sau teme de casă.

Exemple

Erori des întâlnite

Confuzii la accesarea datelor în memorie (operatorul de dereferenţiere)

Pentru cei care sunt la început de drum la a învăţa assembly, este o confunzie foarte mare cum se foloseşte operatorul de dereferenţiere din asamblare: [ ]
Care este diferenţa între op reg, var şi op reg, [var]?
În 99.999999999999% din cazuri, operaţia fără paranteze pătrate înseamnă să foloseşti adresa acelei variabile pe post de operand. Exemple:

section .data
	var: DD 34
section .text
	mov eax, var ; put var's >>address<< into the eax register
	add eax, var ; add to eax, the >>address<< of var

Acest cod este echivalent cu următorul cod din C:

int var = 34;
eax = &var; /* mov eax, var */
eax = eax + &var; /* add eax, var */

În cazul în care foloseşti paranteze pătrate:

section .data
	var: DD 34
section .text
	mov eax, [var] ; put var's >>value<< into eax
	add eax, [var] ; add to eax, the >>value<< of var

Acest lucru ar fi echivalent în C cu:

int var = 34;
eax = var; /* mov eax, [var] */
eax = eax + var; /* add eax, [var] */

Printre singurele instrucţiuni care fac abatare de la aceste reguli, este lea (load effective address).

section .data
	var: DD 34
section .text
	lea eax, [var] ; put var's >>address<< into the eax register

În rest, toate celelalte instrucţiuni aderă la regulile enunţate mai sus. Dacă or mai exista şi alte instrucţiuni care se comportă ca lea, cel mai probabil nu vor fi tratate în aceste laboratoare.

Încărcarea datelor în registre

Adesea apar erori chiar la încărcarea datelor în registre.

load.asm
extern printf
 
section .data
	nr: DB 23
	str: DB 'number: %d', 0
 
section .text
 
global main
 
main:
	mov eax, [nr]
	push eax
	push str
	call printf
	add esp, 8
	ret

În momentul în care se face mov eax, [nr], instrucţiunea mov încearcă să deducă dimensiunea mutării (câte date/bytes să ia de la adresa de la care începe nr?). nr fiind doar o adresă în memorie, nu-i spune nimica compilatorului. Din acest motiv, compilatorul încearcă să se uite dacă nu cumva în această instrucţiune nu există şi un registru implicat. Îl vede pe eax. În consecinţă, compilatorul va codifica instrucţiunea astfel încât în eax să se aducă sizeof(eax) (adică 4 bytes) de la adresa lui nr.
Deşi nr are valoarea 23, programul afişează number: 1836412439.
De ce? Pentru că la nr find un singur byte, procesorul continuă să aducă din memorie încă 3 bytes astfel încât să îl poate umple pe eax. În cazul nostru, după nr, în memorie, este declarat vectorul str, aşa că va lua încă 3 bytes de la el pentru a-l umple pe eax.

Intuitiv, v-aţi aştepta ca asamblorul/compilatorul să urle la voi “că uite domne, eu am declarat variabila de 1 byte, şi am scris din greşeală că vreau să aduc 4 bytes de acolo”. Ca şi în cazul limbajului C, limbajul de asamblare te lasă să te împuşti singur în picior. Nu este treaba lui să facă check-uri. Dacă tu vrei 1 milion de bytes de la adresa 0xB00B5, el o să-ţi codifice programul în binar astfel încât să-ţi aducă date de la adresa 0xB00B5. Că după îţi bubuie programul în faţă cu un Segmentation Fault pentru că ai încercat să accesezi o zonă de memorie care nu ţi-a fost alocată, e deja treaba sistemului de operare şi a procesorului.

O primă rezolvare

O primă încercare de a rezolva problema ar fi să încercăm să-l aducem pe nr direct într-un registru de 1 byte.

load_byte.asm
extern printf
 
section .data
	nr: DB 23
	str: DB 'number: %d', 0
 
section .text
 
global main
 
main:
	mov al, [nr] ; modified line
	push eax
	push str
	call printf
	add esp, 8
	ret

Mie, personal, s-a întâmplat ca acum să-mi dea corect afişarea. Dar programul nu este încă corect. Noi îi transmitem lui printf să afişeze un număr reprezentat pe 4 bytes. Deşi noi am încărcat datele în al, noi îi spunem lui printf să afişeze conţinutul la tot eax, nu doar la al. În unele cazuri, s-ar putea ca conţinutul părţii superioare a lui eax să nu fie curat, din cauza codului care s-a executat anterior. S-ar putea ca cei mai semnificativi 3 bytes să fie plini cu garbage (date random), şi afişarea noastră tot să nu fie corectă. Astfel că mai este nevoie de încă o corectură:

load_byte.asm
extern printf
 
section .data
	nr: DB 23
	str: DB 'number: %d', 0
 
section .text
 
global main
 
main:
	xor eax, eax ; eax = 0
	mov al, [nr] ; modified line
	push eax
	push str
	call printf
	add esp, 8
	ret

Tot registrul eax trebuie iniţializat la 0, ca să fim singuri că nu există junk în partea superioară.

Cum să eviţi să te împuşti singur în picior ?

Există un set de cuvinte cheie în NASM care îi specifică asamblorului/compilatorului pe câţi bytes are loc operaţia. Acestea sunt: byte, word şi dword (double word).

load.asm
extern printf
 
section .data
	nr: DW 23 ; declare a variable of word type (2 bytes)
	str: DB 'number: %d', 0
 
section .text
 
global main
 
main:
	mov eax, word [nr] ; try to access a varible of word type ; try to bring 2 bytes into eax
	push eax
	push str
	call printf
	add esp, 8
	ret

Dacă de exemplu ai declarat un vector/variabilă de words, peste tot unde se accesează un element din acel vector/varibilă prefixează accesul cu tipul variabilei (byte, word, dword, etc.). În felul acesta, asamblorul îţi va da o eroare sugestivă prin care să-ţi dai seama că codul tău nu este tocmai în regulă:

arcade@Arcade-PC:~/workspace/asm_exemple > nasm -f elf32 load.asm 
load.asm:12: error: invalid combination of opcode and operands

Poate că nu ai un cod care compilează, dar măcar nu ai un cod care compilează şi ruleaza greşit.

Segmentation Fault debugging: GDB quicky

gdb este un debugger în linie de comandă. Unul din lucrurile la care ne poate ajuta acesta este să găsim punctele în care ne dă Segmentation Fault un program. Mulţi abordează această problemă prin imbricarea de printf-uri în puncte intermediare în program. Acest lucru nu prea ajută. Uitaţi cam cum este prelucrat un program de un procesor:

  1. Într-o singură etapă se aduc mai multe instrucţiuni din memorie. Accesul la memorie este scump, şi dacă la fiecare instrucţiune de 5-6 bytes ne-am duce în memorie, nu am avea o performanţă foarte bună. Din acest motiv s-a inventat un modul în procesor, numit prefetching, în care se înmagazinează mai multe instrucţiuni de la adresa de la care se aduce cod/instrucţiuni, pentru ca execuţia să fie mai fluidă.
  2. În momentul în care procesorul îşi dă seama că una din instrucţiuni accesează o zonă nevalidă din memorie, trimite un semnal către sistemul de operare. Şi sistemul de operare este tot o bucată de cod care se execută pe procesor. Până când acest semnal trezeşte codul din sistemul de operare, e foarte posibil ca programul să mai fi executat o căruţă de instrucţiuni, din acest motiv, o înşiruire de printf-uri s-ar putea executa şi după instrucţiunea care a produs Segmentation Fault-ul.
  3. Sistemul de operare se trezeşte şi închide forţat programul care a cauzat probleme. Printre datele primite de la semnal se regăseşte şi adresa instrucţiunii care a cauzat Segmentation Fault. Cu un debugger, se poate afla şi din userspace ce instrucţiune a cauzat Segmentation Fault.

Exemplu de cod cu probleme:

segfault.asm
extern printf
 
section .data
	str: DB `number: %d\n`
	nr: DD 1, 2, 3, 4, 5
	len: DD 4000
 
section .text
 
global main
 
main:
	xor ecx, ecx
keep_printing:
	push ecx ; save ecx, because it will be destroyed by printf call
	push dword [nr + 4*ecx]
	push str
	call printf
	add esp, 8
	pop ecx ; restore ecx
	inc ecx
	cmp ecx, [len]
	jl keep_printing
	ret

Programul parcurge un vector şi afişează valorile sale. Deşi programul are doar 5 elemente, len-ul este setat greşit la 4000 de elemente. Dacă compilăm şi rulam programul acesta ne va da un segfault:

# ...
number: 0
number: 0
number: 0
Segmentation fault

Cum rulăm gdb:

gdb nume_binar

Exemplu:

catalin.vasile3004@fep ~ $ gdb ./segfault
GNU gdb (GDB) Red Hat Enterprise Linux (7.2-60.el6)
Copyright (C) 2010 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /export/home/acs/stud/c/catalin.vasile3004/load...(no debugging symbols found)...done.
(gdb)

În acest moment s-a deschis consola debugger-ului, dar programul NU rulează. Pentru a rula programul:

set disassembly-flavor intel
run param1 param2 param3 < fisier.in > fisier.out

Cu run-ul dat ca exemplu, e ca şi cum am fi rulat programul în felul următor:

./segfault param1 param2 param3 < fisier.in > fisier.out

set disassembly-flavor intel vă ajută pentru a afişa eventualele printări de cod de asamblare într-o sintaxă cunoscută1). Limbajul de asamblare reprezintă un set de alias-uri pentru instrucţiunile din binarul unui program. Aceste alias-uri nu au o formă standardizată motiv pentru care acestea diferă de la un asamblor la altul. By default, tool-urile din Linux folosesc sintaxa AT&T. 99% din tool-urile din Linux (gdb NU se află printre ele) pot primii argumentul -M intel pentru a afişa sau a trata codul de asamblare ca şi cum ar fi în sintaxa recomandată de Intel (care se regăseşte şi la NASM). Programe care pot primi acest flag sunt: gcc (gas), objdump, etc.
Revenind la gdb, în momentul în care rulăm o să ne dea următoarea eroare:

# ...
number: 0
number: 0
number: 0
number: 0
 
Program received signal SIGSEGV, Segmentation fault.
0x08048423 in keep_printing ()

Pentru a vedea ce instrucţiunea a provocat segfault, putem da următoarea comandă:

(gdb) display/10i $pc
1: x/10i $pc
=> 0x8048423 <keep_printing+1>:	push   DWORD PTR [ecx*4+0x804a02c]
   0x804842a <keep_printing+8>:	push   0x804a020
   0x804842f <keep_printing+13>:	call   0x80482f0 <printf@plt>
   0x8048434 <keep_printing+18>:	add    esp,0x8
   0x8048437 <keep_printing+21>:	pop    ecx
   0x8048438 <keep_printing+22>:	inc    ecx
   0x8048439 <keep_printing+23>:	cmp    ecx,DWORD PTR ds:0x804a040
   0x804843f <keep_printing+29>:	jl     0x8048422 <keep_printing>
   0x8048441 <keep_printing+31>:	ret    
   0x8048442 <keep_printing+32>:	xchg   ax,ax
  • $pc este o variabilă gdb, şi vine de la Program Counter (este pointer-ul la instrucţiunea curentă).
  • display face dump de la un pointer dat ca argument, în cazul nostru $pc
  • i-ul îi spune lui display să interpreteze datele de acolo ca şi cum ar fi instrucţiuni
  • 10 îi spune lui display câţi operanzi de tipul i (instrucţiune) să afişeze

Prin <keep_printing+some_number>, gdb incearcă să ne arate cam pe unde ar fi această instrucţiune. În cazul nostru instrucţiunea este aproape de label-ul keep_printing.
Pentru a vedea ce valoare a avut un registru la momentul în care s-a declanşat segfault-ul, puteţi da:

(gdb) print $nume_registru

În cazul nostru s-ar putea să ne intereseze ce valoare are ecx. Pentru a afla acest lucru:

(gdb) print $ecx

Șabloane de cod

Această secțiune are scopul de a vă familiariza cu șabloane de cod pe care le puteți folosi în assembly. Cele mai multe sunt inspirate din codul pe care îl generează compilatorul gcc. Inspectarea codului assembly generat de compilator vă familiarizează cu “lumea assembly-ului” și vă pregătește pentru analiza statică a unui executabil. Două resurse foarte bune sunt site-ul: https://gcc.godbolt.org/ unde puteți vedea cum se convertește codul C++ în asm și cartea http://beginners.re/ în care puteți învăța asm pentru x86, ARM și MIPS pornind de la codul generat de compilatoare și de asemenea puteți observa diferențe între diferite compilatoare de C/C++.

De asemenea, vă va asigura o trecere mai ușoara de la a scrie cod C la cod assembly.

Blocul if

Cod scris în C:

int a = ..;
if (a > 5) {
	// bloc cod A
} else if (a == 5)  {
	// bloc cod B
} else {
	// bloc cod C
}

Varianta în assembly:

comparație_ințială:
cmp eax, 5
jg CodA
je CodB
 
CodC: ...
jmp Final_If
 
CodA: ...
jmp Final_If
 
CodB: ....
 
Final_If: ...

Compilatoarele adesea generează nume generice pentru etichete precum L1, L2. Când scrieți cod în assembly recomandăm să le denumiți cât mai sugestiv pentru a fi mai ușor de înteles programul si de depanat.

Compilatoarele generează de obicei două instrucțiuni de cmp, însă a doua nu este necesara în acest caz pentru că else if se raportează la aceeași valoare.

Blocul for

for (int i = 0; i < 100; ++ i) {
    // Bloc cod A..
}
initializare_for:
mov ecx, 100
Bucla_for:
codA
loop bucla_for

Instrucțiunea loop decrementează ecx, iar dacă acesta are valoarea diferită de 0 se execută un jump la etichetă, altfel se încarcă instrucțiunea următoare

Instrucțiunea loop folosește registrul ecx, așadar dacă aveați o valoare importantă în acesta trebuie sa îi faceți o copie (un push ecx înainte și pop ecx la sfârșit).

Bucle for îmbricate

for (int i = 0; i < 100; ++ i) {
	for (int j = 0; j < 50; ++ j) {
		// Bloc cod A
	}
}
initializare_for_1:
mov ecx, 100
 
For_1:
initializare_for_2:
	push ecx
	mov ecx, 50
For_2:
	codA
	loop For_2
 
        pop ecx
	loop For_1

Blocul while

Varianta C:

while (it < total_no) {
	// Bloc cod A
	++ it;
}

Varianta assembly:

; presupunem ca it e variabila globală declarată în secțiunea data
; la fel si total_no
 
Condiție:
	mov eax, dword [it]
	mov ebx, dword [total_no]
	cmp eax, ebx
        jge sfarșit_while
 
Corp_while:
        CodA
        inc dword [it]
        jmp Conditie
Sfarsit_while: ...

Iterarea peste un vector

int vect[100];
for (int i = 0; i < 100; ++ i)  {
	Vect[i] = 500;
}

Presupunând că vectorul este declarat global în secțiunea .data vom avea următoarea variantă:

section .data
vect times 100 dup 0
 
section .text
...
Initializare:
    mov ebx, vect
    mov ecx, 100
 
Bucla_for:
    mov dword [ebx], 500
    add ebx, 4 
    loop Bucla_for
...

Variabile locale

Până acum toate variabilele erau variabile globale declarate în secțiunea .data. Mai putem avea variabile globale in secțiunea .bss, variabile alocate dinamic prin malloc/calloc/realloc/new și variabile locale alocate pe stivă.

Pentru variabilele localte alocate pe stiva avem urmatoarea posibilitate:

int f () {
	int a = 5, b = 7;
 
	return a + b;
}

Varianta assembly:

section .data
    X equ -4
    Y equ -8
    Z equ -12
 
section .text
f:
    push ebp
    mov ebp, esp
    sub esp, 12
 
    mov dword[ebp + X], 5
    mov dword[ebp + Y], 7
 
    mov eax, dword [ebp + X]
    mov ebx, dword [ebp + Y]
    add eax, ebx
    mov dword [ebp + Z], eax
 
    leave
    ret

Funcții

Având funcția:

int f(int a, char b, char*s);

Vom avea următorul cod echivalent in assembly:

f:
    push ebp
    mov ebp, esp
 
    ; a = dword [ebp + 8]
    ; b = byte [ebp + 12]
    ; s = dword [ebp + 16]  
 
    ..Cod Functie..
 
    leave
    ret

Caracteristici:

  • Prolog-ul (push, mov) și epilog-ul (leave, ret).
  • Rezultatul unei funcții este pus în registrul eax.
  • Paramterii sunt puși în ordine inversă înaintea apelării funcției.
  • Spațiul pe stivă pentru ei trebuie alocat de asemenea înaintea și după terminarea apelului funcției.

Locația și modul în care sunt puși paramterii ține de convenția pe care o folosește compilatorul. În cele mai multe cazuri, compilatoarele vor respecta convenția cdecl(cea enunțată mai sus). Pentru a vedea alte convenții accesați acest link.

Funcțiile se recunosc ușor după prolog(push, mov) și epilog(leave, ret). Rezultatul este pus în registrul eax. Parametrii sunt puși în ordine inversa înaintea apelării funcției. Spațiul pe stivă pentru ei trebuie alocat de asemenea înaintea apelării și trebuie eliberat dupa terminarea ei. Aceasta este convenția cdecl pentru parametrii și alocarea/dezalocarea spațiului pe stivă folosită de cele mai multe compilatoare. Puteți să vedeți și alte variante aici: https://en.wikipedia.org/wiki/X86_calling_conventions.

1) salvați această setare în ~/.gdbinit
pclp2/bune-practici.txt · Last modified: 2023/04/06 14:47 (external edit)
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0