Shortcut to seniority

Home
Go to main page

Section level: Junior
A journey into the programming realm

Section level: Intermediate
The point of no return

Section level: Senior
Leaping into the unknown
Go to main page
A journey into the programming realm
The point of no return
Leaping into the unknown
So far, we talked about the hardware and the computer circuits. But how does the software interact with these?
Instructions that can be executed directly by a CPU are called machine language instructions, or machine code. That is possible because the CPU manufacturer has burned into the CPU what each machine instruction will do, and therefore, machine code is dependent on a specific CPU architecture.
Assembly language (‘asm’) is a low-level programming language that acts as the middle layer between the program statements (code) and the architecture’s machine code instructions.
The assembler is responsible for converting the assembly code into machine code, so that the CPU can execute the instructions.
The machine code is then stored in memory by the CPU, and then fetched sequentially and executed.
The software is written in high-level commands that are converted into simpler commands understood by the CPU (the hardware) through the process of compilation.
There are many programming languages, each with their own differences, and due to that they are also behaving differently. Based on the programming language itself, we can divide them into two categories: Compiled languages, and Interpreted languages.
A compiled language is a programming language which requires a compiler (translator that generate machine code from source code) to create an executable. Programs that are compiled tend to be faster than those written in interpreted languages, due to the effect of the optimizations made possible in the compiler.
Once the code has been compiled and the executable has been created, that executable is compatible only for the platform it was created for. That is due to many reasons, including:
An interpreted language is a programming language in which most of the instructions are executed directly, without the need for a compilation to machine-language instructions. The program is executed through an interpreter, which translates each statement as it is run.
Interpreted languages have some advantages, including:
Disadvantages for interpreted language include:
We talked about compiled languages, now let’s also talk about the process that converts high level language code into machine level language. This process is called compiling the code, and this is performed through the following steps: Preprocessing, Compilation, and Linking.
Preprocessing phase is responsible for replacing the include directive (the command which includes/import code from other files) with the actual contents, for replacing the macros (#defines in C++), and selecting the parts of the code based on compiler directives (specific code for specific platforms, for example).
Compilation phase is responsible for converting the source code into assembly code for that specific CPU architecture, and then converting it further to object code with the use of assembler. It also generates the object files (.o) for that platform. At this stage, the developer receives the usual compiler errors, like syntax errors.
Linking phase is responsible for producing the binary based on the object files that the compiler has generated. The most common errors received during linking phase are those related to missing definitions, or duplicate definitions.
So far we talked about executable (binary) files, which can be executed directly by the operating system. Based on our needs, we can also create different types of files, such as libraries.
Libraries are files that contain reusable code, which can be used in other libraries or executables. They are invoked directly by other code, once the library is loaded.
When linking, libraries come in two flavors: static and dynamic libraries.
Static libraries are used at compile-time (when the code is compiled to create the binary), and the code inside the libraries is copied into the final executable.
The advantage when using static libraries is that there is no performance or versioning problem, and that the code is optimized by the compiler.
A disadvantage is that we cannot make use of code reuse. If we have multiple applications that use the same static library, each of them will have its own copy of the code.
Dynamically linked libraries are not copied in the code, but instead the compiler gets a small stub which is used to load them by name when they are needed (at runtime).
If the library is not found when the application is loaded, the application will stop immediately with an error.
An advantage when using dynamic libraries is that we can benefit from code reuse, the executable is smaller, and the library can be updated without rebuilding the application, as long as the interface is the same.
We can also load or unload the library on demand in the process memory.
A disadvantage when using DLLs is that we have a performance drop due to loading the library in memory at runtime.
Modules are the equivalent of a dynamic library, but for interpred languages. Unlike the compiled languages, a module can act both as a module and as an executable.
Ok so we have a piece of code and we compiled it, and there it is – our executable. But what’s next? How do we execute our code? What happens when we attempt to run it ?
The Operating System reads and validates the integrity of the executable.
After the OS confirms that we have a valid executable, it looks into a special section for dynamic libraries and loads them.
An instance of execution is then created, which is called a process. The process is instantiated when the application is loaded into the computer’s memory and begins execution, and it is nothing more than a manager/wrapper, which encapsulates all the resources that are needed by our application.
A process is divided into four sections:
While the stack and the heap are related to the memory allocated statically or dynamically, the text and data fields are filled by data taken from the executable.
Each process contains one or more execution threads, and it uses them to execute code sequentially or in parallel.
A thread is a sequence of instructions that can be passed and processed by a CPU core. It contains the thread id (so we can differentiate between threads), the program counter / instruction pointer (the last executed instruction), a set of registers, and a stack. The stack is a data structure (we’ll learn more about it later) that we use to keep track of the functions that were called on that thread.
Single-threaded applications:
If the process contains only one thread of execution, it is called a single-threaded application, and it means that all instructions run sequentially.
Multi-threaded applications:
If the process contains more than one thread of execution, it is called a multi-threaded application. In this case, each thread can be scheduled independently (can run at the same time as other threads from the same process), and will share the heap memory with all the other threads.
The stack is the memory allocated for a thread of execution. When calling a function, a block is reserved on the top of the stack for local variables and arguments / return value. When that function returns, the objects that were created on that stack are destroyed and the block can be reused for the next function call.
In comparison with the heap, the stack memory is faster. It is easier to allocate and deallocate memory from it (simply increment/decrement a pointer). Other than that, because the memory is reused very frequently, it tends to be mapped to the CPU’s cache. When there’s a lot of nested functions called, or an infinite recursive call, the stack runs out of memory (which is called a stack overflow) – and the application is terminated.
The heap is used for dynamic allocation. You can allocate and deallocate any block of any size, at any time. Data stored on the heap has to be deleted manually by the programmer – the system will not take care of that for you.
A memory leak is something that occurs when we allocate memory on the heap and forget to deallocate it. This is problematic, because we have a resource that is no longer used (or cannot be accessed anymore) that keeps the memory used until the process exits.
The heap is slower than the stack because it is a global resource, so it must be multi-thread safe, therefore each allocation and deallocation must be synchronized with other heap accesses in the application.
An example of platform is the operating system, so we can include windows-related code only if we’re building the binary for windows, for example.
A pointer references a location in memory.
CPU cache is a small storage space that keeps the most frequently used data in order to reduce the accesses to the main memory.
Except for languages that contain garbage collector (which automatically frees memory that is no longer referenced).
NULL refer to the absence of a value, which means that it is not the same as having the value 0.
A Complete binary tree is a binary tree in which all nodes except leaves have two children, and the last level has all keys as left as possible.