C Programming for Systems

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

  1. Write code - Reading about C isn't enough, type and run examples
  2. Compile and run - See what happens with different code
  3. Make mistakes - Segmentation faults teach lessons
  4. Read errors - GCC error messages are informative
  5. 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 library
  • int main(void) - Entry point, returns integer
  • printf() - Library function to print
  • return 0 - Exit with success status

Compilation Process

gcc hello.c -o hello
./hello

What happens:

  1. Preprocessor - Handles #include, #define
  2. Compiler - Converts C to assembly
  3. Assembler - Converts assembly to object code
  4. 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++ for int* advances by 4 bytes (sizeof(int))
  • ptr++ for char* 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 of strcpy()
  • strncat() instead of strcat()
  • snprintf() instead of sprintf()

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

  1. Forgetting to free memory - Causes memory leaks
  2. Using freed memory - Causes crashes
  3. Buffer overflows - Security vulnerabilities
  4. Not checking return values - Errors go unnoticed
  5. 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

  1. Write a program that allocates an array on the heap
  2. Create a function that takes a pointer parameter
  3. Implement your own strlen() function
  4. Write a program that reads a file line by line
  5. Use fork() to create a child process
  6. Define a struct and create pointers to it
  7. Write a program that uses function pointers
  8. Implement a simple linked list
  9. Practice pointer arithmetic with arrays
  10. Write a program that uses system calls (open, read, write, close)

Review Questions

  1. Why is C preferred for system programming?
  2. What's the difference between stack and heap memory?
  3. How do pointers relate to memory addresses?
  4. What is the null terminator in C strings?
  5. 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.