07. Dezvoltarea aplicațiilor
În laboratorul 6 ați prelucrat text cu filtre și ați editat fișiere text cu vim și nano. La cursul de programare
scrieți programe în C, iar IDE-ul le compilează și le rulează când apăsați un buton. În acest laborator le
compilați singuri, în terminal, și vedeți ce se întâmplă în spatele acelui buton. Aflați prin ce diferă un limbaj
compilat de unul interpretat, compilați cu gcc și urmăriți cele patru etape ale compilării:
preprocesorul, compilatorul, asamblorul și linkerul. Împărțiți un program în module, citiți
erorile de compilare din fiecare etapă și configurați un editor care vă arată aceste erori chiar în timp ce
scrieți, cu ajutorul serverului de limbaj clangd. Apoi îl lăsați pe make să compileze programul în locul
vostru, pe baza unui Makefile: reguli, variabile automate, reguli cu șablon, ținte false și dependențe de
headere. La final, opriți un program în timp ce rulează cu debuggerul gdb și salvați versiunile proiectului cu
git.
Cea mai importantă idee a laboratorului este că un program se construiește printr-un lanț de fișiere: fiecare
fișier construit (un fișier obiect, executabilul) se obține din alte fișiere (sursele, headerele, alte fișiere
obiect) printr-o singură comandă. Un Makefile descrie acest lanț sub formă de reguli, iar make compară
timpii de modificare ai fișierelor: rulează o comandă doar dacă rezultatul ei lipsește sau este mai vechi decât
unul dintre fișierele din care se obține.
Obiective
- Să înțelegeți diferența dintre un limbaj compilat, unul interpretat și unul bazat pe bytecode
- Să compilați și să rulați un program C în terminal cu
gcc, fără IDE, și să activați avertismentele cu-Wall - Să urmăriți cele patru etape ale compilării și să vă opriți după fiecare dintre ele cu
gcc -E,-Sși-c - Să vă uitați în fișierele produse de fiecare etapă cu
file,nmșildd - Să împărțiți un program în module (un fișier
.hși unul.c), să compilați fiecare fișier.cîntr-un fișier obiect și să legați fișierele obiect într-un executabil - Să citiți o eroare de compilare, să aflați ce etapă a raportat-o și să o corectați
- Să înțelegeți ce este un server de limbaj și cum comunică editorul cu el, și să folosiți
clangddintr-un editor: erori, informații, completare și salt la definiție - Să scrieți un
Makefilecu reguli și să înțelegeți cum decidemakece construiește, pe baza timpilor de modificare - Să scurtați un
Makefilecu variabile automate ($@,$<,$^), reguli cu șablon și variabile (CC,CFLAGS) - Să adăugați țintele false
all,runșicleanși să le declarați cu.PHONY - Să scrieți dependențele de headere pe regulile potrivite
- Să compilați un program cu informații de depanare (
-g), să îl opriți cugdbși să vă uitați la variabilele lui - Să salvați versiunile unui proiect cu
git init,git add,git commit,git status,git diffșigit log
Resurse
- Răzvan Deaconescu, Răzvan Rughiniș, Mihai Carabaș, Alexandru Radovici, Utilizarea Sistemelor de
Operare, Printech 2021
- Capitolul 6 - Dezvoltarea aplicațiilor, secțiunile 6.2 - 6.5, 6.6.7, 6.7.1 și 6.8
- Brian Ward, How LINUX Works, 3rd Edition, No Starch Press, 2021
- Capitolul 15 - Development Tools, secțiunile 15.1 și 15.2
- GNU Make Manual, capitolul 2 - An Introduction to Makefiles, secțiunea 4.6 - Phony Targets și secțiunea 10.5 - Defining and Redefining Pattern Rules; Pro Git, capitolele 1 - 3 și capitolul 6 - GitHub; clangd, Getting started; Language Server Protocol, Overview
- Slide-urile cursului: 07. Dezvoltarea aplicațiilor
- Paginile de manual:
man gcc,man make,man file,man nm,man objdump,man ldd,man ar,man strip,man ld.so,man gdb,man git,man git-commit,man git-diff,man git-log,man git-restore,man gitignore,man git-push,man git-pull,man git-clone,man ssh-keygen,man bearșiclangd --help
Fedora și Ubuntu
Calculatoarele din laborator rulează Fedora cu Sway, iar exemplele sunt luate de pe ele. Acasă puteți folosi și Ubuntu: comenzile sunt aceleași, cu câteva diferențe:
| Fedora | Ubuntu | |
|---|---|---|
| instalarea uneltelor din acest laborator | sudo dnf install gcc make gdb git clang-tools-extra | sudo apt install build-essential gdb git clangd |
| o funcție apelată fără declarație | eroare (GCC 14 și versiunile mai noi) | Ubuntu 24.04 are GCC 13: doar un avertisment |
biblioteca C, în rezultatul lui ldd | /lib64/libc.so.6 | /lib/x86_64-linux-gnu/libc.so.6 |
directorul lui cc1, un program rulat de gcc | /usr/libexec/gcc/x86_64-redhat-linux/<VERSION>/ | /usr/lib/gcc/x86_64-linux-gnu/<VERSION>/ |
gcc -static (în Biblioteci) | are nevoie de sudo dnf install glibc-static | merge direct |
gcc --version afișează versiunea lui gcc. Exemplele sunt pentru PC (x86-64); pe un calculator cu procesor ARM, de
exemplu un Mac cu Apple silicon, codul în limbaj de asamblare și adresele arată altfel.
Compilarea
Limbaje compilate și limbaje interpretate
Procesorul rulează doar cod mașină: instrucțiuni scrise ca octeți, diferite pentru fiecare familie de procesoare (x86-64, ARM64, RISC-V). Oamenii scriu cod sursă: text, într-un limbaj de programare. Cineva trebuie să traducă codul sursă în cod mașină, iar limbajele diferă prin cine face traducerea și când:
| Tip | Cine traduce codul sursă | Când | Exemple |
|---|---|---|---|
| compilat | un compilator, în cod mașină: rezultatul este un executabil | o singură dată, înainte să ruleze programul | C, C++, Rust, Go |
| interpretat | un interpretor, un program care citește sursa și face ce scrie în ea | la fiecare rulare a programului | scripturi shell (bash, sh) |
| bytecode | un compilator, în bytecode (cod mașină pentru un procesor imaginar); o mașină virtuală îl rulează | înainte să ruleze programul (Java, C#) sau când pornește (Python, JavaScript) | Java, C#, Python, JavaScript |
Gândiți-vă la o carte într-o limbă străină: un program compilat este o traducere tipărită, făcută o dată și citită de multe ori; un program interpretat se citește cu un traducător alături, care traduce fiecare frază pe măsură ce citiți.
[student@fedora lab07]$ ./hello.sh
Hello from bash!
[student@fedora lab07]$ file hello.sh /usr/bin/bash
hello.sh: Bourne-Again shell script, ASCII text executable
/usr/bin/bash: ELF 64-bit LSB pie executable, x86-64, ...
hello.sheste text: interpretorul,bash, îl citește linie cu linie.bashînsuși este cod mașină, un fișier ELF (Executable and Linkable Format, formatul programelor în Linux), scris în C și compilat.
| Compilat | Interpretat | |
|---|---|---|
| viteză | rapid: procesorul rulează direct codul mașină | mai lent: interpretorul traduce sursa la fiecare rulare |
| erori | compilatorul găsește multe dintre ele înainte să ruleze programul | o eroare apare abia când rulează linia ei |
| după o modificare | compilați din nou, apoi rulați | doar îl rulați din nou |
| portabilitate | câte un executabil pentru fiecare procesor și sistem | același fișier rulează oriunde este instalat interpretorul |
Nu limbajul este compilat sau interpretat, ci programul care îl rulează. python3 compilează un script în
bytecode la fiecare pornire, apoi mașina lui virtuală rulează bytecode-ul; doar modulele importate de script se
păstrează compilate, în __pycache__. Mașinile virtuale pentru Java și JavaScript compilează în plus în cod mașină
codul care rulează des, chiar în timpul rulării: just in time (JIT).
Compilarea unui program C: gcc
gcc (GNU Compiler Collection) este compilatorul de C din Linux și, de multe ori, cel care rulează în spatele
butonului Build din IDE. Exemplele folosesc hello.c, pe care îl creează exercițiul 1 în
~/lab07:
#include <stdio.h>
#define TIMES 3
int main(void)
{
// say hello a few times
for (int i = 0; i < TIMES; i++) {
printf("Hello, %d!\n", i);
}
return 0;
}
[student@fedora lab07]$ gcc hello.c -o hello
[student@fedora lab07]$ ./hello
Hello, 0!
Hello, 1!
Hello, 2!
gcc hello.c -o helloa compilathello.cîn executabilulhello; nu a afișat nimic, pentru că nu a apărut nicio problemă. Programul se pornește cu./, deoarece directorul curent nu este înPATH(vedeți Căutarea înPATHdin laboratorul 4).
| Opțiune | Ce face |
|---|---|
-o <FILE> | numele fișierului rezultat; fără ea, executabilul se numește a.out |
-Wall | activează avertismentele despre cod corect din punct de vedere al limbajului, dar probabil greșit |
-g | adaugă informațiile de depanare, pentru gdb (vedeți Depanarea) |
-c, -S, -E | se opresc după o etapă a compilării (vedeți Etapele compilării) |
-l<NAME> | leagă biblioteca <NAME>: -lm pentru funcțiile matematice, precum sqrt |
-I <DIR> | caută fișierele header și în <DIR> |
-static | creează un executabil static (vedeți Linkerul) |
În spatele butonului din IDE
Butonul Build al unui IDE (Code::Blocks, Visual Studio Code, CLion) rulează gcc cu opțiuni ca acestea, iar
Run pornește executabilul; jurnalul de compilare al IDE-ului arată comenzile exacte. În terminal nu există niciun
buton care să compileze programul înainte de rulare: după ce modificați un fișier .c, rulați voi gcc (sau make).
Fișierele C le scrieți cu unul dintre editoarele din Editoare de text, din laboratorul 6; după Erori de compilare, configurați unul care înțelege C.
Etapele compilării
gcc hello.c -o hello pare un singur pas, dar de fapt sunt patru. Fiecare etapă citește fișierul produs de etapa
anterioară și scrie un fișier nou. Cu o opțiune, gcc se oprește după o anumită etapă, ca să vă puteți uita la
fișierul respectiv:
| Etapă | Ce face | Oprire după ea | Scrie |
|---|---|---|---|
| preprocesor | copiază fișierele din #include, înlocuiește #define-urile, elimină comentariile | gcc -E hello.c -o hello.i | hello.i, C preprocesat |
| compilator | traduce C în limbaj de asamblare, forma text a codului mașină | gcc -S hello.c | hello.s, cod în limbaj de asamblare |
| asamblor | traduce limbajul de asamblare în cod mașină | gcc -c hello.c | hello.o, fișier obiect |
| linker | unește fișierele obiect și bibliotecile într-un executabil | gcc hello.o -o hello | hello, executabil |
Gândiți-vă la o fabrică de mașini: prima stație aduce piesele la un loc (preprocesorul), următoarele le dau formă (compilatorul și asamblorul), iar ultima asamblează mașina (linkerul).
Preprocesorul: gcc -E
Liniile care încep cu # sunt pentru preprocesor. Acesta modifică textul înainte să-l vadă compilatorul, cam ca
sed, și nu verifică dacă rezultatul este cod C corect.
[student@fedora lab07]$ gcc -E hello.c -o hello.i
[student@fedora lab07]$ wc -l hello.c hello.i
12 hello.c
825 hello.i
837 total
[student@fedora lab07]$ grep 'int printf' hello.i
extern int printf (const char *__restrict __format, ...);
[student@fedora lab07]$ tail -n 8 hello.i
int main(void)
{
for (int i = 0; i < 3; i++) {
printf("Hello, %d!\n", i);
}
return 0;
}
#include <stdio.h>a copiat înhello.itot fișierul/usr/include/stdio.h(și fișierele incluse de el): sute de linii, printre care și declarația luiprintf. Numărul de linii depinde de versiunea bibliotecii C.#define TIMES 3a înlocuitTIMEScu3, iar comentariul a dispărut.
#include <stdio.h>, cu paranteze unghiulare, caută doar în directoarele sistemului, precum /usr/include;
#include "lib.h", cu ghilimele, caută mai întâi în directorul fișierului .c.
Compilatorul: gcc -S
Compilatorul traduce C în limbaj de asamblare: câte o linie pentru fiecare instrucțiune a procesorului, tot într-un fișier text.
[student@fedora lab07]$ gcc -S hello.c
[student@fedora lab07]$ grep -E 'call|cmpl|jle' hello.s
call printf@PLT
cmpl $2, -4(%rbp)
jle .L3
- Bucla a devenit o comparație (
cmpl: esteicel mult2?) și un salt înapoi (jle, jump if less or equal). printfeste doar apelat: codul lui nu se află înhello.s.
Două opțiuni fac hello.s mai ușor de citit:
| Opțiune | Ce face |
|---|---|
-fverbose-asm | scrie fiecare linie de C ca un comentariu deasupra instrucțiunilor ei și variabila pe care o folosește fiecare |
-masm=intel | scrie sintaxa Intel în loc de sintaxa AT&T, cea implicită pentru gcc (doar pe x86) |
[student@fedora lab07]$ gcc -S -fverbose-asm hello.c
[student@fedora lab07]$ grep -B1 cmpl hello.s
# hello.c:8: for (int i = 0; i < TIMES; i++) {
cmpl $2, -4(%rbp) #, i
[student@fedora lab07]$ gcc -S -masm=intel hello.c
[student@fedora lab07]$ grep -E 'call|cmp|jle' hello.s
call printf@PLT
cmp DWORD PTR -4[rbp], 2
jle .L3
-fverbose-asma arătat că comparația vine de la linia 8 dinhello.c,for-ul, și că-4(%rbp)estei.-masm=intela scris aceleași instrucțiuni în sintaxa Intel, cea din manualele Intel și AMD și din majoritatea cărților: operanzii sunt în ordine inversă, fără%și$, iar dimensiunea este un cuvânt (DWORD PTR, 4 octeți) în loc de o literă (ldincmpl).
Asamblorul: gcc -c
Asamblorul traduce limbajul de asamblare în cod mașină. Rezultatul este un fișier obiect: cod mașină care
încă nu poate rula, pentru că are goluri. Ca să apeleze printf, codul are nevoie de adresa funcției, dar
printf nu este definit în hello.c, deci asamblorul nu știe unde se află: lasă un loc gol și o notă cu numele
printf. Linkerul umple mai târziu aceste goluri.
[student@fedora lab07]$ gcc -c hello.c
[student@fedora lab07]$ file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
[student@fedora lab07]$ nm hello.o
0000000000000000 T main
U printf
nma listat numele (simbolurile) dinhello.o:Tînseamnă definit aici (în text, adică în cod), iarUînseamnă folosit aici, dar nedefinit (undefined).objdump -d hello.oafișează codul mașină, ca octeți, alături de limbajul de asamblare;-M intelfolosește sintaxa Intel. Ca să vedeți fiecare linie de C deasupra codului ei mașină, compilați cu-gși adăugați-S:gcc -g -c hello.c, apoiobjdump -d -S -M intel hello.o.
Linkerul
Linkerul (ld, pe care îl rulează gcc pentru voi) unește fișierele obiect într-un executabil și le umple
golurile. Pentru fiecare nume pe care un fișier obiect îl folosește, dar nu îl definește (un U în nm), linkerul
caută singurul loc care îl definește (un T), într-un alt fișier obiect sau într-o bibliotecă. Dacă nu găsește
niciunul, se oprește cu undefined reference; dacă găsește două, cu multiple definition (vedeți
Erori de compilare).
printf este definit în biblioteca C, fișierul libc.so.6, în care linkerul caută pentru orice program, fără să
i se ceară. Este o bibliotecă partajată, așa că linkerul nu copiază printf în hello: doar notează că hello are
nevoie de libc.so.6.
[student@fedora lab07]$ gcc hello.o -o hello
[student@fedora lab07]$ file hello
hello: ELF 64-bit LSB pie executable, x86-64, ..., dynamically linked, ...
[student@fedora lab07]$ ldd hello
linux-vdso.so.1 (0x00007ffc8d5f2000)
libc.so.6 => /lib64/libc.so.6 (0x00007f2b6a200000)
/lib64/ld-linux-x86-64.so.2 (0x00007f2b6a4b1000)
gcca primit un fișier obiect, nu un fișier.c, așa că a rulat doar linkerul.ldda listat bibliotecile de care are nevoiehellocând pornește:printfnu este copiat înhello, ci rămâne înlibc.so.6, pe care sistemul o încarcă la fiecare pornire a luihello. Numerele din paranteze se schimbă la fiecare rulare.
Cu gcc -static, linkerul copiază printf, împreună cu tot ce îi trebuie, din biblioteca C în executabil:
programul nu mai are nevoie de libc.so.6, dar este mult mai mare. Pe Fedora, -static are nevoie de pachetul
glibc-static. Exercițiile pentru acasă și provocările își construiesc propriile biblioteci: detaliile sunt în
Biblioteci.
gcc -save-temps hello.c -o hello rulează cele patru etape și păstrează fișierele fiecăreia: hello.i,
hello.s și hello.o.
Mai multe fișiere sursă
Un program real are multe fișiere sursă. Fiecare parte a programului, un modul, este o pereche de fișiere:
| Fișier | Ce conține | Cine îl folosește | Exemplu: lib |
|---|---|---|---|
.h, headerul | declarațiile: ce oferă modulul, fără cod; plus #define-urile și tipurile lui | fiecare fișier .c care apelează modulul, cu #include | lib.h: int sum(int n); |
.c, sursa | definițiile: codul funcțiilor | compilatorul, care îl transformă într-un fișier obiect | lib.c: int sum(int n) { ... } |
Fiecare fișier .c care folosește un modul include headerul acestuia, ca să știe compilatorul numele și tipurile
funcțiilor apelate. Exemplele folosesc fișierele din ~/lab07/project, un program care adună numerele de la 1 la
MAX:
project/
├── main.c # include lib.h și print.h; main() apelează sum() și print_result()
├── lib.h # #define MAX 10, declară sum()
├── lib.c # include lib.h, definește sum()
├── print.h # declară print_result()
└── print.c # include <stdio.h> și print.h, definește print_result() cu printf()
#ifndef LIB_H
#define LIB_H
#define MAX 10
int sum(int n);
#endif
#ifndef,#defineși#endifformează garda de includere (include guard): dacă un fișier.cincludelib.hde două ori, a doua copie este sărită.
Fiecare fișier .c se compilează separat, în propriul fișier obiect, iar linkerul unește fișierele obiect:
[student@fedora project]$ gcc -c main.c
[student@fedora project]$ gcc -c lib.c
[student@fedora project]$ gcc -c print.c
[student@fedora project]$ gcc main.o lib.o print.o -o app
[student@fedora project]$ ./app
sum = 55
- În
main.o,sumșiprint_resultsuntU; linkerul le-a găsit înlib.oși înprint.o.
De aceea fiecare fișier .c se compilează în propriul fișier obiect. După o modificare în print.c, doar print.o
este depășit, deci ajung două comenzi: gcc -c print.c îl compilează din nou, iar gcc main.o lib.o print.o -o app
leagă programul.
gcc main.c lib.c print.c -o app produce același program dintr-o singură comandă, dar compilează toate fișierele
de fiecare dată. Cu trei fișiere, nu contează. Nucleul Linux are însă peste 30 000 de fișiere .c: dacă le compilați
doar pe cele modificate, câștigați minute întregi la fiecare compilare.
Erori de compilare
Acestea sunt mesajele pe care le vedeți în jurnalul de compilare al IDE-ului. Cine raportează eroarea vă spune ce este greșit:
| Mesajul | Raportat de | Cauza | Soluția |
|---|---|---|---|
fatal error: lib.h: No such file or directory | preprocesor | un nume greșit în #include sau headerul este în alt director | corectați numele sau adăugați -I <DIR> |
error: implicit declaration of function ‘sum’ | compilator | sum este apelată, dar nimic de deasupra apelului nu o declară | includeți headerul care o declară |
error: expected ‘;’ before ‘}’ token | compilator | o eroare de sintaxă, pe linia indicată sau pe cea dinainte | adăugați ; care lipsește |
undefined reference to `sum' | linker | niciun fișier obiect nu definește sum | adăugați fișierul .o care o definește |
undefined reference to `sqrt' | linker | funcția este într-o bibliotecă pe care nu ați legat-o | adăugați -lm la finalul comenzii |
multiple definition of `sum' | linker | două fișiere obiect definesc sum | păstrați doar unul dintre ele |
[student@fedora lab07]$ gcc roots.c -o roots
/usr/bin/ld: /tmp/ccx5idIu.o: in function `main':
roots.c:(.text+0x1c): undefined reference to `sqrt'
collect2: error: ld returned 1 exit status
[student@fedora lab07]$ gcc roots.c -o roots -lm
[student@fedora lab07]$ ./roots
sqrt(1) = 1.000
sqrt(2) = 1.414
sqrt(3) = 1.732
sqrt(4) = 2.000
/usr/bin/ldșicollect2arată că a eșuat linkerul: compilarea a mers, dar nimeni nu defineștesqrt. Funcția se află în biblioteca matematică,libm, pe care o adaugă-lm.- Erorile preprocesorului și ale compilatorului încep cu
<FILE>:<LINE>:<COLUMN>:, locul problemei.
Un avertisment (warning) nu oprește compilarea, dar de multe ori ascunde un bug. Compilați întotdeauna cu
-Wall și corectați fiecare avertisment.
Un editor cu server de limbaj
În IDE-ul de la cursul de programare, codul este colorat, numele se completează singure, iar erorile sunt marcate în
timp ce scrieți. În terminal, de lucrul acesta se ocupă împreună două programe: un editor și un server de
limbaj (language server), un program care înțelege un singur limbaj. Pentru C, serverul de limbaj este
clangd.
LSP (Language Server Protocol) este limba comună a celor două: orice editor care vorbește LSP poate folosi orice server de limbaj. Fără el, fiecare editor ar avea nevoie de câte un plugin pentru fiecare limbaj.
Gândiți-vă la un coleg care se uită peste umărul vostru în timp ce scrieți: editorul este caietul, iar serverul de limbaj este colegul, care citește fiecare linie și vă răspunde la întrebări despre ea.
Cum funcționează un server de limbaj
Când deschideți un fișier C, editorul pornește clangd ca proces copil (laboratorul 3) și comunică cu el prin
două pipe-uri (laboratorul 6), în mesaje JSON scurte. Editorul trimite textul după fiecare modificare și pune
întrebări; clangd răspunde la ele și trimite din proprie inițiativă erorile găsite.
clangd verifică codul în timp ce scrieți, la fel ca un compilator, dar nu construiește nimic.
Alegerea unui editor
Instalați clangd (Fedora: sudo dnf install clang-tools-extra; Ubuntu: sudo apt install clangd) și unul
dintre aceste editoare:
| Editor | Unde rulează | De ce are nevoie ca să folosească clangd |
|---|---|---|
vim | terminal | de un plugin, vim-lsp, și de vreo 20 de linii în ~/.vimrc |
nvim 0.11 sau mai nou | terminal | de câteva linii în ~/.config/nvim/init.lua |
hx (Helix) | terminal | de nimic |
emacs 29 sau mai nou | ambele | de o linie în ~/.emacs.d/init.el |
code (VS Code) | fereastră | de extensia clangd |
zed (Zed) | fereastră | de nimic |
nvim, nu vim
Dacă vă plac tastele lui vim (vedeți Editoare de text din laboratorul 6), folosiți
nvim: are aceleași taste și un client de limbaj inclus, așa că se configurează în câteva linii. vim are nevoie de
un plugin și de vreo 20 de linii de Vimscript. Celelalte editoare aproape că nu au nevoie de configurare.
Instalați, configurați și verificați editorul ales:
- vim
- nvim
- hx
- emacs
- VS Code
- Zed
Aceasta este cea mai grea configurare
vim nu are un client de limbaj propriu, iar fiecare funcționalitate nouă cere și mai mult Vimscript: folosiți mai
bine nvim, cu aceleași taste de editare (vedeți sfatul de mai sus).
Instalați vim (Fedora: sudo dnf install vim-enhanced; Ubuntu: sudo apt install vim) și pluginul vim-lsp, în
directorul din care vim își încarcă singur pluginurile:
git clone https://github.com/prabirshrestha/vim-lsp ~/.vim/pack/lsp/start/vim-lsp
Apoi adăugați aceste linii în ~/.vimrc: ele pornesc clangd pentru fișierele C și definesc tastele din tabelul de
mai jos.
let g:lsp_diagnostics_virtual_text_enabled = 1
if executable('clangd')
au User lsp_setup call lsp#register_server({
\ 'name': 'clangd',
\ 'cmd': {server_info->['clangd']},
\ 'allowlist': ['c', 'cpp'],
\ })
endif
function! s:on_lsp_buffer_enabled() abort
setlocal omnifunc=lsp#complete
nmap <buffer> gd <plug>(lsp-definition)
nmap <buffer> gr <plug>(lsp-references)
nmap <buffer> K <plug>(lsp-hover)
nmap <buffer> <leader>rn <plug>(lsp-rename)
nmap <buffer> <leader>ca <plug>(lsp-code-action)
nmap <buffer> ]d <plug>(lsp-next-diagnostic)
endfunction
augroup lsp_install
au!
autocmd User lsp_buffer_enabled call s:on_lsp_buffer_enabled()
augroup END
Verificare: deschideți un fișier .c și rulați :LspStatus; apare clangd: running.
În tabel, \ este tasta leader a lui vim, scrisă <leader> în ~/.vimrc.
| Ce vreți | Taste |
|---|---|
| să vedeți erorile | la capătul liniei; ] d sare la următoarea |
| să aflați ce este numele de sub cursor | K |
| să completați un nume | în modul Insert, Ctrl+X Ctrl+O |
| să mergeți la definiție și înapoi | g d, Ctrl+O |
| să listați locurile unde e folosit un nume | g r |
| să redenumiți un nume, în toate fișierele | \ r n |
| să aplicați corectura sugerată | \ c a |
Instalați Neovim (Fedora: sudo dnf install neovim). Aveți nevoie de versiunea 0.11 sau mai nouă (nvim --version);
Ubuntu 24.04 are una mai veche, așa că acolo folosiți Helix sau Zed. Apoi adăugați aceste linii în
~/.config/nvim/init.lua:
vim.lsp.config('clangd', {
cmd = { 'clangd' },
filetypes = { 'c', 'cpp' },
root_markers = { 'compile_commands.json', 'compile_flags.txt', 'Makefile', '.git' },
})
vim.lsp.enable('clangd')
vim.diagnostic.config({ virtual_text = true })
Verificare: deschideți un fișier .c și rulați :checkhealth vim.lsp; clangd apare printre clienții activi.
Configurații gata făcute: în loc să scrieți singuri init.lua, puteți porni de la o configurație care are deja
clangd, completare și un explorator de fișiere. Acestea au nevoie de un Neovim recent și de git. Mutați mai întâi
deoparte ~/.config/nvim și ~/.local/share/nvim, unde își instalează configurațiile pluginurile:
- kickstart.nvim: un singur fișier, explicat prin comentarii; cel mai bun punct de plecare dacă vreți să vă înțelegeți configurația;
- LazyVim, NvChad și AstroNvim: configurații complete, gata de folosit.
Documentație: :help lsp în Neovim (online) și
nvim-lspconfig, cu setările gata făcute pentru multe servere de limbaj.
| Ce vreți | Taste |
|---|---|
| să vedeți erorile | la capătul liniei; ] d sare la următoarea |
| să aflați ce este numele de sub cursor | K |
| să completați un nume | în modul Insert, Ctrl+X Ctrl+O |
| să mergeți la definiție și înapoi | Ctrl+], Ctrl+T |
| să listați locurile unde e folosit un nume | g r r |
| să redenumiți un nume, în toate fișierele | g r n |
| să aplicați corectura sugerată | g r a |
Instalați Helix (Fedora: sudo dnf install helix; Ubuntu: sudo snap install --classic helix); comanda lui este
hx. Nu are nevoie de nicio setare: pornește singur clangd pentru fiecare fișier .c.
[student@fedora project]$ hx --health c
...
Configured language servers:
✓ clangd: /usr/bin/clangd
...
- Helix a găsit
clangdînPATH. Un✘în acest loc înseamnă căclangdnu este instalat.
| Ce vreți | Taste |
|---|---|
| să vedeți erorile | marcate în stânga; Space d le listează |
| să aflați ce este numele de sub cursor | Space k |
| să completați un nume | apare în timp ce scrieți |
| să mergeți la definiție și înapoi | g d, Ctrl+O |
| să listați locurile unde e folosit un nume | g r |
| să redenumiți un nume, în toate fișierele | Space r |
| să aplicați corectura sugerată | Space a |
Instalați Emacs (Fedora: sudo dnf install emacs; Ubuntu: sudo apt install emacs). De la versiunea 29 are inclus un
client de limbaj, Eglot, care îl cunoaște deja pe clangd. Linia de mai jos, pusă în ~/.emacs.d/init.el, îl
pornește pentru orice fișier C; dacă aveți deja un fișier ~/.emacs, puneți linia acolo, pentru că Emacs citește doar
unul dintre ele:
(add-hook 'c-mode-hook 'eglot-ensure)
Verificare: deschideți un fișier .c (cu emacs -nw main.c rămâneți în terminal); linia de mod, din partea de jos,
afișează eglot.
| Ce vreți | Taste |
|---|---|
| să vedeți erorile | subliniate; Alt+X flymake-show-buffer-diagnostics le listează |
| să aflați ce este numele de sub cursor | apare singur în partea de jos |
| să completați un nume | Ctrl+Alt+I |
| să mergeți la definiție și înapoi | Alt+., Alt+, |
| să listați locurile unde e folosit un nume | Alt+? |
| să redenumiți un nume, în toate fișierele | Alt+X eglot-rename |
| să aplicați corectura sugerată | Alt+X eglot-code-actions |
Descărcați pachetul .rpm (Fedora) sau .deb (Ubuntu) de pe code.visualstudio.com,
instalați-l cu sudo dnf install ./code-*.rpm (Ubuntu: sudo apt install ./code_*.deb) și adăugați extensia
clangd, de la proiectul LLVM, din bara Extensions:
- apăsați Ctrl+Shift+X sau dați click pe pictograma cu patru pătrate din bara din stânga;
- scrieți
clangdîn caseta de căutare de sus; - alegeți clangd, publicată de LLVM, și apăsați Install.
Același lucru, din terminal:
code --install-extension llvm-vs-code-extensions.vscode-clangd
Dacă este instalată și extensia C/C++ de la Microsoft, extensia clangd vă cere să-i dezactivați IntelliSense-ul:
acceptați, ca să verifice codul doar clangd. Când clangd lipsește, extensia se oferă să-l descarce:
instalați în schimb pachetul, ca toate editoarele să folosească același clangd. VSCodium, același editor fără
componentele Microsoft, funcționează la fel; comanda lui este codium, nu code.
Verificare: deschideți directorul proiectului (code ~/lab07/project), apoi un fișier .c; bara de stare, din
partea de jos, afișează clangd.
| Ce vreți | Taste |
|---|---|
| să vedeți erorile | subliniate cu roșu; Ctrl+Shift+M le listează |
| să aflați ce este numele de sub cursor | țineți mouse-ul deasupra lui sau Ctrl+K Ctrl+I |
| să completați un nume | apare în timp ce scrieți; Ctrl+Space |
| să mergeți la definiție și înapoi | F12, Ctrl+Alt+- |
| să listați locurile unde e folosit un nume | Shift+F12 |
| să redenumiți un nume, în toate fișierele | F2 |
| să aplicați corectura sugerată | Ctrl+. |
Instalați Zed cu scriptul lui, la fel pe Fedora și pe Ubuntu:
curl -f https://zed.dev/install.sh | sh
Scriptul instalează comanda zed în ~/.local/bin; dacă shell-ul nu o găsește, ieșiți din sesiune și autentificați-vă
din nou.
Zed funcționează din prima: nu are nevoie de setări sau de extensii și pornește singur clangd pentru fiecare
fișier .c. Verificare: deschideți directorul proiectului (zed ~/lab07/project), apoi un fișier .c, și faceți o
greșeală: este subliniată imediat.
| Ce vreți | Taste |
|---|---|
| să vedeți erorile | subliniate cu roșu; Ctrl+Shift+M le listează |
| să aflați ce este numele de sub cursor | țineți mouse-ul deasupra lui |
| să completați un nume | apare în timp ce scrieți |
| să mergeți la definiție și înapoi | F12, Ctrl+Alt+- |
| să listați locurile unde e folosit un nume | click dreapta, Find All References |
| să redenumiți un nume, în toate fișierele | F2 |
| să aplicați corectura sugerată | Ctrl+. |
Când proiectul are headere în alte directoare, clangd are nevoie de opțiunile lui gcc: le găsiți în
Ce trebuie să știe clangd, după secțiunea despre Makefile.
Makefile-ul
După fiecare modificare trebuie să țineți minte ce comenzi rulați din nou și în ce ordine. make face asta în locul
vostru. Citește din directorul curent un fișier numit Makefile, care spune, pentru fiecare fișier de construit, din
ce se obține și ce comenzi îl produc. Apoi make rulează doar comenzile necesare.
Gândiți-vă la o carte de rețete în care fiecare rețetă își listează ingredientele: ca să faceți un tort, pregătiți mai întâi crema de care are nevoie, dar numai dacă nu aveți deja cremă proaspătă în frigider.
O regulă
Un Makefile este o listă de reguli:
target: prerequisites
command 1
command 2
| Parte | Ce este |
|---|---|
| ținta (target) | fișierul de construit |
| dependențele (prerequisites) | fișierele din care se obține, separate prin spații |
| rețeta (recipe) | comenzile care îl construiesc, câte una pe linie, fiecare linie începând cu un TAB |
Makefile-ul pentru hello are două reguli:
hello: hello.o
gcc hello.o -o hello
hello.o: hello.c
gcc -c hello.c
hellose obține legândhello.o;hello.ose obține compilândhello.c.makefără parametri construiește prima țintă,hello;make hello.oconstruiește doarhello.o.
Un TAB, nu spații
Fiecare linie a unei rețete începe cu un caracter TAB. Cu spații, make se oprește cu
Makefile:2: *** missing separator. Stop. Majoritatea editoarelor păstrează TAB-urile într-un fișier numit
Makefile; în vim, :set list afișează un TAB ca ^I.
Cum decide make
Ca să construiască o țintă, make:
- construiește mai întâi dependențele ei, fiecare cu propria regulă, în același fel;
- rulează rețeta dacă ținta nu există sau dacă o dependență este mai nouă decât ținta;
- altfel nu face nimic: ținta este la zi (up to date).
O dependență fără regulă, precum hello.c, trebuie să existe. make compară doar timpii de modificare ai
fișierelor (ls -l --full-time îi afișează, până la fracțiuni de secundă); nu citește niciodată codul.
[student@fedora lab07]$ make
gcc -c hello.c
gcc hello.o -o hello
[student@fedora lab07]$ make
make: 'hello' is up to date.
[student@fedora lab07]$ touch hello.c
[student@fedora lab07]$ make
gcc -c hello.c
gcc hello.o -o hello
- Primul
makea construit ambele fișiere, pentru că nu existau.makeafișează fiecare comandă înainte să o ruleze. - Al doilea
makenu a făcut nimic:hello.ceste mai vechi decâthello.o, iarhello.oeste mai vechi decâthello. touch(vedețitouchdin laboratorul 2) a setat timpul luihello.cla momentul curent, ca și cum l-ați fi editat:hello.ca devenit mai nou decâthello.o, așa cămakel-a compilat din nou, iar apoihello.oa fost mai nou decâthello.
Figura arată regula pe care o urmează make pentru fiecare țintă, apoi ultimul make, pas cu pas: make ține o
stivă cu țintele la care lucrează. Coboară până la dependențe, apoi urcă înapoi și rulează rețetele necesare.
| Comandă | Ce face |
|---|---|
make | construiește prima țintă din Makefile și doar ce nu este la zi |
make <TARGET> | construiește <TARGET> |
make -n | afișează comenzile pe care le-ar rula, fără să le ruleze (dry run, rulare de probă) |
make -B | construiește din nou toate țintele, chiar și pe cele care sunt la zi |
make <VAR>=<VALUE> | schimbă o variabilă doar pentru această rulare; Makefile-ul rămâne la fel (vedeți Variabile) |
make -C <DIR> | intră mai întâi în <DIR> și lucrează acolo, ca și cum ați fi rulat make în <DIR> |
make --debug=v | explică de ce construiește sau sare peste fiecare țintă |
make -j<N> | rulează până la <N> comenzi în același timp |
Un Makefile pentru un proiect
Pentru proiect, fiecare fișier obiect are o regulă, iar app se obține legând cele trei fișiere obiect:
app: main.o lib.o print.o
gcc main.o lib.o print.o -o app
main.o: main.c
gcc -c main.c
lib.o: lib.c
gcc -c lib.c
print.o: print.c
gcc -c print.c
[student@fedora project]$ make
gcc -c main.c
gcc -c lib.c
gcc -c print.c
gcc main.o lib.o print.o -o app
[student@fedora project]$ touch print.c
[student@fedora project]$ make
gcc -c print.c
gcc main.o lib.o print.o -o app
- După
touch print.c,main.oșilib.oerau încă la zi:makea compilat doarprint.c, apoi a legat programul.
Cele trei reguli pentru fișierele obiect sunt aproape identice. Următoarele trei secțiuni scurtează Makefile-ul.
Variabile automate
Într-o rețetă, make înlocuiește variabilele automate cu numele din regulă:
| Variabilă | Ce înseamnă | În regula app: main.o lib.o print.o |
|---|---|---|
$@ | ținta | app |
$< | prima dependență | main.o |
$^ | toate dependențele | main.o lib.o print.o |
Cu ele, rețeta lui app devine gcc $^ -o $@, iar rețetele celor trei fișiere obiect devin identice:
gcc -c $< -o $@.
Reguli cu șablon
O regulă cu șablon (pattern rule) este o singură regulă pentru mai multe fișiere: % se potrivește cu orice
nume, același în țintă și în dependență. %.o: %.c înseamnă main.o: main.c, lib.o: lib.c, print.o: print.c și
orice alt fișier obiect.
app: main.o lib.o print.o
gcc $^ -o $@
%.o: %.c
gcc -c $< -o $@
- Din 11 linii au rămas 5. Un modul nou are nevoie doar ca fișierul lui obiect să fie adăugat în lista lui
app.
make are și reguli predefinite: fără nicio regulă pentru main.o, tot compilează main.c, cu cc -c. Scrieți
totuși propriile reguli, ca să alegeți voi comenzile.
Variabile
O variabilă păstrează o valoare într-un singur loc: o definiți cu NAME = value și o folosiți cu $(NAME). Două
nume sunt o convenție, pe care o așteaptă orice programator C:
| Variabilă | Ce conține | Valoarea obișnuită |
|---|---|---|
CC | compilatorul de C | gcc |
CFLAGS | opțiunile compilatorului | -Wall -g |
CC = gcc
CFLAGS = -Wall -g
app: main.o lib.o print.o
$(CC) $^ -o $@
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
[student@fedora project]$ make -B
gcc -Wall -g -c main.c -o main.o
gcc -Wall -g -c lib.c -o lib.o
gcc -Wall -g -c print.c -o print.o
gcc main.o lib.o print.o -o app
[student@fedora project]$ make -n -B CFLAGS=-O2
gcc -O2 -c main.c -o main.o
...
makea afișat fiecare comandă după ce a înlocuit variabilele.CFLAGS=-O2din linia de comandă a înlocuit valoarea dinMakefile, fără să modifice fișierul;-ndoar a afișat comenzile.
Ținte false
Unele ținte nu sunt fișiere: sunt nume pentru o acțiune pe care i-o cereți lui make. clean șterge ce a construit
make:
clean:
rm -f *.o app
make clean rulează rm de fiecare dată, pentru că nu există niciodată un fișier numit clean. Dacă însă apare
unul, clean nu are dependențe și fișierul există, deci este la zi, iar rm nu mai rulează:
[student@fedora project]$ touch clean
[student@fedora project]$ make clean
make: 'clean' is up to date.
Ținta specială .PHONY listează țintele care nu sunt fișiere, numite ținte false (phony targets): make nu
caută fișiere cu numele lor și le rulează întotdeauna rețetele. Țintele false obișnuite sunt:
| Țintă | Ce face |
|---|---|
all | construiește tot; este prima țintă, deci make înseamnă make all |
run | construiește programul, dacă nu este la zi, apoi îl rulează |
clean | șterge fișierele construite de make |
install | copiază programul într-un director din PATH, ca să-l puteți rula după nume din orice director |
uninstall | șterge ce a copiat install |
CC = gcc
CFLAGS = -Wall -g
.PHONY: all run clean
all: app
run: app
./app
app: main.o lib.o print.o
$(CC) $^ -o $@
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f *.o app
[student@fedora project]$ make run
./app
sum = 55
[student@fedora project]$ make
make: Nothing to be done for 'all'.
make runa verificat mai întâiapp: era la zi, așa cămakedoar a rulat./app.allnu are rețetă, iarappera la zi: nu era nimic de făcut.
Aceste nume sunt o convenție, nu fac parte din make: orice programator C se așteaptă să le găsească, dar make le
tratează ca pe orice altă țintă. Contează doar ordinea: make fără parametri construiește prima țintă din Makefile.
Dependențe de headere
Și un header este o dependență: un fișier obiect trebuie compilat din nou când se modifică un header inclus de
fișierul lui .c. Cu Makefile-ul de mai sus, make nu știe lucrul acesta:
[student@fedora project]$ sed -i 's/MAX 10/MAX 100/' lib.h
[student@fedora project]$ make
make: Nothing to be done for 'all'.
[student@fedora project]$ ./app
sum = 55
- Suma numerelor de la 1 la 100 este
5050, darmain.onu a fost compilat din nou:lib.hnu apare în nicio regulă.
Dependențele urmează liniile #include. Uitați-vă la liniile #include din fiecare fișier .c și adăugați fiecare
header la regula fișierului lui obiect:
main.o: lib.h print.h
lib.o: lib.h
print.o: print.h
- Aceste reguli nu au rețetă: doar adaugă dependențe, iar rețeta vine tot din
%.o: %.c. Puneți-le la finalulMakefile-ului, caallsă rămână prima țintă.
[student@fedora project]$ make
gcc -Wall -g -c main.c -o main.o
gcc -Wall -g -c lib.c -o lib.o
gcc main.o lib.o print.o -o app
[student@fedora project]$ ./app
sum = 5050
main.cșilib.cincludlib.h, așa că fișierele lor obiect au fost compilate din nou;print.oa rămas neschimbat.
Un header se pune pe regula unui fișier obiect, niciodată pe regula executabilului: cu
app: main.o lib.o print.o lib.h, make doar leagă din nou vechiul main.o, în care MAX este tot 10.
Ce trebuie să știe clangd
clangd verifică fiecare fișier așa cum l-ar compila gcc, deci are nevoie de aceleași opțiuni, precum -I <DIR>.
Fără ele, un header dintr-un alt director dă eroarea '<HEADER>' file not found în editor, chiar dacă make
funcționează. Opțiunile i le dați într-un fișier din directorul proiectului:
| Fișier | Ce conține | Cum îl creați | Când îl folosiți |
|---|---|---|---|
compile_flags.txt | opțiunile lui gcc, câte una pe linie, aceleași pentru toate fișierele | de mână, în editor: de exemplu, linia -Iinclude | toate fișierele folosesc aceleași opțiuni |
compile_commands.json | comanda gcc exactă pentru fiecare fișier, așa cum a rulat-o make | make clean, apoi bear -- make; din nou după ce adăugați un fișier | fișierele folosesc opțiuni diferite sau o redenumire trebuie să modifice toate fișierele |
bear (Fedora: sudo dnf install bear; Ubuntu: sudo apt install bear) notează fiecare comandă pe care o rulează
make, așa că porniți de la o compilare curată. Cu compile_commands.json, clangd cunoaște și fișierele pe care nu
le-ați deschis: o redenumire le modifică pe toate. Ce află clangd, adică indexul proiectului, se păstrează în
directorul .cache, care nu intră în repository (vedeți Salvarea versiunilor).
clangd --check=<FILE> verifică un fișier în terminal; ultima linie se termină cu All checks completed, 0 errors
când fișierul nu are nicio eroare.
Depanarea: gdb
Un debugger (depanator) rulează un alt program sub controlul lui: îl oprește la o anumită linie, îl rulează
linie cu linie și îi afișează variabilele, în timp ce programul rulează. Este mai rapid decât să adăugați linii cu
printf și să compilați din nou. gdb este debuggerul proiectului GNU. Are nevoie ca programul să fie compilat cu
-g, care adaugă informațiile de depanare: ce cod mașină provine din ce linie și unde se află fiecare variabilă.
Makefile-ul proiectului are deja -g în CFLAGS.
| Comandă | Scurt | Ce face |
|---|---|---|
gdb ./app | pornește gdb cu programul; programul nu rulează încă | |
break <FUNCTION>, break <FILE>:<LINE> | b | setează un breakpoint (punct de oprire): programul se va opri acolo, înainte să ruleze linia respectivă |
break <PLACE> if <CONDITION> | se oprește acolo doar când condiția C este adevărată: break sum if n > 5 | |
watch <VARIABLE> | se oprește de fiecare dată când variabila se schimbă și afișează valoarea veche și pe cea nouă | |
delete <N> | d | șterge breakpointul cu numărul <N>; fără <N>, le șterge pe toate |
run | r | pornește programul; acesta rulează până la un breakpoint sau până se termină |
next | n | rulează linia afișată de gdb; o funcție apelată pe ea rulează în întregime, fără oprire |
step | s | rulează linia afișată de gdb, dar se oprește în interiorul funcției apelate |
finish | fin | rulează până se întoarce funcția curentă și afișează valoarea pe care o întoarce |
continue | c | rulează până la următorul breakpoint sau până se termină programul |
print <EXPRESSION> | p | afișează o variabilă sau orice expresie C: print n * 2 |
backtrace | bt | afișează stiva de apeluri: funcția curentă, cea care a apelat-o și așa mai departe |
up, down | trece la funcția care a apelat-o pe cea curentă sau înapoi, ca să-i afișați variabilele | |
quit | q | iese din gdb și oprește programul, dacă încă rulează |
Oprim programul la intrarea în sum și ne uităm la parametrul lui:
[student@fedora project]$ gdb -q ./app
Reading symbols from ./app...
(gdb) break sum
Breakpoint 1 at 0x1171: file lib.c, line 5.
(gdb) run
Starting program: /home/student/lab07/project/app
...
Breakpoint 1, sum (n=10) at lib.c:5
5 int s = 0;
(gdb) print n
$1 = 10
(gdb) backtrace
#0 sum (n=10) at lib.c:5
#1 0x0000555555555150 in main () at main.c:6
(gdb) continue
Continuing.
sum = 55
[Inferior 1 (process 4211) exited normally]
(gdb) quit
-q(quiet) a sărit peste textul despre licență.break suma pus un breakpoint pe prima linie din corpul luisum, linia 5 dinlib.c; adresele și numărul procesului diferă de la un calculator la altul.- Programul s-a oprit înainte să ruleze linia 5;
backtracea arătat căsuma fost apelată demain, de la linia 6 dinmain.c. continuea lăsat programul să ruleze până la capăt.
Pe Fedora și pe Ubuntu, gdb poate întreba Enable debuginfod for this session? (y or [n]): răspundeți
n. Se oferă să descarce informațiile de depanare ale bibliotecilor de sistem, de care acest laborator nu
are nevoie; ignorați la fel liniile despre Missing ... debuginfo. gdb -tui afișează codul sursă deasupra
comenzilor.
Salvarea versiunilor: git
Git este un sistem de control al versiunilor (version control system): păstrează istoricul fișierelor unui
proiect. Puteți reveni la orice versiune, puteți vedea ce s-a schimbat și de ce și puteți lucra la același cod
împreună cu alți oameni. Directorul cu fișierele și istoricul lor (păstrat în directorul ascuns .git) se numește
repository. Un commit este o fotografie a tuturor fișierelor, cu un autor, o dată și un mesaj; numele lui este
un hash, de exemplu ed6ba3b.
Gândiți-vă la punctele de salvare dintr-un joc: salvați când lucrurile merg și vă puteți întoarce oricând la un punct de salvare.
O modificare trece prin trei locuri de pe calculatorul vostru: editați fișierele, git add le pune în zona de
staging (staging area, ce va conține următorul commit), iar git commit salvează commitul în repository.
| Comandă | Ce face |
|---|---|
git config --global user.name "<NAME>" | numele vostru, scris în fiecare commit; o singură dată pe fiecare calculator |
git config --global user.email "<EMAIL>" | adresa de email, scrisă în fiecare commit; o singură dată pe fiecare calculator |
git init | transformă directorul curent într-un repository |
git status | afișează fișierele modificate și ce se află în zona de staging |
git add <FILE> | adaugă modificările lui <FILE> în zona de staging; git add . le adaugă pe toate |
git commit -m "<MESSAGE>" | salvează zona de staging ca un commit |
git diff | afișează modificările care nu sunt încă în zona de staging |
git log --oneline | afișează commiturile, câte unul pe linie, cel mai nou primul |
git restore <FILE> | renunță la modificările lui <FILE> care nu sunt în zona de staging; nu le mai puteți recupera |
git switch -c <BRANCH>, git switch <BRANCH> | creează o ramură (branch) și trece pe ea; trece pe o ramură |
git merge <BRANCH> | aduce commiturile din <BRANCH> în ramura curentă |
Fișierele construite de make se pot oricând construi din nou, așa că nu intră în repository; la fel nici directorul
.cache, în care clangd își păstrează indexul. Fișierul .gitignore listează, câte unul pe linie, numele (sau
șabloanele de globbing) pe care git le ignoră:
[student@fedora project]$ git init
Initialized empty Git repository in /home/student/lab07/project/.git/
[student@fedora project]$ printf '*.o\napp\n.cache/\n' > .gitignore
[student@fedora project]$ git add .
[student@fedora project]$ git status --short
A .gitignore
A Makefile
A lib.c
A lib.h
A main.c
A print.c
A print.h
[student@fedora project]$ git commit -m "Add the sum program"
[main (root-commit) ed6ba3b] Add the sum program
7 files changed, 60 insertions(+)
...
git status --shorta afișatA(added) pentru fișierele din zona de staging; fișierele obiect șiapplipsesc, din cauza lui.gitignore.- Hash-ul commitului vostru este altul: depinde de conținut, de autor și de momentul commitului.
Lucrul cu GitHub
Primii pași folosesc doar calculatorul vostru; exercițiile pentru acasă și provocările folosesc și GitHub.
Un commit rămâne doar pe calculatorul vostru. Un depozit la distanță (remote) este o copie a repository-ului pe
un server, precum GitHub, GitLab sau Codeberg: în el vă trimiteți (push) commiturile și din el aduceți
(pull) commiturile colegilor. Un coleg începe prin a clona repository-ul: obține o copie a fișierelor și a
întregului istoric, legată de același remote, origin.
Gândiți-vă la un director partajat în cloud, care păstrează fiecare punct de salvare, împreună cu autorul lui.
| Comandă | Ce face |
|---|---|
git remote add origin <URL> | leagă repository-ul de remote-ul <URL>, sub numele origin |
git remote -v | listează remote-urile |
git push -u origin main | trimite prima dată main către origin; -u ține minte alegerea, apoi ajunge git push |
git push origin <BRANCH> | trimite o altă ramură |
git pull | aduce commiturile noi din origin și le unește (merge) cu ramura curentă |
git clone <URL> <DIR> | copiază repository-ul în <DIR>, cu origin deja setat |
git branch -d <BRANCH> | șterge o ramură locală care a fost unită |
git push origin --delete <BRANCH> | șterge o ramură de pe remote |
git config --global pull.rebase false | git pull face merge când ambele părți au commituri noi; o singură dată pe fiecare calculator |
GitHub nu acceptă parola în terminal. Folosiți o cheie SSH: o pereche de fișiere, o cheie privată, care nu
părăsește niciodată calculatorul, și o cheie publică, pe care i-o dați lui GitHub. Cu o cheie, URL-ul unui
repository are forma git@github.com:<USER>/<REPO>.git. Creați cheia o singură dată pe fiecare calculator:
ssh-keygen -t ed25519 -C "<EMAIL>" # apăsați Enter la fiecare întrebare
cat ~/.ssh/id_ed25519.pub # lipiți-o în GitHub: Settings, SSH and GPG keys, New SSH key
ssh -T git@github.com # răspundeți yes prima dată
- Când GitHub vă cunoaște cheia,
ssh -T git@github.comafișeazăHi <USER>! You've successfully authenticated, but GitHub does not provide shell access.
Un pull request (PR) este o cerere, făcută pe GitHub, de a uni o ramură în main. Colegii citesc modificările în
tab-ul Files changed, le comentează și apasă Merge pull request; apoi toată lumea rulează git pull. Regula
majorității echipelor: nu faceți commituri direct pe main; creați o ramură, trimiteți-o și deschideți un pull
request.
Când două persoane modifică aceleași linii, git pull nu poate alege între ele și se oprește cu un conflict:
[student@fedora bob]$ git pull
...
CONFLICT (content): Merge conflict in motto.txt
Automatic merge failed; fix conflicts and then commit the result.
[student@fedora bob]$ cat motto.txt
<<<<<<< HEAD
keep it clear
=======
keep it short
>>>>>>> 6c37dc458b34401722a29eace880dec81e97dffb
- Între
<<<<<<<și=======este versiunea voastră, iar între=======și>>>>>>>versiunea venită dinorigin. Editați fișierul până când are textul corect și nicio linie de marcaj, apoi rulațigit addpe el șigit commit: rezultatul este un commit de merge (merge commit), cu doi părinți.
Biblioteci
Pentru cei care vor mai mult
Nu aveți nevoie de această secțiune pentru exercițiile din laborator, 🧪 Primii pași. Citiți-o acasă: o folosesc exercițiile 🏠 Mai departe și 📝 Provocările.
O bibliotecă este un fișier cu funcții compilate, gata de folosit de alte programe: printf se află în
biblioteca C, sqrt în biblioteca matematică. Există două tipuri:
| Bibliotecă statică | Bibliotecă partajată | |
|---|---|---|
| fișierul | lib<NAME>.a, o arhivă de fișiere obiect | lib<NAME>.so, un shared object, gata de încărcat |
| linkerul | copiază funcțiile în executabil | doar notează că executabilul are nevoie de bibliotecă |
| la pornirea programului | nu mai e nimic de încărcat: codul este deja în executabil | loaderul (ld-linux-x86-64.so.2) găsește biblioteca și o încarcă |
| o versiune nouă a bibliotecii | trebuie legat din nou fiecare program | fiecare program o folosește de la următoarea pornire |
| dimensiunea | executabile mari: fiecare program are propria copie | executabile mici: o singură copie, pe disc și în memorie, pentru toate programele |
| dacă biblioteca lipsește | programul rulează în continuare | programul nu pornește: error while loading shared libraries |
Implicit, gcc leagă biblioteca C ca bibliotecă partajată. Cu -static, o copiază în executabil:
[student@fedora lab07]$ gcc hello.c -o hello-dynamic
[student@fedora lab07]$ gcc -static hello.c -o hello-static
[student@fedora lab07]$ stat -c '%s %n' hello-dynamic hello-static
8880 hello-dynamic
650832 hello-static
[student@fedora lab07]$ ldd hello-static
not a dynamic executable
- Cele două programe provin din aceeași sursă, cu aceeași comandă; diferă doar
-static.hello-staticeste de aproximativ 70 de ori mai mare: are propria copie a luiprintfși a tot ce îi trebuie luiprintf. Dimensiunile diferă de la un sistem la altul și de la un procesor la altul. hello-staticeste totuși mai mic decât biblioteca C (stat -L -c '%s' /lib64/libc.so.6afișează peste 1 MiB): linkerul a copiat doar părțile din bibliotecă pe care le folosește programul.
Vă puteți construi propriile biblioteci. -l<NAME> leagă lib<NAME>.so sau, dacă nu există, lib<NAME>.a; le caută
în directoarele date cu -L <DIR>, apoi în directoarele sistemului (/usr/lib64 pe Fedora):
| Comandă | Ce face |
|---|---|
ar rcs libsum.a lib.o | creează biblioteca statică libsum.a din lib.o |
ar t libsum.a | listează fișierele obiect din arhivă |
gcc -fPIC -c lib.c | compilează cod care funcționează la orice adresă (Position Independent Code), cum are nevoie o bibliotecă partajată |
gcc -shared lib.o -o libsum.so | creează biblioteca partajată libsum.so |
gcc main.o print.o -L. -lsum -o app | leagă app cu libsum din directorul curent (.) |
LD_LIBRARY_PATH=. ./app | rulează app și îi permite loaderului să caute biblioteci și în . |
Construim o bibliotecă partajată și legăm app cu ea:
[student@fedora project]$ gcc -fPIC -c lib.c
[student@fedora project]$ gcc -shared lib.o -o libsum.so
[student@fedora project]$ gcc main.o print.o -L. -lsum -o app
[student@fedora project]$ ./app
./app: error while loading shared libraries: libsum.so: cannot open shared object file: No such file or directory
[student@fedora project]$ LD_LIBRARY_PATH=. ./app
sum = 55
-L.i-a spus linkerului unde estelibsum.so, dar nu și loaderului: când porneșteapp, loaderul caută doar în directoarele sistemului.LD_LIBRARY_PATH(vedețiman ld.so) adaugă directoare pentru o singură comandă; cealaltă variantă este să instalați biblioteca în/usr/lib64.
Depanare
| Problemă | Soluție |
|---|---|
bash: gcc: command not found (sau make, gdb) | Unealta nu este instalată: vedeți Fedora și Ubuntu |
bash: hello: command not found, deși fișierul există | Directorul curent nu este în PATH: rulați ./hello |
| Programul nu se schimbă după ce modific sursa | Spre deosebire de IDE, terminalul nu compilează programul înainte să-l ruleze: rulați din nou gcc sau make |
fatal error: lib.h: No such file or directory | Preprocesorul nu a găsit headerul: verificați numele sau adăugați -I <DIR> |
implicit declaration of function ‘sum’ | Funcția nu este declarată: includeți headerul ei |
undefined reference to `sum' | Linkerul nu a găsit sum: adăugați în comandă fișierul obiect care o definește |
undefined reference to `sqrt' | sqrt se află în biblioteca matematică: adăugați -lm la finalul comenzii |
undefined reference to `main' | Ați legat module fără main: legați-le împreună cu fișierul obiect care conține main |
multiple definition of `sum' | Două fișiere obiect definesc sum: păstrați doar unul; definiți funcțiile în fișiere .c, nu în headere |
| Editorul nu afișează erori și nu completează nimic | clangd lipsește sau editorul nu îl folosește: clangd --version, hx --health c; pentru nvim, liniile din init.lua |
'<HEADER>' file not found în editor, deși make funcționează | clangd nu știe de opțiunea -I: scrieți-o în compile_flags.txt sau rulați bear -- make |
| O redenumire modifică doar fișierul deschis | clangd cunoaște celelalte fișiere doar din compile_commands.json: make clean, apoi bear -- make |
Makefile:2: *** missing separator. Stop. | Rețeta începe cu spații: înlocuiți-le cu un TAB |
make: *** No rule to make target 'x.c', needed by 'x.o'. Stop. | O dependență lipsește sau are numele greșit |
make: *** [Makefile:5: app] Error 1 | O comandă din rețetă a eșuat: citiți eroarea de deasupra acestei linii |
make spune up to date, dar programul e greșit | Lipsește un header din reguli (vedeți Dependențe de headere); make -B reconstruiește tot |
make: 'clean' is up to date. | Există un fișier numit clean: adăugați clean în .PHONY |
No symbol table is loaded. în gdb | Programul a fost compilat fără -g: adăugați-l în CFLAGS și rulați make -B |
Author identity unknown la git commit | Spuneți-i lui git numele și adresa de email, cu git config --global |
fatal: not a git repository | Nu sunteți în directorul repository-ului: intrați în el cu cd |
error while loading shared libraries: libsum.so | Loaderul nu găsește biblioteca: LD_LIBRARY_PATH=. (vedeți Biblioteci) |
Permission denied (publickey). | GitHub nu vă cunoaște cheia SSH: adăugați ~/.ssh/id_ed25519.pub în GitHub și verificați cu ssh -T git@github.com |
Support for password authentication was removed | Remote-ul are un URL https://: git remote set-url origin git@github.com:<USER>/<REPO>.git |
! [rejected] main -> main (fetch first) | origin are commituri pe care voi nu le aveți: mai întâi git pull, apoi git push |
fatal: Need to specify how to reconcile divergent branches. | Rulați o singură dată git config --global pull.rebase false, apoi din nou git pull |
CONFLICT (content): Merge conflict in <FILE> | Editați <FILE>, păstrați textul corect, ștergeți liniile de marcaj, apoi git add <FILE> și git commit |
/usr/bin/ld: cannot find -lc cu -static | Pe Fedora, instalați glibc-static |
Exerciții
- 🧪 Primii pași (exercițiile 1 - 14): câte o idee pe rând, în ordinea din laborator, de făcut în laborator;
- 🏠 Mai departe (exercițiile 15 - 37): exerciții mai lungi, de făcut acasă; unele își construiesc propriile biblioteci, explicate în Biblioteci;
- 📝 Provocări (exercițiile 38 - 45): cele mai grele, asemănătoare cu cele de la examen.
make clean și rm -f *.o șterg fișiere fără să întrebe. Într-o regulă clean scrieți doar fișierele construite de
make, niciodată * sau *.c: o greșeală de tastare acolo vă șterge sursele. Salvați des cu git: un commit este o
copie pe care o puteți recupera oricând.
🧪 Primii pași
Faceți aceste exerciții în ordine, în timpul laboratorului. Fiecare începe cu ce trebuie să obțineți și încearcă o
singură idee din laborator: dacă vă împotmoliți, citiți secțiunea indicată sub obiectiv. Rulați comenzile în
~/lab07, dacă exercițiul nu spune altceva.
-
Directorul de lucru: Verificați că uneltele sunt instalate și creați în
~/lab07fișierele pe care le folosesc celelalte exerciții. → Fedora și Ubuntu-
afișați versiunea lui
gcc,make,gdb,gitșiclangd; instalați-le pe cele care lipsesc; -
rulați aceste comenzi (copiați-le din browser cu Ctrl+C și lipiți-le în terminal cu Ctrl+Shift+V):
mkdir -p ~/lab07/projectcd ~/lab07cat > hello.c << 'END'#include <stdio.h>#define TIMES 3int main(void){// say hello a few timesfor (int i = 0; i < TIMES; i++) {printf("Hello, %d!\n", i);}return 0;}ENDprintf '#!/bin/bash\necho "Hello from bash!"\n' > hello.shchmod +x hello.shcat > roots.c << 'END'#include <stdio.h>#include <math.h>int main(void){for (int i = 1; i <= 4; i++) {printf("sqrt(%d) = %.3f\n", i, sqrt(i));}return 0;}ENDcd projectcat > main.c << 'END'#include "lib.h"#include "print.h"int main(void){int total = sum(MAX);print_result("sum", total);return 0;}ENDprintf '%s\n' '#ifndef LIB_H' '#define LIB_H' '#define MAX 10' 'int sum(int n);' '#endif' > lib.hcat > lib.c << 'END'#include "lib.h"int sum(int n){int s = 0;for (int i = 1; i <= n; i++) {s = s + i;}return s;}ENDprintf '%s\n' '#ifndef PRINT_H' '#define PRINT_H' \'void print_result(const char *label, int value);' '#endif' > print.hcat > print.c << 'END'#include <stdio.h>#include "print.h"void print_result(const char *label, int value){printf("%s = %d\n", label, value);}ENDcd ~/lab07 -
listați fișierele din
~/lab07și din~/lab07/project.
Indiciu
Majoritatea comenzilor își afișează versiunea cu
--version. Pachetele sunt în Fedora și Ubuntu.Verificare
gcc,make,gdb,gitșiclangdafișează fiecare o versiune cu--version.- Comenzile nu afișează nimic.
~/lab07conținehello.c,hello.sh,roots.cși directorulproject;projectconținelib.c,lib.h,main.c,print.cșiprint.h.
-
Compilarea
-
Fără IDE: Compilați
hello.cși rulați-l în terminal, comparați-l cu un script și aflați de ce programul se pornește cu./. → Limbaje compilate și limbaje interpretate · Compilarea unui program C- rulați
hello.sh; compilațihello.cîn executabilulhelloși rulați-l; - cu o singură comandă, afișați tipul lui
hello.sh, al luihelloși al lui/usr/bin/bash, programul care rulează scriptul; - compilați din nou
hello.c, fără să alegeți numele fișierului rezultat; rulați fișierul creat degcc, apoi ștergeți-l; - rulați
hellodoar după nume, fără./; apoi mergeți în directorul utilizatorului și rulațihellode acolo, fără să vă întoarceți în~/lab07.
Indiciu
man file; Compilarea unui program C: opțiunea-o; Căutarea înPATHdin laboratorul 4.Verificare
./hello.shafișeazăHello from bash!;./helloafișeazăHello, 0!,Hello, 1!șiHello, 2!.hello.sheste unBourne-Again shell script, ASCII text executable;helloși/usr/bin/bashsunt fiecare unELF 64-bit LSB pie executable, ....- Fără
-o, executabilul se numeștea.out. bash: hello: command not found(Fedora se poate oferi să instaleze pachetulhello: răspundeți N); din~,lab07/helloafișează aceleași 3 linii.
De ce:
helloșibashsunt cod mașină, rulat de procesor; scriptul este text, citit debash. Shell-ul caută numele unei comenzi doar în directoarele dinPATH, iar~/lab07nu este printre ele; o cale care conține/se rulează direct. - rulați
-
În interiorul compilării: Uitați-vă la fișierele pe care le scriu primele trei etape ale compilării pentru
hello.c. → Preprocesorul · Compilatorul · Asamblorul- opriți
gccdupă preprocesor și scrieți rezultatul înhello.i; numărați liniile dinhello.cși dinhello.iși găsiți declarația luiprintfînhello.i; - uitați-vă la ultimele linii din
hello.i: aflați ce au devenitTIMESși comentariul; - opriți
gccdupă compilator, cu liniile de C scrise ca comentarii înhello.s; găsiți linia care apeleazăprintf, linia care îl compară peicu2și linia de C din care vine fiecare; - scrieți din nou
hello.s, în sintaxa Intel, și găsiți aceleași două linii; - opriți
gccdupă asamblor; afișați tipul luihello.oși listați-i simbolurile: găsiți-l pe cel definit în el și pe cel pe care doar îl folosește.
Indiciu
Tabelele din Etapele compilării și Compilatorul;
grep,tailșiwcdin laboratorul 6;man nm: 🔍 căutați"U".Verificare
hello.care 12 linii;hello.iare câteva sute (numărul depinde de biblioteca C); declarația esteextern int printf (const char *__restrict __format, ...);.- Bucla conține
i < 3, iar comentariul a dispărut. gcc -S -fverbose-asm hello.c:call printf@PLTeste sub# hello.c:9:,printf-ul, iarcmpl $2, -4(%rbp)sub# hello.c:8:,for-ul (pe ARM:bl printfșicmp w0, 2, după//).gcc -S -masm=intel hello.c:call printf@PLTșicmp DWORD PTR -4[rbp], 2(pe ARM,gccspuneunrecognized command-line option: ARM are o singură sintaxă).hello.o: ELF 64-bit LSB relocatable, ...;T maineste definit înhello.o, iarU printfeste doar folosit.
De ce: preprocesorul doar modifică textul; compilatorul și asamblorul îl traduc, dar lasă un gol pentru
printf, pe care îl umple linkerul. - opriți
-
Linkerul: Transformați fișierul obiect într-un program, aflați de ce bibliotecă are nevoie și comparați-l cu o variantă statică a aceluiași program. → Linkerul
- legați
hello.oînhello, rulați-l și afișați-i tipul; - listați bibliotecile pe care le încarcă
hellocând pornește; - compilați
hello.cînhellocu o singură comandă care păstrează și fișierele fiecărei etape; listați-le, apoi ștergețihello.i,hello.sșihello.o; - construiți din
hello.cprogramulhello-static, cu biblioteca C copiată în el, și rulați-l; comparați-i dimensiunea cu cea a luihello, în octeți, și listați-i bibliotecile.
Indiciu
Linkerul;
man ldd;man gcc: 🔍 căutați-static(pe Fedora, instalați mai întâiglibc-static);stat -c '%s %n'afișează dimensiunea unui fișier, în octeți (laboratorul 5).Verificare
helloafișează cele 3 linii;filespuneELF 64-bit LSB pie executable,dynamically linked.libc.so.6 => /lib64/libc.so.6(Ubuntu:/lib/x86_64-linux-gnu/libc.so.6): biblioteca în care se aflăprintf.gcc -save-temps hello.c -o hellopăstreazăhello.i,hello.sșihello.o.gcc -static hello.c -o hello-static; afișează aceleași 3 linii.helloare sub 20 KiB, iarhello-staticcâteva sute de KiB, de zeci de ori mai mult (numerele depind de sistem);ldd hello-staticafișeazănot a dynamic executable.
De ce:
hello-staticare propria copie a luiprintf;helloo încarcă dinlibc.so.6la fiecare pornire. - legați
-
Mai multe fișiere sursă: În
~/lab07/project, construițiappdin trei fișiere sursă, apoi reconstruiți-l după o modificare, cu cât mai puține comenzi. → Mai multe fișiere sursă- compilați fiecare fișier
.cîn fișierul lui obiect, apoi legați cele trei fișiere obiect înapp; rulați-l; - cu editorul, modificați
print.castfel încât programul să afișezesum: 55în loc desum = 55; - reconstruiți
appdoar cu comenzile necesare și rulați-l; - readuceți
print.cla forma inițială și reconstruițiappîn același fel.
Indiciu
Mai multe fișiere sursă: ce fișier obiect depinde de
print.c?Verificare
appafișeazăsum = 55.print.cconține liniaprintf("%s: %d\n", label, value);.- Ajung două comenzi:
gcc -c print.c, apoigcc main.o lib.o print.o -o app;appafișeazăsum: 55. - Aceleași două comenzi;
appafișează din nousum = 55.
De ce: doar
print.ose obține dinprint.c; celelalte fișiere obiect sunt încă la zi. - compilați fiecare fișier
-
Erori de compilare: Provocați trei erori, pe rând, și aflați ce etapă a compilării o raportează pe fiecare. → Erori de compilare
- compilați
roots.cînroots; citiți eroarea, corectați comanda și rulațiroots; - în
project, legațiappdoar dinmain.oșiprint.o; - în
project, redenumițilib.hînlib.h.bakși compilațimain.c; apoi redenumiți-l înapoi; - notați, pentru fiecare eroare, etapa care a raportat-o.
Indiciu
Tabelul din Erori de compilare;
ldșicollect2înseamnă linkerul.Verificare
undefined reference to `sqrt'; cu-lmla final,rootsafișează de lasqrt(1) = 1.000până lasqrt(4) = 2.000.undefined reference to `sum'.fatal error: lib.h: No such file or directory.- Pașii 1 și 2: linkerul (
ld,collect2); pasul 3: preprocesorul.
- compilați
Serverul de limbaj
-
Editorul vostru și
clangd: Instalați un editor care foloseșteclangdși folosiți serverul de limbaj pe proiect, fără să compilați nimic. → Un editor cu server de limbaj- instalați
clangdși un editor; verificați că editorul îl găsește peclangd; - deschideți
~/lab07/project/main.c; afișați informațiile despresum, apoi mergeți la definiția ei și întoarceți-vă; - pe linia cu
print_result, schimbațitotalîntotlși citiți eroarea, fără să compilați; aplicați corectura sugerată declangd; - pe o linie nouă din
main, tastațipri, alegețiprint_resultdin completări și citiți-i parametrii; anulați toate modificările și salvați fișierul.
Indiciu
Tabelele din Alegerea unui editor; dacă vă plac tastele lui
vim, folosiținvim, nuvim.Verificare
- Verificarea de la finalul tab-ului editorului vostru merge: de exemplu,
hx --health cafișează✓ clangd: /usr/bin/clangd, iar înnvim,:checkhealth vim.lspîl listează peclangd. - Informațiile despre
sumsuntint sum(int n), oferită de"lib.h"; definiția ei deschidelib.c(sau declarația, dinlib.h). - Eroarea este
Use of undeclared identifier 'totl'; did you mean 'total'?; corectura scrie la loctotal. - Completarea arată
print_result(const char *label, int value); la final,main.ceste ca la început.
De ce: toate acestea au venit de la
clangd, în timp ce scriați: nu s-a compilat nimic. - instalați
Makefile-ul
-
Primul
Makefile: Lăsați-l pemakesă construiascăhelloși aflați când rulează comenzile. →Makefile-ul- în
~/lab07, scrieți unMakefilecu două reguli:hellodinhello.oșihello.odinhello.c; - ștergeți
helloșihello.o; rulațimake, apoi rulați-l din nou; - cu editorul, schimbați
HelloînHiînhello.c; afișați comenzile pe care le-ar rulamake, fără să le rulați; apoi rulațimakeși rulațihello; - schimbați
Hiînapoi înHelloși rulați din noumake.
Indiciu
O regulă și Cum decide
make;man make: 🔍 căutațidry-run.Verificare
cat -A Makefileafișează^I, adică un TAB, la începutul celor două rețete.- Primul
makeruleazăgcc -c hello.cșigcc hello.o -o hello; al doilea afișeazămake: 'hello' is up to date. make -nșimakeafișează aceleași două comenzi;./helloafișează de laHi, 0!până laHi, 2!.makerulează din nou ambele comenzi, deșihello.ceste exact ca la început.
De ce:
makecompară doar timpii de modificare, nu ce s-a schimbat: o țintă care lipsește sau este mai veche decât o dependență se construiește din nou. - în
-
Un
Makefilepentru proiect: În~/lab07/project, scrieți unMakefilecu câte o regulă pentru fiecare fișier pe care îl construiți. → UnMakefilepentru un proiect- scrieți cele patru reguli:
appdin cele trei fișiere obiect și fiecare fișier obiect din fișierul lui.c; - ștergeți fișierele obiect și
app; rulațimake, apoi rulațiapp; - rulați
touch print.c, apoimake; - ștergeți
lib.oși construiți doarlib.o.
Indiciu
Verificare
cat -A Makefileafișează^Ila începutul fiecărei rețete.makerulează 4 comenzi, 3 compilări și legarea;appafișeazăsum = 55.- Doar
gcc -c print.cși legarea. make lib.orulează doargcc -c lib.c.
- scrieți cele patru reguli:
-
Un
Makefilemai scurt: ScurtațiMakefile-ul proiectului, fără să schimbați ce construiește. → Variabile- scrieți rețeta lui
appcu variabile automate; înlocuiți cele trei reguli pentru fișierele obiect cu o singură regulă cu șablon; - adăugați variabilele
CC, cu valoareagcc, șiCFLAGS, cu valoarea-Wall -g, și folosiți-le în rețete; - construiți din nou tot, chiar dacă este la zi.
Indiciu
Verificare
- Rețeta lui
appestegcc $^ -o $@; o singură regulă,%.o: %.c, cugcc -c $< -o $@, construiește toate fișierele obiect. Makefile-ul are 8 linii, cu tot cu cele goale.make -Brulează:gcc -Wall -g -c main.c -o main.ogcc -Wall -g -c lib.c -o lib.ogcc -Wall -g -c print.c -o print.ogcc main.o lib.o print.o -o app
- scrieți rețeta lui
-
Ținte false: Adăugați țintele
clean,allșirunînMakefile-ul proiectului. → Ținte false- adăugați o regulă
cleancare șterge fișierele obiect șiapp; rulațimake clean; - rulați
make, apoi creați un fișier gol numitcleanși rulați din noumake clean; listați fișierele; - corectați
Makefile-ul cu.PHONYși rulațimake clean; ștergeți fișierulclean; - adăugați țintele
all, pe prima poziție, șirun; rulațimake,make run, apoi din noumake.
Indiciu
Ținte false:
Makefile-ul de la finalul secțiunii.Verificare
make cleanruleazărm -f *.o app.make: 'clean' is up to date.; fișierele obiect șiappsunt tot acolo.make cleanrulează din nourm -f *.o app.makeconstruieșteapp;make runafișează./appșisum = 55; ultimulmakeafișeazămake: Nothing to be done for 'all'.
De ce: pentru
make, o țintă este un fișier;.PHONYspune căclean,allșirunnu sunt fișiere. - adăugați o regulă
-
Dependențe de headere: Găsiți bugul unui
Makefilefără headere și corectați-l. → Dependențe de headere- în
lib.h, schimbațiMAXîn100; rulațimake, apoi./app; - adăugați câte o regulă pentru fiecare fișier obiect, cu headerele incluse de fișierul lui
.c; rulațimake, apoi./app; - în
print.h, adăugați linia// prints a label and a valueînaintea declarației; mai întâi notați comenzile pe care le va rulamake, apoi rulați-l; - schimbați
MAXînapoi în10și rulațimake run.
Indiciu
Dependențe de headere;
grep '#include' *.clistează toate liniile#include.Verificare
make: Nothing to be done for 'all'., iarappafișează totsum = 55: greșit, suma până la 100 este5050.- Regulile sunt
main.o: lib.h print.h,lib.o: lib.hșiprint.o: print.h;makecompileazămain.cșilib.c, leagă programul, iarappafișeazăsum = 5050. makecompileazămain.cșiprint.c, apoi leagă programul.appafișeazăsum = 55.
De ce: un fișier obiect depinde de headerele incluse de fișierul lui
.c. - în
Depanare și versiuni
-
Opriți programul: Rulați
appsubgdb, opriți-l în interiorul luisumși uitați-vă la variabilele lui. → Depanarea- verificați că
makecompilează cu-g; pornițigdbcuapp; - opriți programul la începutul lui
sum; afișaținșin * 2; - afișați stiva de apeluri;
- rulați următoarele două linii, pe rând;
- lăsați
sumsă ruleze până la capăt și citiți valoarea pe care o întoarce; lăsați programul să se termine; ieșiți.
Indiciu
Tabelul din Depanarea. Dacă
gdbîntreabă dedebuginfod, răspundeți n.Verificare
CFLAGSconține-g;gdbafișeazăReading symbols from ./app....- Se oprește la
sum (n=10) at lib.c:5, înainte deint s = 0;;print ndă$1 = 10, iarprint n * 2dă$2 = 20. #0 sum (n=10) at lib.c:5, apoi#1 ... in main () at main.c:6.nextafișează linia 6, cufor, apoi linia 7,s = s + i;.finishafișeazăValue returned is $3 = 55;continueafișeazăsum = 55și[Inferior 1 (process ...) exited normally].
- verificați că
-
Salvați proiectul: Transformați
~/lab07/projectîntr-un repositorygitși salvați două versiuni ale lui. → Salvarea versiunilor- spuneți-i lui
gitnumele și adresa de email și setați numele primei ramuri lamain:git config --global init.defaultBranch main; - în
project, creați repository-ul; scrieți un.gitignorecare ignoră fișierele obiect,appși directorul.cacheal luiclangd; afișați starea; - adăugați toate fișierele și faceți un commit cu mesajul
Add the sum program; - adăugați comentariul
// adds the numbers from 1 to npe linia dinaintea luiint sum(int n)dinlib.c; afișați modificarea, apoi faceți un commit cu mesajulExplain sum; - afișați istoricul, câte un commit pe linie.
Indiciu
Tabelul din Salvarea versiunilor;
git commit -aadaugă și fișierele modificate.Verificare
git config --global --listafișeazăuser.name,user.emailșiinit.defaultbranch=main.git statuslistează.gitignore,Makefile,lib.c,lib.h,main.c,print.cșiprint.h, dar niciun fișier obiect și niciapp.- Commitul afișează
7 files changed. git diffarată comentariul pe o linie care începe cu+.- 2 linii, cu
Explain sumprima; hash-urile voastre sunt altele.
- spuneți-i lui
🏠 Mai departe
Fiecare exercițiu începe cu obiectivul lui; comenzile le alegeți voi. La final, verificați obiectivul în sine, nu
doar că fiecare comandă a mers. Exercițiile folosesc fișierele din ~/lab07, precum și Makefile-ul și
repository-ul din ~/lab07/project, de la Primii pași.
Compilarea
-
În spatele lui
gcc: Un coleg spune căgcceste compilatorul. Aflați ce programe rulează de faptgccca să construiascăhello. → Etapele compilării- în
~/lab07, compilațihello.cînhelloși puneți-l pegccsă afișeze fiecare program pe care îl rulează; - din acest rezultat, păstrați doar liniile care rulează
cc1,asșicollect2; - găsiți directorul lui
cc1.
Indiciu
man gcc: 🔍 căutați-v(textul e lung: urmărițiPrint (on standard error output));gccscrie acest text la ieșirea de eroare, iar2>&1îl trimite în pipe (laboratorul 3).Verificare
gcc -vafișează multe linii: versiunea, directoarele și fiecare program pe care îl rulează.- Trei linii:
cc1 ... hello.c ..., care scrie/tmp/cc....s;as ... -o /tmp/cc....o /tmp/cc....s;collect2 ... -o hello ..., care ruleazăld.
- Linia cu
cc1de la pasul 2 începe cu calea completă a programului, pe care o afișează șigcc -print-prog-name=cc1: pe Fedora,/usr/libexec/gcc/x86_64-redhat-linux/<VERSION>/cc1; pe Ubuntu, un director asemănător, cux86_64-linux-gnu.
De ce:
gcceste un driver: rulează preprocesorul și compilatorul (amândouă încc1), asamblorul (as) și linkerul (collect2, apoild). - în
-
Macrourile sunt text: Un coleg a scris un macro care ridică un număr la pătrat, iar programul spune că 3 la pătrat face 5. Aflați de ce și corectați macroul. → Preprocesorul
-
creați
square.cîn~/lab07:cat > ~/lab07/square.c << 'END'#include <stdio.h>#define SQUARE(x) x * xint main(void){printf("%d\n", SQUARE(1 + 2));return 0;}END -
compilați-l și rulați-l;
-
uitați-vă la linia cu
printfdupă preprocesare; -
corectați macroul, astfel încât programul să afișeze
9.
Indiciu
Preprocesorul înlocuiește text, ca
sed; nu calculează nimic. Ce operație face C prima,+sau*?Verificare
- Programul afișează
5. printf("%d\n", 1 + 2 * 1 + 2);- Cu
#define SQUARE(x) ((x) * (x)), afișează9.
De ce: preprocesorul doar înlocuiește text; apoi C calculează
*înainte de+, deci macroul are nevoie de paranteze. -
-
Avertismentele arată bugurile: Un program se compilează fără niciun mesaj, dar funcția lui
is_zeroeste greșită. Lăsați compilatorul să găsească bugurile. → Erori de compilare-
creați
warn.cîn~/lab07:cat > ~/lab07/warn.c << 'END'#include <stdio.h>int is_zero(int n){if (n = 0) {return 1;}}int main(void){int unused;printf("is_zero(5) = %d\n", is_zero(5));return 0;}END -
compilați-l fără opțiuni, apoi cu
-Wall; -
corectați fiecare avertisment și compilați din nou cu
-Wall, până nu mai rămâne niciunul; -
găsiți opțiunea care transformă orice avertisment într-o eroare, ca să se oprească compilarea.
Indiciu
Citiți fiecare avertisment: fișierul, linia și numele dintre parantezele drepte de la final.
man gcc: 🔍 căutați-Werror.Verificare
- Fără opțiuni,
gccnu afișează nimic. Cu-Wall, apar 3 avertismente:- linia 5,
suggest parentheses around assignment used as truth value:=în loc de==; - linia 12,
unused variable ‘unused’; - linia 8,
control reaches end of non-void function: lipsește unreturnpentru cazul în carennu este0.
- linia 5,
- Nu mai rămâne niciun avertisment; programul afișează
is_zero(5) = 0. - Cu
-Werror, un avertisment oprește compilarea:all warnings being treated as errors.
-
-
Un nume, două definiții: Un coleg a scris o funcție
summai rapidă, cu formulan * (n + 1) / 2, și o leagă alături delib.o. Aflați ce spune linkerul și construițiappcu varianta mai rapidă a luisum. → Linkerul · Erori de compilare-
în
~/lab07/project, creațifast.c:printf '%s\n' 'int sum(int n)' '{' ' return n * (n + 1) / 2;' '}' > ~/lab07/project/fast.c -
compilați
fast.cîn fișierul lui obiect; legațiappdinmain.o,lib.o,fast.oșiprint.o; -
legați
appcufast.oîn loc delib.oși rulați-l; -
încercați să faceți un program doar din
lib.o; -
ștergeți
fast.cșifast.oși rulațimake -B.
Indiciu
Pentru fiecare simbol
U, linkerul are nevoie de exact unT.Verificare
fast.c:(.text+0x0): multiple definition of `sum', apoi locul primei definiții, înlib.o, șicollect2: error: ld returned 1 exit status.appafișeazăsum = 55.undefined reference to `main'.git status --shortnu afișează nimic.
De ce: linkerul are nevoie de exact o definiție pentru fiecare nume, iar fiecare program are nevoie de exact un
main. -
Biblioteci
-
Ce biblioteci?: Înainte să copiați
apppe calculatorul unui prieten, aflați de ce biblioteci are nevoie și comparați cu alte programe. → Biblioteci- listați bibliotecile lui
app, ale lui/usr/bin/lsși ale lui/usr/bin/python3; - găsiți bibliotecile de care au nevoie toate trei;
- rulați de două ori
ldd appși comparați numerele din paranteze.
Indiciu
Biblioteci;
man ldd.Verificare
- Bibliotecile:
app:linux-vdso.so.1,libc.so.6și/lib64/ld-linux-x86-64.so.2, loaderul;ls: câteva în plus, printre carelibselinux.so.1;python3:libm.so.6, iar pe Fedora șilibpython3...so.
libc.so.6și loaderul.- Adresele se schimbă la fiecare rulare.
De ce: sistemul încarcă bibliotecile la adrese aleatoare, ca să îngreuneze atacurile.
- listați bibliotecile lui
-
În interiorul executabilului static: Înainte să-i trimiteți
hello-staticunui prieten cu un sistem vechi, aflați ce a pus-staticîn el și de unde provine dimensiunea lui. → Biblioteci- în
~/lab07, construiți două programe dinhello.c:hello-dynamic, cu opțiunile implicite, șihello-static, cu biblioteca C copiată în el; rulați-le pe amândouă; - afișați tipul amândurora; numărați simbolurile fiecăruia și găsiți
printfprintre ele; - comparați dimensiunea lui
hello-staticcu cea a bibliotecii C,/lib64/libc.so.6(Ubuntu:/lib/x86_64-linux-gnu/libc.so.6); - faceți câte o copie a celor două programe, fără simboluri, și comparați dimensiunile copiilor.
Indiciu
Biblioteci (pe Fedora, instalați mai întâi
glibc-static);nm ... | wc -l;stat -L -c '%s'urmează o legătură simbolică;man strip: 🔍 căutați-o.Verificare
- Ambele programe afișează aceleași 3 linii.
filespunedynamically linked, respectivstatically linked;hello-dynamicare câteva zeci de simboluri, printre careU printf@GLIBC_...;hello-staticare peste o mie, printre careT printf.- Biblioteca C are peste 1 MiB, mai mult decât
hello-static. - Ambele copii sunt mai mici, dar
hello-staticare tot câteva sute de KiB.
De ce: linkerul a copiat în
hello-staticdoar părțile din biblioteca C pe care le folosește programul; cea mai mare parte a dimensiunii vine din acest cod, nu din nume. - în
-
Propria bibliotecă statică: Puneți
sumîntr-o bibliotecă statică,libsum.a, legați un program cu ea și aflați ce se întâmplă când se schimbă biblioteca. → Biblioteci · Salvarea versiunilor- în
~/lab07/project, creațilibsum.adinlib.oși listați-i conținutul; - legați
app2dinmain.o,print.oși bibliotecă; rulați-l; găsițisumprintre simbolurile luiapp2; - în
lib.c, schimbațis = s + i;îns = s + 2 * i;; reconstruițilib.oșilibsum.ași rulațiapp2; legați din nouapp2și rulați-l; - readuceți ultima versiune salvată în commit a lui
lib.c.
Indiciu
Tabelul din Biblioteci;
nm app2 | grep sum.Verificare
ar t libsum.aafișeazălib.o.app2afișeazăsum = 55;nm app2aratăT sum.app2afișează totsum = 55; după o nouă legare,sum = 110.lib.cconține din nous = s + i;.
De ce: codul lui
suma fost copiat înapp2în momentul legării. - în
-
Propria bibliotecă partajată: Faceți același lucru cu o bibliotecă partajată,
libsum.so, și comparați. → Biblioteci- compilați
lib.cînlib.opentru o bibliotecă partajată și creațilibsum.so; - legați
app3dinmain.o,print.oși bibliotecă; listați-i bibliotecile; rulați-l; - rulați-l din nou, astfel încât loaderul să găsească
libsum.soîn directorul curent; - modificați
lib.cca în exercițiul 21; reconstruițilib.o(cu-fPIC, ca la pasul 1) șilibsum.so, dar nu șiapp3; rulațiapp3; - readuceți
lib.c; ștergețilibsum.a,libsum.so,app2șiapp3și rulațimake -B.
Indiciu
Tabelul din Biblioteci;
-lsumalegelibsum.soînaintea luilibsum.a.Verificare
file libsum.sospuneELF 64-bit LSB shared object.ldd app3aratălibsum.so => not found;app3eșuează cuerror while loading shared libraries: libsum.so: cannot open shared object file: No such file or directory.- Cu
LD_LIBRARY_PATH=.,app3afișeazăsum = 55. app3afișeazăsum = 110, fără o nouă legare;nm app3aratăU sum.git status --shortnu afișează nimic.
De ce:
app3doar spune că are nevoie delibsum.so; loaderul o găsește, în cea mai nouă versiune, la fiecare pornire. - compilați
Makefile-ul
-
Deciziile lui
make: Citiți cum decidemake, pas cu pas, după ce se modifică un header. → Cum decidemake- în
~/lab07/project, adăugați linia// the last numberînaintea liniei#define MAX 10dinlib.h; rulațimakeastfel încât să explice fiecare decizie; - găsiți prima țintă pe care o ia în considerare
makeși linia care spune de ce compilează din noumain.o; - găsiți linia care spune de ce nu compilează din nou
print.o.
Indiciu
Cum decide
make;make --debug=v | less. Indentarea arată la ce țintă lucreazămake.Verificare
make --debug=vafișează câte o linie pentru fiecare decizie.- Prima țintă este
all(Considering target file 'all'.), apoiapp, apoimain.o; urmeazăPrerequisite 'lib.h' is newer than target 'main.o'.șiMust remake target 'main.o'. No need to remake target 'print.o'.
- în
-
Doar timpii contează: Vreți o compilare optimizată, dar
make CFLAGS=-O2nu face nimic. Aflați de ce și construiți-o. → Variabile · Cum decidemake- în
~/lab07/project, rulațimake CFLAGS=-O2; - construiți din nou tot, cu
-O2; - construiți din nou tot, cu opțiunile din
Makefile, rulând până la 4 comenzi în același timp.
Indiciu
Ce compară
make?man make: 🔍 căutațialways-makeșijobs.Verificare
make: Nothing to be done for 'all'.make -B CFLAGS=-O2compilează fiecare fișier cugcc -O2.make -B -j4: cele trei compilări pot apărea în altă ordine, dar legarea este întotdeauna ultima.
De ce:
makecompară doar timpii de modificare și nu ține minte ce opțiuni a folosit. - în
-
Un modul nou: Programul trebuie să afișeze și media numerelor de la 1 la
MAX. Adăugați un modulavgși actualizațiMakefile-ul. → Dependențe de headere · Reguli cu șablon-
în
~/lab07/project, creați modulul:printf '%s\n' '#ifndef AVG_H' '#define AVG_H' 'double average(int n);' '#endif' > avg.hprintf '%s\n' '#include "avg.h"' '#include "lib.h"' '' 'double average(int n)' '{' \' return (double) sum(n) / n;' '}' > avg.c -
în
main.c, includeți<stdio.h>șiavg.hși adăugați liniaprintf("average = %.1f\n", average(MAX));după apelul luiprint_result; -
adăugați
avg.oînMakefile, iar headerele lui în regulile pentru headere; rulațimake run; -
adăugați un comentariu în
avg.h: mai întâi notați ce va compilamake, apoi rulați-l; faceți la fel culib.h; -
adăugați fișierele noi și salvați totul într-un commit cu mesajul
Add the average.
Indiciu
Dependențe de headere:
avg.cinclude două headere, iarmain.cinclude acum trei.Verificare
make runafișeazăsum = 55șiaverage = 5.5.- După modificarea lui
avg.h:main.cșiavg.c, apoi legarea; după modificarea luilib.h:main.c,lib.cșiavg.c, apoi legarea. git log --onelineare 3 commituri.
-
-
Dependențe automate: Într-un proiect cu 100 de fișiere, regulile pentru headere scrise de mână nu sunt niciodată la zi. Lăsați-l pe
gccsă le scrie. → Dependențe de headere- adăugați
-MMDînCFLAGS: când compileazăX.c,gccscrie șiX.d, cu regula luiX.oși toate headerele pe care le-a citit; - adăugați variabila
OBJS = main.o lib.o print.o avg.oși folosiți-o în regula luiapp; - înlocuiți regulile pentru headere cu linia
-include $(OBJS:.o=.d), la finalulMakefile-ului; faceți cacleansă șteargă și fișierele.dși adăugați*.dîn.gitignore; - rulați
make clean, apoimake; afișați cele patru fișiere.d; - adăugați încă un comentariu în
lib.hși rulațimake; salvați modificarea într-un commit cu mesajulAutomatic dependencies.
Indiciu
$(OBJS:.o=.d)înseamnămain.d lib.d print.d avg.d;-din fața luiincludeignoră fișierele care încă nu există. De ce la final? Gândiți-vă ce țintă construieștemakecând nu îi dați niciuna.Verificare
- Cele patru fișiere:
main.d:main.o: main.c lib.h print.h avg.hlib.d:lib.o: lib.c lib.hprint.d:print.o: print.c print.havg.d:avg.o: avg.c avg.h lib.h
main.c,lib.cșiavg.c, apoi legarea, ca în exercițiul 25.
De ce:
gccnotează fiecare header pe care l-a citit, cu excepția celor de sistem, precumstdio.h. - adăugați
-
Nu doar pentru C:
makeconstruiește orice fișier care se obține din alte fișiere prin comenzi. Construiți un raport dintr-o listă de note, cu filtrele din laboratorul 6. →Makefile-ul · Ținte false- creați
~/lab07/report/grades.csvcu liniileerin,9,bob,6,carol,8șidave,7; - în
~/lab07/report, scrieți unMakefileîn carereport.txtestegrades.csvsortat după notă, cu cea mai mare prima;best.txteste prima linie dinreport.txt;allconstruieștebest.txt, iarcleanle șterge pe amândouă; - rulați
makede două ori; afișațibest.txt; - schimbați nota lui
bobîn10și rulațimake; afișațibest.txt; rulațimake clean.
Indiciu
Sortarea:
sortdin laboratorul 6; o rețetă poate folosi orice comandă, cu>.Verificare
- Primul
makerulează comanda cusort, apoi pe cea cuhead; al doilea afișeazămake: Nothing to be done for 'all'.;best.txtconțineerin,9. - Ambele comenzi rulează din nou, iar
best.txtconținebob,10.
- creați
-
Instalați programul: Adăugați țintele
installșiuninstall, ca să puteți rulaappdin orice director. → Variabile · Ținte false- în
Makefile-ul dinproject, adăugați variabilaPREFIX = $(HOME)/.local; - adăugați ținta falsă
install: are nevoie deapp, creează directorul$(PREFIX)/binși copiazăappîn el; adăugațiuninstall, care șterge$(PREFIX)/bin/app; - rulați
make install; din directorul utilizatorului, rulațiappdupă nume și aflați unde se află; - instalați-l în schimb în
/tmp/try, fără să modificațiMakefile-ul; apoi rulațimake uninstall; - salvați modificarea într-un commit cu mesajul
Add install.
Indiciu
mkdir -p;which app. Pe Ubuntu,~/.local/bineste înPATHdoar dacă exista deja când v-ați autentificat.Verificare
appafișeazăsum = 55șiaverage = 5.5din orice director;which appafișează copia din~/.local/bin(Fedora o scrie~/.local/bin/app, Ubuntu/home/student/.local/bin/app).make install PREFIX=/tmp/tryîl copiază în/tmp/try/bin/app; dupămake uninstall,which appnu mai găsește nimic.git log --onelineare 5 commituri.
- în
Serverul de limbaj
-
Urmăriți conversația: Aflați unde rulează
clangd, cum vorbește editorul cu el și verificați un fișier așa cum o face editorul. → Cum funcționează un server de limbaj- deschideți
~/lab07/project/main.cîn editor; într-un al doilea terminal, găsiți procesulclangdși părintele lui; - aflați unde duc intrarea (
0) și ieșirea (1) luiclangd; - în
~/lab07/project, verificațimain.ccuclangd --checkși citiți ultima linie; - ieșiți din editor și căutați din nou procesul
clangd.
Indiciu
pgrep,ps -o pid,ppid,commșipstree -pdin laboratorul 3;readlink /proc/<PID>/fd/0, ca în Cum rulează un pipeline din laboratorul 6.Verificare
pstree -pafișează o linie cahx(4120)───clangd(4131)(saunvim,zed): editorul este părintele./proc/<PID>/fd/0și/proc/<PID>/fd/1sunt amândouăpipe:[...]; unele editoare, precumnvimși VS Code, folosescsocket:[...], un fel de pipe care funcționează în ambele sensuri.- Ultima linie se termină cu
All checks completed, 0 errors. clangda dispărut.
De ce: editorul pornește
clangd, îi scrie mesajele la intrare, citește răspunsurile de la ieșire și îl oprește când iese. - deschideți
-
Spuneți-i lui
clangdcum compilați:clangdcunoaște doar fișierele pe care le deschideți. Dați-i comenzile proiectului, apoi redenumiți o funcție în toate fișierele cu o singură comandă din editor. → Ce trebuie să știeclangd- instalați
bear; în~/lab07/project, construiți tot de la zero cubearși uitați-vă încompile_commands.json; - deschideți
main.cși listați toate locurile în care se foloseșteprint_result, în toate fișierele; - redenumiți
print_resultînshow_result, cu serverul de limbaj; salvați toate fișierele; - găsiți toate aparițiile lui
show_resultcugrep, apoi construiți și rulați programul; - readuceți fișierele din ultimul commit cu
git, ștergețicompile_commands.jsonși rulațimake.
Indiciu
Tabelul din Ce trebuie să știe
clangd;grep -n show_result *.c *.h;git restore .readuce toate fișierele modificate.Verificare
compile_commands.jsonare câte o intrare pentrumain.c,lib.c,print.cșiavg.c, cu directorul și comandagcca fiecăruia.print_resulteste folosită înprint.h,print.cșimain.c.- Cele trei fișiere conțin acum
show_result. grepgăseșteshow_resultîn aceleași trei fișiere;make runafișeazăsum = 55șiaverage = 5.5.git status --shortnu afișează nimic.
De ce: cu
compile_commands.json,clangdcunoaște toate fișierele proiectului, nu doar pe cele deschise. - instalați
Depanarea
-
Un crash: Un program numără vocalele din câteva cuvinte și se blochează cu o eroare. Găsiți linia și cauza cu
gdb. → Depanarea-
creați
crash.cîn~/lab07:cat > ~/lab07/crash.c << 'END'#include <stdio.h>int count_vowels(const char *word){int count = 0;for (int i = 0; word[i] != '\0'; i++) {if (word[i] == 'a' || word[i] == 'e' || word[i] == 'i' || word[i] == 'o' || word[i] == 'u') {count++;}}return count;}int main(void){const char *words[] = { "banana", "kiwi", NULL, "apple" };for (int i = 0; i < 4; i++) {printf("%d\n", count_vowels(words[i]));}return 0;}END -
compilați-l cu informații de depanare și cu avertismentele activate; rulați-l și afișați-i codul de ieșire;
-
rulați-l în
gdbpână când se oprește cu eroare; afișați stiva de apeluri și valoarea luiword; -
treceți în
mainși afișații; -
corectați
main, astfel încât să sară peste cuvinteleNULL.
Indiciu
Tabelul din Depanarea;
128 +numărul unui semnal: Coduri de ieșire din laboratorul 6.Verificare
3și2, apoiSegmentation fault; codul de ieșire este139, adică128 + 11, pentru semnalulSIGSEGV.Program received signal SIGSEGV, Segmentation fault., încount_vowels (word=0x0) at crash.c:6;wordeste0x0, adică pointerulNULL.- În
main,ieste2: al treilea cuvânt. - Programul afișează
3,2și2.
-
-
Urmăriți o variabilă: Aflați valoarea lui
sdinsumcândieste7, apoi urmăriți cum se schimbăs, fără să adăugați niciunprintf. → Depanarea- porniți
gdbcuappîn~/lab07/project; opriți-vă la linias = s + i;dinlib.c(linia 8, după comentariul din exercițiul 14), dar doar cândieste7; rulați; afișațis; - ștergeți breakpointul și opriți-vă de fiecare dată când se schimbă
s; continuați de trei ori și citiți de fiecare dată valoarea veche și pe cea nouă a luis; - ieșiți.
Indiciu
Tabelul din Depanarea;
help deleteîngdb.Verificare
- Programul se oprește cu
iegal cu7;seste21, adică suma1 + 2 + ... + 6. Old value = 21șiNew value = 28; apoi28și36; apoi36și45.
- porniți
Versiuni
-
Anulați o greșeală: Ați șters din greșeală o linie din
lib.c, ați salvat și ați mai șters șiprint.h. Recuperați-le pe amândouă cugit. → Salvarea versiunilor- în
~/lab07/project, ștergeți liniareturn s;dinlib.ccu editorul și salvați; rulațimake; - ștergeți
print.h; afișați starea repository-ului și modificarea dinlib.c; - readuceți
lib.cșiprint.hdin ultimul commit; rulațimake run.
Indiciu
Tabelul din Salvarea versiunilor.
Verificare
makeafișează avertismentulcontrol reaches end of non-void function.git status --shortaratăM lib.c(modified) șiD print.h(deleted);git diffarată- return s;.- După
git restore lib.c print.h, starea este goală;make runafișeazăsum = 55șiaverage = 5.5.
- în
-
O ramură: Vreți să încercați alt mesaj,
sum: 55, fără să atingeți versiunea care funcționează. Încercați-l pe o ramură, apoi aduceți-l înmain. → Salvarea versiunilor- creați ramura
colonși treceți pe ea; - modificați
print.cca să afișezesum: 55; faceți un commit; rulațimake run; - întoarceți-vă pe
mainși rulațimake run; - uniți
colonînmain; rulațimake run; afișați istoricul.
Indiciu
Tabelul din Salvarea versiunilor;
git branchlistează ramurile.Verificare
git brancharată* colonșimain.make runafișeazăsum: 55.make runcompilează din nouprint.cși afișeazăsum = 55.git merge colonafișeazăFast-forward;make runafișeazăsum: 55; istoricul are 6 commituri.
De ce:
git switcha modificatprint.cpe disc, așa cămakea văzut un fișier mai nou. - creați ramura
-
Proiectul vostru pe GitHub: Vreți ca
projectsă fie pe un server, ca să lucrați la el și din laborator, și de acasă. Puneți-l pe GitHub și aduceți-l înapoi pe un „al doilea calculator”. → Lucrul cu GitHub- creați-vă un cont GitHub, dacă nu aveți deja; creați o cheie SSH și dați-i-o lui GitHub; spuneți-i lui
gitsă facă merge lagit pull; - pe GitHub, creați un repository privat și gol, numit
uso-lab07: fără README, fără.gitignore, fără licență; - legați
~/lab07/projectde el și trimitețimain; - pe pagina web a repository-ului, uitați-vă la fișiere și la commituri;
- clonați repository-ul în
~/lab07/clone, „laptopul” vostru; construiți-l acolo și rulați-l.
Indiciu
Comenzile din Lucrul cu GitHub; pagina repository-ului gol le listează și ea.
Verificare
ssh -T git@github.comvă salută cu numele de utilizator.- Pagina repository-ului gol arată comenzile pentru a trimite un repository existent.
git remote -varatăoriginde două ori,(fetch)și(push), cugit@github.com:<USER>/uso-lab07.git;git push -u origin mainafișează* [new branch] main -> mainși se termină cubranch 'main' set up to track 'origin/main'.- GitHub arată 6 commituri și sursele, dar niciun fișier obiect și nici
app. - În
clone,make runcompilează fiecare fișier și afișeazăsum: 55șiaverage = 5.5.
De ce: fișierele obiect nu sunt în repository: fiecare calculator și le construiește singur.
- creați-vă un cont GitHub, dacă nu aveți deja; creați o cheie SSH și dați-i-o lui GitHub; spuneți-i lui
-
Două calculatoare, un repository: Lucrați la
projectdin două locuri:~/lab07/projecteste calculatorul din laborator, iar~/lab07/clonelaptopul vostru. Trimiteți modificări între ele prin GitHub, pe a doua printr-un pull request. → Lucrul cu GitHub- în
clone, creațiREADME.mdcu două linii,# The sum programșiBuild it with make, run it with make run.; faceți un commit cu mesajulAdd the READMEși trimiteți-l; - în
project, aduceți noul commit; - în
project, creați ramuramax-20; schimbațiMAXîn20, faceți un commit cu mesajulSum up to 20și trimiteți ramura; pe GitHub, deschideți un pull request dinmax-20înmain, uitați-vă la Files changed și uniți-l; - în
clone, aduceți merge-ul și rulațimake run; înproject, întoarceți-vă pemainși aduceți-l și acolo; - ștergeți ramura
max-20, înprojectși pe GitHub.
Indiciu
Tabelul din Lucrul cu GitHub; o ramură care nu a fost unită local are nevoie de
git branch -D.Verificare
git pushtrimite commitulAdd the README.- În
project,README.mdexistă, iarAdd the READMEeste prima linie dingit log --oneline. - Files changed arată o linie din
lib.h:-#define MAX 10și+#define MAX 20. - În
clone,make runcompilează din noumain.c,lib.cșiavg.cși afișeazăsum: 210șiaverage = 10.5. git brancharată doarmain;git ls-remote --heads originarată doarrefs/heads/main.
- în
Curățenie
-
Curățenie: Lăsați directorul utilizatorului așa cum era înainte de laborator. → Ținte false
- ștergeți
~/lab07; - ștergeți
~/.local/bin/appși/tmp/try, dacă mai există; - păstrați repository-ul
uso-lab07de pe GitHub sau ștergeți-l: Settings, în partea de jos a paginii.
Verificare
ls ~nu mai aratălab07.which appnu găsește nimic, iarls /tmp/tryspuneNo such file or directory.- Setările
gitși cheia SSH rămân: le folosesc laboratoarele următoare.
- ștergeți
📝 Provocări
Aceste exerciții seamănă cu cele de la examen. Fiecare vă dă un rezultat de obținut, dar nu și pașii: comenzile le alegeți voi. Fiecare se termină cu Verificarea după care va fi notat. Unele au nevoie de Biblioteci.
Toate provocările pornesc de la aceeași configurație, cea pe care o veți găsi la examen: directorul ~/exam, cu câte
un director pentru fiecare provocare. shapes este un program cu două module, area și perimeter; ambele headere
includ shapes.h. Copiați și rulați aceste comenzi înainte de prima provocare; rulați-le din nou ca să recuperați
fișierele originale:
mkdir -p ~/exam/shapes ~/exam/predict ~/exam/broken/include ~/exam/steps ~/exam/debug
cd ~/exam/shapes
printf '%s\n' '#ifndef SHAPES_H' '#define SHAPES_H' 'struct rect {' ' int width;' ' int height;' '};' \
'#endif' > shapes.h
printf '%s\n' '#ifndef AREA_H' '#define AREA_H' '#include "shapes.h"' 'int area(struct rect r);' '#endif' > area.h
printf '%s\n' '#include "area.h"' '' 'int area(struct rect r)' '{' ' return r.width * r.height;' '}' > area.c
printf '%s\n' '#ifndef PERIMETER_H' '#define PERIMETER_H' '#include "shapes.h"' 'int perimeter(struct rect r);' \
'#endif' > perimeter.h
printf '%s\n' '#include "perimeter.h"' '' 'int perimeter(struct rect r)' '{' \
' return 2 * (r.width + r.height);' '}' > perimeter.c
cat > main.c << 'END'
#include <stdio.h>
#include "area.h"
#include "perimeter.h"
int main(void)
{
struct rect r = { 3, 4 };
printf("area = %d\n", area(r));
printf("perimeter = %d\n", perimeter(r));
return 0;
}
END
cd ~/exam/predict
touch main.c util.c util.h
printf 'prog: main.o util.o\n\ttouch prog\n\nmain.o: main.c util.h\n\ttouch main.o\n\n' > Makefile
printf 'util.o: util.c util.h\n\ttouch util.o\n' >> Makefile
cd ~/exam/broken
printf '%s\n' '#ifndef STATS_H' '#define STATS_H' 'double mean(const double *values, int n);' \
'double stddev(const double *values, int n);' '#endif' > include/stats.h
cat > stats.c << 'END'
#include <math.h>
#include "stats.h"
double mean(const double *values, int n)
{
double s = 0;
for (int i = 0; i < n; i++) {
s = s + values[i];
}
return s / n;
}
double stddev(const double *values, int n)
{
double m = mean(values, n);
double s = 0;
for (int i = 0; i < n; i++) {
s = s + (values[i] - m) * (values[i] - m);
}
return sqrt(s / n);
}
END
cat > main.c << 'END'
#include <stdio.h>
#include "stats.h"
int main(void)
{
double grades[] = { 4, 8, 6, 10, 7 };
printf("mean = %.2f\n", mean(grades, 5));
printf("stddev = %.2f\n", stddev(grades, 5));
return 0;
}
END
printf 'CC = gcc\nCFLAGS = -Wall -g\n\nstats: main.o\n\t$(CC) $^ -o $@\n\n%%.o: %%.c\n\t$(CC) $(CFLAGS) -c $< -o $@\n' \
> Makefile
cd ~/exam/steps
cat > power.c << 'END'
#include <stdio.h>
#include <math.h>
int square(int x)
{
return x * x;
}
int main(void)
{
for (int i = 1; i <= 3; i++) {
printf("%d %d %.0f\n", i, square(i), pow(2, i));
}
return 0;
}
END
cd ~/exam/debug
cat > collatz.c << 'END'
#include <stdio.h>
int collatz(int n)
{
int steps = 0;
while (n != 1) {
if (n % 2 == 0) {
n = n / 2;
} else {
n = 3 * n + 1;
}
steps++;
}
return steps;
}
int main(void)
{
int total = 0;
for (int i = 1; i <= 10; i++) {
total = total + collatz(i);
}
printf("total = %d\n", total);
return 0;
}
END
cd ~/exam
Provocările 44 și 45 au nevoie și de un cont GitHub și de o cheie SSH, ca în exercițiul 35. După ce
terminați toate provocările, ștergeți tot cu rm -rf ~/exam și ștergeți de pe GitHub repository-urile
uso-exam-shapes și uso-exam-conflict.
-
Makefile-ul pentrushapes: Scrieți~/exam/shapes/Makefile, astfel încâtmakesă construiască programulshapesși să reconstruiască exact ce este necesar după orice modificare. → UnMakefilepentru un proiect · Ținte false · Dependențe de headereMakefile-ul trebuie:- să folosească variabilele
CC = gccșiCFLAGS = -Wall -gși o singură regulă cu șablon pentru toate fișierele obiect; - să lege
shapesdinmain.o area.o perimeter.o, în această ordine, cu variabile automate; - să aibă țintele false
all(prima),runșiclean(care șterge doar fișierele obiect șishapes); - să aibă dependențele de headere corecte: urmăriți fiecare
#include, inclusiv pe cele din headere.
Verificare
în
~/exam/shapes, fiecare dintre aceste linii afișează ce este scris lângă ea:Comandă Afișează makegcc -Wall -g -c main.c -o main.o, la fel pentruareașiperimeter, apoigcc main.o area.o perimeter.o -o shapesmake run./shapes,area = 12,perimeter = 14touch shapes.h; makecele 3 comenzi de compilare, apoi legarea touch perimeter.h; makecomenzile de compilare pentru main.cșiperimeter.c, apoi legareatouch area.c; makecomanda de compilare pentru area.c, apoi legareatouch clean; make cleanun singur rm -fcare șterge fișierele obiect șishapes, de exemplurm -f *.o shapes; apoirm clean - să folosească variabilele
-
Ghiciți ce face
make:~/exam/predictconține unMakefileale cărui rețete doar dautouchțintelor, astfel încât rezultatul arată ce construieștemake. Prevedeți ce afișează fiecare linie, apoi demonstrați. → Cum decidemakeMai întâi completați tabelul pe hârtie; apoi rulați liniile, în această ordine, în
~/exam/predict:Linie Afișează 1. make2. make3. touch util.h; make4. touch main.c; make5. rm util.o; make6. make util.o7. touch main.o; make8. rm main.c; makeVerificare
Afișează 1. touch main.o,touch util.o,touch prog2. make: 'prog' is up to date.3. touch main.o,touch util.o,touch prog: ambele depind deutil.h4. touch main.o,touch prog5. touch util.o,touch prog6. make: 'util.o' is up to date.7. touch prog:main.oeste mai nou decâtprog8. make: *** No rule to make target 'main.c', needed by 'main.o'. Stop. -
Reparați compilarea:
makeeșuează în~/exam/broken. Reparați-l fără să modificați fișierele.cși.hși faceți ca și editorul să găsească headerele. → Erori de compilare · Dependențe de headere · Ce trebuie să știeclangdMakefile-ul trebuie să construiascăstatsdinmain.cșistats.c;stats.hse află în directorulinclude, iarstats.cfoloseștesqrt. O modificare îninclude/stats.htrebuie să recompileze ambele fișiere obiect. Și editorul trebuie să găseascăstats.h: adăugați uncompile_flags.txtpentruclangd.Verificare
-
makeconstruieștestatsfără nicio eroare și fără niciun avertisment, iar./statsafișează exact:mean = 7.00stddev = 2.00 -
după
touch include/stats.h,makecompileazămain.cșistats.c, apoi leagă programul; -
clangd --check=main.cse termină cuAll checks completed, 0 errors(fărăcompile_flags.txt, raportează'stats.h' file not found); -
fișierele
.cși.hnu s-au schimbat.
-
-
Opriți-vă după fiecare etapă: În
~/exam/steps, construițipowerdinpower.c, câte o etapă pe rând, și uitați-vă în fișierele produse de fiecare etapă. → Etapele compilării · Erori de compilare- scrieți
power.i,power.sșipower.o, fiecare cu propria comandăgcc, apoi legațipower; - listați simbolurile lui
power.oși bibliotecile luipower.
Verificare
-
lsaratăpower,power.c,power.i,power.oșipower.s; -
nm power.oafișează exact aceste patru simboluri (adresele pot diferi):0000000000000018 T mainU powU printf0000000000000000 T square -
ldd powerlisteazălibm.so.6șilibc.so.6; -
./powerafișează1 1 2,2 4 4și3 9 8.
- scrieți
-
Biblioteca pentru
shapes: Construiți moduleleareașiperimeterdinshapesca bibliotecă statică și ca bibliotecă partajată, într-un director nou,~/exam/lib, și legați câte un program cu fiecare dintre ele. → Biblioteci- copiați fișierele
.cși.hdin~/exam/shapesîn~/exam/lib; - construiți
libshapes.ași legațishapes-staticcu ea; - construiți
libshapes.soși legațishapes-sharedcu ea.
Verificare
în
~/exam/lib:Comandă Afișează ar t libshapes.aarea.oșiperimeter.onm shapes-static | grep ' area$'o linie cu T areanm shapes-shared | grep ' area$'U arealdd shapes-shared | grep shapeslibshapes.so => not foundLD_LIBRARY_PATH=. ./shapes-sharedarea = 12,perimeter = 14./shapes-staticarea = 12,perimeter = 14 - copiați fișierele
-
Depanați cu
gdb:~/exam/debug/collatz.cadună numărul de pași de care are nevoie fiecare număr de la 1 la 10 ca să ajungă la 1 (dacă este par, se împarte la 2; dacă este impar, se triplează și se adună 1). Aflați aceste valori doar cugdb, fără să modificați codul. → Depanarea- valoarea lui
totaldinmaincândcollatzeste apelată cunegal cu7; - valoarea întoarsă de
collatzpentrunegal cu9; - stiva de apeluri când
collatzeste apelată cunegal cu7.
Verificare
totaleste23.finishafișeazăValue returned is ... = 19.- Două linii:
#0 collatz (n=7) at collatz.c:5, apoi#1 ... in main () at collatz.c:21.
./collatzafișeazătotal = 67. - valoarea lui
-
shapespe GitHub: Transformați~/exam/shapesîntr-un repositorygit, publicați-l pe GitHub și adăugați un README printr-un pull request. → Salvarea versiunilor · Lucrul cu GitHub- primul commit,
Add the sources, conține toate fișierele din~/exam/shapes, cu excepția celor construite demake; - al doilea commit,
Ignore the build files, adaugă un.gitignorecare ignoră fișierele obiect șishapes; maineste pe GitHub, într-un repository privat nou,uso-exam-shapes;- commitul
Add the README, cu unREADME.mdcare conține o singură linie,# Shapes, ajunge înmainprintr-un pull request din ramurareadme, unit pe GitHub cu un commit de merge; apoi ramurareadmedispare, atât de pe GitHub, cât și local, iarmainlocal este la zi.
Verificare
În
~/exam/shapes:Comandă Afișează git log --oneline4 linii: Merge pull request #1 from <USER>/readme,Add the README,Ignore the build files,Add the sourcesgit ls-files.gitignore,README.md,area.c,area.h,main.c,perimeter.c,perimeter.h,shapes.h(șiMakefile, dacă ați făcut provocarea 38)git branch* maingit ls-remote --heads origino singură linie, care se termină cu refs/heads/maingit statusYour branch is up to date with 'origin/main'.; dupămake, tot nu este nimic de salvat în commit - primul commit,
-
Un conflict: Două persoane,
carolșibob, modifică în același timp aceeași linie din același fișier. Jucați ambele roluri, cu două clone ale aceluiași repository GitHub, și uniți munca lor. → Lucrul cu GitHub- pe GitHub, un repository nou, gol și privat,
uso-exam-conflict, clonat de două ori:~/exam/carolși~/exam/bob; carolsalvează în commitulAdd the greetingfișierulgreeting.txt, cu o singură linie,hello, și îl trimite;bobîl aduce;- apoi
carolschimbă linia înhello, world, face commitulGreet the worldși îl trimite;bob, fără să aducă mai întâi modificările, schimbă aceeași linie înhiși face commitulSay hi; bobaduce commitul luicarol, rezolvă conflictul astfel încât linia să fiehi, worldși trimite rezultatul;caroladuce tot.
Verificare
Atât în
~/exam/carol, cât și în~/exam/bob:Comandă Afișează cat greeting.txthi, worldși nimic altcevagit log --oneline | wc -l4: cele trei commituri și commitul de merge, cel mai nougit log --merges --oneline | wc -l1grep -c '^<<<<<<<' greeting.txt0git statusYour branch is up to date with 'origin/main'. - pe GitHub, un repository nou, gol și privat,
Întrebări de recapitulare
Folosiți ultimele 5 minute ale laboratorului ca să răspundeți la aceste întrebări împreună cu colegii și cu asistentul. La ultimele două nu există răspunsuri greșite.
- Care este diferența dintre un program compilat și unul interpretat? Ce fel de program este
helloși ce fel estehello.sh? - Care sunt cele patru etape ale lui
gccși ce fișier rezultă din fiecare? - De ce este
printfun simbolUînhello.o? Cine îl găsește și unde? - Ce se pune într-un fișier
.hși ce într-un fișier.c? De ce compilați fiecare fișier.cîn propriul fișier obiect? - Cum vă dați seama, dintr-o eroare de compilare, ce etapă a raportat-o?
- Ce face un server de limbaj și cum comunică editorul cu el?
- Cum decide
makedacă rulează o rețetă? De ce are nevoiecleande.PHONYși pe ce regulă se pune un header? - Care este diferența dintre
git addșigit commit? De ce nu sunt fișierele obiect în repository? - Ce eroare de compilare v-a surprins cel mai mult astăzi?
- Ce unealtă din acest laborator veți folosi la următoarea temă în C?
Extra
- Un debugger în editor: așa cum LSP-ul aduce
clangdîn editor, DAP (Debug Adapter Protocol) aduce debuggerul: breakpointuri și variabile chiar lângă cod. Zed și VS Code îl au inclus;nvimfolosește pluginulnvim-dap. Cu amândouă, orice editor devine un IDE (Integrated Development Environment). → 07. Dezvoltarea aplicațiilor clang: un alt compilator de C, din proiectul LLVM, cu propriile mesaje. Instalați-l cusudo dnf install clang(Ubuntu:sudo apt install clang) și construiți proiectul cumake -B CC=clang. →man clang- Compiler Explorer: godbolt.org afișează codul în limbaj de asamblare al unei funcții C
în timp ce o scrieți, pentru multe compilatoare și procesoare, și dă fiecărei linii de C și instrucțiunilor ei
aceeași culoare. Încercați
-O2pesum. → Compilatorul - CMake și Meson: proiectele mari nu își scriu
Makefile-ul de mână; o unealtă îl generează dintr-o descriere mai scurtă a proiectului. → cmake.org, mesonbuild.com