If you have ever wondered how a program written in C++ or any other language finally gets executed on the hardware and gives us the intended results,this is the article for you. This article will give you a high level overview of what happens behind the scenes.
A C++ program is compiled into object code by the compiler. Object code is nothing but the Machine code (binary). A separate file with object code is created for each translation unit in C++. The linker then links these object files together to produce the final executable program. In the old days, compilers generated assembly code on disk which was then converted into object code by the assembler. But compilers today do not generate assembly on disk unless explicitly asked. They might create some form of assembly in memory before generating the object code. Please note that the object code generated by the compiler is very similar to assembly code. It is just that the assembly code has a human readable format.
Syntax related errors are thrown by the compiler. A compiler takes the source code, processes all the pre-processor directives and then proceeds with machine code generation. A compiler does not necessarily check for the definition of objects in the source code. It just looks for their declarations. A compiler assumes that the definitions are present in another translation unit part of the project. It is the linker that checks for the definition of these objects. If it is not able to find them in other translation units, or if it finds multiple definitions of the same object in multiple translation units, it throws a linker error. Please note that it is the compiler that usually catches multiple definitions within a translation unit and throws an error. To learn more about it, please read my article about the One Definition Rule here
Once all the translation units are linked together, a final executable is generated. In Linux, executables are stored in ELF format (Executable and Linkable Format) whereas in Windows, executables are stored in PE format (Portable Executable). Executable files (or programs) when double-clicked, are handled by the OS specific handlers (loaders) to run them as processes. A process is just a running instance of a program. The handler for PE files in Linux is a software called Wine. The executable file contains all the information that the loader will need to create a process out of the program. The header of the executable file contains the virtual address of the first instruction to be executed along with other information.
Each process gets a separate Virtual Address Space. In a 64-bit OS, a 64-bit process gets to use a maximum of 8tb of VAS (each memory address is 64 bits long) whereas a 32-bit process gets to use a maximum of 4gb of VAS (each memory address is 32 bits long). The loader parses the executable file and memory maps the content (data segment, code segment etc) along with other dependencies (system files, libraries etc) to form the process’s Virtual Address Space. Compiler and linker may also provide virtual address mapping which may or may not be used by the loader when it lays out the memory in VAS. We can in fact find virtual addresses within the object code of a program which are assigned by the compiler or linker. Please note that compiler and linker are able to provide such addresses (even before a program is loaded in memory) because virtual addresses are not real physical RAM addresses. As far as they are concerned, the program gets to use the entire available memory (which is actually virtual memory) and assigns addresses accordingly.
VAS consists of
A text space – that holds the executable code
An initialized data space – to store initialized global objects and static objects. This is again divided into read-write space and read only space. String literals are stored here (char* = “string”; ).
An uninitialized data space – that holds uninitialized global objects.
Heap space – which is the Heap Memory
Stack Space – which is the stack Memory
Heap and stack uses the same memory space but grows in opposite directions until they meet. A process contains a call stack that houses the stack frames. Each thread in a process gets its own call stack. But threads can access memory of other threads’ call stack via pointers. Heap, text space, data spaces are shared by all threads.
Please note that the Virtual Address Space(VAS) of a process is not exactly in the RAM. It is an abstraction over the Main Memory (RAM). OS does this to mimic the presence of more main memory than what it actually has. OS maintains a page table, which is an internal data structure used to translate virtual addresses into their corresponding physical memory addresses (in RAM). Most of the OSes today have one page table per process. A unit of memory in VAS is called as a page and a unit of memory in the physical memory is called as a frame. Each page in VAS is mapped to a frame in the physical memory. A frame is typically 64 bits long in 64-bit OS and 32 bits long in a 32-bit OS.
Processor executes the microcodes corresponding to the first instruction of the process(entry point instruction). This can take one or more cpu clock cycles. Once completed, processor runs the microcode to fetch the next instruction and thus, the process execution progresses. For the processor to execute an instruction, it needs to fetch its data from the physical memory. With the virtual address, processor checks its Translation Lookaside Buffer (TLB) to see if it contains the virtual address to physical address mapping for the data. If it does, then it gets the data directly from the physical memory and exeutes the instruction. Otherwise, the page table of the process is checked to see if the mapping exists. If one exists, it is written into TLB and executed. If the mapping does not exist in the page table, a page fault is generated. Operating System catches the fault and then pulls the memory segment/ memory page into main memory from secondary memory (page files / swap files). It updates the page table to refer to the new page frame and then transfers the control back to the process. If the page fault is generated for a virtual address that is not part of the process’s VAS, a segmentation fault signal is sent to the process by the OS.
Just remember that everything that I have explained above only scratches the surface. Processor and the Operating System do much more than this. I would suggest that you read a book on Operating Systems fundamentals to learn these concepts further.