Tech Handbook Null Yard

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 .c i .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:

callback jest wskaźnikiem do funkcji przyjmującej int i 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 foo istnieje, 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.

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:

  1. Co przyjmuje?
  2. Co zwraca?
  3. Czy argument może być NULL?
  4. Czy funkcja modyfikuje przekazany obiekt?
  5. Kto jest właścicielem pamięci?
  6. Kto ma wykonać free()?
  7. Jak sygnalizowany jest błąd?
  8. Czy funkcja otwiera zasób?
  9. Gdzie ten zasób jest zamykany?
  10. Czy funkcja jest publiczna czy static?
  11. Czy kod zależy od Linux/BSD/POSIX?
  12. 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/