Lucrarea 3

Programare în C sub Linux

1. Introducere

C este un limbaj de programare de nivel înalt, care există încă din primele zile ale existenţei sistemelor UNIX. Limbajul C a fost creat de Dennis Ritchie la Bell Laboratoires pentru a ajuta la dezvoltarea sistemului de operare UNIX.

        Printre motivele care au facut din C unul din cele mai folosite limbaje de programare la ora actuală:

- este un limbaj foarte portabil. Aproape toate calculatoarele şi sistemele de operare dispun de un compilator de C, iar sintaxa limbajului şi biliotecile de funcţii sunt standardizate.

- programele scrise în C sunt rapide

- C este limbajul de sistem pentru toate versiunile de UNIX.

 

Pentru a veni în întâmpinarea noilor cerinţe în materie de dezvoltare de programe, există extenii ale limbajului C, ca de exemplu C++ - limbaj de programare orientat pe obiecte.

 

Compilatorul de C care este disponibil pentru Linux se numeşte GNU C sau GCC. Compilatorul este creat sub licenţa Free Software Foundation şi poate fi distribuit în mod liber.

2. Desfăşurarea lucrării

2.1. Compilatorul GNU C.

Compilatorul GNU C care este distribuit cu sistemul Linux este un compilator complet operaţional, compatibil ANSI C. Această lucrare presupune ca sunteţi familiari cu limbajul C.

 

2.1.1. Apelarea compilatorului.

Compilatorul este invocat în felul următor:

 

gcc [opţiuni] [nume_fişiere]

 

Operaţiile specificate de opţiuni vor fi executate asupra fiecărui fişier care este specificat în linia de comandă. În continuare sunt descrise opţiunile care sunt cel mai des utilizate.

 

2.1.2. Opţiuni GCC

Există mai mult de 100 de opţiuni ale compilatorului, multe dintre ele probabil că nu le veţi folosi niciodată. O documentaţie completă poate fi obţinută cu "info gcc". Pentru a învăţa cum să folosiţi texinfo, apelaţi "info info". Multe din opţiunile GCC sunt formate din mai mult de un caracter, de aceea este necesar ca fiecare opţiune să înceapă cu "-" (nu se pot grupa mai multe opţiuni cu acelaşi "-").

Dacă compilaţi un program folosind GCC fără nici o opţiune, va creea un fişier executabil cu numele a.out. Exemplu:

 

gcc test.c

 

Pentru a specifica un alt nume pentru fişierul executabil, folosiţi opţiunea "-o nume_executabil":

 

gcc -o test test.c

 

Opţiunea "-c": generează doar cod obiect (extensia .o), fără asamblare şi editare de legături.

Opţiuni pentru depanare şi monitorizare a timpului de execuţie:

  "-g": GCC va genera informaţii folosite la depanare (cu depanatorul gdb)  

           "-pg": GCC va adauga cod suplimentar care, atunci când este executat, generază informaţii care pot fi folosite de gprof pentru a afla detalii cu privire la viteza de execuţie a programului.

           "-Wall":  activeaza diverse warning-uri compilatorului

 

Exemplu:

 

int prod(int a, int b) {
return a * b;
}

int main(int argc, char **argv) {
printf("\n%s",argv[0]);
int i, j, p;
p = 1;
for (j = 0; j < 10000000; ++j) // 7 zero-uri
for (i = 0; i < 4; ++i)
p = prod(p, i + j);

printf("%d\n",p);
return p;
}

 

2.1.1 Biblioteci

 

Bibliotecile de functii (en: libraries) au aparut pentru a oferi o mai mare flexibilitate dezvoltatorilor de software. Acestea reunesc functii des utilizate de catre mai multe programe, fara a fi necesar astfel rescrierea acestora pentru fiecare program in parte.

 

Trebuie sa amintim intai cum sunt utilizate aceste biblioteci de functii. Dupa cum se stie, programele sursa trec prin faza de compilare pentru a ajunge programe executabile. De cele mai multe ori, prin „compilare” se inteleg implicit de fapt doua procese:

1. compilare = trecerea din limbaj de programare in cod obiect

2. link-editare = editarea legaturilor cu bibliotecile de functii

 

La apelul unei comenzi precum

$ gcc <sursa> -Wall -o <fisier_executabil>

 

GCC-ul apeleaza implicit si linker-ul – ld. Pentru a inhiba acest comportament, se poate utiliza parametrul -c, in acest fel obtinandu-se doar un simplu fisier obiect. Putem folosi aceasta facilitate pentru a compila functiile unei aplicatii in fisiere obiect separate. Ulterior, putem

compila aplicatia mare, incluzand fisierele obiect compilate anterior.

 

Exemplu:

 

In a21.c:

 

#include <stdio.h>

 

int prod(int a, int b) {

printf("%d * %d = %d\n", a, b, a * b);

return a * b;

}

in a22.c:

 

#include <stdio.h>

 

int prod(int a, int b);

int main() {

int a = 4;

int b = 3;

int p;

printf("a is %d, b is %d\n", a, b);

p = prod(a, b);

printf("p is %d\n", p);

return p;

}

 

$ gcc a22.c -o a2 → eroare de linkare

 

Corect este:

$ gcc -c a21.c

$ gcc -c a22.c

$ gcc a21.o a22.o -o a2

$ ./a2

 

 

 

2.2. Depanarea programelor folosind gdb

gdb este un depanator puternic, care poate fi folosit pentru depanarea programelor C şi C++. El permite inspectarea structurii interne a memoriei care este utilizată de program în timpul execuţiei. Câteva din funcţiile gdb:

 

- permite monitorizarea valorilor variabilelor programului.

- permite setarea punctelor de întrerupere (breakpoints) care vor opri programul la o linie de cod specificată.

- permite execuţia pas cu pas a programului.

 

2.2.1. Apelarea gdb

gdb se apelează cu comanda "gdb [nume_executabil]". Pentru a afla mai multe despre gdb daţi "man gdb" sau "info gdb". Pentru a folosi corect depanatorul, nu uitaţi să compilaţi programul cu opţiunea "-g".

 

2.2.2. Comenzi de bază

file nume_executabil - Încarcă programul executabil care urmează a fi depanat

kill - termină programul care este depanat

list - listează secţiunile din codul sursă care au fost folosite pentru a genera fişierul executabil

next - execută o linie de cod, fără a intra în alte funcţii

step - execută o linie de cod, intrând în funcţii

run - execută programul care este în depanare

quit - iese din gdb

watch - permite examinarea unei variabile a programului ori de câte ori

variabila se modifică

break - setează un punct de oprire (breakpoint). De fiecare dată când

programul ajunge în acest punct, se opreşte  

info break – vezi breakpoint-urile deja setate

delete, clear – sterge un breakpoint

make - recompilează programul fără a ieşi din gdb

shell - execută comenzi shell

help - informaţii despre comenzi

 

gdb suportă comenzile de editare ale shell-ului: TAB pentru completarea automată a comenzilor, săgeţile sus, jos pentru navigarea printre comenzile care au fost deja introduse. Foarte multe comenzi permit prescurtari (ex. r in loc de run, b in loc de breakpoint, etc.)

 

Exemplu de sesiune gdb:

 

Folosind un editor de text simplu, introduceţi următorul program:

 

#include <stdio.h>

#include <string.h>

#include <stdlib.h>

 

void my_print(char *);

void my_print2(char *);

 

main ()

{

char my_string[] = "salut";

my_print (my_string);

my_print2 (my_string);

}

 

void my_print (char *string) {

printf ("Sirul este: %s\n", string);

}

 

void my_print2 (char *string) {

char *string2;

int size, i;

size = strlen (string);

string2 = (char *) malloc (size + 1);

for (i = 0; i < size; i++)

string2[size - i] = string[i];

string2[size + 1] = '\0';

printf ("Sirul afisat invers: %s\n", string2);

}

 

Compilaţi programul cu numele salut (folosiţi -o) şi executaţi-l. Observaţi că a doua linie nu a afişat ceea ce trebuia. A doua linie trebuia să afişeze:

 

Şirul afişat invers: tulas

 

Să rezolvăm problema folosind gdb. Mai întâi, recomplilaţi programul folosind opţiunea -g:

 

gcc -g -o salut salut.c

 

Apelaţi gdb:

 

gdb salut

sau

 

gdb

(gdb) file salut

 

care va încarca programul salut. Încercaţi apoi:

 

(gdb) run

 

veţi observa că programul rulează la fel, gdb specificând şi codul de ieşire al programului. Codul de ieşire poate fi setat la o valoare dorită dacă în program, funcţia main este de tip int şi este prezentă în program o instrucţiune return. Pentru a găsi problema, setaţi un breakpoint după instrucţiunea for din funcţia my_print2. Pentru aceasta, listaţi codul sursă folosind list:

 

(gdb) list

 

 ! apăsând Enter la promptul gdb veţi repeta ultima comandă introdusă

 

    Observaţi că fiecare linie începe cu un număr. Găsiţi numărul liniei specificate, după care:

 

(gdb) break nr

 

Rulaţi programul cu run. Veţi observa că programul se opreşte la linia specificată. Ca să puteţi să vă daţi seama de problemă, urmăriţi valoarea lui string2[size-i]:

(gdb) watch string2[size-i]

 

în acest moment, gdb va opri programul de fiecare dată când valoarea specificată se modifică. Întrucât am mai setat un breakpoint la aceeaşi linie, programul se va opri de două ori în acelaşi loc. Pentru a elimina această redundanţă, eliminaţi breakpoint-ul cu:

 

(gdb) disable 1

 

în care 1 înseamnă primul breakpoint. Numerele breakpoint-urilor sunt date în ordinea în care au fost setate. Continuaţi execuţia cu:

 

(gdb) cont

 

  ! puteţi afişa valoarea oricărei variabile sau expresii folosind comanda print expresie (ex. print i sau print (size - i)).

 

Continuaţi executia, observând de fiecare dată valorile expresiei afişate de watch. Se observă că nu este nici o valoare asignată pentru string2[0]. Întrucât malloc iniţializează memoria alocată cu 0, primul caracter al şirului este 0. Reamintim că în C, şirurile se termină cu \0. Acum, că v-aţi dat seama de problemă, găsiţi o cale de a o rezolva.

 

2.3. make

make este unul din cele mai importante unelte folosite în dezvoltarea de soft pentru Linux. make este un program care ţine cont de dependenţele dintre fişiere şi actualizează doar acele fişiere care au fost modificate de la ultima compilare.

make generează comenzi folosind un fişier descriptiv numit Makefile (e valabil si makefile) . Aceste comenzi sunt executate de shell. Acest fişier conţine seturi de reguli care trebuie urmate când se actualizează un program.

 

2.3.1. Formatul makefile

makefile e format din mai multe intrări. O intrare are forma:

 

Instructiunile pentru make se gasesc intr-un Makefile/makefile si determina ce actiuni trebuie facute pentru a satisface anumite cerinte.

 

Sintaxa unui makefile este de forma:

 

Target... : dependencies ...

<tab>command

<tab>command

<tab>...

unde:

• target = numele unui fisier ce trebuie generat sau numele unei actiuni

• dependencies = lista de actiuni (fisiere) ce trebuie indeplinite (ce trebuie sa existe) pentru a se realiza target-ul – make determina daca trebuie re-executat target-ul daca una dintre dependete s-a modificat (unul din fisierele din lista de dependente s-a modificat)

• command = comanda ce duce la realizarea target-ului (de obicei la comenzi sunt trecute comenzile de compilare care duc la realizarea target-ului)

• <tab> = caracterul tab

 

Exemplu 1:

 

in fisierul Makefile:

 

pregatire: mancare curatenie

echo "Putem primi musafiri"

touch pregatire

 

mancare: cumparaturi

echo "Gata MANCARE"

touch mancare

 

curatenie:

echo "Gata CURATENIE"

touch curatenie

 

cumparaturi:

echo "Gata CUMPARATURI"

touch cumparaturi

 

pierde_cumparaturi:

rm -f cumparaturi

echo "pierdut CUMPARATURI"

 

clean:

rm -f cumparaturi

rm -f curatenie

rm -f mancare

rm -f pregatire

echo "Gata ANULARE"

 

 

 

Exemplu 2:

 

in fisierul Makefile:

 

build: a2

a2: a21.o a22.o

gcc a21.o a22.o -o a2

a21.o: a21.c

gcc -c a21.c

a22.o: a22.c

gcc -c a22.c

run: a2

./a2

clean:

rm -f a2 a21.o a22.o

 

 

Exercitiul 1

Introduceti urmatorul program, compilati si executati fisierul rezultat.

main()

{ int i;

printf("\t Numarul \t\t patratul numarului\n\n");

for (i=0; i<=25; ++i)

printf("\t %d \t\t\t %d \n",i,i*i);

}

 

Exercitiul 2

Introduceti urmatorul program ( utilizeaza o librarie matematica), compilati si executati

fisierul rezultat.

#include <math.h>

main()

{ int i;

printf("\t Numarul \t\t patratul numarului\n\n");

for (i=0; i<=360; ++i)

printf("\t %d \t\t\t %d \n",i, sqrt((double) i));

}

 

Exercitiul 3

Priviti in directorul /lib si in directorul /usr/lib si vedeti ce librarii sunt disponibile.

- utilizati comanda man pentru a vedea detalii despre aceste librarii

- explorati fiecare librarie in parte si vedeti ce contine prin lansarea comenzii

ar t <libfile>

 

Exercitiul 4

Priviti in directorul /usr/include si vedeti ce librarii sunt disponibile.

- utilizati comanda more sau comanda cat pentru a vedea aceste fisiere

- explorati fiecare fisier header si vedeti ce contine

 

Exercitiul 5

Considerand ca avem un program C a carui functie main este in main.c si alte functii in

fisierele input.c si respectiv output.c

- ce comenti veti utiliza pentru compilarea si linkeditarea acestui program?

- cum veti modifica aceste comenzi astfel incat sa legati si libraria denumita

process1 din directorul sistem de librarii?

- cum veti modifica aceste comenzi astfel incat sa legati si libraria denumita

process2 din directorul home?

 

3. Întrebări şi aplicaţii

3.1. Explicaţi comanda:  gcc -o file file.c -lm

3.2. Explicaţi utilitatea folosirii opţiunii "-g" la compilarea unui program.

3.3. Explicaţi linia: string2=(char *) malloc(size + 1); din programul din platformă.

3.4. Cum se lansează şi cum se opreşte debugerul gdb?

3.5. Explicaţi comenzile de bază "watch" şi "shell" din debug-erul gdb.

3.6. Cum şi cu ce scop se foloseşte comanda "break" ţn gdb?

3.7. Explicaţi diferenţa dintre comenzile "next" şi "step" ale gdb-ului.

3.8. Explicaţi diferenţa dintre comenzile "run" şi "make" ale gdb-ului.

3.9. Explicaţi ce face comanda "cont" în gdb şi cum se anulează un breakpoint.

3.10. Ce este gcc-ul şi la ce se foloseşte opţiunea "-pg"?

3.11. Scrieţi un program care ordonează crescător un şir de numere introduse de la tastatură.

3.12. Scrieţi un program care copiază fisiere.

3.13. Scrieţi un program care afişează numărul de apariţii ale unui subşir într-un şir.

3.14. Să se scrie un program care afişează în ordine inversă literele unui şir de caractere citit de la tastatură.

3.15. Să se scrie un program care afişează valorile funcţiei x*sin(x) pe intervalul [0,10], cu pasul 0.1.

3.16. Să se scrie un program care generează 100 de numere aleatoare (numere întregi inferioare lui 10000).

3.17. Să se scrie un program care sortează crescător 10 numere introduse de la tastatură.

3.18. Să se scrie un program care numără elementele negative dintr-o secvenţă de n elemente.

3.19. Să se scrie un program care să transforme literele unui şir în litere mici.

3.20. Să se scrie un program care să calculeze x la puterea y (unde x şi y pot fi introduse de la tastatură).