C rehearsal - MarekBykowski/readme GitHub Wiki
C standard versions
GCC (GNU Compiler Collection)
| Level | Operators | Note |
|---|---|---|
| 1 (highest) |
() [] -> . postfix++ --
|
always wins |
| 2 | prefix++ -- * & ! ~ sizeof
|
rightβleft |
| 3β4 |
* / % + -
|
arithmetic |
| 5 |
<< >>
|
shifts |
| 6β7 |
< <= >= > == !=
|
== is above & ! |
| 8 | & |
bitwise AND |
| 9 | ^ |
bitwise XOR |
| 10 | | |
bitwise OR |
| 11β12 |
&& ||
|
logical |
| 14 (lowest) |
= += -= β¦ |
assignment |
// TRAP β == binds tighter than &
val & 0x01 == 0 // β val & (0x01==0) β val & 0 β always 0!
(val & 0x01) == 0 // correct
// TRAP β postfix++ beats *
*p++ // β *(p++) β deref old p, then advance
(*p)++ // increment the value p points toconst int *p1 = &x; // pointer to const int
int *const p2 = &x; // const pointer to int
const int *const p3 = &x; // const pointer to const int| Pointer |
*p (value) |
p (address) |
|---|---|---|
const int *p1 |
β locked | β allowed |
int *const p2 |
β allowed | β locked |
const int *const p3 |
β locked | β locked |
π‘ Read rightβleft:
const int *p= "p is a pointer to const int"
// Macros (Linux kernel style)
#define BIT(n) (1u << (n))
#define GENMASK(h,l) (((1u << ((h)-(l)+1))-1) << (l))
val |= (1u << n); // SET bit n
val &= ~(1u << n); // CLEAR bit n
val ^= (1u << n); // TOGGLE bit n
// SET bits 3β7 to 'mode' β raw, no macros
val = (val & ~(0x1Fu << 3)) | ((mode & 0x1Fu) << 3);
// step 1: build mask β shift to pos 3 β negate β AND (clears bits 3-7)
// step 2: shift mode to pos 3 β OR (writes new value)
// READ bits 3β7
uint32_t m = (*reg >> 3) & 0x1F;
β οΈ Always clear first, then set.|=alone cannot zero bits already set to 1.
// Bad β 12 bytes (wasted padding)
struct foo { char a; int b; char c; };
// offsets: a=0, [3B pad], b=4, c=8, [3B pad] = 12B
// Good β 8 bytes (largest field first)
struct bar { int b; char a; char c; };
// offsets: b=0, a=4, c=5, [2B pad] = 8B| Tool | What it does | When to use |
|---|---|---|
offsetof(T, f) |
field offset in bytes | always safe |
sizeof(struct T) |
total size with padding | always |
__attribute__((packed)) |
removes all padding | protocols, not HW registers |
_Alignas(N) |
forces alignment to N | DMA buffers, cache lines |
β οΈ packed= slower load (4ΓLDRBinstead ofLDR). On Cortex-M0 β HardFault on unaligned pointer access.
uint32_t x = 0x12345678
LITTLE ENDIAN (ARM / x86) BIG ENDIAN (network / TCP)
addr byte addr byte
+0 0x78 β *p (LSB) +0 0x12 β *p (MSB)
+1 0x56 +1 0x34
+2 0x34 +2 0x56
+3 0x12 (MSB) +3 0x78 (LSB)
// Endianness test
uint32_t x = 0x12345678;
uint8_t *p = (uint8_t *)&x;
if (*p == 0x78) puts("little endian");
else puts("big endian");
htonl(val); // host β network (big endian)
ntohl(val); // network β hostπ‘ MAVLink = little endian Β· TCP/IP = big endian Β· ARM = little endian by default
// Bad β compiler caches the read at -O2
uint32_t *reg = (uint32_t *)0x40011004;
while (*reg & 0x01 == 0); // also a precedence bug!
// Correct
volatile uint32_t *reg = (volatile uint32_t *)0x40011004;
while ((*reg & 0x01) == 0); // fresh load every iteration| Requirement | Why |
|---|---|
volatile |
compiler must not cache β every access = actual load/store |
| aligned address |
LDR requires addr % sizeof(type) == 0
|
~MASK before |=
|
OR alone cannot clear bits already set to 1 |
// PACKED β removes padding between fields
struct __attribute__((packed)) pkt {
uint8_t magic; // offset 0
uint32_t seq; // offset 1 β no padding inserted
}; // sizeof = 5
// BITFIELD β controls field size in bits
struct uart_reg {
uint32_t enable : 1; // 1 bit, values 0β1
uint32_t mode : 3; // 3 bits, values 0β7
uint32_t baud : 4; // 4 bits, values 0β15
uint32_t reserved : 24; // 24 bits β 1+3+4+24 = 32 total
}; // sizeof = 4packed |
bitfield |
|
|---|---|---|
| Controls | padding between fields | field size in bits |
| Min. field | 1 byte | 1 bit |
&field |
works | β compile error |
| Bit order | N/A | unspecified by standard |
| Linux kernel | network protocols | IP header only |
β οΈ Mixing container types (uint8_t+uint32_t) in bitfields β unexpected padding between groups. Use one type throughout.
| # | Name | What happens | Fix |
|---|---|---|---|
| β | Signed overflow |
INT_MAX + 1 β compiler may remove checks |
use unsigned or check before |
| β | Strict aliasing |
*(uint32_t*)&float_var β wrong value at -O2
|
use memcpy() or union
|
| β | No sequence point |
printf("%d %d", *p++, *p++) β anything goes |
one *p++ per statement |
| β | Null deref before check |
*p = 42; if(p==NULL) β branch eliminated |
check before deref |
// ; is a sequence point β side effects committed before next statement
printf("%d ", *p++); // ok β p++ committed after ;
printf("%d ", *p++); // ok β p already advanced
printf("%d\n", *p); // ok β no modificationint a[] = {1,2,3,4,5};
int *p = &a[1]; // points to a[1]
int *q = &a[4]; // points to a[4]
q - p; // β 3 (elements, not bytes!) type: ptrdiff_t
p + 2; // β &a[3], advances 2Γsizeof(int) bytes in memory
// Pointer TO an array (not to an element)
int (*ap)[3] = &a; // type: int(*)[3]
(*ap)[0]; // β 1: deref first (β array), then index
ap++; // advances sizeof(int[3]) = 12 bytes!
*ap[0] // β *(ap[0]) β [] beats * β DIFFERENT THING!| Type | 32-bit | 64-bit (Linux) |
|---|---|---|
int |
4 bytes | 4 bytes |
long |
4 bytes | 8 bytes |
| pointer | 4 bytes | 8 bytes |
ptrdiff_t |
4 bytes | 8 bytes |
uint32_t |
4 bytes | 4 bytes β always, use this in embedded! |
How to compute -x: flip all bits, add 1
+3 = 0000 0011
β 1111 1100 (flip bits)
β 1111 1101 (+1) = -3
Why it works: 2βΏ - x (complement to a power of two)
2β΄ - 3 = 13 = 1101
check: 0011 + 1101 = 10000 β carry discarded β 0000 β
| 4-bit | value | 4-bit | value |
|---|---|---|---|
0111 |
+7 (MAX) | 1000 |
-8 (MIN) |
0001 |
+1 | 1111 |
-1 |
0000 |
0 | 1001 |
-7 |
// Trap: INT_MIN has no positive counterpart!
INT_MAX = 2147483647 = 0x7FFFFFFF
INT_MIN = -2147483648 = 0x80000000
abs(INT_MIN) β UB! (result doesn't fit in int)| Instruction | Size | Required alignment |
|---|---|---|
LDR X0, [X1] |
8 bytes (64-bit) | addr % 8 == 0 |
LDR W0, [X1] |
4 bytes (32-bit) | addr % 4 == 0 |
LDRH W0, [X1] |
2 bytes | addr % 2 == 0 |
LDRB W0, [X1] |
1 byte | none |
; packed: int at offset 1 (unaligned)
; compiler replaces one LDR with 4Γ LDRB + bit-shifts:
LDRB W0, [X1, #1] ; byte 0 of int
LDRB W1, [X1, #2] ; byte 1 of int
LDRB W2, [X1, #3] ; byte 2 of int
LDRB W3, [X1, #4] ; byte 3 of int| Architecture | Unaligned access result |
|---|---|
ARMv8-A Linux (SCTLR.A=0) |
hardware fixup, slower |
| Cortex-M0 / M0+ | HardFault! |
| Cortex-M3 / M4 / M7 | partial fixup (LDR ok, LDM/STM no) |
| x86 | works, slightly slower |
High address
βββββββββββββββββββ
β stack β local vars, function args, return addr β grows DOWN
β β β fixed size (usually 8MB on Linux), overflow = segfault
βββββββββββββββββββ€
β (gap) β
β β β
β heap β malloc/free β grows UP, you manage it manually
βββββββββββββββββββ€
β BSS β uninitialised globals & statics β zero-initialised by OS
βββββββββββββββββββ€
β data β initialised globals & statics (e.g. int x = 42;)
βββββββββββββββββββ€
β text (code) β executable instructions β read-only
βββββββββββββββββββ
Low address
int g_init = 42; // data segment
int g_uninit; // BSS β zeroed automatically
void foo() {
int local = 1; // stack
int *p = malloc(4); // p on stack, *p on heap
static int s = 0; // BSS (uninit static) or data (init static)
}π‘ In embedded (no OS): BSS is zeroed by startup code (crt0), heap may not exist at all β everything on stack or statically allocated.
// Basic usage
int *p = malloc(sizeof(int) * 10);
if (!p) { /* always check! malloc returns NULL on failure */ }
memset(p, 0, sizeof(int) * 10); // heap memory is NOT zeroed
free(p);
p = NULL; // good habit β prevents use-after-free
// calloc β allocates AND zeroes
int *p2 = calloc(10, sizeof(int));
// realloc β resize existing allocation
p2 = realloc(p2, 20 * sizeof(int)); // may move the block!| Bug | What happens | How to detect |
|---|---|---|
| Memory leak |
malloc without free β heap grows until OOM |
Valgrind, AddressSanitizer |
| Double-free |
free(p); free(p); β UB, heap corruption |
ASan, guard pages |
| Use-after-free | access *p after free(p) β UB, stale data or crash |
ASan |
| Buffer overflow | write past end of malloc'd block β heap corruption | ASan, bounds checking |
| NULL deref | skipping the if (!p) check β crash |
code review |
// Safe pattern
void *safe_malloc(size_t n) {
void *p = malloc(n);
if (!p) { perror("malloc"); exit(EXIT_FAILURE); }
return p;
}
β οΈ In embedded: avoid dynamic allocation entirely if possible. Heap fragmentation on a microcontroller with 64KB RAM can cause allocation failures hours into a flight.
void *memcpy (void *dst, const void *src, size_t n); // UB if regions overlap
void *memmove(void *dst, const void *src, size_t n); // safe if regions overlapmemcpy β assumes NO overlap, may use SIMD/vectorised copy β faster
memmove β handles overlap: copies via temp buffer or adjusts direction
char buf[] = "hello world";
// Safe β regions don't overlap
memcpy(buf + 6, "there", 5); // "hello there"
// UNSAFE with memcpy β overlapping regions
memcpy(buf + 1, buf, 5); // UB! use memmove instead
memmove(buf + 1, buf, 5); // correct
// Classic use: shifting array elements
memmove(&a[1], &a[0], n * sizeof(int)); // insert at frontπ‘ Rule of thumb: if in doubt, use
memmove. The performance difference is negligible unless you're in a hot loop.
// Declaration: pointer to function taking int, returning int
int (*fp)(int);
// Assign and call
int square(int x) { return x * x; }
fp = square;
int result = fp(5); // β 25
int result = (*fp)(5); // same β both are valid
// typedef makes it readable
typedef int (*transform_fn)(int);
transform_fn fn = square;
// As function parameter (callback)
void apply(int *arr, int n, transform_fn fn) {
for (int i = 0; i < n; i++) arr[i] = fn(arr[i]);
}
// Dispatch table β replaces switch/if-else chains
typedef void (*cmd_handler)(void);
cmd_handler dispatch[] = {
[CMD_ARM] = handle_arm,
[CMD_DISARM] = handle_disarm,
[CMD_TAKEOFF]= handle_takeoff,
};
dispatch[cmd](); // O(1) dispatch, no branchingπ‘ Dispatch tables are the embedded alternative to virtual functions in C++. Used heavily in drone firmware for command handling.
// static at file scope β internal linkage (invisible outside this .c file)
static int counter = 0; // not accessible from other .c files
static void helper(void) {} // private function
// static at function scope β persistent across calls
void count(void) {
static int n = 0; // initialised once, lives for program lifetime
printf("%d\n", ++n);
}
// extern β declared here, defined in another .c file
extern int g_system_state; // tells compiler "trust me, it exists"
extern void uart_init(void);
// inline β hint to compiler to expand inline (no function call overhead)
static inline uint32_t clamp(uint32_t v, uint32_t lo, uint32_t hi) {
return v < lo ? lo : v > hi ? hi : v;
}
// static inline = inline + internal linkage β the correct pattern for headers| Keyword | Scope | Effect |
|---|---|---|
static (file) |
translation unit | internal linkage β invisible to linker |
static (local) |
function | persistent storage, init once |
extern |
file | external linkage β defined elsewhere |
inline |
function | hint to expand inline, no call overhead |
static inline |
header | inline + no multiple-definition linker error |
// volatile β prevents compiler caching, does NOT guarantee atomicity
volatile uint32_t *reg = (volatile uint32_t *)0x40011004;
while ((*reg & 0x01) == 0); // compiler must re-read every iteration
// C11 atomics β guarantee both visibility AND atomicity
#include <stdatomic.h>
atomic_int shared = ATOMIC_VAR_INIT(0);
atomic_fetch_add(&shared, 1); // thread-safe increment
int v = atomic_load_explicit(&shared, memory_order_acquire);volatile |
atomic |
|
|---|---|---|
| Prevents compiler caching | β | β |
| Prevents CPU reordering | β | β |
| Atomic read-modify-write | β | β |
| Use for HW registers | β | β |
| Use for shared variables (threads/ISR) | β alone | β |
// Classic ISR pattern β volatile is correct here (single read/write, no RMW)
volatile bool flag = false;
void ISR_handler(void) {
flag = true; // single store β atomic on most architectures
}
void main_loop(void) {
while (!flag); // volatile ensures re-read every time
flag = false;
process();
}
β οΈ volatileis NOT a substitute for atomics in multi-threaded code. On multi-core ARM, you need memory barriers too.
// Variables shared between ISR and main must be volatile
volatile uint32_t tick_count = 0;
void SysTick_Handler(void) { // ISR β called by hardware
tick_count++; // write
}
void main(void) {
while (1) {
uint32_t t = tick_count; // volatile β reads fresh value
do_work(t);
}
}Critical section β disable interrupts for atomic read-modify-write:
// ARM Cortex-M
uint32_t save = __get_PRIMASK();
__disable_irq(); // enter critical section
shared_var += 1; // safe β interrupts disabled
__set_PRIMASK(save); // restore (don't just re-enable blindly)
// Linux kernel equivalent
unsigned long flags;
spin_lock_irqsave(&lock, flags);
shared_var += 1;
spin_unlock_irqrestore(&lock, flags);ISR rules:
| Rule | Why |
|---|---|
| Keep ISR short | interrupts blocked while ISR runs |
No malloc/free in ISR |
heap is not reentrant |
No blocking calls (printf, sleep) |
will deadlock or corrupt |
| Set a flag, process in main loop | standard pattern |
All shared vars must be volatile
|
or compiler optimises reads away |
A hardware counter that resets the CPU if not "petted" (reset) within a timeout. Prevents firmware from hanging silently.
// Initialise watchdog with 1s timeout
watchdog_init(1000); // 1000ms
// Main loop β must pet before timeout
while (1) {
watchdog_kick(); // reset the counter ("I'm alive")
do_work();
}
// If do_work() hangs β watchdog fires β CPU reset β system recoversπ‘ In drone firmware: watchdog is critical. A hung flight controller that doesn't reset is more dangerous than one that reboots. Always enable it in production.
// Traditional include guard β portable, standard C
#ifndef MY_HEADER_H
#define MY_HEADER_H
// ... header content ...
#endif // MY_HEADER_H
// #pragma once β simpler, supported by GCC/Clang/MSVC, not standard C
#pragma once
// ... header content ...| Include guard | #pragma once |
|
|---|---|---|
| Standard C | β | β (compiler extension) |
| GCC / Clang / MSVC | β | β |
| Handles symlinks/copies | β | β (may include twice) |
| Less boilerplate | β | β |
| Linux kernel uses | β guards | β |
π‘ For embedded/cross-compiler work: use include guards.
#pragma onceis fine for most projects but fails edge cases with symlinked headers.
// Object-like macro
#define MAX_DRONES 16
#define PI 3.14159f
// Function-like macro β parenthesise everything!
#define MAX(a, b) ((a) > (b) ? (a) : (b)) // safe
#define SQUARE(x) ((x) * (x)) // safe
#define BAD(x) x * x // unsafe: BAD(1+2) β 1+2*1+2 = 5!
// Stringification β # turns token into string literal
#define STRINGIFY(x) #x
STRINGIFY(hello) β "hello"
// Token pasting β ## concatenates tokens
#define REG(n) reg_##n
REG(status) β reg_status
// Conditional compilation
#ifdef DEBUG
#define LOG(msg) printf("[DBG] %s\n", msg)
#else
#define LOG(msg) ((void)0) // compiles to nothing
#endif
// X-macro pattern β define list once, generate multiple things
#define STATES(X) X(IDLE) X(ARMED) X(FLYING) X(FAULT)
typedef enum {
#define ENTRY(n) STATE_##n,
STATES(ENTRY)
#undef ENTRY
} state_t;
static const char *state_names[] = {
#define ENTRY(n) [STATE_##n] = #n,
STATES(ENTRY)
#undef ENTRY
};The pipeline:
source.c β [preprocessor] β source.i
source.i β [compiler] β source.s (assembly)
source.s β [assembler] β source.o (object file)
source.o β [linker] β binary / .elf / .so
CC = arm-none-eabi-gcc
CFLAGS = -Wall -Wextra -O2 -mcpu=cortex-m4 -mthumb
LDFLAGS = -T linker.ld -lc
# Pattern rule β compile any .c to .o
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# Link all objects into final binary
firmware.elf: main.o uart.o mavlink.o
$(CC) $(LDFLAGS) $^ -o $@
# Useful targets
clean:
rm -f *.o *.elf
# Force rebuild if header changes
main.o: main.c uart.h mavlink.hKey flags to know:
| Flag | Meaning |
|---|---|
-c |
compile only, don't link β produces .o
|
-o |
output filename |
-Wall -Wextra |
enable warnings (always use) |
-O0 |
no optimisation (debug) |
-O2 |
optimise (production) |
-g |
include debug symbols |
-I path |
add include search path |
-L path |
add library search path |
-l name |
link library libname.a
|
-D NAME |
define preprocessor macro |
-mcpu=cortex-m4 |
target CPU (embedded) |
-mthumb |
use Thumb instruction set |
-fstack-usage |
report stack usage per function |
π‘ For embedded: always check
.mapfile after linking β it shows exactly where each symbol lands in flash/RAM. Critical for catching BSS/data overflows on small MCUs.
Generated during prep session Β· Swarmer Integration Engineer Β· 2026
Co-authored by Claude Sonnet 2026