This is an old revision of the document!
În acest laborator, vom trece prin fiecare nivel de procesare al unui limbaj de nivel înalt și prin toolchain-ul pe care îl vom folosi de acum încolo.
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). Din punct de vedere al sintaxei, o eticheta consta dintr-un nume, urmat de caracterul :.
Un exemplu de cod:
#include <stdio.h> 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, executia programului sare la eticheta do_some_work. do_some_work marcheaza punctul din program în care începe procesarea unui nou job. Pentru a sări la acest punct 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 <stdio.h> int main() { int i, j, k; /* some code */ do { /* some other code */ work(); } while (any_work()); /* some code */ return 0; }
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 cazuri în care este folosita. În codul kernel-ului de Linux (exemplu), 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 să 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 este complet. Nu este complet pentru că în caz că apare o eroare în mijlocul funcției, comunicația cu mouse-ul este lăsată deschisă. O variantă îmbunătățită 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).
Etapele prin care trece un program din momentul în care este scris până când este rulat ca un proces sunt, in ordine:
În imaginea de mai jos sunt reprezentate si detaliate aceste etape:
Î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.
Majoritatea compilatoarelor oferă opțiunea de a genera și un fișier cu programul scris în limbaj de asamblare.
În arhiva de TODO 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.
$ 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 <main>: 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 <main+0xe> e: b8 00 00 00 00 mov $0x0,%eax 13: 5d pop %rbp 14: c3 retq
cc -Wall -m32 -S -masm=intel -o hello.s hello.i
În cadrul laboratoarelor vom folosi:
gcc
Pentru analiza codului si debugging vom folosi gdb
si Ghidra
.
Ghidra
este o unealtă foarte utilă pentru investigarea programelor si reverse engineering
.
Decompilare
Prin decompilare, se obtine dintr-un fisier executabil, un fisier cod sursa scris intr-un limbaj de nivel inalt. Fisierul
sursa obtinut poate fi compilat cu succes.
Dezasamblare
TODO
Function Call Graph
TODO
Un tool interesant pentru a observa cum se traduce codul C în limbaj de asamblare este Compiler Explorer.
-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).<Compilation failed>
, adăugați opțiunea -std=c99
.Compiler options
).-m32
setat anterior. Se poate observa cum codul generat diferă de la o arhitectură la alta.
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.
Pentru exercițiul acesta va trebui să descarcați arhiva de laborator și să navigați în directorul aferent task-ului curent.
2.1 Modificați codul sursă din fișierul bogosort.c
(Bogosort) prin înlocuirea
instrucțiunii break
cu o instrucțiune goto
astfel încât funcționalitatea să se păstreze.
2.2 În mod asemănător modificați instrucțiunea continue
din ignore_the_comments.c
astfel încât funcționalitatea codului să se păstreze.
goto
poate fi util
Pentru algoritmii de mai jos scrieți cod în C fără a folosi:
if
care conțin return
If-urile trebuie sa contina cel mult un goto.
Adică va trebui să folosiți if
și multe instrucțiuni goto
.
3.1 Implementați maximul dintr-un vector folosind cod C și constrângerile de mai sus.
3.2 Implementați căutare binară folosind cod C și constrângerile de mai sus.
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.