This post is going to explain in a short and simple manner:
- What happens after you’ve written a program in e.g. C and compile it for your operating system
- How programs run on your operating system
I am going to start with a simple program written C:
#include <stdio.h>
int main(void) {
printf("hello world!\n");
}
As you probably already know, this program writes “hello world!” to the console.
I will now try to explain to you the three fundamental basics in a simplified manner to give you a rough overview of the “magic” that’s happening after compiling and executing your program.
These basics are:
- How does the underlying architecture work?
- How is your code transferred into a form that your CPU can understand?
- How does your operating system handle your program?
How does the underlying architecture work?
When I am talking about the underlying architecture I am referring to the Von Neumann architecture:

von neumann architecture
The Von Neumann architecture is a computer design that has evolved over the years but in its core hasn’t changed and is still being used today.
Here is a short summary of how it works in theory in terms of running code:
At first the code your CPU is going to execute is loaded into the memory. Afterwards the code is being fetched and then it’s executed.
Here is an example of how it works in practice:
The code is loaded into the memory. The memory is an array of bytes and each byte has its own adress. The so called Random Adress Memory.
Let’s assume your compiler compiled our “hello world!” program into the following array of bytes (to spare some space it’s represented in hexadecimal, in practice it’s in binary):
0x4401410248034D042000
So your program is now loaded into the memory in the form of an array of bytes. The next step is to fetch the program.
I will use a 16-bit architecture CPU as example to explain this step. The instruction words of the CPU look like this:
![]()
16 Bit architecture
So the first 4 bits (15-12) represent the operation that the CPU is supposed to execute (also called opcode). The next 4 bits define the source being used for the operation. The next four bits (As, B/W and Ad) are used to define specific modes for the operation. We won’t cover these modes in our example to keep stuff simple.
The last 4 bits define the destination being used for the operation.
Since it’s a 16 bit architecture we’ll fetch the first 2 bytes (16bits) of our program from the memory. Which would be:
0x4401
Now we need the binary representation in order to see what this instruction looks like:
0100 0100 0000 0001
So the opcode is 0100. In our example 0100 represents the MOV instruction.
Our source is 0100. We interpret the source as a number in this case. So the source simply is a decimal 4. (The modes I mentioned are responsible for how to interpret the source and destination. Alternatively the source could be interpreted as Register 4 but in this case we assume that the modes define it as a decimal 4.)
The modes are 0000 so we don’t need to think about these.
The destination is 0001 which we interpret as Register 1 (or eax).
So in assembly the instruction we fetched from the memory could look like this:
MOV 4, EAX
After the instruction has been fetched it can be executed. Which would simply put a decimal 4 in the register EAX.
Now we hopefully understand how the Von Neumann architecture works in theory in terms of executing code.
How is your code transferred into a form that your CPU can understand?
In the 16-bit architecture example I used we saw how the CPU handles the program that has been loaded into the memory. But how has your “hello world!” program been turned into the array of bytes that the CPU can understand?
#include <stdio.h>
int main(void) {
printf("hello world!\n");
}
This code is easy to produce and read for programmers. That’s because it is an abstraction of what a machine can understand.
The compiler is responsible to translate this code into machine instructions.
We already know how our instructions are supposed to look from our last example.
Now we will try to reproduce what the compiler does with our little “hello world!” program.
To do this we need to understand the basics of assembly. Assembly consists of simple instructions that can be directly translated into machine code.
In assembly we control the registers of our CPU. We can move values from the RAM into registers, do basic mathematic operations, compare values in registers, etc.
What we also need to know is that writing to the console (what our “hello world” program does) is an operation that requires a system call (telling the kernel that we need him to do a task for us because we aren’t allowed to simply write something to the console. I will cover that topic later on.)
So we need to make a system call by moving stuff into registers and then telling the kernel to take care of our program.
Our program is using the printf() function of the C-library <stdio.h>.
The POSIX defines an API which most operating system are compatible with (Windows, Linux, …). This API defines that specific values in specific registers trigger specific operations when the kernel is called. Since the <stdio.h> library sticks to the POSIX API we could theoretically look at the POSIX API to know what printf() could look like in assembly.
So what do we need to put in our registers to tell the kernel to print “hello world!” to the console?
A 4 in register EAX is telling the kernel to write something.
An int value in register EBX is telling the kernel where to write it (1 means standard output).
A pointer in ECX tells the kernel where to look for the string that is supposed to be printed.
An int value in EDX tells the kernel the length of the string.
To tell the kernel that we need it to take care of our program we are using a software interrupt which has the value 0x80. We literally interrupt the CPU and kernel and give the interrupt handler of the kernel the value 0x80 so it knows that the interrupt was made by a program.
The interrupt handler then will look into the EAX register in order to know what system call we are requesting (in our example we want it to write to the standard output).
So the assembly of our “hello world!” program could look like this:
MOV 4, eax
MOV 1, ebx
MOV txt, ecx
MOV 13, edx
int 0x80
txt: .ascii "hello world!\n"
Transferring assembly code into binary is a piece of cake. In our 16-bit CPU example we already did it the other way around.
So let’s reproduce what the assembler would do if we were on the mentioned 16-bit architecture.
![]()
16 Bit architecture
MOV has the value 4
EAX, … , EDX has the value 1, … , 4
int (interrupt) has the value 2
For our pointer txt we’ll simply use the value 8 for simplicity.
So the first line
MOV 4, eax
Would translate into 0100 0100 0000 0001. (0x4401)
MOV 1, ebx
translates into 0100 0001 0000 0010 (0x4102)
If we keep going like that we end up with the program: 0x4401410248034D042000
That’s how our compiler took care of our program and transferred it into machine code.
How does your operating system handle your program?
We already know that our program is loaded into the memory in the form of byte arrays.
Your operating system is also a program which has been loaded into memory after booting your machine.
So your operating system consists of the same assembly instructions our “hello world!” consists of.
The only difference is that the operating system is taking care of “meta” tasks like memory management, process handling and so on.
To make sure that small programs like ours can’t crash the PC, the operating system separates the memory into two parts. One part is called “kernelland” and the other part is called “userland”. Programs that run in userland can’t directly access the hardware. They can only do system calls that tell the kernel that it needs to do stuff for our program in userland.
Here is a little sketch of it:
So when we execute our little “hello world!” program it is loaded into userland with its own stack and then uses a system call to write to the screen you are looking at.
In summary your operating system is just a program which has been loaded into memory by the bootloader and from there on it takes control over what your hardware does.
