NANDHOO.

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.