Disclaimer: I'm a beginner to programming, and especially to x64 assembly. Some smart folks have pointed out that a few of the assumptions made in this article about the assembly instructions aren't quite right. Please review the comments and do your own research before taking my surface-level understanding as fact. This project is meant to be a gentle exploration of how variables are internally represented, and nothing more.

In my brief stint as a computer science major, a tremendous amount of time was devoted to learning object oriented techniques for organizing code. When to create a class, what properties and methods the class should have and the allowed access thereof, if the class should perhaps be split into a base class and one or more derived classes, and so forth. While I typically enjoyed mulling these things over, the mental framework of classes caused a more insidious side effect: nonchalantly sweeping things under the rug. ints having a toString method, for instance. This was easy to accept, int must just be a class with a method toString. No need to worry about it.

But C does not have classes. This makes “weird things” much harder to hand-wave away. Pointers, in particular, are extremely troubling. A pointer seems to imply that a variable is aware of its own memory address. Otherwise, how would we be able to retrieve it using the & operator? In “object” psuedocode, I envisioned something like this:

class int 
{
   value;
   address;
}

There are not classes in C, so such a thing is not possible. How then is a variable not only able to retain its value, but its address? If it was “just memory” I supposed an address would not need to be stored separately, but then how would the value’s type be retained? I then thought there must exist some kind of metadata or tagging system in memory, similar to the above psuedocode, but this time storing the type instead of the address.

Disassemblers are available that show the output of C code in ssembly language. I used godolt.org with the latest version of clang x64 to investigate (I chose clang because the output looked the least weird to me). Let’s start by getting to the bottom of what’s going on when we create a variable. Here’s some example code:

int main(void)
{
	int i = 2025;
}

And the disassembly:

main:
push rbp
mov rbp, rsp
mov dword ptr [rbp - 4], 2025
xor eax, eax
pop rbp
ret

Everything now looks quite different. Notably, there is no mention of i anywhere! Where has it gone? We can see the value it’s assigned, 2025, on the 4th line of the assembly. But what does everything else on that line mean? Let’s break it down.

Hovering over the mov instruction, we get the following tooltip: “Copies the second operand (source operand) to the first operand (destination operand).” So it’s copying 2025 into… dword ptr [rbp – 4]? What is rbp? This is the “Register Base Pointer” and stores the starting address of the memory to be used for our function (that’s all the digging I’m doing for now). So, we subtract 4 from the starting address of our memory. Why four? Because we need four bytes to store the int value i. Why subtract? Because somebody decided that was the direction that functions use memory (as far as I can tell). What about the []s? In the case of the mov instruction, these specify that we are talking about the value at the memory address rbp - 4, not the memory address itself. Essentially, we are dereferencing the memory address. Note the early mov from rsp into rbp, which moved the memory address in rsp to rbp, not the value at that memory address. Now we just have to decode dword ptr. This is apparently a size directive, which tells the CPU how many bytes of memory we want to work with.

So “move the value 2025 into the four bytes starting at the address stored in rbp, minus 4”. No mention of i, no mention of this being an int. The computer doesn’t know about the variable at all, it just knows it needs to put four bytes of data somewhere! And since that “somewhere” is relative to a known value, there’s no need to store the value of “somewhere”, well, somewhere else. But what does the computer actually do when we request the address of a variable? Let’s expand our example code a little:

int main(void)
{
	int i = 2025;
	int* p = &i;
}

And here is the disassembly for the above code:

main:
push rbp
mov rbp, rsp
mov dword ptr [rbp - 4], 2025
lea rax, [rbp - 4]
mov qword ptr [rbp - 16], rax
xor eax, eax
pop rbp
ret

We see two new instructions have been added for our pointer variable, lea and mov. The tooltip for lea state that it “computes the effective address of the second operand (the source operand) and stores it in the first operand (destination operand).” The “effective address” in this case is the resultant memory address from the operation rbp-4 (so, the base of our function’s memory minus four bytes). This is stored in register rax. Since the name rbp was pretty literal (Register Base Pointer), I assumed rax would also be an abbreviation for something. As it turns out, the meaning is much less straightforward (there’s an awesome article about it here). For our purposes, it’s just a bit of scratch space to store the memory address we asked for.

Now that we’ve computed the value for p, we just need to move it into the memory we’ve designated which is sixteen bytes less than rbp! Memory addresses on x64 are 64-bit values, which should only take up 8 bytes. So why is it not at rbp – 12? And for that matter, why were we storing i at rbp – 4? Sure, it’s 4 bytes, but if rbp is inclusive, why not rbp – 3? It turns out that computers don’t like it when the memory address of something is not divisible by the its size, or “aligned”. So, since i is 4 bytes, we want to make sure that its address is divisible by 4. Likewise, since p is 8 bytes, we want to make sure its address is divisible by 8. I do wonder if it is safe to assume rbp is always properly aligned… Finally, the qword ptr size directive specifies a “quad” word: 4*16 or 64 bits (edit: there was a typo here originally, it's 64 bits, not bytes!), the size of a pointer on an x64 system.

As an aside, I found myself wondering why this needs two instructions. Why can we not simply put the result of lea into rbp-16? Apparently in x64 this is not a thing. I didn’t dig too deeply due to time constraints, but I am curious why this is so. Perhaps it is because the CPU is limited in the number of memory addresses it can “figure out” at one time?

Looking back at the above disassembly, it seems evident that the variables I spend so much time fretting over do not matter to the computer very much at all! They are merely numbers, placed in predetermined locations, operated on by the correct instructions as determined by the compiler. Finding the address of some variable is trivial not because of some sort of tag or hidden value, but because the computer has been referring to that address the entire time, with no care of it being anything more than an address, and maybe fussing with the value stored there from time to time. What that value is, or does, is dictated not by metadata, but by the operations it’s used with.

When I discovered all of this during the jam, I found myself at first feeling a little upset about it. I thought about the amount of time I had spent agonizing over class design, let alone small things like variable names, that never make it into the “final product”. I reflected on how much I struggled with C and how it challenged the many assumptions I’d simply taken for granted about the way things worked. But, of course, that’s all missing the point. C is a helpful abstraction for our benefit, not the computer’s. The source code in this article is much clearer than the assembly (who am I kidding most of HMN probably writes x64 assembly for fun). And it’s wondrous to see this transposed into an entirely different language, with entirely different semantics and considerations (or to watch it disappear entirely when passing -O2).

I was pleasantly surprised how much I was able to understand just by wrestling with a bunch of strange three letter acronyms for a few hours. I had fun looking at what, or perhaps how little, our code actually means to the computer.