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 sum

Explanation:

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, array

    This stores the address of array into the SI (source index) register. Remember, array is just a label — the assembler converts it into an actual memory address at runtime.

    Important detail: when we write array without 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 array at address 0109. This means after this instruction, SI = 0109.

  • Next instruction: mov cx, [arrSize]

    This loads the constant value (5) from arrSize into the CX register. As you recall, the LOOP instruction automatically decrements CX each time it runs.

  • Then: mov ax, 0

    We clear the AX register 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 to AX. Since SI holds the address, [si] means “the value stored at that address.” Initially, SI = 0109, which corresponds to the first element of array (2). So now AX = 2.

    • add si, 2 → Because our array elements are words (2 bytes each), we increment SI by 2 to point to the next element. If SI was 0109, now it becomes 010B.

    • loop nextElement → This instruction first decrements CX by 1, then checks if it’s zero. If not, it jumps back to nextElement.

      Example: Initially, CX = 5. After the first loop, it becomes 4 (not zero), so we jump back. This keeps happening until CX = 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 element

    Each 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], ax saves that final result into the variable Sum.

  • Finally:

    The last two instructions terminate the program as usual (mov ax, 0x4C00int 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, array

    Store the address of array into 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, 2

    Move SI to the second element (because word = 2 bytes). So now SI points to 3.

  • dec cx

    We 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 skipUpdate

    If AX[SI], then AX is still larger, so no update is needed. Jump to skipUpdate.

  • mov ax, [si]

    If the comparison showed that [SI] > AX, then we update AX with the new larger value.

  • skipUpdate:

    Regardless of whether AX was updated or not, we now:

    • add si, 2 → move SI to the next array element.

    • loop nextElementCX is 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  0

Step-by-Step Explanation

  1. Main Program Section

    • The instruction mov cx, [num] loads the value of num (in this case 5) into the CX register.

    • CX is special because the loop instruction automatically uses it as a counter. Every time we hit loop 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 ret executes, control comes back to the instruction after the call.

    • Finally, the result in AX is moved into our variable Fact with mov [Fact], ax.

  2. Procedure Section

    • Inside the factorial procedure, we start by clearing AX and setting it to 1. This is important because multiplying by 0 would always result in 0, so factorial must start at 1.

    • Then we enter a loop labeled factorialLoop.

    Now here’s the important part:

    mul cx
    • In x86 assembly, the mul instruction is implicit. It always assumes one operand is AX, and the other operand is what you specify.

    • That means mul cx really means:

      AX = AX * CX
    • There is no instruction like mul ax, cx. The CPU design doesn’t allow it — the mul instruction always uses AX as one of the operands automatically.

    So in our case:

    • First iteration: AX = 1 * 5 = 5

    • Second iteration: AX = 5 * 4 = 20

    • Third iteration: AX = 20 * 3 = 60

    • Fourth iteration: AX = 60 * 2 = 120

    • Fifth iteration: AX = 120 * 1 = 120

    • When CX becomes 0, the loop stops.

    • Finally, the procedure executes ret, which pops the return address from the stack and jumps back to the main program.

  3. Variables Section

    • num: dw 5 stores our input value (5 in this case).

    • Fact: dw 0 is where the result will be saved. After running, Fact will hold 120.

<aside> 📢

Key Notes

  • mul is a bit different from most instructions — it does not let you choose both operands explicitly.

  • If you do mul reg16 (like mul cx), the CPU does AX = AX * reg16.

  • If you do mul mem16, the CPU does AX = 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 string

Explanation:

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 .com programs.

  • At the end we again use mov ax, 0x4C00 and int 0x21 to terminate — as always.

  • mov si, str

    Loads 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.

  1. mov al, [si] → Load the character at [SI] into AL.

    • Example: if SI points to 'H', then AL = 'H'.

  2. push ax → Push AX onto the stack.

    • Even though only AL has meaningful data, the entire AX register (16 bits) gets pushed. That’s fine — only the low byte (AL) matters when we pop later.

  3. inc si → Move SI forward to the next character in the string.

  4. loop pushLoop → Decrement CX and 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.

  1. mov si, str → Reset SI to the start of the string. We’ll overwrite characters starting from the first position.

  2. mov cx, [strLen] → Reload the length into CX, so we know how many pops to perform.

  3. Loop:

    • pop ax → Pop the top of the stack into AX.

    • mov [si], al → Write the character in AL back 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 push stores 2 bytes (a word). That’s why we used push ax instead of just push al. When we pop, we still only care about AL.

  • 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 elements

Explanation:

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-1 times until the array is sorted.

3. Outer Loop

mov cx, [arrSize]   ; CX = array size
dec cx              ; Only n-1 passes needed
  • CX is 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 cx
  • Inner loop compares each element with its next neighbor.

  • Uses SI as the pointer to the current element.

  • add si, 2 moves 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 swap

Inside 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], ax
  • Push 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, 9

Key Concepts

Arrays → storing numbers in memory and walking with SI.

VariablesarrSize to keep track of length.

Loops → inner and outer loops using CX.

Stack → used to save loop counters and pass parameters.

Proceduresswap procedure makes code modular.

Last updated

Was this helpful?