C - czytanie, kompilacja i debugowanie
C nadal pojawia się w systemach operacyjnych, bibliotekach, sterownikach, narzędziach CLI i projektach embedded. Ten materiał służy przede wszystkim do czytania istniejącego kodu, budowania projektu i rozumienia błędów kompilatora, linkera oraz pamięci.
Powiązane tematy: Testowanie oprogramowania, Programowanie w shellu oraz Linux permissions i bezpieczeństwo serwera.
Cel: nie nauczyć Cię „programować w C od zera”, tylko dać Ci taki poziom orientacji, żebyś po sklonowaniu projektu potrafił:
- rozpoznać strukturę kodu,
- zrozumieć podstawowe konstrukcje języka,
- wiedzieć, gdzie program startuje i jak płynie wykonanie,
- odróżnić deklarację od definicji,
- zrozumieć pliki
.ci.h,- skompilować projekt,
- rozpoznać Makefile/CMake/Meson,
- znaleźć błędy kompilacji i linkowania,
- uruchomić debugger,
- użyć sanitizerów,
- znaleźć podstawowe problemy z pamięcią,
- zorientować się, z jakich bibliotek korzysta program.
1. C w jednym zdaniu
C jest małym, stosunkowo prostym językiem kompilowanym, który daje programiście bardzo bezpośredni dostęp do pamięci i systemu operacyjnego.
Nie ma w nim wielu rzeczy znanych z języków wyższego poziomu:
- klas,
- garbage collectora,
- wyjątków,
- modułów w stylu JS/Pythona,
- automatycznego zarządzania pamięcią,
- rozbudowanej biblioteki standardowej.
Za to bardzo łatwo zobaczyć, co komputer faktycznie robi.
Typowy przepływ:
kod .c
↓
preprocesor
↓
kompilator
↓
kod obiektowy .o
↓
linker
↓
program wykonywalny
2. Najprostszy program
#include <stdio.h>
int main(void)
{
printf("Hello, world!\n");
return 0;
}
Najważniejsze elementy:
#include <stdio.h>
dołącza deklaracje funkcji standardowego wejścia/wyjścia.
int main(void)
to punkt wejścia programu.
printf(...)
wywołuje funkcję.
return 0;
oznacza poprawne zakończenie programu.
Kompilacja:
cc hello.c -o hello
Uruchomienie:
./hello
Na Debianie cc zwykle wskazuje GCC lub Clang.
Sprawdzenie:
cc --version
Możesz też użyć wprost:
gcc hello.c -o hello
lub:
clang hello.c -o hello
3. Podstawowe rozszerzenia plików
Najczęściej spotkasz:
main.c
server.c
parser.c
database.c
utils.c
Kod źródłowy C.
Nagłówki:
server.h
parser.h
database.h
utils.h
Pliki nagłówkowe.
Kod pośredni:
server.o
parser.o
Pliki obiektowe.
Biblioteki:
libfoo.a
libfoo.so
Linux:
.a biblioteka statyczna
.so biblioteka współdzielona
FreeBSD również używa .a i .so.
4. Z czego składa się typowy projekt
Przykład:
myproject/
├── README.md
├── LICENSE
├── Makefile
├── CMakeLists.txt
├── include/
│ ├── server.h
│ └── parser.h
├── src/
│ ├── main.c
│ ├── server.c
│ └── parser.c
├── tests/
│ └── parser_test.c
└── build/
Nie każdy projekt będzie miał wszystko.
Najważniejsze miejsca:
README.md
zacznij od niego.
src/
kod programu.
include/
publiczne nagłówki.
tests/
testy.
Makefile
instrukcje dla make.
CMakeLists.txt
konfiguracja CMake.
meson.build
projekt wykorzystuje Meson.
configure
często projekt oparty na Autotools.
5. Najważniejsza rzecz: main()
W zwykłym programie wykonywalnym szukasz:
int main(...)
Najprościej:
grep -R "int main" .
albo:
rg "main\s*\("
jeżeli masz ripgrep.
Typowa wersja:
int main(int argc, char **argv)
{
...
}
Znaczenie:
argc
liczba argumentów.
argv
tablica argumentów tekstowych.
Dla:
./app --port 8080
możesz mieć:
argc = 3
argv[0] = "./app"
argv[1] = "--port"
argv[2] = "8080"
6. Zmienne
Przykłady:
int age = 46;
float temperature = 21.5f;
double pi = 3.1415926535;
char letter = 'A';
Najczęstsze typy:
char
short
int
long
long long
float
double
Wartości całkowite bez znaku:
unsigned int count;
Typy o określonej szerokości znajdziesz w:
#include <stdint.h>
Przykłady:
uint8_t
uint16_t
uint32_t
uint64_t
int8_t
int16_t
int32_t
int64_t
W kodzie systemowym są bardzo popularne.
7. const
const int max_users = 100;
oznacza:
tego obiektu nie powinno się zmieniać przez tę nazwę.
Przy wskaźnikach sytuacja robi się ciekawsza:
const char *name;
dane tekstowe są traktowane jako tylko do odczytu.
char *const name;
sam wskaźnik jest stały.
To ważne podczas czytania API bibliotek.
8. sizeof
Operator:
sizeof
zwraca rozmiar obiektu lub typu.
sizeof(int)
sizeof(buffer)
Przykład:
printf("%zu\n", sizeof(int));
sizeof jest bardzo często używany przy zarządzaniu pamięcią:
malloc(sizeof(struct user))
9. Operatory
Podstawowe:
+
-
*
/
%
Porównania:
==
!=
<
>
<=
>=
Logiczne:
&&
||
!
Przypisanie:
=
Ważne:
if (a == b)
porównanie.
a = b;
przypisanie.
Klasyczny błąd:
if (a = b)
To jest legalne C.
10. Instrukcja if
if (age >= 18)
{
printf("adult\n");
}
else
{
printf("minor\n");
}
Możesz spotkać:
if (!ptr)
oznacza:
jeżeli ptr jest NULL
lub ogólnie:
jeżeli wartość jest równa zero
11. switch
switch (command)
{
case 1:
start();
break;
case 2:
stop();
break;
default:
show_help();
break;
}
Brak break może oznaczać przejście do kolejnego case.
Czasami jest celowy.
12. Pętle
for
for (int i = 0; i < 10; i++)
{
printf("%d\n", i);
}
while
while (running)
{
process();
}
do while
do
{
read_input();
}
while (running);
nieskończona pętla
Bardzo częsta w serwerach i daemonach:
while (1)
{
...
}
albo:
for (;;)
{
...
}
13. Funkcje
Definicja:
int add(int a, int b)
{
return a + b;
}
Użycie:
int result = add(2, 3);
Typ zwracany:
int
Nazwa:
add
Argumenty:
int a, int b
14. Funkcja nic nie zwracająca
void log_message(const char *message)
{
printf("%s\n", message);
}
void oznacza brak wartości zwrotnej.
15. Deklaracja a definicja
Deklaracja mówi:
taka funkcja istnieje.
int add(int a, int b);
Definicja zawiera kod:
int add(int a, int b)
{
return a + b;
}
To rozróżnienie jest fundamentalne dla C.
16. Pliki .h
Nagłówek może zawierać:
#ifndef USER_H
#define USER_H
struct user
{
int id;
const char *name;
};
int user_create(struct user *u);
void user_destroy(struct user *u);
#endif
Plik .c:
#include "user.h"
int user_create(struct user *u)
{
...
}
void user_destroy(struct user *u)
{
...
}
Nagłówek mówi innym modułom:
takie typy i funkcje są dostępne.
17. #include
Biblioteka systemowa:
#include <stdio.h>
Własny nagłówek:
#include "server.h"
Umowna różnica:
<...> nagłówki systemowe / biblioteki
"..." pliki projektu
18. Include guards
Klasyczny wzorzec:
#ifndef SERVER_H
#define SERVER_H
...
#endif
Chroni przed wielokrotnym dołączeniem tego samego nagłówka.
Możesz też zobaczyć:
#pragma once
Jest powszechnie obsługiwane, ale nie należy do klasycznego standardu C.
19. Preprocesor
Przed właściwą kompilacją działa preprocesor.
Dyrektywy zaczynają się od:
#
Najczęstsze:
#include
#define
#if
#ifdef
#ifndef
#endif
Przykład:
#define MAX_USERS 100
Potem:
int users[MAX_USERS];
20. Warunkowa kompilacja
Bardzo częsta w projektach przenośnych.
#ifdef __linux__
...
#endif
albo:
#ifdef __FreeBSD__
...
#endif
Przykład:
#ifdef DEBUG
printf("x=%d\n", x);
#endif
Kompilacja:
cc -DDEBUG app.c -o app
Opcja -DDEBUG definiuje makro DEBUG.
21. Makra
Przykład:
#define SQUARE(x) ((x) * (x))
Użycie:
int y = SQUARE(4);
Makro nie jest funkcją.
Preprocesor po prostu manipuluje tekstem kodu.
Dlatego makra mogą być zdradliwe.
22. typedef
Alias typu:
typedef unsigned long ulong;
Częściej:
typedef struct user
{
int id;
char name[64];
} User;
Potem:
User u;
zamiast:
struct user u;
23. struct
Najważniejsza konstrukcja danych w C.
struct user
{
int id;
char name[64];
int active;
};
Tworzenie:
struct user u;
Dostęp:
u.id = 1;
u.active = 1;
24. Dostęp przez wskaźnik: ->
Jeżeli masz:
struct user *u;
to używasz:
u->id
u->name
To skrót od:
(*u).id
To jeden z najważniejszych symboli podczas czytania kodu C.
25. enum
Lista nazwanych wartości:
enum state
{
STATE_STOPPED,
STATE_RUNNING,
STATE_ERROR
};
Użycie:
enum state s = STATE_RUNNING;
Często stosowany do:
- statusów,
- typów komunikatów,
- rodzajów zdarzeń,
- kodów operacji.
26. union
Kilka interpretacji tego samego obszaru pamięci.
union value
{
int i;
float f;
};
W danej chwili pamięć zawiera jedną z reprezentacji.
Często spotkasz w:
- protokołach,
- parserach,
- sterownikach,
- kodzie embedded.
27. Tablice
int numbers[10];
Indeksy:
0..9
Przykład:
numbers[0] = 42;
C nie sprawdza granic tablicy.
To znaczy:
numbers[100] = 42;
może uszkodzić pamięć.
28. Tekst w C
Nie istnieje specjalny typ string.
Tekst to tablica char.
char name[] = "Anna";
W pamięci:
K a r o l \0
'\0' oznacza koniec tekstu.
29. Podstawowe funkcje tekstowe
Nagłówek:
#include <string.h>
Często spotkasz:
strlen()
strcmp()
strncmp()
strcpy()
strncpy()
memcpy()
memmove()
memset()
Przykład:
strlen(name)
długość tekstu.
strcmp(a, b)
porównanie.
Jeżeli:
strcmp(a, b) == 0
teksty są identyczne.
30. Wskaźniki - rzecz, której trzeba się nauczyć czytać
Wskaźnik przechowuje adres pamięci.
int value = 42;
int *ptr = &value;
&value
oznacza:
adres zmiennej
value.
ptr
zawiera ten adres.
*ptr
oznacza:
wartość znajdującą się pod tym adresem.
31. Przykład wskaźnika
int value = 42;
int *ptr = &value;
printf("%d\n", *ptr);
wynik:
42
Zmiana:
*ptr = 10;
spowoduje:
value == 10
32. Po co są wskaźniki
Między innymi:
- przekazywanie dużych struktur bez kopiowania,
- modyfikowanie argumentów funkcji,
- dynamiczna pamięć,
- tablice,
- struktury danych,
- API systemowe,
- callbacki,
- biblioteki.
33. NULL
Wskaźnik niewskazujący na obiekt.
char *ptr = NULL;
Sprawdzenie:
if (ptr == NULL)
częściej:
if (!ptr)
Jeżeli program spróbuje zrobić:
*ptr
gdy ptr == NULL, najczęściej zakończy się błędem typu:
Segmentation fault
34. Wskaźnik do wskaźnika
char **argv;
oznacza:
wskaźnik do wskaźnika do char
W praktyce:
char **argv
jest tablicą tekstów przekazywanych do programu.
35. Funkcje modyfikujące dane przez wskaźnik
void set_value(int *value)
{
*value = 42;
}
Wywołanie:
int x = 0;
set_value(&x);
Po funkcji:
x == 42
36. Dynamiczna pamięć
Nagłówek:
#include <stdlib.h>
Podstawowe funkcje:
malloc()
calloc()
realloc()
free()
Przykład:
int *numbers = malloc(100 * sizeof(int));
Po użyciu:
free(numbers);
37. Typowy wzorzec malloc
struct user *u = malloc(sizeof(*u));
if (u == NULL)
{
return -1;
}
To:
sizeof(*u)
jest często preferowane nad:
sizeof(struct user)
bo automatycznie dopasowuje się do typu zmiennej.
38. Memory leak
Jeżeli:
malloc(...)
rezerwuje pamięć, a program nie wykona:
free(...)
może powstać wyciek pamięci.
Przykład:
char *buffer = malloc(1024);
/* ... */
return 0;
bez:
free(buffer);
39. Use-after-free
Bardzo niebezpieczny błąd:
char *p = malloc(100);
free(p);
p[0] = 'A';
Program używa pamięci, która została już zwolniona.
Sanitizery świetnie wykrywają takie problemy.
40. Stack i heap
Uproszczenie:
stack
int x;
char buffer[1024];
lokalne zmienne.
Żyją zwykle do końca funkcji.
heap
malloc(...)
pamięć dynamiczna.
Żyje aż do:
free(...)
41. Przekazywanie struktur
Przez wartość:
void print_user(struct user u)
tworzy kopię.
Przez wskaźnik:
void print_user(const struct user *u)
bez kopiowania całej struktury.
To drugie jest bardzo częste.
42. Funkcyjne wskaźniki
Możesz spotkać coś takiego:
void (*callback)(int);
oznacza:
callbackjest wskaźnikiem do funkcji przyjmującejinti niczego nie zwracającej.
Używane m.in. w:
- callbackach,
- bibliotekach GUI,
- bibliotekach sieciowych,
- event loopach,
- systemach pluginów.
43. static
static ma kilka znaczeń.
W pliku .c:
static int helper(void)
{
...
}
oznacza zwykle:
funkcja jest prywatna dla tego pliku.
To bardzo przydatne podczas czytania projektu.
Jeżeli widzisz:
static void parse_header(...)
to funkcja nie jest publicznym API modułu.
44. extern
Deklaruje symbol znajdujący się gdzie indziej.
extern int global_count;
Definicja może być w innym pliku:
int global_count = 0;
45. Zmienne globalne
int debug_enabled = 0;
Globalne zmienne są dostępne poza funkcjami.
Duże projekty często ograniczają ich używanie.
46. Kod zwrotny funkcji
W C funkcje często sygnalizują błędy liczbą.
Przykład:
int result = connect_to_server();
if (result != 0)
{
fprintf(stderr, "connection failed\n");
}
Umownie często:
0 sukces
!= 0 błąd
ale API może stosować inne zasady.
Zawsze sprawdzaj dokumentację.
47. errno
Wiele funkcji systemowych ustawia:
errno
Nagłówki:
#include <errno.h>
#include <string.h>
Przykład:
if (open(...) == -1)
{
fprintf(stderr, "%s\n", strerror(errno));
}
Popularne:
perror("open");
48. Standardowe wejście i wyjście
Nagłówek:
#include <stdio.h>
Podstawowe strumienie:
stdin
stdout
stderr
Wypisywanie:
printf("hello\n");
Błędy:
fprintf(stderr, "error\n");
49. Formatowanie printf
Przykłady:
printf("%d", number);
printf("%u", unsigned_number);
printf("%ld", long_number);
printf("%f", floating);
printf("%s", text);
printf("%c", character);
printf("%p", pointer);
printf("%zu", size);
printf jest bardzo ważny przy czytaniu i prostym debugowaniu kodu.
50. Pliki
FILE *f = fopen("data.txt", "r");
if (!f)
{
perror("fopen");
return 1;
}
Potem:
fgets(...)
fprintf(...)
fread(...)
fwrite(...)
Zamykanie:
fclose(f);
51. Deskryptory plików
Kod systemowy często nie korzysta z FILE *, lecz z deskryptorów.
Przykład:
int fd = open(...);
fd to zwykła liczba.
Typowo:
0 stdin
1 stdout
2 stderr
Systemowe funkcje:
open()
read()
write()
close()
To POSIX, nie czysty standard C.
52. C vs POSIX
To bardzo ważne podczas czytania projektu.
Standard C dostarcza język i podstawową bibliotekę.
Linux, BSD i Unix dostarczają POSIX/API systemowe.
Przykłady POSIX:
fork()
exec()
pipe()
socket()
pthread_create()
open()
read()
write()
mmap()
Jeśli projekt działa głównie na Linux/FreeBSD, zobaczysz tego dużo.
53. Najważniejsze biblioteki standardowe
stdio.h
I/O:
printf
fprintf
fopen
fclose
fread
fwrite
stdlib.h
m.in.:
malloc
free
exit
atoi
strtol
qsort
string.h
strlen
strcmp
memcpy
memset
stdint.h
typy:
uint32_t
int64_t
stdbool.h
bool
true
false
ctype.h
znaki:
isdigit
isalpha
tolower
toupper
time.h
czas.
errno.h
obsługa błędów.
assert.h
asercje.
54. bool
Klasyczne C historycznie używało:
0 = false
niezero = true
Nowocześniejszy kod często:
#include <stdbool.h>
bool running = true;
55. Asercje
#include <assert.h>
assert(ptr != NULL);
Jeżeli warunek jest fałszywy, program zostaje przerwany.
Asercje służą głównie do sprawdzania założeń programisty.
56. Kompilacja jednego pliku
cc main.c -o app
57. Kompilacja kilku plików naraz
cc main.c server.c parser.c -o app
58. Kompilacja etapami
cc -c main.c
cc -c server.c
cc -c parser.c
Powstają:
main.o
server.o
parser.o
Linkowanie:
cc main.o server.o parser.o -o app
59. Co robi -c
cc -c server.c
oznacza:
skompiluj, ale jeszcze nie twórz końcowego programu.
Powstaje:
server.o
60. Najważniejsze flagi kompilatora
Dobry zestaw do czytania/debugowania:
cc \
-Wall \
-Wextra \
-Wpedantic \
-g \
-O0 \
main.c \
-o app
Znaczenie:
-Wall
włącza wiele ostrzeżeń.
-Wextra
więcej ostrzeżeń.
-Wpedantic
ostrzeżenia dotyczące zgodności ze standardem.
-g
informacje dla debuggera.
-O0
brak optymalizacji.
61. Standard języka
Możesz spotkać:
-std=c99
-std=c11
-std=c17
-std=c23
Przykład:
cc -std=c17 app.c -o app
W starszych projektach możesz zobaczyć:
-std=c89
lub:
-std=c90
62. Optymalizacja
Popularne:
-O0
-O1
-O2
-O3
-Os
-Og
Podczas debugowania:
-O0
lub:
-Og
W buildzie produkcyjnym często:
-O2
63. Include path
Jeżeli nagłówki są w:
include/
kompilatorowi można powiedzieć:
cc -Iinclude src/main.c src/server.c -o app
-I dodaje katalog z nagłówkami.
64. Biblioteki
Przykład:
cc main.c -lm -o app
-lm
linkuje bibliotekę matematyczną.
Inny przykład:
cc app.c -lcurl -o app
linkowanie libcurl.
65. -L i -l
-L/path/to/lib
dodaje katalog bibliotek.
-lfoo
linkuje:
libfoo.so
lub:
libfoo.a
Przykład:
cc app.o -L/usr/local/lib -lfoo -o app
66. Kompilacja a linkowanie
To jedno z najważniejszych rozróżnień.
Błąd kompilacji:
syntax error
unknown type name
undeclared identifier
oznacza problem z kodem źródłowym.
Błąd linkera:
undefined reference to `foo`
oznacza:
kompilator wie, że funkcja
fooistnieje, ale linker nie może znaleźć jej implementacji.
67. Typowy błąd linkera
Masz:
int add(int, int);
i wywołujesz:
add(1, 2);
ale nie dołączasz pliku zawierającego definicję.
Dostaniesz coś w rodzaju:
undefined reference to `add`
Rozwiązaniem może być:
cc main.c math.c -o app
68. pkg-config
Bardzo przydatne przy bibliotekach.
Przykład:
pkg-config --cflags --libs libcurl
Możesz dostać:
-I/usr/include/... -lcurl
Kompilacja:
cc app.c $(pkg-config --cflags --libs libcurl) -o app
69. Make
Najczęściej:
make
Program make czyta:
Makefile
70. Prosty Makefile
CC = cc
CFLAGS = -Wall -Wextra -g
app: main.o server.o
$(CC) main.o server.o -o app
main.o: main.c server.h
$(CC) $(CFLAGS) -c main.c
server.o: server.c server.h
$(CC) $(CFLAGS) -c server.c
clean:
rm -f *.o app
Uwaga:
polecenia Makefile tradycyjnie zaczynają się tabulatorem.
71. Najważniejsze polecenia make
make
standardowy build.
make clean
usuwa wynik kompilacji.
make test
jeżeli projekt ma taki target.
make install
instaluje program.
Czasem:
sudo make install
ale lepiej najpierw wiedzieć, gdzie projekt zamierza instalować pliki.
72. Podejrzenie targetów Makefile
Nie ma jednego obowiązkowego standardu, ale można sprawdzić:
grep -E '^[a-zA-Z0-9_.-]+:' Makefile
albo po prostu:
less Makefile
73. Równoległa kompilacja
make -j
lub:
make -j8
Kompiluje wiele plików równocześnie.
74. CMake
CMake nie jest kompilatorem.
Generuje konfigurację buildu.
Projekt ma zwykle:
CMakeLists.txt
Typowy workflow:
cmake -S . -B build
cmake --build build
Potem program może znaleźć się np.:
build/app
75. Debug build w CMake
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build
Release:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
76. Instalacja projektu CMake
cmake --install build
czasem wymagane są uprawnienia administratora.
77. Czyszczenie CMake
Najprościej:
rm -rf build
i ponownie:
cmake -S . -B build
To jedna z zalet buildów out-of-source.
78. Meson
Projekt ma:
meson.build
Workflow:
meson setup build
meson compile -C build
Testy:
meson test -C build
79. Autotools
Starsze projekty często używają:
configure
Makefile.in
Makefile.am
Typowa instalacja:
./configure
make
make check
sudo make install
Czasem najpierw:
./autogen.sh
lub:
autoreconf -fi
80. Jak rozpoznać system budowania
Po sklonowaniu:
ls -la
Szukaj:
Makefile
CMakeLists.txt
meson.build
configure
configure.ac
Makefile.am
build.ninja
Najpierw jednak:
less README.md
81. Pierwsze kroki po git clone
Przykład:
git clone https://github.com/example/project.git
cd project
Potem:
ls -la
Następnie:
less README.md
Sprawdź:
find . -maxdepth 2 -type f | sort | less
Potem szukaj:
rg "main\s*\("
Jeśli projekt jest duży:
tree -L 2
82. Jak czytać obcy projekt C
Dobra kolejność:
1. README
2. plik build systemu
3. main()
4. publiczne nagłówki
5. moduły wywoływane przez main()
6. testy
7. dopiero potem szczegóły implementacji
Nie zaczynaj od losowego pliku .c.
83. Publiczne API modułu
Załóżmy:
include/database.h
src/database.c
Najpierw czytaj:
database.h
Możesz znaleźć:
struct database;
int database_open(struct database **db, const char *path);
void database_close(struct database *db);
int database_query(struct database *db, const char *query);
Już z nagłówka rozumiesz dużą część modułu.
84. Opaque struct
Częsty wzorzec:
struct database;
bez pokazania pól.
Implementacja znajduje się w .c.
To oznacza:
użytkownik API może korzystać z typu, ale nie zna jego wewnętrznej struktury.
To odpowiednik ukrywania implementacji.
85. Przepływ programu
Przy czytaniu:
int main(...)
{
config_load();
server_init();
server_run();
server_shutdown();
}
Nie czytaj od razu każdej funkcji.
Najpierw zbuduj sobie mentalne drzewo:
main
├── config_load
├── server_init
├── server_run
└── server_shutdown
Potem schodź poziom niżej.
86. Wyszukiwanie symbolu
Przydatne:
rg "server_run"
lub:
grep -R "server_run" .
Dzięki temu znajdziesz:
- deklarację,
- definicję,
- wywołania.
87. ctags
Bardzo przydatne w dużym kodzie.
Instalujesz np.:
sudo apt install universal-ctags
Generowanie:
ctags -R .
W Vimie możesz potem skakać do definicji symboli.
To bardzo pasuje do czytania projektów C.
88. compile_commands.json
Nowoczesne projekty C/C++ często generują:
compile_commands.json
Zawiera dokładne polecenia kompilacji każdego pliku.
CMake:
cmake -S . -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
Plik:
build/compile_commands.json
Jest używany m.in. przez:
- clangd,
- IDE,
- edytory,
- analizatory statyczne.
89. Debugowanie przez printf
Najprostsza metoda:
fprintf(stderr, "x=%d\n", x);
Do szybkiego sprawdzania:
- czy kod został wykonany,
- jaka jest wartość zmiennej,
- gdzie program się zatrzymuje.
W poważniejszym debugowaniu użyj GDB lub LLDB.
90. GDB
Kompilacja:
cc -g -O0 main.c server.c -o app
Uruchom:
gdb ./app
91. Najważniejsze polecenia GDB
Uruchom program:
run
Argumenty:
run --port 8080
Breakpoint:
break main
lub:
break server_run
Kontynuuj:
continue
Krok do następnej linii:
next
Wejdź do funkcji:
step
Wyświetl zmienną:
print variable
Przykład:
print user->name
92. Przydatne polecenia GDB
Backtrace:
bt
Pokazuje stos wywołań.
Ramka:
frame 2
Lokale:
info locals
Argumenty funkcji:
info args
Breakpointy:
info breakpoints
Kasowanie breakpointa:
delete 1
Wyjście:
quit
93. Segmentation fault i GDB
Jeżeli program robi:
Segmentation fault
uruchom:
gdb ./app
potem:
run
Po crashu:
bt
Bardzo często od razu zobaczysz miejsce problemu.
94. Core dump
System może zapisać stan programu po awarii.
Sprawdź:
ulimit -c
Możesz chwilowo włączyć:
ulimit -c unlimited
Potem analizować:
gdb ./app core
Mechanizm przechowywania core dumpów zależy od systemu.
95. LLDB
Alternatywa dla GDB.
Start:
lldb ./app
Breakpoint:
breakpoint set --name main
Uruchomienie:
run
Backtrace:
bt
LLDB jest mocno związany z toolchainem LLVM/Clang.
96. Sanitizery
To jedna z najlepszych rzeczy przy debugowaniu C.
AddressSanitizer:
cc \
-fsanitize=address \
-g \
-O1 \
main.c \
-o app
Uruchamiasz normalnie:
./app
Może wykryć m.in.:
- buffer overflow,
- use-after-free,
- double free,
- część wycieków pamięci.
97. UndefinedBehaviorSanitizer
cc \
-fsanitize=undefined \
-g \
main.c \
-o app
Wykrywa wiele form niezdefiniowanego zachowania.
Można łączyć:
cc \
-fsanitize=address,undefined \
-g \
-O1 \
main.c \
-o app
98. ThreadSanitizer
Dla programów wielowątkowych:
-fsanitize=thread
Pomaga wykrywać wyścigi danych.
Nie należy zwykle łączyć go z AddressSanitizerem w jednym buildzie.
99. Valgrind
Na systemach, gdzie jest dostępny:
valgrind ./app
Dokładniejsze sprawdzenie pamięci:
valgrind \
--leak-check=full \
--show-leak-kinds=all \
./app
Sanitizery są zwykle szybsze, ale Valgrind nadal jest bardzo użyteczny.
100. Statyczna analiza kodu
Clang
clang --analyze file.c
cppcheck
cppcheck src/
Przydatne do szybkiego przeglądu projektu.
101. clang-format
Automatyczne formatowanie kodu:
clang-format file.c
Zmiana pliku:
clang-format -i file.c
Projekt może zawierać:
.clang-format
z zasadami formatowania.
102. clang-tidy
Bardziej zaawansowana analiza kodu:
clang-tidy file.c -- ...
Najlepiej działa z:
compile_commands.json
103. Biblioteki współdzielone
Sprawdzenie zależności programu w Linux:
ldd ./app
Przykład:
libc.so
libssl.so
libcrypto.so
Na FreeBSD:
ldd ./app
również jest dostępne.
104. Symbole programu
Narzędzie:
nm
Przykład:
nm ./app
Możesz zobaczyć funkcje i symbole znajdujące się w binarce.
105. file
Bardzo użyteczne:
file ./app
Możesz dostać informacje typu:
ELF 64-bit LSB pie executable...
106. readelf
Linux:
readelf -h ./app
Pokazuje informacje o formacie ELF.
Symbole:
readelf -s ./app
107. objdump
Przykład:
objdump -d ./app
Pokazuje kod maszynowy/disassembly.
Nie musisz rozumieć assemblera, ale warto wiedzieć, że takie narzędzie istnieje.
108. Biblioteki statyczne
Tworzenie:
ar rcs libfoo.a foo.o bar.o
Linkowanie:
cc main.o libfoo.a -o app
109. Biblioteki współdzielone
Na systemach Unix-like spotkasz:
libfoo.so
Projekt może je linkować przez:
-lfoo
110. pthread
Wątki POSIX.
Nagłówek:
#include <pthread.h>
Kompilacja:
cc app.c -pthread -o app
Typowe konstrukcje:
pthread_create()
pthread_join()
pthread_mutex_lock()
pthread_mutex_unlock()
111. Sieć
Kod sieciowy C często zawiera:
socket()
bind()
listen()
accept()
connect()
send()
recv()
Nagłówki zależą od platformy, np.:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
To POSIX/BSD sockets.
112. Event loop
Serwery i narzędzia sieciowe mogą używać:
Linux:
epoll
BSD:
kqueue
bardziej przenośnie:
poll
select
Biblioteki:
libevent
libev
libuv
113. Popularne biblioteki C, które możesz spotkać
libcurl
HTTP, FTP i inne protokoły.
OpenSSL
TLS, kryptografia.
SQLite
wbudowana baza SQL.
libxml2
XML.
jansson
JSON.
cJSON
lekka obsługa JSON.
libpng
PNG.
SDL
gry, multimedia, okna, wejście.
raylib
prosta biblioteka do gier i grafiki.
ncurses
interfejsy terminalowe.
libuv
event loop, async I/O.
114. goto
C ma:
goto
Przykład:
if (error)
goto cleanup;
Potem:
cleanup:
free(buffer);
fclose(file);
return -1;
W C goto bywa używane rozsądnie do obsługi cleanupu.
Nie zakładaj automatycznie, że jest to zły kod.
115. Typowy wzorzec cleanup
int process(void)
{
FILE *f = NULL;
char *buffer = NULL;
int result = -1;
f = fopen("file.txt", "r");
if (!f)
goto cleanup;
buffer = malloc(1024);
if (!buffer)
goto cleanup;
result = 0;
cleanup:
free(buffer);
if (f)
fclose(f);
return result;
}
To bardzo typowe C.
116. Bity
C bardzo często operuje bitami.
Operatory:
&
|
^
~
<<
>>
Przykład flag:
#define FLAG_READ 0x01
#define FLAG_WRITE 0x02
#define FLAG_EXEC 0x04
Łączenie:
flags = FLAG_READ | FLAG_WRITE;
Sprawdzenie:
if (flags & FLAG_WRITE)
{
...
}
117. Hexadecimal
W kodzie systemowym często:
0xFF
0x1000
0xDEADBEEF
To zapis szesnastkowy.
Bardzo często używany przy:
- bitach,
- protokołach,
- adresach,
- sterownikach,
- maskach.
118. Endianness
W kodzie sieciowym/systemowym możesz spotkać:
htons()
htonl()
ntohs()
ntohl()
Służą do konwersji kolejności bajtów.
Sieć używa określonego porządku bajtów niezależnie od architektury CPU.
119. volatile
Możesz zobaczyć:
volatile int flag;
Sugeruje kompilatorowi, że wartość może zmienić się poza normalnym przepływem kodu.
Częste w:
- embedded,
- sterownikach,
- memory-mapped I/O,
- kodzie niskopoziomowym.
volatile nie jest zamiennikiem poprawnej synchronizacji wielowątkowej.
120. Atomics
Nowoczesne C ma:
#include <stdatomic.h>
Przykład:
atomic_int counter;
W dużych projektach wielowątkowych możesz spotkać atomiki zamiast zwykłych zmiennych.
121. inline
Przykład:
static inline int max(int a, int b)
{
return a > b ? a : b;
}
inline jest wskazówką związaną z generowaniem kodu i semantyką funkcji.
Kompilator nie ma obowiązku faktycznie wstawić kodu funkcji inline.
122. Operator trójargumentowy
condition ? value_if_true : value_if_false
Przykład:
int max = a > b ? a : b;
123. Inicjalizatory struktur
struct user u = {
.id = 1,
.name = "Anna",
.active = 1
};
Bardzo czytelny sposób tworzenia struktur.
124. Designated initializers
struct config cfg = {
.port = 8080,
.debug = true
};
Pola niepodane zwykle otrzymują wartość zerową.
125. Zero initialization
Popularny wzorzec:
struct config cfg = {0};
zeruje całą strukturę.
126. memset
Alternatywa:
memset(&cfg, 0, sizeof(cfg));
Często spotykana w starszym kodzie.
127. Typowe konwencje nazw
Projekt może używać:
snake_case
np.:
server_start()
user_create()
Makra i stałe:
MAX_USERS
DEFAULT_PORT
Typy:
struct user
user_t
User
Nie ma jednego obowiązkowego standardu.
128. Sufiks _t
Często:
size_t
uint32_t
pthread_t
Projekty też czasem definiują:
typedef struct user user_t;
W kodzie przenośnym trzeba uważać, bo część nazw kończących się _t może być zarezerwowana przez system.
129. size_t
Typ przeznaczony do reprezentowania rozmiarów.
size_t length;
Funkcje takie jak:
strlen()
sizeof
używają size_t.
Do printf:
printf("%zu\n", length);
130. Typowe błędy podczas kompilacji
implicit declaration of function
Przykład:
implicit declaration of function 'foo'
Najczęściej brakuje odpowiedniego nagłówka.
unknown type name
Brakuje definicji typu lub include.
undeclared identifier
Zmienna/symbol nie jest widoczny.
conflicting types
Deklaracja i definicja nie zgadzają się.
131. Typowe błędy linkera
undefined reference
Brakuje implementacji lub biblioteki.
multiple definition
Ten sam symbol został zdefiniowany więcej niż raz.
132. Typowe błędy wykonania
Segmentation fault
najczęściej nieprawidłowy dostęp do pamięci.
Bus error
błędny dostęp do pamięci zależny od architektury/systemu.
Aborted
program został przerwany np. przez abort() albo nieudaną asercję.
133. Ostrzeżenia kompilatora są ważne
Nie ignoruj:
warning:
W C ostrzeżenie często oznacza prawdziwy błąd.
Dobry projekt powinien kompilować się przynajmniej z:
-Wall -Wextra
bez lawiny ostrzeżeń.
134. -Werror
-Werror
zamienia ostrzeżenia w błędy.
Przydaje się w CI, ale czasami utrudnia kompilację starego projektu nowym kompilatorem.
Jeżeli klonujesz stary projekt i build pada tylko przez warning potraktowany jako error, sprawdź, czy projekt dodaje:
-Werror
135. Debug vs Release
Debug:
-g
-O0 / -Og
assertions
sanitizers
Release:
-O2
czasem -DNDEBUG
brak sanitizerów
NDEBUG wyłącza standardowe assert().
136. Instalowanie zależności - Debian
Podstawowy toolchain:
sudo apt update
sudo apt install build-essential
Daje m.in.:
gcc
make
libc headers
Przydatne dodatkowo:
sudo apt install \
clang \
cmake \
meson \
ninja-build \
gdb \
valgrind \
pkg-config \
universal-ctags \
cppcheck
137. Instalowanie zależności - FreeBSD
Podstawowy kompilator Clang znajduje się zwykle w systemie bazowym.
Dodatkowe narzędzia:
pkg install \
cmake \
meson \
ninja \
gdb \
pkgconf \
universal-ctags \
cppcheck
Na FreeBSD odpowiednikiem pkg-config może być pakiet:
pkgconf
polecenie nadal zwykle działa jako:
pkg-config
138. configure: brak biblioteki
Jeżeli:
./configure
kończy się błędem:
library foo not found
zwykle potrzebujesz wersji developerskiej biblioteki.
Debian często:
libfoo-dev
Przykład:
sudo apt install libssl-dev
139. Nagłówki developerskie
Do uruchomienia programu wystarcza czasem biblioteka runtime.
Do kompilacji potrzebne są również:
*.h
Dlatego Debian rozdziela pakiety:
libfoo
libfoo-dev
140. Jak znaleźć pakiet zawierający plik na Debianie
Jeżeli masz apt-file:
sudo apt install apt-file
sudo apt-file update
Potem:
apt-file search header.h
141. Jak sprawdzić, czego wymaga projekt
Najpierw:
cat README.md
Potem:
grep -Ri "depend" .
Sprawdź też:
INSTALL
BUILDING
CONTRIBUTING.md
docs/
142. Submodules Git
Po klonowaniu projekt może mieć brakujące katalogi.
Sprawdź:
git submodule status
Pobranie:
git submodule update --init --recursive
Albo klonuj:
git clone --recursive URL
143. Git branch i build
Sprawdź:
git status
git branch
git log --oneline -10
W dużym projekcie warto wiedzieć, czy jesteś na:
main
master
develop
release/*
144. Generowane pliki
Nie wszystko w projekcie musi być pisane ręcznie.
Możesz spotkać:
generated/
config.h
version.h
parser.c
które są generowane podczas buildu.
Nie zakładaj, że każdy plik .c należy ręcznie edytować.
145. config.h
Autotools i CMake często generują:
#define HAVE_OPENSSL 1
#define HAVE_EPOLL 1
Kod używa potem:
#ifdef HAVE_OPENSSL
...
#endif
146. Biblioteki vendored
Projekt może zawierać:
vendor/
third_party/
deps/
external/
czyli kopie kodu zewnętrznych bibliotek.
Podczas czytania projektu zwykle nie zaczynaj od nich.
147. Testy
Typowe nazwy:
tests/
test/
unit/
Uruchamianie może wyglądać:
make test
lub:
ctest --test-dir build
lub:
meson test -C build
148. CTest
Projekt CMake może używać:
ctest --test-dir build
Więcej informacji:
ctest --test-dir build --output-on-failure
149. Debugging testu
Jeżeli test jest zwykłym programem:
gdb ./build/tests/parser_test
Możesz debugować test tak samo jak aplikację.
150. strace
Linux.
Pokazuje wywołania systemowe:
strace ./app
Przydatne gdy nie wiesz:
- jaki plik program otwiera,
- z czym się łączy,
- czego szuka,
- dlaczego dostaje
ENOENT, - gdzie czeka.
Przykład:
strace -f ./app
-f śledzi również procesy potomne.
151. FreeBSD: truss
Na FreeBSD podobną rolę pełni:
truss ./app
152. ltrace
Na Linux może pokazywać wywołania bibliotek:
ltrace ./app
Nie zawsze jest równie użyteczne jak strace, ale warto je znać.
153. gprof
Klasyczne profilowanie:
cc -pg app.c -o app
./app
gprof ./app gmon.out
Dziś często używa się nowocześniejszych profilerów, ale możesz spotkać gprof w starszych projektach.
154. perf
Linux:
perf record ./app
perf report
Do profilowania wydajności.
Nie musisz znać go na początku, ale warto wiedzieć, co to jest.
155. Debugowanie wielowątkowe
W GDB:
info threads
Zmiana wątku:
thread 2
Backtrace wszystkich wątków:
thread apply all bt
To bardzo przydatne przy deadlockach.
156. Deadlock
Przykład problemu:
thread A czeka na mutex B
thread B czeka na mutex A
Program „wisi”, ale się nie crashuje.
Przydatne:
gdb -p PID
potem:
thread apply all bt
157. Podłączenie GDB do działającego procesu
Znajdź PID:
pgrep app
Potem:
gdb -p PID
Możesz sprawdzić, gdzie program aktualnie się znajduje.
158. Procesy potomne
W kodzie Unixowym możesz zobaczyć:
fork()
Po fork() powstaje nowy proces.
Często potem:
exec(...)
zastępuje kod procesu innym programem.
159. Sygnały
Nagłówek:
#include <signal.h>
Przykłady:
SIGINT
SIGTERM
SIGSEGV
SIGPIPE
Program może rejestrować handler:
signal(SIGINT, handler);
lub korzystać z bardziej zaawansowanego:
sigaction()
160. Daemony
Program działający w tle może:
- odłączyć się od terminala,
- zapisywać PID,
- reagować na sygnały,
- logować do sysloga.
Możesz spotkać:
daemon(...)
albo własną implementację daemonizacji.
161. Logowanie
Popularne warianty:
printf()
fprintf(stderr, ...)
syslog()
albo własne:
log_info(...)
log_error(...)
log_debug(...)
Jeżeli projekt ma makra:
LOG_DEBUG(...)
LOG_ERROR(...)
najpierw znajdź ich definicję.
162. Systemowe logi
Linux:
journalctl
FreeBSD:
/var/log/
Jeżeli program jest uruchamiany jako usługa, jego output niekoniecznie pojawia się w terminalu.
163. README kontra kod
Jeżeli README mówi:
make
to zacznij od:
make
Nie próbuj ręcznie odtwarzać polecenia cc, dopóki nie ma takiej potrzeby.
Build system zna:
- zależności,
- flagi,
- biblioteki,
- generowanie plików,
- konfigurację platformy.
164. Jak zobaczyć polecenia kompilacji
make często pokazuje:
cc -Iinclude -Wall -O2 -c src/main.c -o main.o
Jeżeli nie pokazuje, można czasem użyć:
make V=1
lub:
make VERBOSE=1
CMake:
cmake --build build --verbose
Ninja:
ninja -C build -v
To świetny sposób na zrozumienie buildu.
165. Jak ustawić Clang zamiast GCC w CMake
CC=clang cmake -S . -B build
GCC:
CC=gcc cmake -S . -B build
Najlepiej robić to dla świeżego katalogu build.
166. Jak ustawić flagi
Jednorazowo:
CFLAGS="-Wall -Wextra -g -O0" make
W CMake można np.:
cmake \
-S . \
-B build \
-DCMAKE_C_FLAGS="-Wall -Wextra -g"
Nie zawsze projekt pozwala bezproblemowo nadpisywać flagi.
167. Debug build z sanitizerami w CMake
Prosty wariant:
cmake \
-S . \
-B build \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_C_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer"
cmake --build build
W dużych projektach mogą istnieć własne opcje sanitizerów.
Sprawdź README i:
cmake -LH -S . -B build
168. cmake -L
Lista opcji:
cmake -L build
Więcej:
cmake -LAH build
Pozwala odkryć opcje projektu.
169. Feature flags
Projekt może mieć np.:
-DENABLE_TLS=ON
-DENABLE_TESTS=ON
-DBUILD_SHARED_LIBS=OFF
Dlatego warto przejrzeć:
CMakeLists.txt
cmake/
170. Cross compilation
C pozwala łatwo kompilować na inne architektury.
Możesz spotkać kompilatory:
aarch64-linux-gnu-gcc
arm-none-eabi-gcc
x86_64-w64-mingw32-gcc
Jeżeli zwykły:
cc
nie działa, sprawdź dokumentację projektu.
171. Compile-time vs runtime
Błąd compile-time:
kod nie może zostać zbudowany
Błąd runtime:
program się buduje, ale pada podczas działania
W C to bardzo ważne rozróżnienie.
172. Undefined behavior
C dopuszcza sytuacje, w których zachowanie programu nie jest zdefiniowane.
Przykład:
int a[10];
a[50] = 1;
Albo:
int *p = NULL;
*p = 10;
Kompilator nie musi Cię przed tym ochronić.
Dlatego sanitizery są tak ważne.
173. Buffer overflow
char buffer[8];
strcpy(buffer, "bardzo dlugi tekst");
To może nadpisać pamięć poza tablicą.
Właśnie z takich problemów słynie C.
174. Integer overflow
Dla typów całkowitych mogą wystąpić przepełnienia.
Kod systemowy często sprawdza rozmiary bardzo dokładnie przed:
malloc(count * size)
bo samo mnożenie może być problemem.
175. Return value checking
W C trzeba często sprawdzać wynik funkcji.
Źle:
FILE *f = fopen("file.txt", "r");
fgets(buffer, sizeof(buffer), f);
Jeżeli fopen zwróci NULL, program może się wywrócić.
Poprawny kod sprawdza:
if (!f)
{
...
}
176. Wzorzec *_create / *_destroy
C często naśladuje obiektowość przez funkcje.
struct server *server_create(void);
void server_destroy(struct server *s);
int server_start(struct server *s);
void server_stop(struct server *s);
Mentalnie możesz traktować:
struct server
jak „obiekt”, a:
server_start()
server_stop()
jak „metody”.
177. Wzorzec init / cleanup
Inny styl:
struct server s;
server_init(&s);
server_run(&s);
server_cleanup(&s);
Nie ma new i delete.
178. Context object
W dużych projektach często zobaczysz:
struct context *ctx
przekazywany prawie wszędzie.
Może zawierać:
- konfigurację,
- logger,
- połączenia,
- pule pamięci,
- stan aplikacji.
To sposób na ograniczenie globalnych zmiennych.
179. Callback + void *userdata
Bardzo popularny wzorzec:
void callback(void *userdata)
void * oznacza wskaźnik do nieokreślonego typu.
Biblioteka przekazuje go bez interpretowania.
Twój kod może potem zrobić:
struct app *app = userdata;
180. Rzutowania
(int)value
lub:
(struct user *)ptr
W C często spotkasz casty wskaźników.
Uwaga: zbyt dużo castów może ukrywać błędy typów.
181. void *
Uniwersalny wskaźnik:
void *ptr;
Może wskazywać na obiekt dowolnego typu.
malloc() zwraca:
void *
W C nie trzeba pisać:
(int *)malloc(...)
Wystarczy:
int *p = malloc(...);
182. Flexible array member
Możesz spotkać:
struct packet
{
size_t length;
unsigned char data[];
};
Tablica na końcu struktury ma rozmiar określany dynamicznie.
Używane w kodzie niskopoziomowym i protokołach.
183. container_of
W dużych projektach systemowych możesz zobaczyć makra obliczające adres struktury na podstawie adresu jej pola.
Linux kernel słynie z:
container_of
Nie musisz od razu rozumieć wszystkich szczegółów.
To już zaawansowane C.
184. Lista rzeczy, które wyglądają groźnie, ale są normalne
char **argv;
tablica stringów.
const char *s;
wskaźnik do tekstu tylko do odczytu.
struct foo *f;
wskaźnik do struktury.
foo->bar
pole struktury przez wskaźnik.
(*callback)(...)
wskaźnik do funkcji.
void *userdata
generyczny kontekst.
#ifdef SOMETHING
kompilacja warunkowa.
goto cleanup;
często sensowna obsługa cleanupu.
185. Jak podejść do zupełnie obcego repo
Załóżmy:
git clone URL
cd projekt
Krok 1
ls
Krok 2
less README.md
Krok 3
tree -L 2
Krok 4
Rozpoznaj build system.
ls \
Makefile \
CMakeLists.txt \
meson.build \
configure \
2>/dev/null
Krok 5
Znajdź main():
rg "main\s*\("
Krok 6
Zbuduj projekt dokładnie według README.
Krok 7
Uruchom testy.
Krok 8
Uruchom program.
Krok 9
Jeżeli się wywraca - debugger.
186. Minimalny workflow dla Make
git clone URL
cd project
less README.md
make
make test
./app
Jeżeli potrzebujesz debug:
make clean
CFLAGS="-g -O0 -Wall -Wextra" make
Potem:
gdb ./app
187. Minimalny workflow dla CMake
git clone URL
cd project
cmake \
-S . \
-B build \
-DCMAKE_BUILD_TYPE=Debug
cmake --build build
Testy:
ctest \
--test-dir build \
--output-on-failure
188. Minimalny workflow z sanitizerami
Dla małego projektu:
cc \
-g \
-O1 \
-Wall \
-Wextra \
-fsanitize=address,undefined \
*.c \
-o app
Uruchom:
./app
189. Debugowanie crasha - gotowy schemat
Program:
./app
Crash:
Segmentation fault
Kompiluj z debug symbols:
cc -g -O0 *.c -o app
Uruchom:
gdb ./app
W GDB:
run
bt
frame 0
info locals
print variable
To powinien być Twój pierwszy odruch.
190. Debugowanie memory buga - gotowy schemat
Build:
cc \
-g \
-O1 \
-fsanitize=address,undefined \
*.c \
-o app
Uruchom:
./app
Jeżeli program ma problem z pamięcią, sanitizer zwykle poda:
- rodzaj błędu,
- adres,
- stack trace,
- linię kodu.
191. Debugowanie „program nic nie robi”
Linux:
strace -f ./app
Możesz sprawdzić:
- czy czeka na plik,
- czeka na socket,
- szuka konfiguracji,
- dostaje permission denied,
- robi timeout.
FreeBSD:
truss ./app
192. Debugowanie „brakuje biblioteki”
Uruchomienie:
error while loading shared libraries: libfoo.so...
Sprawdź:
ldd ./app
Możesz zobaczyć:
libfoo.so => not found
Potem trzeba zainstalować bibliotekę albo poprawić jej ścieżkę.
193. Debugowanie undefined reference
Przykład:
undefined reference to `SSL_CTX_new`
To prawdopodobnie problem linkera.
Kod korzysta z OpenSSL, ale build nie linkuje odpowiedniej biblioteki.
Sprawdź polecenie linkowania.
Szukaj np.:
-lssl
-lcrypto
194. Debugowanie „header not found”
Przykład:
fatal error: curl/curl.h: No such file or directory
Brakuje nagłówków developerskich.
Na Debianie prawdopodobnie potrzebny będzie pakiet w rodzaju:
libcurl*-dev
Nie zgaduj nazwy bez sprawdzenia dokumentacji projektu lub pakietów systemowych.
195. Przydatne polecenia shellowe do projektu C
Lista plików C:
find . -name '*.c'
Nagłówki:
find . -name '*.h'
Liczba linii:
find src include \
\( -name '*.c' -o -name '*.h' \) \
-print0 |
xargs -0 wc -l
Szukaj TODO:
rg 'TODO|FIXME'
Szukaj malloc:
rg 'malloc|calloc|realloc|free'
Szukaj socketów:
rg 'socket|bind|listen|accept|connect'
196. Jak sprawdzić standard kodowania projektu
Szukaj:
.clang-format
.editorconfig
CONTRIBUTING.md
CODING_STYLE
STYLE.md
To może wyjaśnić:
- indentację,
- nazwy funkcji,
- długość linii,
- format nawiasów.
197. Czego nie musisz znać, żeby czytać 80-90% projektów
Na początku nie musisz znać:
- assemblera,
- ABI w szczegółach,
- implementacji linkera,
- ręcznego ELF,
- zaawansowanych makr preprocesora,
- lock-free programming,
- SIMD,
- compiler intrinsics,
- szczegółów C23.
Warto wiedzieć, że istnieją, ale nie są potrzebne do wejścia w większość repozytoriów.
198. Co trzeba znać naprawdę dobrze
Jeżeli chcesz tylko czytać C, opanuj przede wszystkim:
if / switch
for / while
funkcje
struct
enum
typedef
wskaźniki
&
*
->
NULL
malloc/free
char*
.c / .h
#include
#define
static
const
return codes
Do pracy z repozytorium:
cc
gcc / clang
make
cmake
gdb
sanitizery
pkg-config
git
199. Mini słownik
compiler
kompilator.
Przykłady:
gcc
clang
preprocessor
obsługuje m.in.:
#include
#define
#ifdef
object file
plik .o.
linker
łączy pliki .o i biblioteki w program.
header
plik .h.
symbol
nazwa funkcji lub zmiennej widoczna dla kompilatora/linkera.
shared library
biblioteka .so.
static library
biblioteka .a.
ABI
zasady współpracy kodu binarnego.
API
interfejs funkcji i typów dostępnych dla programisty.
200. Ściąga: składnia
int x = 10;
if (x > 5)
{
...
}
for (int i = 0; i < 10; i++)
{
...
}
while (running)
{
...
}
int add(int a, int b)
{
return a + b;
}
struct user
{
int id;
char name[64];
};
struct user u;
u.id = 1;
struct user *p = &u;
p->id = 2;
201. Ściąga: wskaźniki
int x = 10;
int *p = &x;
*p = 20;
Mentalnie:
x
wartość.
&x
adres x.
p
adres.
*p
wartość pod adresem.
202. Ściąga: build
Jeden plik:
cc app.c -o app
Debug:
cc -g -O0 app.c -o app
Ostrzeżenia:
cc -Wall -Wextra app.c -o app
Sanitizery:
cc \
-g \
-fsanitize=address,undefined \
app.c \
-o app
203. Ściąga: Make
make
make -j
make test
make clean
204. Ściąga: CMake
cmake -S . -B build
cmake --build build
ctest --test-dir build
Debug:
cmake \
-S . \
-B build \
-DCMAKE_BUILD_TYPE=Debug
205. Ściąga: GDB
gdb ./app
W debuggerze:
break main
run
next
step
print variable
bt
info locals
continue
quit
206. Ściąga: diagnostyka
Zależności:
ldd ./app
Typ pliku:
file ./app
Symbole:
nm ./app
Linux:
strace ./app
FreeBSD:
truss ./app
Memory debugging:
valgrind ./app
207. Ściąga: wejście do obcego repo
git clone URL
cd project
less README.md
tree -L 2
rg "main\s*\("
make
albo:
cmake -S . -B build
cmake --build build
Potem:
make test
lub:
ctest --test-dir build
Jeżeli crash:
gdb ./app
Jeżeli problem z pamięcią:
AddressSanitizer
Jeżeli program dziwnie korzysta z systemu:
Linux:
strace
FreeBSD:
truss
208. Jak mentalnie czytać funkcję C
Przykład:
int user_load(struct user *user, const char *path)
{
FILE *f;
if (!user || !path)
return -1;
f = fopen(path, "r");
if (!f)
return -2;
/* ... */
fclose(f);
return 0;
}
Czytaj ją tak:
user_load
funkcja ładuje użytkownika.
struct user *user
dostaje wskaźnik na strukturę, którą może zmienić.
const char *path
dostaje tekstową ścieżkę, której nie powinna zmieniać.
if (!user || !path)
sprawdza NULL.
fopen
otwiera plik.
return -2
sygnalizuje konkretny błąd.
fclose
sprząta zasób.
return 0
sukces.
To właśnie jest sposób czytania C: typy + przepływ + własność zasobów + kody błędów.
209. Najważniejsze pytania podczas czytania kodu C
Przy każdej funkcji zapytaj:
- Co przyjmuje?
- Co zwraca?
- Czy argument może być
NULL? - Czy funkcja modyfikuje przekazany obiekt?
- Kto jest właścicielem pamięci?
- Kto ma wykonać
free()? - Jak sygnalizowany jest błąd?
- Czy funkcja otwiera zasób?
- Gdzie ten zasób jest zamykany?
- Czy funkcja jest publiczna czy
static? - Czy kod zależy od Linux/BSD/POSIX?
- Czy występuje warunkowa kompilacja?
Jeżeli odpowiesz na te pytania, zwykle rozumiesz większość funkcji.
210. Najważniejsze pytanie: kto owns the memory?
W C nie ma garbage collectora.
Musisz wiedzieć:
kto odpowiada za zwolnienie pamięci?
Przykład:
char *get_name(void);
Nie wiadomo.
Może:
- zwracać wskaźnik do statycznego bufora,
- zwracać pamięć zaalokowaną przez
malloc, - zwracać wskaźnik należący do innego obiektu.
Dokumentacja API powinna to wyjaśnić.
W C własność pamięci jest częścią interfejsu.
211. Typowe nazwy sugerujące własność
Nie jest to standard, ale często:
create
new
alloc
dup
clone
oznaczają, że powstaje nowy obiekt/pamięć.
A:
destroy
free
release
unref
zwalniają zasób.
Przykład:
user_create()
user_destroy()
212. Refcount
Duże biblioteki mogą używać liczników referencji.
Typowe funkcje:
ref()
unref()
retain()
release()
Obiekt zostanie zwolniony dopiero, gdy licznik referencji spadnie do zera.
213. Bardzo krótka mapa świata C
Jeżeli widzisz:
.c
.h
to kod C.
Jeżeli widzisz:
Makefile
najpierw próbujesz:
make
Jeżeli:
CMakeLists.txt
próbujesz:
cmake -S . -B build
cmake --build build
Jeżeli:
meson.build
próbujesz:
meson setup build
meson compile -C build
Jeżeli crash:
gdb ./program
Jeżeli pamięć:
ASan
UBSan
Valgrind
Jeżeli system:
strace / truss
Jeżeli nie wiesz, skąd bierze się funkcja:
rg "nazwa_funkcji"
214. Ostateczna checklista
Po sklonowaniu projektu powinieneś być w stanie odpowiedzieć:
[ ] Co projekt robi?
[ ] Gdzie jest main()?
[ ] Jaki system budowania wykorzystuje?
[ ] Jakie biblioteki są wymagane?
[ ] Jak uruchomić build?
[ ] Jak uruchomić testy?
[ ] Jak uruchomić program?
[ ] Gdzie są publiczne nagłówki?
[ ] Jakie są główne moduły?
[ ] Jak moduły się ze sobą komunikują?
[ ] Gdzie alokowana jest pamięć?
[ ] Gdzie pamięć jest zwalniana?
[ ] Jak sygnalizowane są błędy?
[ ] Czy projekt korzysta z POSIX?
[ ] Czy ma kod zależny od Linux/FreeBSD?
[ ] Jak włączyć debug symbols?
[ ] Jak uruchomić GDB/LLDB?
[ ] Jak uruchomić sanitizery?
[ ] Jak zobaczyć zależności bibliotek?
Jeżeli potrafisz przejść tę listę, możesz już całkiem sprawnie poruszać się po większości projektów napisanych w C.
215. Minimalny zestaw wiedzy do zapamiętania
Nie zapamiętuj całego dokumentu.
Zapamiętaj ten zestaw:
.c implementacja
.h deklaracje/API
main() start programu
struct struktura danych
enum zestaw wartości
typedef alias typu
* wskaźnik / dereferencja
& adres
-> pole struktury przez wskaźnik
NULL brak obiektu
malloc rezerwacja pamięci
free zwolnienie pamięci
#include dołączenie nagłówka
#define makro
static często prywatne dla pliku
const nie modyfikuj
cc/gcc/clang kompilator
make klasyczny build
cmake generator build systemu
gdb debugger
ASan błędy pamięci
UBSan undefined behavior
ldd biblioteki
strace system calls Linux
truss system calls FreeBSD
To wystarczy, żeby zacząć czytać prawdziwy kod C bez poczucia, że patrzysz na hieroglify.
Oficjalne źródła
- GCC documentation: https://gcc.gnu.org/onlinedocs/
- Clang documentation: https://clang.llvm.org/docs/
- CMake documentation: https://cmake.org/documentation/
- GDB documentation: https://sourceware.org/gdb/documentation/