Procedures Extended
Subroutines or procedures — we already talked about them in detail in the Control Flow Instructions section. Now we’ll use the knowledge we’ve gained so far — variables, arrays, and the stack. We’ll bring them all together with procedures and see how it works in the backend: how the values of SP and IP change in different parts of the code.
I’ll use a simple example program whose purpose is to find the minimum number between two values. We start with two variables that contain any two numbers of our choice. Our code will compare them and store the minimum/maximum number in another variable called Min/Max.
Problem:
Write a program in DOS assembly that takes two numbers from variables and determines which one is minimum and which one is maximum. In the end, the Min variable should contain the minimum number and the Max variable should contain the maximum.
Code:
; Main Program
; ------------
[org 0x100]
mov ax, [num1] ; Store value of num1 into AX
mov bx, [num2] ; Store value of num2 into BX
push ax ; Push value stored in AX onto the stack
push bx ; Push value stored in BX onto the stack
call min ; Call min subroutine
mov [Min], ax
mov [Max], bx
pop bx
pop ax
mov ax, 0x4C00 ; Exit Prep
int 0x21
; Subroutines
; -----------
min: ; min subroutine label
cmp ax, bx ; Compare the value stored in AX and BX (AX - BX)
jle return ; JLE means (Jump if AX is less or equal to BX)
mov dx, bx ; Store value of BX in DX
mov bx, ax ; Store value of AX in BX
mov ax, dx ; Store value of DX in AX
ret
; JUMPS
; -----
return: ; Jump label
ret
; Variables
; ---------
num1: dw 10 ; Variable num1 with the value 10
num2: dw 15 ; Variable num2 with the value 15
Max: dw 0 ; Variable Max initialized with 0
Min: dw 0 ; Variable Min initialized with 0Above I’ve provided a complete code that solves the given problem. I’ve added comments to every statement to explain what it does, and I also partitioned the code into different sections such as Main Program, Subroutines, and Variables.
Now, if you recall, we already learned how to translate assembly code into machine code using nasm in our Warmup section.
C:\\> nasm.exe findMinMax.asm -o findMinMax.comThe above command generates a .com file that we can execute in DOSBox. We can also analyze it using the AFD debugger to understand what the code does internally.
C:\\> afd.exe findMinMax.com
I already explained every screen window of the AFD debugger back in the Warmup section. If you need a refresher, you can go back and check it again. At that time, I’m sure most of it didn’t really click — you saw the windows, but you probably didn’t fully get what they were showing. But now, after everything we’ve learned, you not only recognize them but also understand their purpose.
Still, let’s be honest: even though the debugger makes more sense now, it’s a lot to take in when we don’t have much practical experience. That’s why, for now, I just want you to focus on the specific parts which are visible — especially the ones that are not highlighted in red.

The figure above shows everything we need to focus on. Every single visible window there is important for understanding the inner workings of our code. We are mainly looking at the following windows:
Register Section — where the General Purpose Registers are highlighted with a yellow block.
Instruction Pointer (IP) — also highlighted with a yellow block.
Stack Section — highlighted with a yellow block.
Flags Section — where all the flags (OF, DF, IF, etc.) are highlighted with a yellow block.
Code Section — this shows our program instructions in listing format, with a yellow block highlighting
[0126].Memory Windows — there are two of them: one just after the code window and one below it. Both have a yellow block highlighting a cell containing
00.
Hopefully, I don’t need to re-explain the registers, IP, stack, and flags sections — we already studied them in detail. But the Code Window and Memory Windows need some explanation before we move on.
Code Window:
In the code window, we see our instructions in a slightly different format, called Listing format. For example:
MOV AX, [0126]
Here [0126] is the variable address, which we can understand. But before that, we also see something like this:
0100 A12601
Every line in the listing starts with numbers like that. Let’s break it down:
The first part,
0100, is the address of the instruction in the code segment. Why0100? Because that’s what we specified in our very first instruction:[org 0x100].The second part,
A12601, is the opcode. Every assembly instruction is assigned a unique opcode. When we assemble our code, the assembler first generates a Listing file with opcodes and addresses, and then converts that listing file into pure machine code.
We can generate the listing file ourselves using the following command:
C:\\> nasm.exe -l findMinMax.asm -o findMinMax.lstAfter running this command, we get a new .lst file that contains our code in listing format.

The screenshot above shows four windows. The first window contains my source file along with two directories I created to keep things organized: Executable File and Listing Files. The second and third windows show the contents of those two directories. The last window is our DOSBox, where I ran two commands:
To generate the listing file (saved in the Listing Files directory, shown in the third window).
To generate the executable file (saved in the Executable File directory, shown in the second window).
For now, we are only concerned with the listing file we just generated — what’s actually inside it. The following content is from that file:

That’s our code with some subtle changes. Below I’ve explained what each change means:
1. Line Number
First column shows us the sequential number of the line as it appears in the listing file.
It is not part of our original source code; the assembler inserts it only for reference.
Its purpose is to help us quickly locate and cross-reference specific instructions within the listing output.
2. Address (Offset in Memory)
2nd column contains the memory offset where the corresponding instruction or data will be placed when the program is loaded.
It reflects how the assembler maps our source instructions and variables into memory.
For example, if the program is assembled with
org 0x100, then an address like00000007corresponds to memory location0107hin real execution.
3. Machine Code (Opcode + Operands in Hex)
This column displays the actual machine code produced by the assembler in hexadecimal form.
Each assembly instruction corresponds to one or more bytes of opcodes and operands.
What appears here is the true binary representation that the processor will execute. For instance,
50is the single-byte opcode forpush ax, while39D8encodescmp ax, bx.
4. Source Code
The final column reproduces our original source line exactly as we wrote it, including instructions, labels, and comments.
It exists to allow comparison between the human-readable assembly and the generated machine code.
This ensures that we can trace how every line in our program is translated and where it is stored in memory.
That’s everything we were missing. Now we can start debugging our code and see how things change step by step as we execute each instruction. Use F1 to run one instruction at a time, from here onward, we’ll step through each instruction with the debugger screenshots.

Above shows the result after executing the first instruction: mov ax, [num1]. In the debugger, we don’t actually see the label num1; instead, it has been converted to its real memory address. Still, from our source code we know it refers to num1. This updates the AX register, which now contains 000A (10 in decimal). The IP register has advanced to the address of the next instruction: mov bx, [num2]. Again, the debugger shows the address instead of the label, but we know it points to num2. Once executed, this will load num2 into BX (currently BX is still empty).

I’ve executed the instruction that loads the value into BX, which you can clearly see. If you look carefully, I also executed the first PUSH instruction. This pushes the value stored in AX onto the stack. As a result, the stack now contains the value 000A. Meanwhile, the IP register has advanced to the address of the next instruction waiting to be executed.

Now, not only did I push the value of AX onto the stack, but I also pushed the value stored in BX. Notice how the first value we pushed (
000A) moves one step down, and the new value (000F) is placed above it. Why? Because of the rule we already discussed: LIFO (Last In, First Out) — the last value pushed is always the first one to be popped.
Also notice that the IP register now holds the address of the next instruction (
0109), which is theCALL. Things are about to get spicy here.

Here we’ve executed the
call mininstruction. First, look at the stack: you’ll notice a new entry has been pushed on top. That value is the address of the next instruction that comes after thecallin our code.Check Fig 5.7 and notice what the next instruction is after
call 011A. It’s at address010C, and that exact value has been pushed onto the stack.This is exactly what we learned earlier about the
callinstruction (forgot already? go back and revisit it). Thecallpushes the return address (the next instruction’s address) onto the stack, and then updates the IP register to point to the subroutine’s first instruction. If you look at Fig 5.8, the IP now contains the address ofcmp AX, BX, which is the first line of our subroutine in the code.

Fig 5.9: Flags gets updated In this step, we executed the
cmp AX, BXinstruction. As we already know, in the backend this subtracts the value in BX from AX, but it doesn’t update the registers — instead, it only updates the flags.Notice how the SF flag is now
1, and both the AF and CF flags are also set. Why? BecauseAXcontains10(decimal) andBXcontains15.Subtracting
10 - 15gives us a negative result. Assembly works in binary, so if we convert both values into binary and subtract, we get this:
0000 1010 (10)
0000 1111 (15)
-----------
1111 1011 (-5 in 2's Complement)
-----------If you’re not sure how the above subtraction produces
1111 1011, please review binary subtraction. The key point here is that the most significant bit (MSB) tells us whether the number is negative. Since the MSB is1(look the first one on left side), the result is negative, which is why the SF flag is set.The CF flag is set because the subtraction required a borrow (carry) during calculation. Anytime there’s a borrow in calculation, the CF flag is set.
The AF flag is set because the rightmost nibble (
1010, i.e., the lower 4 bits) generated a carry into the higher nibble. If a carry occurs from the lower nibble to the higher one, the AF flag is set.
That’s what you need to understand about how the cmp instruction affects the flags. Also, notice that the IP register has now moved on to the address of the next instruction (a jump).

See how in the code window the JUMP instruction is shown as
JNGinstead ofJLE. Don’t worry—they mean the same thing.JNGmeans Jump if Not Greater, whileJLEmeans Jump if Less or Equal.Since this is a conditional jump, it depends on the result of the previous
cmpinstruction. The comparison was between the registersAXandBX, so the jump instruction checks their relationship to decide whether to jump or not.Put simply, the instruction now means: Jump if AX is less than or equal to BX, or equivalently, Jump if AX is not greater than BX.
Here,
AX = 10andBX = 15, so the condition is true becauseAXis indeed less thanBX.
Because the condition is satisfied, notice the IP register now contains the jump target address
0125instead of the usual next instruction address011E.That new address points to the
retinstruction

In this step, we have executed the return (
ret) instruction. As we learned before,retremoves the top value from the stack and updates the IP register with it. Notice how the address010Cthat was in the stack is now placed into theIPregister.
This means the subroutine has finished, and execution has returned to the usual flow right after the
callinstruction.The instruction at address
010Cismov [012C], AX. In our source code, we wrote it asmov [Min], AX, but the assembler replaces labels with their actual addresses. The[]means “the value at that address,” which we’ve already covered.Now focus:
AXcontains000A, while memory at[012C]currently contains00. You can confirm this in the memory window below, where the highlighted yellow block at offsetCshows00.Notice there are two highlighted blocks: one at offset
A(value00) and another at offsetC(value00).Our target address is
[012C]. The memory window showsDS:0120. HereDSmeans Data Segment, which is the section of memory reserved for data (variables, arrays, strings, etc.).The base address is
0120, and we need to reach offsetC. In hexadecimal,Cequals12in decimal. If you look closely, the memory window also labels columns in hexadecimal, helping us locate the correct cell at[012C].

These numbers are offsets (or indexes). Since we need to look at
[012C], we focus on the yellow-highlighted block atC. Later, for the next instruction, we’ll be checking the yellow block atA.The instruction tells us to
mov [012C], AX, which means move the value stored inAXinto that memory address. The next instruction will work the same way.Once we execute these instructions, the yellow blocks that currently contain
00will be updated with the values stored inAXandBX.

Take a look — after executing both
movinstructions, the memory locations now contain the values stored inAXandBX. We did it… Yay! 🎉
But hold on, we’re not done yet. Notice the stack still contains 000F and 000A, the values we pushed earlier. The reason is simple: if we ever need those values back, they’re safely stored in the stack. In this specific case, the subroutine didn’t modify AX or BX, so they remain unchanged.
Now, here’s the twist: if you change the source code so that num1 contains the bigger number and num2 the smaller one, you’ll see that both registers get updated inside the subroutine. In that case, if you didn’t save the original values in the stack, you’d lose them forever.
That’s the whole point of the stack: it allows us to save data temporarily and restore it later when needed. For demonstration, let’s assume AX and BX held different values — we’ll now pull them back from the stack.

Observe that, we executed the first
POPinstruction:POP BX. Notice how the current top stack value is now000Ainstead of000F. This means the value from the stack has been restored into theBXregister. If we execute the nextPOPinstruction, the000Awill also be popped out, updating theAXregister.
After that, there’s nothing new left for the remaining instructions. If we keep pressing
F1to step through one by one, eventually the program will finish and terminate.

Phew… that was a lot to digest, right? 😅 But trust me, you just picked up so many practical insights that probably felt like magic before. We’ve come a long way—be proud of ourselves. By now, we don’t just “know” assembly — we understand the intricacies behind the scenes that make it all work.
Now we have seen why procedures depend so much on the stack — for arguments, saving registers values, and returning to the caller. This isn’t just about finding min and max — this is the blueprint for every function we’ll ever write in assembly
Last updated
Was this helpful?