I’ll start this post by saying that yeah, beginner programming tutorials aren’t going to tell you how a computer works because it largely doesn’t matter. The whole point of abstraction is that you can just focus on the layer you’re working with, and trust that the layers underneath are carrying out the high-level commands you’re giving them.
As far as explanation by analogy, well, unfortunately, basically every layer of a modern computer is complicated enough that almost any explanation is a gross oversimplification of what it’s actually doing.
If you’re very interested in how computers work at lower levels, that’s cool! But that information is totally unnecessary if you just want to make a game. Programmers can be pretty elitist and gatekeepy and you might think you’re missing something important or fundamental or enlightening if you don’t understand every level, but you’re not, really! If you’re making something that really stretches the hardware and is performance critical then you might need to do a little more research on the next level down. But like John Carmack doesn’t understand every detail of every processor Doom ever ran on because he doesn’t need to.
I can’t even keep the entire context of the game I’m currently making in my head (less than 2k lines of code), let alone the architecture of my whole computer.
Programming a game in assembly ultimately isn’t that different than programming a game in Lua. The syntax and amount of overhead and context you have to keep in your head is different, but the algorithms and logic you’re having to employ are basically the same. Like a series of multiplications might involve loading values into different registers in assembly over multiple instructions, while in Lua in might all fit onto a single line, but in both cases the computer is multiplying and you, the programmer, have to consider the higher-level algorithm and the reason for that multiplication.
High-level programming languages just remove the burden the programmer has on telling the processor EXACTLY what to do and gives them tools that can simplify the implementation of an algorithm. Like, “add these two numbers together and give me the result”, rather than “put this number in this register, put this number in this register, call the add instruction, and then read the result from this register”.
Most of the abstractions listed in that paper still exist in modern computers, yes. Assembly and C and Lua ALL use those abstractions at some level. The thing that I was surprised about when I was learning comp sci was that things like calling a function and returning a value – stuff that seems like it’s a pretty high level of abstraction already – is actually directly hardware supported. When you call a function in Assembly or C or Lua, they’re all using the hardware-supported calls to like store data and instruction pointers on the stack, and the pop it back off the stack when the function returns.
If you’re super interested in how computers work at a low level, studying some form of assembly language is going to give you the most bang for your buck. Programming languages are just layers of abstraction on top of assembly (which is a pretty thin layer on top of machine code, which contains the instructions that the processor actually carries out).
At the lowest level, a processor basically reads a series of instructions in the form of numbers (e.g. binary or machine code). A processor has a defined “instruction set”, and each instruction has a unique number to define the instruction, and then a list numbers after (how many depends on the instruction) which are used to pass arguments or additional data to the instruction.
To understand the assembly level, we first need to understand what a compiler is. A compiler is simply a piece of software that reads instructions written in one language, and then outputs instructions in another language. The very first assembly compiler would have been written in machine code directly. Once your compiler is robust enough, you can start writing the compiler in the language itself (e.g. an assembly compiler written in assembly), by using the compiler you wrote in a lower-level language to compile your compiler (a process known as “bootstrapping”).
So to get from assembly to machine code, we just need a compiler. The translation here is pretty straightforward – again, assembly instructions pretty closely match the instructions the processor carries out, they’re just more human-readable.
To get from C to assembly, well, it’s just another compiler. This conversion here is a little less straight-forward. C somewhat abstracts away what instructions the computer is doing to complete a certain task or algorithm, so a few lines of C code may compile down to lots and lots of lines of assembly.
To get from Lua to C, that’s also a bit more complicated. Lua compiles to an intermediate byte code, which is read by a Lua virtual machine. A virtual machine is essentially software that acts like a processor (carries out commands based on its defined instruction set). The Lua virtual machine was written in C, so it’s a C program that reads Lua byte code and carries out those instructions.
So the whole process looks something like: the Lua code you write compiles to byte code, the byte code is read by a Lua virtual machine that was written in C. When the virtual machine was compiled, that C code was translated to assembly, which was then translated to machine code by an assembly compiler, which is then carried out by your computer’s processor.
But again this outline is a gross oversimplification at almost every step; there’s so much complexity in any of these layers that individuals that work professionally on a specific layer don’t even know the whole of it.
Hopefully that gives you some of the context you were looking for, or at least some jumping off points for your own research. Again, totally valid if you’re interested in it and if that’s what excites you, but certainly not a requirement to make a game.