Systems Programming

An introduction to Low Level Programming

Digital Layers

Welcome to our low-level programming website!

Our mission is to demystify the intricacies of systems programming, making them accessible to all skill levels.

Currently, we're in the early stages of development, focusing on presenting fundamental concepts in straightfoward ways.

We're fully committed to expanding our content library with more in-depth information and valuable resources. Let us know what specific topics you'd like to see covered!

Two's Complement
...is 11111011 a negative number?

Read along and start experimenting! Do some binary math, if you can.
  • Two's complement is a way for us to represent (and work with) negative numbers in binary. Since computers only understand 0's and 1's, we need a way to represent and work with negative numbers.
  • Negative values will always have the leading bit as a 1. That will be the negative value in the number, so to speak. Take 10000000. This signed integer would represent the decimal number -128. All bits to the right will be positive.
  • Thus, while unsigned values range from 0 (00000000) to 255 (11111111), signed values will range from -1 28 (100000000) To 127 (011111111).
  • Here's a trick to quickly get from a number to its symmetric:
    1) Pick a number, let's say 00000101 (5 in decimal)
    2) Flip its bits (...so 11111010)
    3) Now add one bit, which results in 11111011 (-5 in decimal)
  • How do we take a number from a nibble (4 bits) to a byte (8 bits)? Easy. If the number is positive, fill 0's to its left. If it's negative, fill 1's to its left.
Compiling
When programmers discuss code compilation, they're referring to the process converts source code into binary, the language understood by computers.

However, this process doesn't occur in a single step. In fact, it involves four distinct stages: Preprocessing, Compiling, Assembling and Linking. Let's take a closer look at each.

Code Compilation

During preprocessing, directives like #include<...> are processed to include the content of header files into the source code. Preprocessing handles tasks like including header files and macro expansions, which, in turn, help organizing code and making it more mantainable.

GIF

In this step, the source code written in C is translated into Assembly language, which provides a human-readable representation of the code's machine instructions. Assembly language acts as an intermediary between high-level languages (in which we can include C, believe it or not) and the binary machine code that computers understand.

GIF

The assembly code that was generated in the previous step is then assembled into binary machine code (a sequence of 1's and 0's that the computer's CPU can execute). This involves generating object files (.o files) from assembly code and then converting those objects into machine code.

GIF

Finally, the linking step involves combining different binary files, such as object files generated from multiple source files, into a single executable program. Note that this process resolves references to external functions and libraries, ensuring that all components are working together seamlessly.

GIF

Interesting Links

GCC Compiler
Compiler
OWASP
Buffer Overflow
Binary to Decimal Converter
Two's Complement
Geeksforgeeks
Bitwise Operators
Assembling

Assembly is a low-level programming language that serves as a human-readable abstraction of machine code, enabling programmers to write code without delving into the binary representation of instructions. Despite the development of higher-level programming languages with increased levels of abstraction, Assembly remains relevant today for tackling low-level challenges such as optimizing performance or accessing hardware directly (in embedded systems programming or in writing device drivers, as examples), and malware analysis.

GIF

Unlike higher-level languages, Assembly does not have a single standardized version; instead, there are various Assembly languages for specific CPU architectures. This close tie to hardware makes Assembly highly dependent on the actual system's architecture. Assembly language(s) consists of a set of instructions that directly correspond to machine instructions, manipulating data stored in extremely fast memory locations known as registers (think of them as variables). These registers hold values that are manipulated by the program's instructions to perform computations and control flow. Registers are part of the CPU and are used to store data temporarily during program execution.

GIF

Remember, though, that by using these registers and by having its own working logic, the compiler doesn't really care about the original variable names of our program. So, if we reassemble our code directly without much ado, we will be greeted with 'pure' Assembly, and it might make the job of reading the code a bit harder. Fortunately, tools like GDB are able to debug our code, forcing Assembly to actually give adequate variable names to our code, making it easier to understand and debug.

GIF

When working with Assembly code, developers can use tools like GCC for compiling and debugging, along with utilities like GDB (the GNU Debugger) for analyzing code step by step, setting breakpoints, and learning how the computer really processes the instructions set in the code. Working with Assembly is not only technically challenging but also very rewarding. Programmers can unlock new levels of control and efficiency by achieving a deeper understanding of the intricate relationship between code and hardware.

GIF

Interesting Links

Wikipedia Link
Assembly Language
Godbolt
Compiler Explorer
Dogbolt
Decompiler Explorer
Microcorruption
Assembly Game

Get in Touch with Us

Contact Us