Chapter 5: C Programming for Systems
Introduction
C is the language of system programming. Created in the 1970s for writing Unix, C provides low-level access to memory, minimal runtime overhead, and direct mapping to machine instructions. Most operating systems, embedded systems, and performance-critical software are written in C.
Why This Matters
To write system software, you need a language that gives you control. C doesn't hide what the computer is doing - it exposes it. You manage memory yourself. You can access hardware directly. You understand exactly what assembly code your C will generate. This level of control is essential for kernels, drivers, and system tools.
How to Study This Chapter
- Write code - Reading about C isn't enough, type and run examples
- Compile and run - See what happens with different code
- Make mistakes - Segmentation faults teach lessons
- Read errors - GCC error messages are informative
- Use debuggers - GDB helps understand memory
Why C for System Programming?
1. Low-Level Access
C lets you:
- Access specific memory addresses
- Manipulate individual bits
- Interface directly with hardware
- Control memory layout
2. Minimal Runtime
- No garbage collector
- No large runtime library
- Predictable performance
- Small binary size
3. Portable Assembly
- C code maps closely to assembly
- You understand what the CPU will do
- Easy to mix C and assembly
4. Operating System Support
- System calls are C APIs
- Kernel APIs are C
- POSIX standard is C
- Most libraries provide C interfaces
5. Mature Ecosystem
- Decades of tools (compilers, debuggers)
- Extensive documentation
- Large community
- Battle-tested
C Program Structure
Hello World
#include <stdio.h>
int main(void) {
printf("Hello, World!\n");
return 0;
}
Breakdown:
#include <stdio.h>- Include standard I/O libraryint main(void)- Entry point, returns integerprintf()- Library function to printreturn 0- Exit with success status
Compilation Process
gcc hello.c -o hello
./hello
What happens:
- Preprocessor - Handles #include, #define
- Compiler - Converts C to assembly
- Assembler - Converts assembly to object code
- Linker - Combines object files and libraries into executable
Memory Management in C
Stack vs Heap
Stack:
- Automatic memory
- Local variables
- Function call frames
- Fast allocation/deallocation
- Limited size (typically a few MB)
Heap:
- Dynamic memory
- Manually managed with malloc/free
- Slower than stack
- Much larger (limited by available RAM)
Stack Example
void function() {
int x = 10; // Allocated on stack
char name[20]; // Allocated on stack
// When function returns, x and name are automatically freed
}
Stack frame for each function call:
+------------------+
| Return address |
| Parameters |
| Local variables |
+------------------+
Heap Example
#include <stdlib.h>
int main() {
// Allocate memory on heap
int *ptr = malloc(sizeof(int));
if (ptr == NULL) {
// Allocation failed
return 1;
}
*ptr = 42;
printf("Value: %d\n", *ptr);
// MUST free memory
free(ptr);
return 0;
}
Rules:
- Always check if malloc returns NULL
- Always free what you allocate
- Don't use memory after freeing it
- Don't free the same memory twice
Common Memory Allocation Functions
// Allocate memory
void *malloc(size_t size);
// Allocate and zero-initialize
void *calloc(size_t nmemb, size_t size);
// Resize allocated memory
void *realloc(void *ptr, size_t size);
// Free memory
void free(void *ptr);
Example:
// Allocate array of 10 integers
int *arr = malloc(10 * sizeof(int));
// Resize to 20 integers
arr = realloc(arr, 20 * sizeof(int));
// Free
free(arr);
Pointers
Pointers are the most powerful and dangerous feature of C.
What is a Pointer?
A pointer is a variable that stores a memory address.
int x = 42; // Regular variable
int *ptr = &x; // Pointer to x (stores address of x)
int value = *ptr; // Dereference pointer (get value at address)
Visualization:
Memory:
Address Value
0x1000 42 ← x is here
0x1004 0x1000 ← ptr stores address of x
&x = 0x1000 (address of x)
ptr = 0x1000 (ptr stores that address)
*ptr = 42 (value at that address)
Pointer Syntax
int *p; // Declare pointer to int
p = &x; // Set p to address of x (& = address-of operator)
*p = 10; // Set value at address p points to (* = dereference)
Pointer Arithmetic
Pointers can be incremented/decremented to navigate memory:
int arr[5] = {10, 20, 30, 40, 50};
int *ptr = arr; // Points to arr[0]
printf("%d\n", *ptr); // 10
ptr++; // Move to next int
printf("%d\n", *ptr); // 20
ptr += 2; // Move 2 ints forward
printf("%d\n", *ptr); // 40
Important: Pointer arithmetic accounts for type size.
ptr++forint*advances by 4 bytes (sizeof(int))ptr++forchar*advances by 1 byte
Arrays and Pointers
Arrays and pointers are closely related:
int arr[5] = {1, 2, 3, 4, 5};
// These are equivalent:
arr[0] == *(arr + 0)
arr[1] == *(arr + 1)
arr[i] == *(arr + i)
Array name = pointer to first element.
Function Pointers
C can store and call functions through pointers:
int add(int a, int b) {
return a + b;
}
int main() {
// Declare function pointer
int (*func_ptr)(int, int);
// Point to function
func_ptr = add;
// Call through pointer
int result = func_ptr(5, 3); // Returns 8
return 0;
}
Use cases: Callbacks, plugin systems, state machines.
Strings in C
C has no built-in string type - strings are arrays of characters ending with '\0'.
char name[] = "Alice";
// Stored in memory as:
// 'A' 'l' 'i' 'c' 'e' '\0'
// [0] [1] [2] [3] [4] [5]
String Functions (from string.h)
#include <string.h>
char str1[20] = "Hello";
char str2[20] = "World";
strlen(str1); // Length (5, not including \0)
strcpy(str1, str2); // Copy str2 to str1
strcat(str1, str2); // Concatenate str2 to str1
strcmp(str1, str2); // Compare (0 if equal)
Warning: Many string functions are unsafe (buffer overflows). Use safer versions:
strncpy()instead ofstrcpy()strncat()instead ofstrcat()snprintf()instead ofsprintf()
File I/O
System programming often involves reading and writing files.
Using Standard Library (stdio.h)
#include <stdio.h>
int main() {
FILE *file;
// Open file for writing
file = fopen("test.txt", "w");
if (file == NULL) {
perror("Error opening file");
return 1;
}
// Write to file
fprintf(file, "Hello, File!\n");
// Close file
fclose(file);
// Open file for reading
file = fopen("test.txt", "r");
if (file == NULL) {
perror("Error opening file");
return 1;
}
// Read from file
char buffer[100];
if (fgets(buffer, sizeof(buffer), file) != NULL) {
printf("Read: %s", buffer);
}
fclose(file);
return 0;
}
File modes:
"r"- Read"w"- Write (creates or truncates)"a"- Append"r+"- Read and write"rb","wb"- Binary mode
Using POSIX System Calls (unistd.h)
Lower-level, closer to OS:
#include <fcntl.h>
#include <unistd.h>
int main() {
// Open file (returns file descriptor)
int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("open");
return 1;
}
// Write
const char *text = "Hello, System Call!\n";
write(fd, text, strlen(text));
// Close
close(fd);
return 0;
}
Difference:
fopen/fprintf/fclose- Buffered, portable, higher-levelopen/write/close- Unbuffered, POSIX, lower-level
System Calls
System calls are the interface between programs and the kernel.
Common System Calls
// Process management
pid_t fork(void); // Create new process
int execve(const char *pathname, char *const argv[], ...);
void exit(int status); // Terminate process
pid_t wait(int *status); // Wait for child process
// File operations
int open(const char *pathname, int flags, mode_t mode);
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
int close(int fd);
// Memory
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);
// Signals
int kill(pid_t pid, int sig);
sighandler_t signal(int signum, sighandler_t handler);
Example: fork() System Call
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == -1) {
// Fork failed
perror("fork");
return 1;
}
else if (pid == 0) {
// Child process
printf("Child process (PID: %d)\n", getpid());
}
else {
// Parent process
printf("Parent process (PID: %d), child PID: %d\n", getpid(), pid);
wait(NULL); // Wait for child to finish
}
return 0;
}
Structures
Group related data together:
struct Point {
int x;
int y;
};
int main() {
struct Point p1;
p1.x = 10;
p1.y = 20;
struct Point *ptr = &p1;
printf("x: %d, y: %d\n", ptr->x, ptr->y);
return 0;
}
typedef for Convenience
typedef struct {
int x;
int y;
} Point;
// Now can use "Point" instead of "struct Point"
Point p1 = {10, 20};
Memory Layout of a C Program
When a C program runs, memory is organized:
+------------------+ High addresses
| Stack | (grows down)
| ↓ |
| |
| ↑ |
| Heap | (grows up)
+------------------+
| BSS segment | (uninitialized global variables)
+------------------+
| Data segment | (initialized global variables)
+------------------+
| Text segment | (program code)
+------------------+ Low addresses
Common C Pitfalls
1. Buffer Overflow
char buffer[10];
strcpy(buffer, "This is a very long string"); // OVERFLOW!
2. Memory Leaks
void leak() {
int *p = malloc(sizeof(int));
// Forgot to free!
}
3. Dangling Pointers
int *ptr = malloc(sizeof(int));
free(ptr);
*ptr = 10; // ERROR: Using freed memory!
4. Uninitialized Variables
int x;
printf("%d\n", x); // Undefined behavior
5. Off-by-One Errors
int arr[10];
for (int i = 0; i <= 10; i++) { // Should be i < 10
arr[i] = i; // Buffer overflow when i = 10
}
Key Concepts
- C provides low-level control essential for system programming
- Stack memory is automatic, heap requires manual management
- Pointers store memory addresses and enable powerful operations
- Strings are null-terminated character arrays
- System calls interface with the kernel
- Memory safety is the programmer's responsibility
Common Mistakes
- Forgetting to free memory - Causes memory leaks
- Using freed memory - Causes crashes
- Buffer overflows - Security vulnerabilities
- Not checking return values - Errors go unnoticed
- Pointer confusion - Dereferencing NULL or invalid pointers
Debugging Tips
- Use valgrind - Detects memory leaks and errors
- Use GDB - Step through code, inspect memory
- Enable warnings - Compile with
-Wall -Wextra - Initialize variables - Avoid undefined behavior
- Bounds checking - Always check array indices
Mini Exercises
- Write a program that allocates an array on the heap
- Create a function that takes a pointer parameter
- Implement your own strlen() function
- Write a program that reads a file line by line
- Use fork() to create a child process
- Define a struct and create pointers to it
- Write a program that uses function pointers
- Implement a simple linked list
- Practice pointer arithmetic with arrays
- Write a program that uses system calls (open, read, write, close)
Review Questions
- Why is C preferred for system programming?
- What's the difference between stack and heap memory?
- How do pointers relate to memory addresses?
- What is the null terminator in C strings?
- What's the difference between library functions and system calls?
Reference Checklist
By the end of this chapter, you should be able to:
- Explain why C is used for system programming
- Manage memory with malloc/free
- Use pointers effectively and safely
- Perform file I/O using stdio and system calls
- Work with strings in C
- Understand the C memory layout
- Make basic system calls
- Debug C programs
Next Steps
Now that you understand C programming, the next chapter explores the compilation toolchain: compilers, linkers, and libraries. You'll learn how C source code becomes executable machine code and how to use GCC effectively.
Key Takeaway: C gives you the control needed for system programming. With great power comes great responsibility - you must manage memory, handle pointers carefully, and understand exactly what your code does at the machine level.