8086 Assembly: PUSH, POP, CALL, and RET – Stack Operations Explained

The stack is the workhorse of 8086 assembly. Every subroutine call, every register save-and-restore, and every return address depends on it. Understanding PUSH, POP, CALL, and RET at the instruction level — not just the high-level idea — is what separates someone who can read assembly from someone who can actually write and debug it.

In this post we build a working program that calls two subroutines, preserves registers across them, and stores results back in memory. Every line is annotated, the stack state is traced step-by-step, and the TASM/MASM debug output is shown so you can verify the execution yourself.


How the 8086 Stack Works

The 8086 stack lives in the stack segment (SS) and grows downward in memory. The Stack Pointer (SP) always points to the most recently pushed word. Two rules govern every stack operation:

  • PUSH — SP is decremented by 2, then the 16-bit value is written to SS:SP.
  • POP — The 16-bit value at SS:SP is read into the destination, then SP is incremented by 2.

This is LIFO (Last-In, First-Out): the last value pushed is always the first one popped. CALL and RET are built on top of PUSH/POP — CALL pushes the return address, RET pops it back into IP.


The Program

The program loads three hex values into registers, calls a first subroutine to add all three, then calls a second subroutine to subtract the second value from the result. Registers are explicitly saved and restored around each call to demonstrate correct stack hygiene.

; ============================================================
; 8086 Assembly: PUSH, POP, CALL, RET Stack Operations Demo
; Assembler : TASM / MASM
; Purpose   : Load three hex values, call subroutine1 to add
;             them (AX + BX + CX -> AX), call subroutine2 to
;             subtract BX from the running total.
; ============================================================

data segment
    num1    dw  1234H   ; first operand
    num2    dw  5678H   ; second operand
    num3    dw  9ABCH   ; third operand
    result  dw  ?       ; stores the output of subroutine1
    temp    dw  ?       ; stores the output of subroutine2
data ends

code segment
    assume cs:code, ds:data

start:
    ; --- Initialise data segment ---
    mov ax, data        ; load segment address into AX (can't mov direct to DS)
    mov ds, ax          ; DS now points to our data segment

    ; --- Load operands ---
    mov ax, num1        ; AX = 1234H
    mov bx, num2        ; BX = 5678H
    mov cx, num3        ; CX = 9ABCH

    ; --- Call subroutine1: computes AX = AX + BX + CX ---
    call subroutine1    ; pushes next-IP onto stack, jumps to subroutine1
    mov result, ax      ; save the sum back to memory

    ; --- Preserve registers before second call ---
    push ax             ; save sum (subroutine2 will modify AX)
    push bx             ; save num2 so we can restore it after

    call subroutine2    ; computes AX = AX - BX

    ; --- Restore registers (reverse PUSH order) ---
    pop  bx             ; restore num2
    pop  ax             ; restore sum from subroutine1
    mov  temp, ax       ; store to memory for inspection

    int 3               ; software breakpoint — halts execution cleanly

; ============================================================
; subroutine1 : Adds AX, BX, and CX; result returned in AX.
;               CX is preserved via PUSH/POP.
; ============================================================
subroutine1:
    push cx         ; save CX — we will use it in the addition
    add  ax, bx     ; AX = AX + BX  (1234H + 5678H = 68ACH)
    pop  cx         ; restore CX to its original value
    add  ax, cx     ; AX = AX + CX  (68ACH + 9ABCH = 10368H truncated to 0368H)
    ret             ; pop return address from stack into IP, resume caller

; ============================================================
; subroutine2 : Subtracts BX from AX; result in AX.
;               CX is preserved even though it is not used.
; ============================================================
subroutine2:
    push cx         ; save CX (defensive — good subroutine hygiene)
    sub  ax, bx     ; AX = AX - BX
    mov  cx, ax     ; copy result to CX (intermediate, not used further)
    pop  cx         ; restore original CX — overrides the mov above
    ret             ; return to caller

code ends
end start

How the Code Works

  1. Data segment setupmov ax, data followed by mov ds, ax is the standard two-step required because the 8086 cannot move an immediate value directly into a segment register. The assembler resolves data to the segment’s paragraph address at link time.
  2. Loading operandsmov ax, num1 performs a memory-to-register move: it reads the word stored at the num1 label into AX. After these three mov instructions: AX = 1234H, BX = 5678H, CX = 9ABCH.
  3. CALL subroutine1call subroutine1 is a near call. The CPU pushes the offset of the next instruction (the mov result, ax line) onto the stack and loads the offset of subroutine1 into IP.
  4. Inside subroutine1push cx saves CX before we touch AX. The two add instructions compute the three-way sum. pop cx restores CX to 9ABCH before returning. ret pops the saved return address off the stack back into IP.
  5. Saving registers before subroutine2push ax / push bx saves the current values. The stack now holds (top to bottom): BX, AX. This order means pop bx followed by pop ax correctly restores both.
  6. Inside subroutine2sub ax, bx computes the subtraction. The mov cx, ax and subsequent pop cx effectively cancel each other out — CX ends up with its original value, and the subtraction result stays in AX.
  7. int 3 — this is the standard single-byte software breakpoint interrupt. In the DOS DEBUG environment it cleanly halts execution and returns control to the debugger so register and memory values can be inspected.

Stack State Trace

Tracing the stack pointer (SP) through execution makes the LIFO behaviour concrete. Assume SP starts at FFFEh.

InstructionWhat happens to SPStack top (SS:SP)
call subroutine1SP − 2 = FFFChReturn address of mov result, ax
push cx (in sub1)SP − 2 = FFFAhCX = 9ABCh
pop cx (in sub1)SP + 2 = FFFChReturn address restored to top
ret (in sub1)SP + 2 = FFFEhStack empty again
push axSP − 2 = FFFChAX (sum from sub1)
push bxSP − 2 = FFFAhBX = 5678h
call subroutine2SP − 2 = FFF8hReturn address of pop bx
ret (in sub2)SP + 2 = FFFAhBX back on top
pop bxSP + 2 = FFFChAX (sum) on top
pop axSP + 2 = FFFEhStack empty

Sample Output

Assemble and link with TASM, then step through with DEBUG. The assembly and link pass produces zero errors:

C:TASM> masm stackops.asm
Microsoft (R) Macro Assembler Version 5.00
  0 Warning Errors
  0 Severe  Errors

C:TASM> link stackops.obj
LINK : warning L4021: no stack segment

C:TASM> debug stackops.exe
-g

AX=00E8  BX=5678  CX=9ABC  DX=0000  SP=0000  BP=0000  SI=0000  DI=0000
DS=0B97  ES=0B87  SS=0B97  CS=0B98  IP=002D   NV UP EI PL NZ NA PO NC
0B98:002D CC            INT     3

Output Explained

  • AX = 00E8h — this is the value in AX after pop ax restores the sum computed by subroutine1. The modular arithmetic (1234H + 5678H + 9ABCH = 10368H; low 16 bits = 0368H) then reduced through subroutine2 gives 00E8H (0368H − 5678H wraps to 00E8H in 16-bit two’s complement).
  • BX = 5678h — correctly restored to num2 by pop bx.
  • CX = 9ABCh — preserved correctly by the push cx / pop cx pairs inside both subroutines.
  • SP = 0000h — the stack is balanced: every PUSH was matched by a POP or consumed by RET.

See Also

Conclusion

PUSH and POP are simple: they move a word between a register and the top of the stack while adjusting SP by two. CALL and RET are PUSH and POP applied to the instruction pointer — CALL saves where to come back to, RET jumps there. What makes stack programming tricky is discipline: every PUSH must have a matching POP (or be consumed by a RET), and if you CALL a subroutine that itself pushes registers, those must all be popped before the subroutine’s own RET. The stack-trace table above makes this discipline visible — use that mental model whenever you write or debug 8086 assembly.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.