No Deus ex Machina
Logic is the real god of systems
Recently, I have been reading the excellent book "Computer Systems: A Programmer’s Perspective," and I decided to discuss systems programming: a deeply interesting subject, and explore how its philosophy and style differ from that of regular application programming. Systems programming is often done in a resource-constrained world, which creates a philosophy of economy. But I am getting ahead of myself.
Computers have advanced to such a point that their operation almost seems like magic. It reminds one of what Arthur Clarke once said: Any sufficiently advanced technology is indistinguishable from magic. But there really is no deus ex machina going on here; it’s just the power of abstraction at its zenith. Let’s go on a small tour of computer-land and see things under the hood. It is also important to know how this thing works; when everyone thinks it is magic, it pays to be the technician.
The OS PoV
Let’s go on a whirlwind tour of the major aspects of systems programming, following which we can better appreciate the nuances and idiosyncrasies of its aspects.
At the core of it, systems programming is about giving the computer hardware a software interface that other programs can rely on. The major role of an operating system (OS), a popular systems program, is to make sure that application programs don’t have to worry about interacting with the hardware directly. This means that the operating system itself is constantly jumping between low-level programming like registers and more abstract high-level programming of interfacing with applications.
Due to this, several things have to go into the making of an OS to make sure that it can deliver up to its promise, and not throw any hidden surprises later. We have, among others, issues like:
Memory management
Computation and processing
I/O
Concurrency
Security
Let’s examine a few of these issues and how they are addressed by the OS.
Memory Management
Systems programming sits at this interesting boundary between abstraction and physical reality. Code is abstract, but the transistors on which it runs are real. And the closer we are to the metal, the more important it becomes to acknowledge the constraints of it. And nowhere does it appear more clearly than in the memory.
The computer has a finite memory. All program variables, functions, and the operating system have to live in this same memory. Therefore, it is important that:
Variables take up finite memory. This leads to some important caveats for integer and floating-point arithmetic
Variables that are not used anymore should be freed so that they do not eat up memory
Programs should not access memory that does not belong to them
In virtually all application programming languages, like Java, Python, etc., memory management is done automatically by the garbage collector. Thus, we never need to worry about all this when programming in those languages. On the other hand, most system languages do not have automatic garbage collection, and it is the programmer's responsibility to allocate and free memory as needed. Rust goes a step further and introduces ownership, which prevents the program from compiling until everything is memory-safe.
Computation and Processing
When we run a program, something actually needs to make the transistors act according to the code and make things happen. This is done by the CPU, which comes into action once the OS communicates with it about the program. This is typically accomplished through mechanisms such as interrupts, which are designed to trigger specific circuitry within the hardware.
Now, let’s look at how people actually program these things in the computer. Programming is one of the most creative acts of humankind, and The Beauty of Programming is best captured by Linus Torvalds:
Within the confines of the computer, you’re the creator. You get to ultimately control everything that happens. If you’re good enough, you can be God. On a small scale.
And I’ve probably offended roughly half the population on Earth by saying so.
Now you C me
The language closest to the hardware is binary itself, where programming is done in 1s and 0s. However, this is neither human-readable nor maintainable, so we proceed to the next level of abstraction, which is assembly. This is a step above the machine level, where instead of speaking in terms of bits, we can speak in terms of registers and simple operations, such as loading, storing, and adding. The actual instructions available depend on the processor's architecture.
However, this still requires a significant amount of effort just to write a simple “Hello World” program. As we move up the chain of abstraction, we relinquish more control, and in return, we gain more execution power. By that I mean that we can do more by typing less. This is generally true in life, as the more high-level we go, the more we can accomplish by saying less; however, we have less control over each process individually. Consider the control and power a CEO has over a process compared to the person actually performing the job.
The next level of abstraction brings us to high-level programming languages, or systems programming languages. These are still relatively low-level compared to application programming languages, but they occupy a sweet spot on the abstraction ladder. They have more execution power than assembly, so a program’s complexity is reasonably contained by the number of lines it takes, and they still retain a lot of control over the hardware processes going on underneath. Popular examples include C and C++, as well as their spiritual successors, Zig and Rust.
First Vision
C was one of the first systems languages to gain widespread adoption in the world of computer systems. It was developed in 1972 by Dennis Ritchie and Brian Kernighan in Bell Laboratories, AT&T. C was written with the primary purpose of writing the UNIX operating system, and was deliberately kept very simple, so much so that the original K&R book written by the authors describes the entire language in just under 300 pages.
However, this belies the versatility and ubiquity of the language. Since its primary purpose of existence was to write an OS, C is the defacto standard for systems programming even today; Linus Torvalds vouches for this and has said that in his experience, “no other language has come even close to C that can generate good code for hardware” (arguably this was before Rust came into prominence, but even then I think his opinion would not change much).
++More
Bjarne Stroustrup was a PhD student working at Nokia Bell Labs in 1979 when he sought to combine the low-level aspects of C with additional features like type checking, basic inheritance, default function argument, classes, inlining, etc as present in high-level languages like Simula. The result was a language that he called “C With Classes”, though a more popular name nowadays is C++.
It is a middle-level programming language, combining the best of both worlds above and below it in the abstraction hierarchy. It is the preferred language for competitive programming due to its powerful STL (Standard Template Library). It also introduced generics, which allowed programmers to avoid duplicating work by writing similar functions for multiple data types. It supports both procedure-oriented and object-oriented programming. With the addition of lambda functions in recent versions, functional programming is also gaining support.
The New Kids on the Block
Many new programming languages for systems have emerged in recent decades, each with its own quirks and unique advantages. Let’s compare a few major ones: GoLang, Rust, and Zig, on an important aspect of systems: memory management.
Go is the simplest for the user in this respect. It comes with a built-in garbage collector, so programmers never have to worry about memory management. Indeed, this almost makes it like an application programming language, but it has other features like built-in concurrency handlers via goroutines, good execution speed (compared to application languages), which make it popular for use in large-scale software engineering. Indeed, it was developed by Google for simplicity and developer productivity.
Rust, on the other hand, has the steepest learning curve of all three when it comes to memory management. It features a complex ownership system, resulting in compile-time memory safety. This means that if the program will potentially lead to a memory error, like a dangling pointer, it will never compile. C++ has also introduced RAII to achieve the same goal via smart pointers, but Rust takes this concept to a whole new level. As a result, Rust programs are guaranteed not to leak memory, and features like Traits and Generics also provide high-level language features similar to those found in Ruby. I like to imagine Rust as the spiritual successor of C++.
Zig takes this in yet another direction. It sits somewhere in the middle of the difficulty level. Zig makes everything more explicit and transparent, and its philosophy is that no memory allocations will occur behind the scenes. The programmer has to initialise the allocator, use it, manage memory, and then free the allocator. In this aspect, it is like C, in that it gives the responsibility of memory management to the programmer, but it is often more granular and you even have to decide how to manage memory allocation and deallocation. In C, I just call malloc to allocate memory; here, I need to determine whether to use a fixed-buffer allocator, an arena allocator, or something else. If Rust is the protege of C++, Zig is that of C itself.
Now that we have seen some of the quirks of systems languages, let’s see some of the reasons why you should probably learn them, even if we have application languages like Python.
Patience
Initially, it feels annoying to have such a limited API and to have to build anything useful yourself. Especially if you have a Writergate scandal where the APIs keep changing. But later, you realise that the struggle is worth it: this is training your mind to do hard things and teaching yourself an altogether different way of thinking.
Consider this: most application programming languages are almost the same. The syntactic sugar is a bit different for each, but once you master that, there is not much left to learn about the language itself. However, with systems languages, it can be a great learning experience just to use the language to write Hello World, because they often don’t hide anything behind abstract APIs like print.
You have to go through things like understanding pointers, memory, being responsible for garbage collection, either explicitly in languages like C, or obeying the borrow checker and ownership rules in Rust. It is not just about learning the syntax; it often opens up a whole new way of seeing things. At least that’s what I observed in my case.
If you are an application programmer, you might ask: Why should I care? My program does not depend on individual bits or the system architecture. Why should I invest my time and effort in this?
Relevance
The truth is, even application programs benefit from being written in systems languages. Sure, it is quite painful to write large programs in them compared to, say, Python, but the increase in speed and memory efficiency compensates heavily for it. Let’s see how.
Application languages usually sacrifice efficiency for readability. With Python, we don’t need to worry about integer overflow, type-casting, or the internal representation of lists, but underneath, it can often consume a lot of bits for storing a simple value. This is not that important for something like an email application, but for programs that run on small systems like IoT devices, digital watches, or even games that have to run on the limited memory of consoles, every bit counts.
In fact, game development is one of the biggest areas where systems languages shine. Games are often made on the cutting edge of hardware, and they have to make the most of it. They have to do vector processing in parallel, 3D rendering, textures, animations, etc., sometimes on even less memory than your phone has. In such cases, game developers want fine-grained control over the memory so they can use it more efficiently. Most games today are made in C/C++.
Even today’s AI would not be possible without Nvidia’s chips, which are necessary for parallel processing. Training and using AI involves large matrix multiplications, and they are often done in parallel on multi-core chips with the help of Nvidia’s CUDA. The non-intensive parts of ChatGPT, like the user interface, are written in Python, while the intensive bottlenecks fall back on systems languages for speed and efficiency.
Most large programs nowadays are distributed in nature. Whether the application itself needs them or not, the bottlenecks of a distributed system are best handled by languages close to the metal.
Conclusion
Systems programmers are powerful. They can know and manipulate computer systems from a very close view. If I make an analogy with motorcycle maintenance, application programming is like giving over your bike to a registered service center for repair. Systems programming is doing the repairs yourself. You need much more knowledge of how bikes work, but you might end up with a bike that is highly customised to your needs.
Or you might not get a working bike in the end at all. There is a very high chance that you can end up breaking your bike instead of repairing it, especially if you don’t know what you are doing. When applied to computer systems, it is easy to corrupt the system, access invalid memory, and cause the entire system to crash. I experienced it when I dual-booted for the first time, and only succeeded recently on my seventh attempt after learning the proper techniques. It reminds me of what Uncle Ben said:
With great power comes great responsibility
Until later.
If you like my work, this would be a great way to show appreciation!






