====== Laborator 02: Toolchain ====== În acest laborator, vom trece prin fiecare nivel de procesare a unui limbaj de nivel înalt și prin toolchain-ul pe care îl vom folosi de acum încolo. ===== C basics: GOTOs ===== Un concept mai puțin abordat în tutoriale de C este instrucțiunea **goto**. Prin instrucțiunea **goto**, un program poate sări în puncte intermediare în cadrul unei funcții. Aceste puncte intermediare se numesc **label**-uri (etichete). Un exemplu de cod: #include int main() { int i, j, k; /* some code */ do_some_work: /* some other code */ work(); if (any_work()) goto do_some_work; /* some code */ return 0; } Programul execută un job prin **work()**. În caz că mai sunt alte joburi neterminate, programul sare la labelul **do_some_work**.\\ **do_some_work** este punctul din program în care începe procesarea unui nou job. Acesta e marcat printr-un nume urmat de **:**. Pentru a sări la acest punct din program se folosește instrucțiunea **goto** urmată de numele etichetei declarate. Prin diferite combinații de **if**-uri si **goto**-uri se pot echivala alte instrucțiuni din C, cum ar fi **else**, **for** si **while**.\\ Codul dat exemplu mai sus ar putea fi un candidat care să înlocuiască o instrucțiune ''%%do { /* ... */ } while ();%%'': #include int main() { int i, j, k; /* some code */ do { /* some other code */ work(); } while (any_work()); /* some code */ return 0; } ==== The "WHYs" of goto ==== Această instrucțiune nu doar că adesea lipsește din tutorialele de C, dar se fac recomandări împotriva abordării ei deoarece de cele mai multe ori duce la cod ofuscat (greu de înțeles, întreținut și depanat).\\ Există totuși mici cazuri în care este folosit. În codul kernel-ului de Linux, instrucțiunile de **goto** sunt folosite ca o formă de **try-catch** din limbaje de nivel mai înalt (precum C++, Java, C#, etc.).\\ Exemplu: int process_data_from_mouse_device(...) { int err; int x, y; /* >>try<< instructions */ err = init_communication_with_mouse(); if (err) goto error; err = get_x_coord_from_mouse(&x); if (err) goto error; err = get_y_coord_from_mouse(&y); if (err) goto error; err = announce_upper_layers_of_mouse_movement(x, y); if (err) goto error; err = close_communication_with_mouse(); if (err) goto error; return 0; /* >>catch<< instructions' exceptions */ error: print_message("Failed to get data from mouse device. Error = %d", err); return err; } Acest cod încearcă să proceseze datele venite de la un mouse și sa le paseze altor părți superioare din kernel care le-ar putea folosi. În caz că apare vreo eroare, se afișează un mesaj de eroare și se termină procesarea datelor. Codul pare corect, dar nu prea este. Nu este corect pentru că în caz că apare o eroare în mijlocul funcției, comunicația cu mouse-ul este lăsată deschisă.\\ O variantă mai corectă ar fi următoarea: int process_data_from_mouse_device(...) { int err; int x, y; /* >>try<< instructions */ err = init_communication_with_mouse(); if (err) goto error; err = get_x_coord_from_mouse(&x); if (err) goto error_close_connection; err = get_y_coord_from_mouse(&y); if (err) goto error_close_connection; err = announce_upper_layers_of_mouse_movement(x, y); if (err) goto error_close_connection; err = close_communication_with_mouse(); if (err) goto error; return 0; /* >>catch<< instructions' exceptions */ error_close_connection: close_communication_with_mouse(); error: print_message("Failed to get data from mouse device. Error = %d", err); return err; } În varianta îmbunătățită, dacă apare o eroare, se face și o parte de curățenie: conexiunea cu mouse-ul va fi închisă, și apoi codul va continua cu tratarea generală a oricărei erori din program (afișarea unui mesaj de eroare). Totuși acest curs/laborator de ce abordează un astfel de subiect?\\ Când vom ajunge în limbajul de asamblare se va putea observa ca o bună parte din instrucțiunile unui limbaj de nivel înalt, chiar și precum C, sunt inexistente, iar o bună parte din workflow seamănă cu un program format din **goto**-uri. În orice alt caz, această formă de programare ar trebui evitată pe cât posibil. [[https://xkcd.com/292/|{{http://imgs.xkcd.com/comics/goto.png}}]] ===== De la C la program în rulare ===== Un compilator trece prin următoarele etape: * preprocesare * compilare * asamblare * link editare În etapa de **compilare** codul este tradus din cod de nivel înalt în limbaj de asamblare. Limbajul de asamblare este o formă human-readable a ce ajunge procesorul să execute efectiv. Dacă programele scrise în limbaje de nivel înalt ajung să fie portate ușor pentru procesoare diferite (arm, powerpc, x86, etc.), cele scrise în limbaj de asamblare sunt implementări specifice unei anumite arhitecturi. Limbaje de nivel înalt reprezintă o formă mai abstractă de rezolvare a unei probleme, din punctul de vedere al unui procesor, motiv pentru care și acestea trebuie traduse în limbaj de asamblare în cele din urmă, pentru a se putea ajunge la un binar care poate fi rulat. Mai multe detalii în laboratorul următor. În imaginea de mai jos sunt reprezentate etape prin care trece un program din momentul în care este scris până când este rulat ca un proces. Putem observa între acestea etapele descrise mai sus: preprocesare, compilare, asamblare, link-editare. {{ :laboratoare:phases-full.png?500 | Etapele procesului de compilare: de la program la proces}} Majoritatea compilatoarelor oferă opțiunea de a genera și un fișier cu programul scris în limbaj de asamblare. În cazul compilatorului **gcc** este de ajuns să adăugați flag-ul **-S** și vă va genera un fișier ***.s** cu codul aferent. În arhiva de {{:laboratoare:compiler-phases.zip|aici}} aveți un exemplu de trecere a unui program foarte simplu ''hello.c'' prin cele patru faze. Îl puteți testa pe un sistem Unix/Linux și pe un sistem Windows cu suport de MinGW. Pe sistemele din laborator există suport MinGW și îl veți putea testa folosind comanda ''make'' la fel ca mai jos și apoi diversele fișiere: $ make cc -E -o hello.i hello.c cc -Wall -S -o hello.s hello.i cc -c -o hello.o hello.s cc -o hello hello.o $ ls Makefile hello hello.c hello.i hello.o hello.s $ ./hello Hello, World! $ tail -10 hello.i # 5 "hello.c" int main(void) { puts("Hello, World!"); return 0; } $ cat hello.s .file "hello.c" .section .rodata .LC0: .string "Hello, World!" .text .globl main .type main, @function main: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl $.LC0, %edi call puts movl $0, %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size main, .-main .ident "GCC: (Debian 5.2.1-17) 5.2.1 20150911" .section .note.GNU-stack,"",@progbits $ file hello.o hello.o: ELF 64-bit LSB relocatable, x86-64, [...] $ file hello hello: ELF 64-bit LSB executable, x86-64, [...] $ objdump -d hello.o hello.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000
: 0: 55 push %rbp 1: 48 89 e5 mov %rsp,%rbp 4: bf 00 00 00 00 mov $0x0,%edi 9: e8 00 00 00 00 callq e e: b8 00 00 00 00 mov $0x0,%eax 13: 5d pop %rbp 14: c3 retq ===== Toolchain ===== În cadrul laboratorului vom folosi asamblorul [[http://www.nasm.us/|NASM]]. Acesta este foarte folosit în tutoriale și cărți legate de scrierea programelor în limbaje de asamblare.\\ Ca IDE vom folosi [[http://dman95.github.io/SASM/english.html|SASM]]. Pe Windows dispune de un installer de tipul next-next-next. Vine cu tool-urile aferente(inclusiv NASM) și este preconfigurat corespunzător.\\ Ambele programe menționate sunt open-source și valabile pe toate platformele cunoscute (Windows, Linux și MacOS). ===== Exerciții: ===== ==== 1. [20p] Online C Compiling ==== Un tool interesant pentru a observa cum se traduce codul C în limbaj de asamblare este **Compiler Explorer**. - Intrați pe [[http://gcc.godbolt.org/|Compiler Explorer]]. - Încărcați programul "sum over array" din exemple (accesibile folosind intrările ''Source: Examples'' și ''Name: sum over array''). - Asigurați-vă că **x86 gcc 4.8.2** este selectat la **Compiler:**. - Selectați **Intel syntax**. Acesta este un format mai ușor de digerat și mai asemănător formatului acceptat de NASM. - Folosiți opțiunea ''-m32'' (la ''Compiler options'') pentru a afișa cod în limbaj de asamblare pe 32 de biți (față de 64 de biți în mod implicit). - Pentru a putea să vă dați seama ce cod C s-a tradus într-un anumit set de instrucțiuni de asamblare selectați **Colourise**. - În continuare codul este destul de greoi. Pentru a putea fi mai human-readable adăugați opțiunea **-O2** la opțiunile de compilare (''Compiler options''). - Se poate observa existența simbolurilor **.L3:** și **.L4:**. Acestea reprezintă puncte fixe în program, label-uri, destul de asemănătoare cu ceea ce se găsește și în C. - Treceți, pe rând, prin compilatoarele corespunzătoare următoarelor arhitecturi: ARM, ARM64, AVR, PowerPC. Se poate observa cum codul generat diferă de la o arhitectură la alta. - Mai încercați și următoarele compilatoare: **clang** și **icc**. După cum se poate observa, deși este același cod C și aceeași arhitectură, codul generat diferă. Acest lucru se întâmplă pentru că fiecare compilator poate avea o strategie de optimizare și generare de cod diferită. [[http://clang.llvm.org/|clang]] este un compilator open-source de C\C++. Adesea este folosit în IDE-uri datorită mesajelor de eroare de compilare foarte sugestive pe care le produce. **icc** este compilatorul de C\C++ al celor de la compania Intel. ---- Scrieți în zona ''Code editor'' următoarea secvență de cod: int simple_fn(void) { int a = 1; a++; return a; } Observați codul în limbaj de asamblare atunci când opțiunile de compilare (''Compiler options'') sunt ''-m32'', respectiv atunci când opțiunile de compilare sunt ''-m32 -O2''. Observați ce efect au opțiunile de optimizare asupra codului în limbaj de asamblare generat. ==== 2.[20p] Microsoft Visual Studio: from C to assembly ==== La exercițiul anterior am abordat compilatoarele gcc, clang și icc pentru a observa cam ce cod de asamblare produc. În acest exercițiu vom trece în revistă și compilatorul celor de la Microsoft (valabil pe Microsoft Windows, evident).\\ În primă fază deschideți Microsoft Visual Studio și creați un nou proiect gol. Pentru a crea un proiect gol, folosiți ''File -> New project'', apoi selectați ''Win32 Console Application'' și selectați ''Empty Project''. Pentru a adăuga un fișier nou în cadrul proiectului mergeți cu mouse-ul în zona ''Solution Explorer'' (din partea dreapta sus a ecranului) și apoi folosiți click dreapta pe intrarea ''Source files'', apoi folosiți ''Add'' și apoi, după caz, ''New Item'' sau ''Existing Item''. Adăugați un fișier nou în **cadrul proiectului**, numit ''main.cpp'' și adăugați următorul cod în fișier: #define DATA_LEN 100 int testFunction(int* input, int length) { int sum = 0; for (int i = 0; i < length; ++i) { sum += input[i]; } return sum; } int main() { int random_data[DATA_LEN]; testFunction(random_data, DATA_LEN); return 0; } Mergeți în proprietățile proiectului, la secțiunea indicată în următoarea imagine: {{ :laboratoare:project-properties.png |Microsoft Visual Studio: Project Properties}} Asigurați-vă că la câmpul **Assembler Output** este selectat **Assembly With Source Code (/FAs)** și dați **OK** pentru ca proprietățile să ia efect.\\ Dați **Clean Project** și apoi **Build Project**. În acest moment vi s-a generat un fișier ***.asm**, dar care nu va apărea în Visual Studio.\\ Pentru a vizualiza fișierul în cauză, dați click dreapta pe **Solution //nume_proiect//** și selectați **Open Folder in File Explorer**. În directoare **//nume_proiect//\Debug** veți găsi fișierul **Source.asm** corespunzător programului scris în C. După cum puteți observa, acesta conține codul de asamblare intercalat cu comentarii în care este prezentat codul original. Există două directoare cu numele ''Debug'': unul aferent soluției și altul aferent proiectului. Alegeți directorul ''Debug'' aferent proiectului. ==== 3. [20p] SASM: walkthrough ==== === 3.1 Proiect nou === Pentru a crea un proiect nou intrați la **File -> New**. === 3.2 Ferestre === {{ :laboratoare:sasm-window.png |}} Legendă: - editorul - fereastră în care se pot transmite date programului - fereastră în care se afișează outputul programului - logging pentru build, clean, run === 3.3 Butoane === - {{:laboratoare:build.png|Build}} Build - construiește executabilul - {{:laboratoare:run.png|Run}} Run - rulare normală a programului - {{:laboratoare:stop.png|Stop}} Stop - oprește execuția programului - {{:laboratoare:debug.png|Debug}} Debug - rularea programului cu suport de debugging - {{:laboratoare:stepinto.png|Step into}} Step into - dacă instrucțiunea curentă apelează o funcție, se va arăta și execuția din interiorul ei - {{:laboratoare:stepover.png|Step over}} Step over - dacă instrucțiunea curentă apelează o funcție, debugger-ul va executa funcția ca și cum ar fi o singură instrucțiune - {{:laboratoare:continue.png|Continue}} Continue - continuă execuția până la următorul breakpoint sau până se termină programul === 3.4 Modul debugging === În modul debugging (programul rulat cu {{:laboratoare:debug.png|Debug}}), vă mai apare o ferestră cu resursele procesorului (starea registrelor sale):\\ {{:laboratoare:registers-window.png|}}\\ Opțional, puteți activa și o fereastră prin care puteți monitoriza valorile unor variabile în timpul sesiunii de depanare:\\ {{:laboratoare:memory-menu.png|}}\\ Activarea acestei ferestre se face din meniu astfel:\\ {{:laboratoare:show-memory.png|}}\\ Pentru a introduce un breakpoint dați click în dreapta linenumber-ului corespunzător liniei pe care vreți să o depanați. Când va fi setat un breakpoint pentru o linie, în stanga ei va apărea și un cerculeț roșu care să desemneze acest lucru: {{:laboratoare:insert-breakpoint.png|Insert breakpoint}}\\ O alternativă ar fi, având cursorul pe linia în cauză, să apăsați tasta **F8** sau să dați comanda din meniu:\\ {{:laboratoare:breakpoint-menu.png|Breakpoint menu entry}} ==== 4. [40p] C: GOTOs ==== Când scrieți cod cu etichete (label-uri) țineți cont de următoarele recomandări de indentare: * Nu indentați etichetele (label-urile). "Lipiți-le" de marginea din stânga a ecranului de editare. * O etichetă este singură pe linie. Nu există cod după etichetă. * Nu țineți cont de indentare în indetarea codului. Codul trebuie indendat în același mod și cu etichete și fără etichete. * Puneți o linie liberă înaintea linie care conține o etichetă. Pentru algoritmii de mai jos scrieți cod în C **fără** a folosi: * apeluri de funcţii (exceptând //scanf()// şi //printf()//) * else * for * while * do {} while; * construcțiile ''if'' care conțin return Adică va trebui să folosiți ''if'' și multe instrucțiuni ''goto''. **[20p]** Implementați maximul dintr-un vector folosind cod C și constrângerile de mai sus. **[20p]** Implementați căutare binară folosind cod C și constrângerile de mai sus. Reiterăm ideea că scenariile de utilizare ale instrucțiunii ''goto'' sunt limitate. Exercițiile acestea au valoare didactică pentru a vă acomoda cu instrucțiuni de salt (//jump//) pe care le vom folosi în dezvoltarea în limbaj de asamblare. Pentru scrierea de programe puteți folosi orice editor indicat pe Desktop sau IDE-urile Code::Blocks sau Visual Studio. Pentru compilarea/rularea codului puteți folosi IDE-urile Code::Blocks sau Visual Studio sau puteți folosi linia de comandă ca mai jos. Pentru a compila un fișier cod sursă C/C++ în linia de comandă folosind Visual Studio, urmați pașii: - Deschideți butonul de start, selectați ''All apps'' , apoi mergeți la litera ''V'', selectați directorul ''Visual Studio 2015'' și alegeți opțiunea ''Visual Studio x86 Native Command Prompt''. - Accesați directorul în care aveți codul sursă. - Folosiți comanda cl .cpp unde '''' este numele fișierului. - Rulați executabilul obținut folosind comanda .\nume-fisier unde '''' este numele fișierului. /* Pentru a compila un fișier cod sursă C/C++ în linia de comandă folosind GCC, urmați pașii: - Deschideți un Command Prompt. - Adăugați calea către GCC la ''PATH'' folosind comanda PATH=%PATH%;C:\MinGW\bin - Folosiți comanda g++ .cpp unde '''' este numele fișierului. Sau puteți folosi gcc .c pentru fișier cod sursă scris în C (nu în C++). - Rulați executabilul obținut folosind comanda .\nume-fisier unde '''' este numele fișierului. */ ==== 5. [20p] Bonus ==== Implementați cu aceleași constrângeri ca la exercițiul anterior și algoritmii: - bubble sort [10p] - căutarea unui substring într-un string [10p]