Laborator 11 - Suport teoretic

Programare multi-modul (asm+asm)

Programare multi-modul (asm+asm)

Programele non-triviale (de amploare) prezintă problematici specifice complexității crescute a codului, necesitând ca atare și instrumente care adresează aceste aspecte.  Natural apar următoarele întrebări:

  • cum este posibil a fi împărțită (descompusă) problema atacată în sub-probleme de dificultate redusă?
  • care dintre sub-problemele identificate după descompunere sunt deja cunoscute și au rezolvări consacrate, bine cunoscute și care pot fi refolosite?

Subprograme în limbajul de asamblare x86

  • O variantă pentru împărțirea codului în sub-probleme o reprezintă modularizarea codului. Limbajul de asamblare nu recunoaște noțiunea de subprogram. Putem însa crea o secvență de instrucțiuni care să poată fi apelată din alte zone ale programului și după terminarea ei să returneze controlul programului apelant.
  • O astfel de secvență se numește procedură. Apelul unei proceduri se poate face cu o instructiune jmp. Problema care apare la un astfel de salt este că procesorul nu ține minte de unde a fost trimis la "procedură" și prin urmare nu știe unde sa revină cu execuția după terminarea procedurii. Este necesar deci ca la apelul unei proceduri să salvăm undeva adresa de revenire, iar revenirea din procedură este de fapt o instrucțiune de salt la acea adresă.
  • Locul unde se salvează adresa de revenire este stiva de executie. Este nevoie de stivă deoarece o procedură poate apela o altă procedură, acea procedură poate apela alta, s.a.m.d.
  • Există două instrucțiuni ce permit apelul și revenirea din proceduri: call si ret.

Sintaxa:

call eticheta
  • Instrucțiunea call este de fapt o instrucțiune jmp care în plus introduce în varful stivei adresa instrucțiunii care urmează - valoarea din EIP (instrucțiunea care vine imediat dupa call și nu destinația saltului produs de instructiunea call).
  • Instrucțiunea ret extrage din vârful stivei o adresă si execută un salt la acea adresă (practic se modifică EIP, valoarea extrasă de pe stivă este stocată în registrul EIP). Instrucțiunea nu are argumente deoarece adresa la care sare programul este extrasă din vârful stivei (nu este dată explicit).

Observație:

  • Pentru a evita posibilele ambiguități ce pot sa apară atunci când numele unei etichete definită în procedură (etichetă locală) este identic cu numele altei etichete definite în programul "principal", numele etichetelor "locale" trebuie să înceapă cu caracterul "."

Exemplu

.eticheta:; etichetă utilizată în procedură
eticheta:; etichetă globală
  • Asamblorul  NASM oferă prin intermediul preprocesorului un mecanism simplu prin care un program de multe linii poate fi împărțit în mai multe fișiere. În general, datorită similarității cu directiva #include a limbajului C, rolul de utilizare în practică al acesteia este același: permite separarea declarațiilor programului în unul sau mai multe fișiere ce vor fi incluse acolo unde respectivele declarații sunt necesare, întocmai cum și în C se obișnuiește separarea/gruparea declarațiilor în fișiere header (fișiere cu extensia .h), fișiere ce sunt ulterior incluse de către codul scris în fișire C care necesită (referă) declarațiile în cauză.
  • La nivel de preprocesor, directiva %include îi permite unei componente de program să fie construită din mai multe fișiere ce vor fi asamblate împreună.
  • Ca urmare o procedura poate fi definită în același fisier sursa (a se vedea lab11_procedura.asm) sau într-un fișier separat (a se vedea lab11_proc_main.asm și factorial.asm, ambele fișiere sunt în același director, asamblarea, editarea de legaturi si executia se fac ca și până acum). În aceste cazuri, în urma asamblării veți obține un singur fisier *.obj!

lab11_procedura.asm

; programul calculeaza factorialul unui numar si afiseaza in consola rezultatul
; procedura factorial este definita in segmentul de cod si primeste pe stiva ca  si parametru un numar
bits  32
global  start
extern  printf, exit
import  printf msvcrt.dll
import  exit msvcrt.dll
segment  data use32 class=data
	format_string db "factorial=%d",  10, 13, 0
segment  code use32 class=code
;  urmeaza definirea procedurii
factorial: 
	mov eax, 1
	mov ecx, [esp + 4] 
	; mov ecx, [esp + 4] scoate de pe stiva  parametrul procedurii
	; ATENTIE!!! in capul stivei este adresa de  retur
	; parametrul procedurii este imediat dupa  adresa de retur
	; a se vedea desenul de mai jos
	;
	; stiva
	;
	;|-------------------|
	;| adresa retur  |  <-  esp
	;|-------------------|
	;|   00000006h  |  <- parametrul pasat procedurii, esp+4
	;|-------------------|
	; ....
	.repet: 
		mul ecx
	loop .repet ; atentie, cazul ecx = 0 nu e  tratat!
	ret
;  programul "principal"       
start:
	push dword 6        ; se salveaza pe stiva numarul  (parametrul procedurii)
	call factorial      ; apel procedura
	; afisare rezultat
	push eax
	push format_string
	call [printf]
	add esp, 4*2
	push 0
	call [exit]

lab11_proc_main.asm - Diferența față de lab11_procedura.asm este ca procedura factorial este definită în alt fisier (factorial.asm) fiind necesara includerea acestuia folosind directiva %include.

;  programul calculeaza factorialul unui numar si afiseaza in consola rezultatul
;  procedura factorial este definita in fisierul factorial.asm
bits  32
global  start
import  printf msvcrt.dll
import  exit msvcrt.dll
extern  printf, exit
;  codul definit in factorial.asm va fi copiat aici
%include  "factorial.asm"
segment  data use32 class=data
	format_string db  "factorial=%d", 10, 13, 0
segment  code use32 class=code
start:
	push dword 6
	call factorial
	push eax
	push format_string
	call [printf]
	add esp, 2*4
	push 0
	call [exit]

factorial.asm

%ifndef  _FACTORIAL_ASM_ ; continuam daca _FACTORIAL_ASM_ este nedefinit
%define  _FACTORIAL_ASM_ ; si ne asiguram ca devine definit
;  astfel %include permite doar o singura includere
;definire  procedura
factorial:   ; int _stdcall factorial(int n)
	mov eax, 1
	mov ecx, [esp + 4]
	;  mov ecx, [esp + 4] scoate de pe stiva parametrul procedurii
	; pentru explicatii a se vedea  lab11_procedura.asm
	repet: 
		mul ecx
	loop repet ; atentie, cazul ecx  = 0 nu e tratat!
	ret 4
%endif

Programe din mai multe module

Un program scris în limbaj de asamblare poate fi împărțit în mai multe fișiere sursă, fiecare fiind asamblat separat în fișiere .obj. Pentru a scrie un program din mai multe fișiere sursă trebuie să respectăm urmatoarele:

  • toate segmentele vor fi declarate cu modificatorul public, pentru că în programul final segmentul de cod este construit din concatenarea segmentelor de cod din fiecare modul; la fel și segmentul de date.
  • etichetele și numele variabilelor dintr-un modul care trebuie "exportate" în alte module trebuie sa faca obiectul unor declaratii global
  • etichetele și variabilele care sunt declarate într-un modul și sunt folosite în alt modul trebuie să fie "importate" prin directiva extern
  • o variabila trebuie declarată în întregime într-un modul (nu poate fi jumătate într-un modul și jumătate într-altul). De asemenea, trecerea executiei dintr-un modul în altul se poate face doar prin instructiuni de salt (jmp, call sau ret).
  • punctul de intrare este prezent doar în modulul ce contine "programul principal"

Fiecare modul se va asambla separat, folosind comanda:

nasm -fobj nume_modul.asm
apoi modulele se vor lega impreuna cu comanda
alink modul_1.obj modul_2.obj ...  modul_n.obj -oPE -subsys console -entry start

Etape asamblare / link-editare/ debugging / executie

ASAMBLARE:

nasm -f obj modul.asm
  • Optiunea -f indica tipul de fisier care sa fie generat, in cazul acesta fisier obj.

LINK-EDITARE

alink modul1.obj ... moduln.obj -oPE -subsys console -entry start
  • In folder-ul nasm din asm_tools exista fisierul "ALINK.TXT" care descrie optiunile pentru alink.
  • Alink options:
    -o xxx
    xxx:
    • COM = output COM file
    • EXE = output EXE file
    • PE = output Win32 PE file (.EXE)
    -subsys xxx:
    Optiunea specifica tipul de aplicatie generata (default=windows)
    • windows, win or gui => windows subsystem
    • console, con or char => console subsystem
    • native => native subsystem
    • posix => POSIX subsystem
    -entry name
    Optiunea specifica punctul de intrare in program (prima instructiune de executat)

DEBUG

OLLYDBG.EXE modul.exe 

EXECUTIE

modul.exe