While we rarely need to care about the particulars of stack instructions, we need to know how to read them because they occur all the time in compiler-generated machine code.
This is the twelfth video in Part 1 of the Performance-Aware Programming series. Please see the Table of Contents to quickly navigate through the rest of the course as it is updated weekly. There is no homework associated with this post.
Nearly all programming done today is done in a structured programming language. Whether it’s a lower-level language like C, or a higher-level language like Python, all languages provide certain basic building blocks at their core. These building blocks are things like basic callable functions which can have parameters and local variables.
Because these things appear in almost every language, they seem second-nature to us. Writing and calling functions are things we do all the time without giving it much thought. But there are specific operations that the CPU must do to make function calling work, and that’s what I’d like to go over with you today.
The compilers and interpreters for our higher-level languages are actually generating a bunch of machine code to enable us to have this seamless experience of “calling a function”. The machine code is based on conventions and techniques that date back to well before even the original 8086.
The primary technique that makes function calls possible is called the stack. When we write higher-level programs we don’t think about the stack, because the compiler or interpreter hides the details from us. But if we want to read assembly-language code — especially if that code was generated by a compiler — we must learn to read the instructions that manipulate the stack. If we don’t, the stack manipulation instructions intertwined with the rest of our program will confuse us and make it hard for us to understand what the machine code is doing.
So let's build an understanding of the stack from the ground up. Let’s imagine we have an assembly language program that we are writing, and we would like to do the equivalent of calling a function in a higher-level language. We want to write a little piece of assembly language code that we can reuse, and we want to call that piece of assembly language code from some other place in the assembly language program.
We’ll start with the “caller” — the part of the code where we want to call the function. We can imagine anything here. I’m going to pretend we have some input value in bx, and we are going to subtract it from 1000. Then we want to call our function, and then we’re going to compare the value of the subtraction with 500: