Tech Handbook Null Yard

C - Reading, Building and Debugging Projects

C remains common in operating systems, libraries, drivers, CLI tools and embedded software. This handbook focuses on reading existing code, building projects and understanding compiler, linker and memory-related failures.

Related topics: Software Testing, Shell Scripting and Linux Permissions and Server Security.

1. C in one sentence

C is a small, compiled systems language that gives you direct control over memory, data layout and operating-system APIs.

2. The simplest program

#include <stdio.h>

int main(void) {
    puts("hello");
    return 0;
}

3. Basic file extensions

Typical files: .c source, .h headers, .o object files, .a static libraries and .so shared libraries on Unix-like systems.

4. What a typical project contains

src/        source files
include/    public headers
tests/      tests
Makefile / CMakeLists.txt / meson.build
README.md   build and usage notes

5. The most important thing: main()

For a normal executable, execution starts in main. Common signatures are int main(void) and int main(int argc, char **argv).

6. Variables

int count = 3;
double price = 9.99;
char letter = 'A';

7. const

const expresses that code should not modify a value through that name or pointer.

8. sizeof

size_t bytes = sizeof(int);
size_t n = sizeof array / sizeof array[0];

9. Operators

Arithmetic, comparison, logical, bitwise, assignment and pointer operators are all common in C.

10. The if statement

if (value > 0) {
    puts("positive");
} else {
    puts("not positive");
}

11. switch

switch (state) {
case READY:
    break;
default:
    break;
}

12. Loops

for

Use when initialization, condition and update naturally belong together.

for (size_t i = 0; i < n; i++) {
    use(items[i]);
}

while

Runs while the condition remains true.

do while

Runs the body at least once before checking the condition.

infinite loop

Common in event loops and daemons; provide an explicit exit or signal path.

13. Functions

int add(int a, int b) {
    return a + b;
}

14. A function returning nothing

void log_message(const char *msg) {
    puts(msg);
}

15. Declaration vs definition

A declaration tells the compiler that a symbol exists; a definition provides storage or function body.

16. .h files

Headers normally expose declarations, types, constants and macros shared between translation units.

17. #include

#include <stdio.h>
#include "project.h"

18. Include guards

#ifndef PROJECT_H
#define PROJECT_H
/* declarations */
#endif

19. The preprocessor

The preprocessor handles directives such as #include, #define and conditional compilation before normal compilation.

20. Conditional compilation

#ifdef DEBUG
fprintf(stderr, "debug\n");
#endif

21. Macros

Macros perform token substitution. Prefer functions or typed constants when they are clearer and safer.

22. typedef

typedef unsigned long user_id_t;

23. struct

struct user {
    int id;
    const char *name;
};

24. Pointer member access: ->

struct user *u = get_user();
printf("%d\n", u->id);

25. enum

enum status { STATUS_OK, STATUS_ERROR };

26. union

A union stores different members in the same memory region. Only the active interpretation is meaningful.

27. Arrays

int values[4] = {1,2,3,4};

28. Text in C

A C string is a sequence of bytes terminated by \0. It is not a distinct string object type.

29. Basic string functions

Recognize strlen, strcmp, strcpy/strncpy, snprintf, strchr, strstr and their safety constraints.

30. Pointers - the thing you must learn to read

A pointer stores an address. Read pointer-heavy code by asking what object it points to, whether it may be NULL and who owns that object.

31. Pointer example

int value = 10;
int *p = &value;
printf("%d\n", *p);

32. Why pointers exist

Pointers enable mutation through references, dynamic data structures, buffers, callbacks and interaction with OS/library APIs.

33. NULL

NULL represents a null pointer constant. Check it before dereferencing when an API may return no object.

34. Pointer to pointer

char **argv;
struct node **head;

35. Functions modifying data through pointers

void reset(int *value) {
    *value = 0;
}

36. Dynamic memory

Heap allocation commonly uses malloc, calloc, realloc and free.

37. Typical malloc pattern

struct user *u = malloc(sizeof *u);
if (u == NULL) {
    return -1;
}
/* use u */
free(u);

38. Memory leak

Allocated memory that is no longer reachable but was never freed.

39. Use-after-free

Accessing memory after it has been freed. This is undefined behavior and a serious security bug class.

40. Stack and heap

stack

Automatic storage tied to scope/function lifetime.

heap

Dynamically allocated storage controlled explicitly by the program.

41. Passing structures

Small structs may be passed by value; larger/mutable objects are often passed by pointer.

42. Function pointers

int (*compare)(const void *, const void *);

43. static

At file scope, static gives internal linkage. Inside a function, it gives static storage duration.

44. extern

Declares an object or function defined in another translation unit.

45. Global variables

Globals simplify access but create hidden coupling. Keep them limited and well-defined.

46. Function return code

Many C APIs return 0 or a value for success and a negative/nonzero code for failure. Always read the API contract.

47. errno

Some library/system calls set errno on failure. Read it only when the called API documents that behavior.

48. Standard input and output

Use stdin, stdout, stderr with functions such as fgets, printf, fprintf and perror.

49. printf formatting

Match format specifiers to types. Use <inttypes.h> macros for fixed-width integer portability.

50. Files

FILE *f = fopen("data.txt", "r");
if (f == NULL) {
    perror("fopen");
    return 1;
}
fclose(f);

51. File descriptors

POSIX uses integer file descriptors with calls such as open, read, write, close.

52. C vs POSIX

C defines the language and standard library; POSIX adds Unix APIs such as sockets, processes, pthreads and file descriptors.

53. Most important standard headers

stdio.h

Formatted I/O, FILE streams.

stdlib.h

Allocation, conversions, process helpers.

string.h

Byte/string operations.

stdint.h

Fixed-width integer types.

stdbool.h

bool, true, false for pre-C23 codebases.

ctype.h

Character classification/conversion.

time.h

Time/date functions.

errno.h

errno and error constants.

assert.h

Debug-time assertions.

54. bool

#include <stdbool.h>
bool ready = true;

55. Assertions

#include <assert.h>
assert(ptr != NULL);

Assertions are for programmer invariants, not normal user-input validation.

56. Compiling one file

cc -Wall -Wextra -g main.c -o app

57. Compiling several files at once

cc -Wall -Wextra main.c util.c net.c -o app

58. Compiling in stages

cc -c main.c -o main.o
cc -c util.c -o util.o
cc main.o util.o -o app

59. What -c does

Compiles source to an object file without performing the final link.

60. Most important compiler flags

cc -Wall -Wextra -Wpedantic -g -O0 file.c -o app

61. Language standard

cc -std=c17 file.c -o app
cc -std=c23 file.c -o app

62. Optimization

Typical levels include -O0, -O1, -O2, -O3, -Os. Debug builds usually favor -O0 or moderate optimization.

63. Include path

cc -Iinclude main.c -o app

64. Libraries

Libraries may be static archives or shared objects, and can be linked directly or through build-system discovery.

65. -L and -l

cc main.c -L/usr/local/lib -lfoo -o app

66. Compilation vs linking

Compilation turns source into object code; linking resolves symbols and combines objects/libraries into a final binary.

67. Typical linker error

undefined reference usually means a required definition/object/library was not linked, or library order/configuration is wrong.

68. pkg-config

pkg-config --cflags --libs libcurl

69. Make

Make executes build rules based on targets, prerequisites and recipes.

70. Simple Makefile

app: main.o util.o
    $(CC) main.o util.o -o app

71. Most important make commands

make
make clean
make install

72. Inspecting Makefile targets

Read the Makefile; make -n previews commands without executing them.

73. Parallel compilation

make -j"$(nproc 2>/dev/null || sysctl -n hw.ncpu)"

74. CMake

cmake -S . -B build
cmake --build build

75. Debug build in CMake

cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug

76. Installing a CMake project

cmake --install build

77. Cleaning CMake

Removing the out-of-tree build directory is the cleanest full reset for many CMake projects.

78. Meson

meson setup build
meson compile -C build

79. Autotools

./configure
make
make check

80. How to recognize the build system

Look for Makefile, CMakeLists.txt, meson.build, configure, configure.ac, Makefile.am and project documentation.

81. First steps after git clone

Read README, inspect build files, identify dependencies, build out-of-tree if possible, then run tests before changing code.

82. How to read an unfamiliar C project

Start from build files and main, then follow headers/APIs, ownership and call flow.

83. Public module API

Usually exposed in headers while implementation details remain in .c files.

84. Opaque struct

typedef struct client client_t;

Callers know the type exists but not its fields, preserving encapsulation.

85. Program flow

Trace from main into initialization, event loop/request handlers and cleanup.

86. Searching for a symbol

rg 'symbol_name' .
grep -Rni 'symbol_name' .

87. ctags

ctags -R .

88. compile_commands.json

Compilation database used by clangd, static analysis and editors to know exact compiler flags.

89. Debugging with printf

Still useful for state/timing questions, but remove noisy ad-hoc diagnostics or convert them to structured logging.

90. GDB

gdb ./app

91. Most important GDB commands

break main
run
next
step
continue
print variable
bt
quit

92. Useful GDB commands

info locals
info args
watch variable
frame N
thread apply all bt

93. Segmentation fault and GDB

gdb ./app
run
bt

94. Core dump

A core file captures process memory/state at crash time and can be examined with a debugger.

95. LLDB

lldb ./app

96. Sanitizers

cc -g -fsanitize=address,undefined -fno-omit-frame-pointer main.c -o app

97. UndefinedBehaviorSanitizer

UBSan detects many forms of undefined behavior such as invalid shifts and signed overflow.

98. ThreadSanitizer

TSan detects many data races in multithreaded programs.

99. Valgrind

valgrind --leak-check=full ./app

100. Static code analysis

Clang

Use Clang warnings, scan-build and clang-tidy.

cppcheck

Lightweight static analyzer useful as an additional check.

101. clang-format

clang-format -i src/*.c include/*.h

102. clang-tidy

Performs configurable static analysis/refactoring using compilation information.

103. Shared libraries

Dynamically loaded .so libraries reduce duplication and allow independent updates but introduce ABI/runtime search concerns.

104. Program symbols

Functions/global objects become symbols that linkers/debuggers/loaders may reference.

105. file

file ./app

106. readelf

readelf -h ./app
readelf -Ws ./app

107. objdump

objdump -d ./app

108. Static libraries

ar rcs libfoo.a foo.o bar.o

109. Shared libraries

cc -shared -fPIC foo.c -o libfoo.so

110. pthread

POSIX threads provide threads, mutexes, condition variables and related synchronization primitives.

111. Networking

POSIX networking commonly uses sockets: socket, bind, listen, accept, connect, send, recv.

112. Event loop

An event loop waits for I/O/timers/signals and dispatches callbacks, often using poll, select, epoll, kqueue or libraries.

113. Popular C libraries you may encounter

libcurl

HTTP and other URL transfers.

OpenSSL

TLS and cryptography.

SQLite

embedded SQL database.

libxml2

XML parsing.

jansson

JSON library.

cJSON

small JSON library.

libpng

PNG image handling.

SDL

multimedia/game/window/input library.

raylib

simple game/multimedia library.

ncurses

terminal UI.

libuv

cross-platform async I/O/event loop.

114. goto

Often avoided for general control flow, but a single cleanup path using goto cleanup is idiomatic in many C codebases.

115. Typical cleanup pattern

int rc = -1;
resource_t *r = acquire();
if (!r) goto cleanup;
/* work */
rc = 0;
cleanup:
release(r);
return rc;

116. Bits

flags |= FLAG_READ;
flags &= ~FLAG_WRITE;
if (flags & FLAG_READ) { ... }

117. Hexadecimal

unsigned mask = 0xffu;

118. Endianness

Byte order matters for binary formats and networking. Do not cast arbitrary buffers and assume host endianness.

119. volatile

Tells the compiler that a value may change outside ordinary code flow. It is not a thread-synchronization primitive.

120. Atomics

C11 <stdatomic.h> provides atomic operations and memory-order primitives for lock-free/synchronization code.

121. inline

A language/linkage hint related to function definitions; the compiler may inline regardless of the keyword.

122. Ternary operator

const char *label = ok ? "yes" : "no";

123. Structure initializers

struct point p = { 1, 2 };

124. Designated initializers

struct point p = { .y = 2, .x = 1 };

125. Zero initialization

struct config cfg = {0};

126. memset

memset(buffer, 0, sizeof buffer);

Do not assume memset is correct for every semantic initialization of complex objects.

127. Typical naming conventions

Projects often use prefixes for modules/types and _create, _destroy, _init, _free patterns.

128. The _t suffix

Common for typedef names, though POSIX reserves many _t names for its own types.

129. size_t

Unsigned type used for object sizes and many library lengths/indexes.

130. Common compile errors

implicit declaration of function

The compiler has not seen a declaration/prototype before the call.

unknown type name

Required typedef/header/feature macro is missing.

undeclared identifier

Name is not visible in the current scope.

conflicting types

Declarations/definitions disagree about a symbol's type.

131. Common linker errors

undefined reference and duplicate-symbol errors point to missing or conflicting definitions at link time.

132. Common runtime errors

Segfaults, aborts, assertion failures, leaks, races and corrupted output often stem from invalid memory/lifetime assumptions.

133. Compiler warnings matter

Build with strong warnings and fix them rather than normalizing noisy output.

134. -Werror

Turns warnings into errors. Useful in CI when the warning set/compiler version is controlled.

135. Debug vs Release

Debug favors symbols/assertions/sanitizers; Release favors optimization and production settings.

136. Installing dependencies - Debian

sudo apt install build-essential pkg-config cmake ninja-build

137. Installing dependencies - FreeBSD

pkg install pkgconf cmake ninja

138. configure: missing library

Read the exact check failure and install the development package/header/library it requests.

139. Development headers

On Debian, libraries often have separate -dev packages; FreeBSD packages commonly install headers with the package.

140. Find the package containing a file on Debian

Use apt-file search after installing/updating apt-file, or dpkg -S for already-installed files.

141. How to check what a project requires

Read README, build files, pkg-config checks, CI workflows and container/package manifests.

142. Git submodules

git submodule update --init --recursive

143. Git branch and build

Do not reuse stale build artifacts blindly after switching branches with major build-system changes.

144. Generated files

Recognize generated sources/configure files and avoid hand-editing unless the project explicitly expects it.

145. config.h

Often generated by configure/CMake to record detected platform features.

146. Vendored libraries

Dependencies copied into the repository. Check version, patches, license and update process.

147. Tests

Look for make test, make check, CTest, Meson test or custom harnesses.

148. CTest

ctest --test-dir build --output-on-failure

149. Debugging a test

Run the failing test directly under GDB/LLDB or with sanitizer options.

150. strace

strace -f ./app

151. FreeBSD: truss

truss ./app

152. ltrace

On systems where available, traces dynamic-library calls.

153. gprof

Traditional compiler-instrumentation profiler; recognize it in older projects.

154. perf

Linux performance tooling for CPU sampling, counters and profiling.

155. Multithreaded debugging

Inspect all threads, locks, waits and shared state; use TSan when practical.

156. Deadlock

Occurs when threads wait in a cycle for locks/resources that can never become available.

157. Attach GDB to a running process

gdb -p PID

158. Child processes

POSIX code may use fork, exec, pipes and waitpid.

159. Signals

Asynchronous process notifications such as SIGTERM and SIGINT. Signal handlers must use only async-signal-safe operations.

160. Daemons

Long-running background services need lifecycle, logging, privileges, signals and supervision.

161. Logging

Prefer structured levels/context and avoid leaking secrets.

162. System logs

On Linux services may log to journald/syslog; on FreeBSD commonly syslog/files under /var/log.

163. README vs code

README explains intended build/use; code and build scripts reveal the actual behavior. Check both.

164. How to see compilation commands

make V=1
ninja -v
cmake --build build --verbose

165. Set Clang instead of GCC in CMake

CC=clang cmake -S . -B build

166. How to set flags

Prefer build-system options/toolchain configuration over globally editing source files.

167. Debug build with sanitizers in CMake

cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_FLAGS='-fsanitize=address,undefined -fno-omit-frame-pointer'

168. cmake -L

cmake -L build

169. Feature flags

Build-time options enable/disable optional components and platform capabilities.

170. Cross compilation

Build for another target using a cross compiler/toolchain file and target sysroot/libraries.

171. Compile-time vs runtime

Compile-time decisions affect generated binary; runtime decisions depend on input/config/environment.

172. Undefined behavior

Behavior not defined by the C standard; the compiler may optimize under the assumption it never occurs.

173. Buffer overflow

Writing past an object's bounds can corrupt memory and become a security vulnerability.

174. Integer overflow

Unsigned overflow wraps; signed overflow is undefined behavior in standard C.

175. Checking return values

Always check APIs where failure matters: allocation, file/network I/O, parsing and system calls.

176. The *_create / *_destroy pattern

Signals ownership: create allocates/returns a resource; destroy releases it.

177. The init / cleanup pattern

Often initializes caller-owned storage and later releases contained resources.

178. Context object

A struct that groups subsystem state/config/dependencies and is passed through APIs instead of globals.

179. Callback + void *userdata

typedef void (*callback_fn)(int event, void *userdata);

Common C pattern for generic callbacks with caller-owned context.

180. Casts

Casts can document intentional conversion but can also hide type errors. Avoid unnecessary casts.

181. void *

Generic object pointer type used by allocators, callbacks and generic containers.

182. Flexible array member

struct packet {
    size_t len;
    unsigned char data[];
};

183. container_of

Macro technique used to recover a containing struct from a member pointer; common in kernels/low-level libraries.

184. Things that look scary but are normal

Pointers, double pointers, bit masks, callbacks, macros, opaque structs and cleanup gotos are common once you know the local ownership rules.

185. How to approach a completely unfamiliar repository

Step 1

Read README and build instructions.

Step 2

Identify the build system and dependencies.

Step 3

Build the unmodified project.

Step 4

Run tests.

Step 5

Find executable entry points.

Step 6

Read public headers/APIs.

Step 7

Trace one feature from caller to implementation.

Step 8

Inspect ownership and error paths.

Step 9

Use debugger/search tools only after you know the structure.

186. Minimal workflow for Make

make clean
make -j4
make test

187. Minimal workflow for CMake

cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build -j4
ctest --test-dir build --output-on-failure

188. Minimal sanitizer workflow

CC=clang CFLAGS='-g -O1 -fsanitize=address,undefined -fno-omit-frame-pointer' make

189. Crash debugging - ready-made pattern

Reproduce → capture exact input/version → run under debugger/sanitizer → get backtrace → inspect faulting frame and ownership.

190. Memory bug debugging - ready-made pattern

Build with ASan/UBSan or Valgrind, reproduce, inspect allocation/free stack traces, then add a regression test.

191. Debugging “program does nothing”

Check exit status, stdout/stderr, logs, blocking syscalls, expected inputs and whether you are running the correct binary.

192. Debugging “missing library”

Inspect linker/runtime-loader error, pkg-config output, library search paths and package installation.

193. Debugging undefined reference

Find the symbol definition and make sure its object/library is linked in the correct order/configuration.

194. Debugging “header not found”

Verify dependency development headers and compiler include paths (-I, pkg-config, build config).

195. Useful shell commands for a C project

rg 'main\(' .
find . -maxdepth 2 -type f | sort
file build/app
ldd build/app 2>/dev/null || true

196. How to check the project's coding standard

Look for .clang-format, .clang-tidy, CONTRIBUTING, CI lint jobs and formatting scripts.

197. What you do not need to know to read 80-90% of projects

You usually do not need compiler internals, linker-script mastery, exotic atomics or advanced macro metaprogramming.

198. What you really need to know well

Pointers/lifetimes, structs, functions, headers, build/linking, error handling, memory ownership and debugger basics.

199. Mini glossary

compiler

Translates source into machine/object code.

preprocessor

Processes # directives before compilation.

object file

Compiled but not fully linked machine code.

linker

Resolves symbols and produces binaries/libraries.

Declarations and shared definitions.

symbol

Named function/object visible to compiler/linker/debugger.

shared library

Runtime-loadable library.

static library

Archive copied into link output.

ABI

Binary calling/data-layout contract.

API

Source-level interface contract.

200. Cheat sheet: syntax

int x = 1;
if (x) { }
for (...) { }
return 0;

201. Cheat sheet: pointers

&x address
*p dereference
T * pointer to T
T ** pointer to pointer
-> member through pointer

202. Cheat sheet: build

cc -Wall -Wextra -g main.c -o app

203. Cheat sheet: Make

make
make -j4
make clean
make test

204. Cheat sheet: CMake

cmake -S . -B build
cmake --build build
ctest --test-dir build

205. Cheat sheet: GDB

break
run
next
step
continue
print
bt

206. Cheat sheet: diagnostics

compiler warnings
ASan/UBSan
Valgrind
GDB/LLDB
strace/truss
logs

207. Cheat sheet: entering an unfamiliar repo

README → build files → build → tests → main → headers → ownership → one feature flow

208. How to mentally read a C function

Identify inputs, outputs, nullable pointers, allocations, ownership transfers, error exits and cleanup.

209. Most important questions when reading C code

What can be NULL? Who allocates? Who frees? What is the lifetime? What does each return code mean? Can lengths overflow?

210. Most important question: who owns the memory?

Ownership determines who may mutate/free data and how long pointers remain valid.

211. Names that suggest ownership

Words such as new, create, alloc, dup, copy, free, destroy, release, borrow, ref, unref often reveal lifetime conventions.

212. Refcount

Reference counting keeps an object alive while its count is nonzero. Every retained reference must eventually be released.

213. Very short map of the C world

source/header → compiler → object files → linker → binary
runtime: stack + heap + OS/lib APIs
debug: warnings + sanitizers + debugger + tracing

214. Final checklist

Build cleanly, run tests, understand entry point, know ownership, check warnings, reproduce failures, use sanitizers/debugger, review cleanup/error paths.

215. Minimum knowledge to remember

Pointers and lifetimes, structs, headers, compilation/linking, Make/CMake, return-value checking, sanitizers and GDB are enough to understand most everyday C projects.

Official references

  • 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/