Getting Our Hands Dirty — Practice Problems in Assembly
We’ve done a lot so far. From registers and memory all the way to addressing modes, instructions, variables, arrays, the stack, and even full-blown procedures with debugger walkthroughs. That was a lot of theory plus hands-on breakdowns.
But now it’s time to switch gears. This section is not about me spoon-feeding every single step like I did in the Procedures Extended section. That chapter was special — I gave you a detailed, screenshot-heavy tour because I wanted you to get a real taste of how assembly works in practice.
From here on, though, the training wheels are off 🚴. I’ll give you line-by-line explanations of the code, and whenever a screenshot is really needed, I’ll include it. But not for every step. Why? Because now it’s your turn to practice.
This section is all about solving practical problems in assembly. We’ll write small programs, explain the thought process behind them, and see how everything you’ve learned so far comes together. The difference is, this time you’ll be doing more of the heavy lifting yourself.
By the end of this section, you won’t just be reading assembly — you’ll be writing it, debugging it, and thinking in it.
Problem 1 — Sum Of Array Elements
Statement:
Write a program that takes an array of numbers and finds the total sum of all elements. Store the result in a variable called Sum.
Solution:
; Main Program
; ------------
[org 0x100]
mov si, array ; Load the address of array into SI (source index register)
mov cx, [arrSize] ; Load the array size into CX (loop counter)
mov ax, 0 ; Clear AX, we'll use it to hold the running sum
nextElement:
add ax, [si] ; Add the current array element to AX
add si, 2 ; Move to the next element (word = 2 bytes)
loop nextElement ; Repeat until CX = 0
mov [Sum], ax ; Store the final sum into variable Sum
; Termination
; -----------
mov ax, 0x4C00 ; Exit program
int 0x21
; Variables Section
; -----------------
array: dw 2, 4, 6, 8, 10 ; Example array with 5 elements
arrSize: dw 5 ; Constent size of Array
Sum: dw 0 ; Variable to store the sumExplanation:
The code above is pretty straightforward if you ask me, but let’s break it down step by step.
The first line [org 0x100] is the standard starting point for our DOS assembly programs. The last two instructions in the Main Program section are the termination lines (mov ax, 0x4C00 and int 0x21), which will appear in every program we write. The logic in between is what changes depending on the problem. Let’s focus on that part here.
First instruction:
mov si, arrayThis stores the address of
arrayinto the SI (source index) register. Remember,arrayis just a label — the assembler converts it into an actual memory address at runtime.Important detail: when we write
arraywithout brackets, we are talking about the address itself, not the value. If we used[array], that would mean the value stored at that address.For the sake of example, let’s assume the assembler sets
arrayat address0109. This means after this instruction, SI =0109.Next instruction:
mov cx, [arrSize]This loads the constant value (
5) fromarrSizeinto theCXregister. As you recall, theLOOPinstruction automatically decrementsCXeach time it runs.Then:
mov ax, 0We clear the
AXregister to avoid carrying any garbage values into our calculation.Main loop begins:
At the label
nextElement, we start iterating through the array.add ax, [si]→ This adds the current element of the array toAX. SinceSIholds the address,[si]means “the value stored at that address.” Initially, SI =0109, which corresponds to the first element ofarray(2). So now AX =2.add si, 2→ Because our array elements are words (2 bytes each), we incrementSIby 2 to point to the next element. IfSIwas0109, now it becomes010B.loop nextElement→ This instruction first decrementsCXby 1, then checks if it’s zero. If not, it jumps back tonextElement.Example: Initially,
CX = 5. After the first loop, it becomes 4 (not zero), so we jump back. This keeps happening untilCX = 0.
Inside the loop, the pattern repeats:
add ax, [si] ; Add array element with AX current value & store the result back into AX add si, 2 ; Point SI to next elementEach time,
[si]gives us the next array value (4, 6, 8, 10), and we keep adding them to AX.After the loop finishes:
At this point, AX contains the sum of all elements in the array. The instruction
mov [Sum], axsaves that final result into the variableSum.Finally:
The last two instructions terminate the program as usual (
mov ax, 0x4C00→int 0x21). Afterwards we just declared some variables and array.
Now the program ends, and the value in Sum will hold the total sum of the array elements.
Problem 2 — Find The Maximum Element In An Array
Statement:
Write a program that scans through an array and determines the maximum value. Store the result in a variable called Max.
Solution:
; Main Program
; ------------
[org 0x100]
mov si, array ; Load the address of array into SI (points to first element)
mov cx, [arrSize] ; Load array size into CX (loop counter)
mov ax, [si] ; Load the first element into AX (assume it's the max for now)
add si, 2 ; Move SI to point to the next element
dec cx ; We've already checked the first element, so decrement count
nextElement:
cmp ax, [si] ; Compare current max (AX) with array element
jge skipUpdate ; If AX >= [SI], no need to update max
mov ax, [si] ; Otherwise, update AX with the new maximum
skipUpdate:
add si, 2 ; Move to the next element
loop nextElement ; Repeat until CX = 0
mov [Max], ax ; Store the maximum value in Max
; Termination
; -----------
mov ax, 0x4C00
int 0x21
; Variables Section
; -----------------
array: dw 7, 3, 15, 9, 12 ; Example array with 5 elements
arrSize: dw 5 ; Constant size of Array
Max: dw 0 ; Variable to store the maximum
Explanation:
This program is almost like the sum program, but instead of continuously adding values, we keep track of the largest value found so far.
Program setup:
[org 0x100]is our usual starting point.
Let’s go line by line through the logic.
mov si, arrayStore the address of
arrayinto the SI register. Initially, SI points to the first element of the array.mov cx, [arrSize]Load the size of the array (5 in this case) into the CX register. This will serve as our loop counter.
mov ax, [si]Load the first element of the array into AX. For now, we assume this is the maximum value. If the array starts as
7, 3, 15, 9, 12, then AX = 7.add si, 2Move SI to the second element (because word = 2 bytes). So now SI points to
3.dec cxWe already processed the first element (by putting it into AX), so we reduce CX by 1. Now CX = 4, meaning we’ll check 4 more elements.
The Loop
cmp ax, [si]Compare the current max (AX) with the element pointed to by
SI.jge skipUpdateIf
AX≥[SI], thenAXis still larger, so no update is needed. Jump toskipUpdate.mov ax, [si]If the comparison showed that
[SI]>AX, then we updateAXwith the new larger value.skipUpdate:Regardless of whether
AXwas updated or not, we now:add si, 2→ move SI to the next array element.loop nextElement→CXis decremented, and if it’s not zero, the program jumps back to compare again.
Step-by-step Example Walkthrough
Let’s assume the array is: 7, 3, 15, 9, 12
Start: AX = 7 (first element), SI → 3, CX = 4.
Compare AX (7) with 3 → AX is larger → no update.
Next element: AX = 7, SI → 15, CX = 3.
Compare AX (7) with 15 → update AX = 15.
Next element: AX = 15, SI → 9, CX = 2.
Compare AX (15) with 9 → AX stays 15.
Next element: AX = 15, SI → 12, CX = 1.
Compare AX (15) with 12 → AX stays 15.
Loop ends (CX = 0).
Final result: Max = 15.
Problem 3 — Factorial Of A Number (Using Procedure)
Statement:
Write a program that calculates the factorial of a number stored in a variable. Use a procedure to handle the calculation, and use the stack to save/restore registers.
Solution:
; Main Program
; ------------
[org 0x100]
mov cx, [num] ; CX = number
call factorial ; Call factorial procedure
mov [Fact], ax ; Store the result
; Termination
; -----------
mov ax, 0x4C00
int 0x21
; Procedure Section
; -----------------
factorial:
mov ax, 1 ; Start with 1
factorialLoop:
mul cx ; AX = AX * CX
loop factorialLoop ; Repeat until CX = 0
ret
; Variables Section
; -----------------
num: dw 5
Fact: dw 0Step-by-Step Explanation
Main Program Section
The instruction
mov cx, [num]loads the value ofnum(in this case5) into the CX register.CX is special because the
loopinstruction automatically uses it as a counter. Every time we hitloop factorialLoop, CX decrements by 1 until it becomes 0.Next, we call the factorial procedure with
call factorial. Execution jumps to that procedure, and the return address is automatically pushed onto the stack.After the procedure finishes and
retexecutes, control comes back to the instruction after thecall.Finally, the result in AX is moved into our variable
Factwithmov [Fact], ax.
Procedure Section
Inside the
factorialprocedure, we start by clearing AX and setting it to1. This is important because multiplying by0would always result in0, so factorial must start at1.Then we enter a loop labeled
factorialLoop.
Now here’s the important part:
mul cxIn x86 assembly, the
mulinstruction is implicit. It always assumes one operand is AX, and the other operand is what you specify.That means
mul cxreally means:AX = AX * CXThere is no instruction like
mul ax, cx. The CPU design doesn’t allow it — themulinstruction always uses AX as one of the operands automatically.
So in our case:
First iteration:
AX = 1 * 5 = 5Second iteration:
AX = 5 * 4 = 20Third iteration:
AX = 20 * 3 = 60Fourth iteration:
AX = 60 * 2 = 120Fifth iteration:
AX = 120 * 1 = 120When
CXbecomes 0, the loop stops.Finally, the procedure executes
ret, which pops the return address from the stack and jumps back to the main program.
Variables Section
num: dw 5stores our input value (5 in this case).Fact: dw 0is where the result will be saved. After running,Factwill hold120.
<aside> 📢
Key Notes
mulis a bit different from most instructions — it does not let you choose both operands explicitly.If you do
mul reg16(likemul cx), the CPU doesAX = AX * reg16.If you do
mul mem16, the CPU doesAX = AX * [memory value].So whenever you see
mul, just remember: AX is always involved.That’s why you’ll never see something like
mul ax, cx— it’s invalid syntax.
Problem 4 — Reverse A String (Using Stack)
Statement:
Write a program that stores a string in memory, then reverses it using the stack. The reversed string should overwrite the original string in memory.
Solution:
; Main Program
; ------------
[org 0x100]
mov si, str ; SI = address of string
mov cx, [strLen] ; CX = string length (loop counter)
; Step 1: Push all characters of the string onto the stack
pushLoop:
mov al, [si] ; Load current character into AL
push ax ; Push AX (only AL has data, AH is garbage but harmless)
inc si ; Move to next character
loop pushLoop ; Repeat until all characters are pushed
; Step 2: Pop characters from stack back into the string (reversing)
mov si, str ; Reset SI to point back to the start of string
mov cx, [strLen] ; Reload length into CX
popLoop:
pop ax ; Pop top of stack into AX
mov [si], al ; Store popped character back into string
inc si ; Move forward in string
loop popLoop ; Repeat until CX = 0
; Termination
; -----------
mov ax, 0x4C00
int 0x21
; Variables Section
; -----------------
str: db 'HELLO' ; Original string
strLen: dw 5 ; Length of stringExplanation:
This program takes the string "HELLO", pushes each character onto the stack, and then pops them back into memory, effectively reversing the order. Let’s break it down carefully.
Step 1: Setup
[org 0x100]→ Standard origin for DOS.comprograms.At the end we again use
mov ax, 0x4C00andint 0x21to terminate — as always.mov si, strLoads the address of the string into
SI. This register will walk through the string.mov cx, [strLen]Loads the length of the string (5 in this case) into
CX. This gives us a loop counter to know how many characters to process.
Step 2: Pushing characters onto the stack
This loop moves through the string from first character to last character, and pushes each one onto the stack.
mov al, [si]→ Load the character at[SI]intoAL.Example: if
SIpoints to'H', thenAL = 'H'.
push ax→ PushAXonto the stack.Even though only
ALhas meaningful data, the entireAXregister (16 bits) gets pushed. That’s fine — only the low byte (AL) matters when we pop later.
inc si→ MoveSIforward to the next character in the string.loop pushLoop→ DecrementCXand repeat until all characters are pushed.
At this point, the stack (top to bottom) holds: O, L, L, E, H.
Step 3: Popping characters back
Now we pop characters off the stack. Since the stack is Last-In-First-Out (LIFO), popping reverses the order automatically.
mov si, str→ ResetSIto the start of the string. We’ll overwrite characters starting from the first position.mov cx, [strLen]→ Reload the length intoCX, so we know how many pops to perform.Loop:
pop ax→ Pop the top of the stack intoAX.mov [si], al→ Write the character inALback into the string at[SI].inc si→ Move to the next memory location in the string.loop popLoop→ Repeat until the whole string is filled.
After all pops, the string in memory has become "OLLEH".
Key Points to Remember
The stack reverses order automatically: pushing forward, then popping back, gives us reversed data.
Every
pushstores 2 bytes (a word). That’s why we usedpush axinstead of justpush al. When we pop, we still only care aboutAL.We overwrote the original string, so no extra memory was needed (unlike the previous array problems where we used separate variables).
This is the first time we’re using the stack in a real program, not just for subroutines.
Problem 5 — Bubble Sort
Statement:
Write a program that sorts a 10-element array using the Bubble Sort algorithm. The array initially contains the values 9, 3, 4, 8, 6, 2, 5, 1, 7, 0. The program should use arrays, variables, loops, stack, and procedures. After sorting, the array in memory should be in ascending order.
Solution:
; Main Program
; ------------
[org 0x100]
mov cx, [arrSize] ; Outer loop counter = number of elements
dec cx ; Bubble sort requires n-1 passes
outerLoop:
push cx ; Save outer loop counter on stack
mov si, array ; Start of array
mov cx, [arrSize] ; Inner loop counter = number of elements
dec cx ; Compare up to n-1 elements
innerLoop:
mov ax, [si] ; Load current element into AX
mov bx, [si+2] ; Load next element into BX
cmp ax, bx ; Compare AX and BX
jbe noSwap ; If AX <= BX, no swap needed
; Call Swap Procedure
push si ; Push address of current element
call swap ; Perform swap using procedure
noSwap: ; jump label
add si, 2 ; Move to next element
loop innerLoop ; Repeat inner loop
pop cx ; Restore outer loop counter
loop outerLoop ; Repeat outer loop
; Termination
; -----------
mov ax, 0x4C00
int 0x21
; Procedure Section
; -----------------
swap:
pop di ; Get return address into DI
pop si ; Get array address passed to procedure
mov ax, [si] ; Load current element
mov bx, [si+2] ; Load next element
mov [si], bx ; Store next element in current position
mov [si+2], ax ; Store current element in next position
push si ; Push parameter back (to keep stack balanced)
push di ; Push return address back
ret ; Return to caller
; Variables Section
; -----------------
array: dw 9, 3, 4, 8, 6, 2, 5, 1, 7, 0 ; Unsorted array
arrSize: dw 10 ; Number of elementsExplanation:
This is our biggest program yet — the one where every concept comes together. Let’s break it down piece by piece.
1. Setup
[org 0x100]→ Standard DOS origin.
2. Bubble Sort Algorithm Refresher
Bubble Sort repeatedly passes through the array.
Each pass compares adjacent pairs and swaps them if they are out of order.
After the first pass complete, the largest element come to the end.
Repeat this
n-1times until the array is sorted.
3. Outer Loop
mov cx, [arrSize] ; CX = array size
dec cx ; Only n-1 passes neededCXis our outer loop counter. Each pass moves the next-largest number to its final position.We push it onto the stack before the inner loop so it’s not lost.
4. Inner Loop
mov si, array ; SI = start of array
mov cx, [arrSize] ; CX = number of comparisons
dec cxInner loop compares each element with its next neighbor.
Uses
SIas the pointer to the current element.add si, 2moves to the next element (because array elements are words = 2 bytes).
5. Comparison & Conditional Swap
mov ax, [si] ; Current element
mov bx, [si+2] ; Next element
cmp ax, bx
jbe noSwap ; Jump if Below or Equal (AX <= BX)If current element ≤ next element, no swap is needed → jump to
noSwap.Otherwise, swap them by calling a procedure.
6. Swap Procedure (with stack usage)
push si ; Pass current element address
call swapInside the procedure:
pop di→ Pops return address into DI.pop si→ Pops the parameter (the address of the current element).Perform swap:
mov ax, [si] mov bx, [si+2] mov [si], bx mov [si+2], axPush parameter and return address back in reverse order.
ret→ Returns to the caller safely.
This shows procedure + stack usage in action.
7. Final Result
When the program finishes, the array in memory (array) is sorted in ascending order:
0, 1, 2, 3, 4, 5, 6, 7, 8, 9Key Concepts
✔ Arrays → storing numbers in memory and walking with SI.
✔ Variables → arrSize to keep track of length.
✔ Loops → inner and outer loops using CX.
✔ Stack → used to save loop counters and pass parameters.
✔ Procedures → swap procedure makes code modular.
Last updated
Was this helpful?