Laborator 12 - Suport teoretic

Programare multi-modul (asm+C)

Programare multi-modul (asm+C)

Legătura dintre două module, indiferent de limbajele în care sunt scrise, presupune că dintr-un modul se apelează un subprogram descris în celălalt modul. Vom presupune că unul din module este descris în limbaj de asamblare, iar celălalt într-un limbaj de programare de nivel înalt.

Motivație:

  • viteză mare de execuție în rezolvarea task-urilor cu consum minim de resurse;

Codul de apel

Codul de intrare

Codul de ieșire

  • Restaurarea resurselor nevolatile alterate;
  • Eliberarea variabilelor locale ale funcției;
  • Dezafectarea cadrului de stivă;
  • Revenirea din funcție și eliberarea argumentelor.
Exceptând resursele volatile și rezultatele directe ale funcției, starea programului după acești pași trebuie să reflecte starea inițială, de dinainte de apel!

Declararea simbolurilor externe:

  • Pentru a accesa din cadrul unui program C o funcție definită în asamblare, funcția trebuie declarată global în programul asm și trebuie să conțină caracterul '_' în fața numelui ei
  • Dacă funcția va fi apelată în programul C ca
    fun()
    , atunci programul asm va conține următoarele:
    global _fun
    segment code public code use32
    _fun:
    

Nealterarea valorilor unor regiștri

Limbajele de nivel înalt impun ca anumiți regiștri să își păstreze la ieșire valoarea cu care intră într-o procedură. În acest scop, dacă subprogramul definit în limbaj de asamblare modifică unii din aceștia, atunci valorile lor de intrare trebuie salvate (eventual pe stivă). Ele vor fi restaurate înainte de revenirea din procedură.
  • PUSHAD și POPAD pot fi utilizați pentru a salva și restaura valorile celor 8 regiștri generali.

Transmiterea parametrilor unei funcții

  • parametrii sunt transmiși prin intermediul stivei, ceea ce oferă o flexibilitate mai mare decât transmiterea parametrilor prin regiștri (referitor la numărul de parametri);

Crearea cadrului de stivă

  • La intrarea în funcție se va seta registrul EBP←ESP, valoare care va fi restaurantă la ieșirea din funcție; Deoarece ESP se modifică odată cu introducerea paramtrilor în stivă, cel mai indicat mod este de a accesa valorile parametrilor prin intermediul unui registru de bază sau index. Dintre aceste variante, registrul EBP este cel mai indicat scopului nostru, deoarece orice referire la EBP se face relativ la segmentul de stivă. Secvența care pregătește accesul la stivă este:
    push ebp
    mov ebp, esp

Alocarea de spațiu de memorie pentru datele locale

Uneori este necesar ca procedura să aibă date locale. Dacă valoarea lor nu trebuie să se păstreze într două apeluri consecutive ale procedurii atunci acestea vor fi alocate în segmentul de stivă și le vom numi date volatile. În caz contrar spunem că este vorba despre date statice și spațiul pentru ele va fi alocat într-un segment diferit de segmentul de stivă, de exemplu în segmentul de date. Alocarea unui număr n de octeți (n fiind multiplu de 4) pentru datele locale se poate face relativ la EBP.
sub esp,n
Așadar:
  • registrul EBP va fi folosit pentru a accesa parametrii (de exemplu [EBP+8] accesează primul parametru reprezentat pe 32 de biți);
  • primul parametru accesibil din stivă este ultimul parametru adăugat pe stivă de către programul apelant;
  • se rezervă spațiu pe stivă pentru variabilele locale, de exemplu:
    sub esp,4*1
  • această metodă simplifică modalitatea de accesare a parametrilor în special în cadrul funcțiilor cu număr variabil de parametri;
  • este responsabilitatea programatorului să scoată parametrii de pe stivă.

Returnarea valorilor de către o funcție

  • dacă funcția returnează un întreg, atunci acesta este returnat în EAX;
  • dacă funcția returnează un șir, atunci adresa acestuia este returnată în EAX;
  • prin folosirea convenției CDECL se presupune că regiștrii EBX, ESI, EDI, EBP și ESP nu își modifică valoarea în timpul apelului de funcții;

Revenirea din procedură

La revenirea din procedură trebuie să se execute operațiile:
  • refacerea valorilor regiștrilor (vezi secțiunea Nealterarea valorilor unor regiștri);
  • refacerea stivei astfel încât în vârf să conțină adresa de revenire:
    mov esp, ebp
    pop ebp 

Scheletul unei funcții:

global _fun
segment code public code use32 
_fun:      
        push ebp
        mov ebp, esp   
        pushad 
        ;... codul funcției ... 
        popad 
        mov eax, returned_value 
        mov esp, ebp
        pop ebp 
        end
	

Utilizarea procedurilor definite în asamblare în cadrul unui program C

Exemplul 1

Se definește în cadrul unui program asm o procedură numită hello_world care nu primește nici un parametru și nu returnează nimic. Aceasta afisează pe ecran textul "Hello World!".

hello_world.asm

hello_world.c

bits 32
extern _printf
global _hello_world
segment data public data use32
	mesaj db 'Hello world!', 0
segment code public code use32
_hello_world:
	push ebp
	mov ebp,esp
	push dword mesaj	
	call _printf
	add esp, 4*1
	pop ebp
    ret
#include <stdio.h>


void hello_world();

int main()
{
	hello_world();
	printf("Programul care nu face nimic e gata!");
	return 0;
	
}


Observați utilizarea cuvântului cheie extern. Acesta transmite compilatorului că funcția / variabila este definită într-un alt fișier (și nu în fișierul curent). Linker-ul are datoria creării unei conexiuni între această declarație a funcției / variabilei și definiția acesteia.

Exemplul 2

Se definește în cadrul unui program asm o procedură numită return_10 care nu primește nici un parametru și returnează un întreg.

return_10.asm

return_10.c

bits 32
global _return_10
segment data public data use32
segment code public code use32
_return_10:
	mov eax, 10
	ret
#include <stdio.h>
int return_10();
int main()
{
	printf("Programul returneaza %d!",return_10());
	return 0;
}

Exemplul 3

Se definește în cadrul unui program asm o procedură numită sum care primește doi parametri întregi și returnează suma lor (un întreg).

sum.asm

sum.c

bits 32

global _sum
segment data public data use32
segment code public code use32
_sum:
	push ebp
	mov ebp, esp	                   	
	mov eax, [ebp+8]
	add eax, [ebp+12]  
	mov esp, ebp
	pop ebp
    ret

#include <stdio.h>

int sum(int, int);

int main()
{
	
	printf("%d\n", sum(2, 3));
	return 0;
	
}

Exemplul 4

Se definește în cadrul unui program asm o procedură numită factorial care primește un parametru întreg pozitiv și returnează factorialul lui (un întreg pozitiv).

factorial.asm

factorial.c

bits 32
global _factorial
segment data public data use32
segment code public code use32
_factorial:
	push ebp
	mov ebp,esp
	sub esp, 4                   
	mov eax, [ebp+8] 
	cmp eax,2
	jbe   .trivial
	.recursiv:
		dec  eax
		push eax
		call _factorial
		add  esp, 4    
		mov  [ebp-4], eax   ; m = (n-1)!
		mov  eax, [ebp+8]   ; n
		mul  dword [ebp-4]  ; edx:eax ← n * m
		jmp  .final
	.trivial:
		xor  edx, edx     
	.final:
	add esp, 4
    mov esp, ebp
    pop ebp
    ret
    





#include <stdio.h>

int factorial(int);

int main()
{
	int n, f;
	printf("n = ");
	scanf("%d", &n);

	f = factorial(n);

	printf("factorial(%d) = %d\n", n, f);

	return 0;
}







Programare multi-modul (asm+C) în Visual Studio

Următorul tutorial se bazează pe Visual Studio 2015, se presupune că aveți instalat pe calculator o versiune de Visual Studio. Pentru mai multe detalii accesati in TEAMS, in canalul General, sectiunea Files, fisierul procedura_instalare.doc.

Exemplul urmator arata cum putem compila, rula si depana programul din sectiunea Exemple.

Se utilizează linia de comandă pentru a compila/asambla modulele

Pașii urmați pentru a compila programul main.c sunt:

  • se deschide linia de comandă Visual Studio, pentru aceasta se navighează în meniul Windows Start la Visual Studio și se alege opțiunea VS2015 x86 Native Tools Command Prompt, ca în figura de mai jos.

  • În fereastra terminal se navighează către directorul unde se găsesc sursele programului. În exemplul din figura de mai jos sursele se găsesc în directorul tmp, comanda dir listează conținutul directorului curent. Pe lângă fișierele sursă ale programului, în directorul tmp se găsește și programul executabil nasm.exe folosit pentru asamblarea lui modulAsm.asm.

  • În primul pas se asamblează modulAsm.asm folosind comanda:
nasm modulAsm.asm -fwin32 -o modulAsm.obj
(a se vedea figura de mai jos). Rezultatul este fisierul modulAsm.obj.

  • Folosind compilatorul Visual Studio (cl.exe) se compileaza main.c, se dorește editarea legăturilor în acest pas -> se specifică ca și parametru /linker fișierul modulAsm.obj. Rezultatul este programul main.exe.

  • programul poate fi executat apelând din linia de comandă main.exe:

Se poate depana programul folosind Ollydbg, pentru aceasta la asamblare/compilare trebuie specificat acest lucru, comenzile sunt:

> nasm modulAsm.asm -fwin32 -g -o modulAsm.obj
> cl /Z7 main.c /link modulAsm.obj
Din Ollydbg, File -> Open și se deschide main.exe.
În Visual C dacă se dorește includerea de informații de depanare, opțiunile sunt /Z{7|i|I} (a se vedea https://msdn.microsoft.com/en-us/library/958x11bc.aspx).