How your Computer Really Runs C++?

Josh Segal
May 24 · 4 min read
Photo by Alexandre Debiève on Unsplash

Let’s talk about all the complex things that go into actually executing simple C++ code. We’re going to talk about memory, compilers, assembly code, and more, so buckle down and let’s see how this executes.

struct A {
public:
A(int _a, char _b, double _c): a(_a), b(_b), c(_c) {}
int foo(int i);
int a;
char b;
double c;
}
int main(){
A A1(1,'c',146.2);
cout << A1.c;
cout << A1.foo(3);
}

1. We create a new stack frame by assigning our stack frame register, S=0.

2. We allocate 16 bytes of memory on the stack for A1. An int has 4 bytes, char has 1 byte, and double has 8 bytes. Each field needs to be aligned as a multiple of its field size. For example, ints can only be defined at an address that’s a multiple of 4. Basically, the compiler optimizes the alignment of the fields so that they can be read into memory with one read. Computers read in 32 or 64 bits of memory at a time (depending on your computer's system) starting at multiples of 32 or 64 addresses, so you save an extra read by aligning the memory. If this concept isn’t too familiar, take a look at https://fresh2refresh.com/c-programming/c-structure-padding/.

3. The constructor for is invoked on S+0 and turns the allocated memory into useful bits. Before the constructor, the memory is just garbage.

4. For , memory address S+8 is fetched from memory and stored in register r1. The assembly would roughly look like . All operations, including loading and storing, are done through registers. You can’t just print out memory S+8, instead, you need to load that into a register and then print out the register.

5. Next, we create a new stack frame so that we can call . We do this by saving the current S on the stack (prev_S) and updating the S register to 16 because we’ve currently used 16 bytes of memory in our previous stack for storing .

6. Next, we need to load in the arguments for on the stack. We push the constant 3 onto the stack, .

7. We also need to push a pointer to to the stack, (in real assembly you need to load memory for into a register and then push it to the stack). Why would we want to push to the stack if it isn’t an argument for ? Well, since the actual instructions for are shared among all A objects, by itself doesn’t have access to the specific A object that called it. So, the compiler will implicitly pass the address of the object to , so can run on this specific A object. This is why we have the “this” pointer in C++ classes. The “this” pointer is implicitly passed to all methods and implicitly put before each field access or method call within the class.

8. Now that our stack frame and arguments are all set up, we call by moving the program counter (pc) to the address of the instructions. a pc is a special register that points to the current instruction line. Simply moving the pc to the start makes the computer execute .

9. When finishes, it will write the return value of the function to a special register, EAX. Also, the S got set to its prev_S=0 because we’re done with the function. Note that the things on the stack weren’t actually cleared, but rather the S, was just moved back to its previous position.

10. Now, we out the EAX.

11. Our main stack is about to end which triggers the destructors of all objects that have been created. ’s destructor is invoked and the program exits.

And we’re done! Note that even this walkthrough glossed over a few parts of what happens for the sake of avoiding redundancy and avoiding some complicated assembly. I hope this sheds some light on what's really happening when you compile and execute C++ code.

CodeX

Everything connected with Tech & Code. Follow to join our 500K+ monthly readers