Many operating-system interfaces are designed around C.
Linux system calls are exposed through C functions, kernels are largely written in C, and tools such as xv6 use C to demonstrate how processes, memory, scheduling, and file systems work internally.
You do not need to become an expert C programmer before continuing with this course. However, you should be comfortable reading small C programs, compiling them, working with pointers, and managing memory explicitly.
This chapter covers that essential subset.
C gives programmers direct control over memory and hardware while still providing useful abstractions such as functions, structures, and data types.
Unlike languages such as Java or Python, C does not normally provide automatic memory management, built-in bounds checking, or a large runtime environment.
That makes C less forgiving, but it also makes important system behavior visible.
When a C program allocates memory, opens a file, creates a thread, or communicates with the operating system, there is usually very little hidden between the program and the underlying OS interface.
This makes C especially useful for learning operating systems.
A basic C program looks like this:
The #include line makes declarations from the standard input-output library available to the program. The printf function prints formatted output.
Every executable C program begins from a function called main.
The value returned from main becomes the program’s exit status. Returning 0 usually indicates that the program completed successfully.
Save the program as hello.c, then compile it:
Run the generated executable:
You should see:
The compiler flags used here are useful throughout the course:
| Flag | Purpose |
|---|---|
-Wall | Enables common compiler warnings |
-Wextra | Enables additional warnings |
-g | Includes debugging information |
-o hello | Names the generated executable hello |
Always pay attention to compiler warnings. In systems programming, a warning about an invalid pointer, incorrect type, or uninitialized value often indicates a real bug.
The source file you write is not directly executed by the CPU.
A simplified build process looks like this:
Your source file never contains the code for printf. The linker is the stage that connects your call to an implementation that lives somewhere else.
The preprocessor handles directives such as #include and #define.
The compiler translates C code into lower-level instructions. The assembler converts those instructions into machine code, and the linker combines your code with required libraries.
The gcc command usually performs all these steps for you.
Later in the course, this process will help explain executable files, shared libraries, loading, and program startup.
C is statically typed, which means every variable has a declared type.
Common types include:
| Type | Typical use |
char | A character or small integer |
int | A general-purpose integer |
long | A larger integer |
float | A single-precision decimal value |
double | A double-precision decimal value |
The exact size of some C types can vary between platforms. Use sizeof when the size matters:
The %zu format specifier is used for values returned by sizeof.
For system-level code that requires an exact integer width, C provides fixed-width types through <stdint.h>:
Here, uint32_t represents an unsigned 32-bit integer, while int64_t represents a signed 64-bit integer.
Conditionals and loops in C will look familiar if you have used Java, JavaScript, or C++.
A basic loop looks like this:
Functions declare the type of their return value and each parameter:
They can then be called normally:
A function that returns no value uses the void type:
Writing void inside the parameter list explicitly states that the function accepts no arguments.
An array stores multiple values of the same type in consecutive memory locations.
Array indexes begin at zero:
C does not automatically check whether an index is within the array’s bounds.
This code is invalid:
The compiler may not stop you, but the program may read unrelated memory, produce an incorrect result, or crash.
This is one of the most important differences between C and many higher-level languages.
The number of elements in a local array can be calculated using:
This works because sizeof(values) gives the total size of the array, while sizeof(values[0]) gives the size of one element.
C does not have a built-in string type.
A string is represented as an array of characters ending with a special null character, written as '\0'.
The memory contains:
The null character tells string functions where the string ends.
You can print a string using %s:
The standard string library provides functions such as strlen, strcmp, and memcpy:
You must always ensure that a character buffer has enough space for both the text and the final null character.
For safer formatted string construction, prefer snprintf over sprintf:
The buffer size prevents the function from writing beyond the allocated array.
Pointers are the most important C concept for operating-system programming.
A pointer is a variable that stores a memory address.
Consider:
Here:
value stores the integer 10.&value means “the address of value.”pointer stores that address.int * means “pointer to an integer.”You can access the value stored at that address using the dereference operator:
This prints:
You can also modify the original variable through the pointer:
The result is:
The mental model is:
The pointer and the value are separate variables with their own storage. What connects them is not a copy of the data, but an address.
The pointer does not contain the integer itself. It contains the address where the integer is stored.
C normally passes function arguments by value.
This means the function receives a copy:
Calling this function does not modify the original variable:
The output remains 10.
To modify the original value, pass its address:
The output is now:
Pointers allow functions to modify existing data, return multiple results, and work efficiently with large structures without copying them.
Operating-system interfaces frequently use pointers to exchange buffers, structures, and status information.
A pointer that does not currently refer to a valid object should be set to NULL:
Never dereference a null pointer:
Doing so will usually crash the program.
Before using a pointer returned by a function, check that it is valid.
Arrays and pointers are closely related in C.
When passed to a function, an array usually becomes a pointer to its first element:
The function receives both the pointer and the number of elements because the pointer alone does not contain the array’s length.
It can be called like this:
The const keyword means the function promises not to modify the array through this pointer.
Use const whenever a function only needs to read data.
A structure groups related values into one type.
You can create and access a structure like this:
Structures are heavily used in operating-system programming. They can represent processes, files, memory regions, network addresses, and many other system objects.
A common style uses typedef to create a shorter type name:
You can then write:
instead of:
When you have a pointer to a structure, you can access its fields using the -> operator:
This:
is equivalent to:
The arrow syntax is easier to read and is used extensively in C systems code.
C programs commonly use two important memory regions: the stack and the heap.
Local variables are usually stored on the stack:
The variable exists while the function is executing. When the function returns, its stack storage is automatically reclaimed.
The heap is used for memory that must remain available until the program explicitly releases it. The two regions differ in who ends the lifetime of the memory:
Stack memory is reclaimed on its own when the function returns. Heap memory is reclaimed only if your code calls free. Skip that call and the block stays reserved for as long as the process runs, which is what a memory leak actually is.
Memory is allocated from the heap using malloc:
Always check whether allocation succeeded:
You can then use the allocated memory:
When the memory is no longer needed, release it with free:
A complete example looks like this:
Using sizeof(*value) instead of sizeof(int) keeps the allocation correct even if the pointer’s type changes later.
Manual memory management makes several mistakes possible.
A memory leak occurs when allocated memory is never released:
The program has lost the address returned by malloc, so it can no longer call free on that memory.
A use-after-free occurs when a program accesses memory after releasing it:
A double free occurs when the same allocation is released twice:
An invalid access occurs when a program reads or writes beyond an allocated region.
These errors can cause crashes, corrupted data, security vulnerabilities, or unpredictable behavior.
A useful habit is to assign NULL after freeing a pointer:
This does not prevent every memory bug, but it makes accidental reuse easier to detect.
Loading simulation...
An enumeration defines a group of named integer values.
You can then use the type directly:
Named values are easier to understand than unexplained integers.
C also supports compile-time constants through #define:
For typed constants, const is often clearer:
Operating systems frequently store multiple boolean settings inside a single integer.
Bitwise operations allow a program to manipulate individual bits.
Suppose we define three permissions:
Their bit patterns are:
The bitwise OR operator combines flags:
The result is:
The bitwise AND operator checks whether a flag is present:
A flag can be removed using AND with a complemented mask:
You do not need advanced bit manipulation yet. For now, understand that bit flags provide a compact way to represent permissions, options, and hardware states.
Small programs can live in a single source file. Larger programs are usually divided into multiple files.
A header file contains shared declarations.
calculator.h:
The #ifndef, #define, and #endif lines form an include guard. They prevent the header from being processed more than once in the same source file.
The implementation goes in a .c file.
calculator.c:
Another file can use the function.
main.c:
Compile both source files together:
System-level projects often separate public interfaces into headers and implementations into source files.
C functions often report success or failure through their return value.
For example, fopen returns a pointer to an opened file. If the operation fails, it returns NULL.
The perror function prints a message describing the most recent system or library error.
Do not ignore return values from functions that can fail.
Weak systems code assumes every operation succeeds. Reliable systems code checks errors and decides how to recover or terminate safely.
The following program combines structures, pointers, dynamic memory, strings, and bit flags:
Compile and run it:
The output should be similar to:
You do not need to memorize every line. Make sure you can identify the structure, pointer, heap allocation, null check, field access, bit flags, and memory cleanup.
As you work through the course, follow a few simple rules.
Compile with warnings enabled:
Initialize variables before using them. Check pointers before dereferencing them. Check the return value of operations that can fail.
Keep track of who owns dynamically allocated memory and where it should be released. Never assume that arrays automatically prevent out-of-bounds access.
Prefer clear code over clever code. Low-level programs are difficult enough to debug without unnecessary complexity.
C provides direct control over data and memory, which makes operating-system behavior easier to observe.
For this course, the most important concepts are:
The most important mental model is:
In C, your program works directly with memory addresses and resources, so it is responsible for using them correctly and releasing them when they are no longer needed.
5 quizzes