Debugging Registers, Flags, and Memory
Find and fix bugs like a detective — single-step, inspect, and trace your 8086 programs
Open interactive version (quiz + challenge)Real-world analogy
What is it?
Debugging 8086 programs means systematically inspecting CPU state — registers, flags, memory, and stack — to find where actual behavior diverges from expected behavior. Single-stepping executes one instruction at a time. Breakpoints skip to the interesting code. Flag analysis reveals arithmetic edge cases. Memory dumps verify data layout. Mastering these techniques turns opaque bugs into clear root causes.
Real-world relevance
In industry, embedded engineers debug microprocessors using JTAG probes that provide single-step and memory inspection on real hardware. Reverse engineers use debuggers (OllyDbg, x64dbg) to analyze unknown binaries instruction by instruction. Boot loader developers debug pre-OS code where no printf exists — only register and memory inspection. These skills are timeless.
Key points
- Single-Stepping (Trace Mode) — Single-stepping executes one instruction at a time, pausing after each. This lets you see exactly how each instruction changes registers, flags, and memory. In emu8086, click the 'single step' button (or press F8). In DEBUG.COM, use the T (Trace) command.
- Breakpoints — A breakpoint pauses execution at a specific instruction without single-stepping through everything before it. Set a breakpoint at the interesting code, run at full speed to reach it, then single-step from there. In emu8086, click the line number to toggle a breakpoint.
- Watching Registers — After each step, check all register values. Look for unexpected zeros, incorrect values, overflow indicators, and segment register issues. In emu8086, the register panel updates in real-time. Changed values are highlighted.
- Flag Analysis — Flags reveal what really happened during arithmetic and logic operations. ZF=1 means result was zero. CF=1 means unsigned overflow/borrow. SF=1 means result was negative. OF=1 means signed overflow. Always check flags after CMP, ADD, SUB operations.
- Memory Dump Inspection — View raw memory contents in hexadecimal. In emu8086, use the memory viewer to see bytes at any address. Compare expected vs actual values in your data segment. Watch for little-endian byte ordering — 1234h is stored as 34h, 12h.
- Common Bug: Wrong Segment — If your program reads garbage instead of your data, DS probably points to the wrong segment. Always verify DS after initialization. In emu8086, check that DS matches the data segment value shown in the segment panel.
- Common Bug: Off-by-One in Loops — LOOP decrements CX THEN checks for zero. If CX starts at 0, it wraps to FFFFh and runs 65535 times. Always guard with JCXZ before LOOP. Also verify array index bounds — accessing array[N] in an N-element array reads past the end.
- Common Bug: Stack Imbalance — Every PUSH must have a matching POP. If you push 3 values but pop 2, the stack is corrupted — RET will pop the wrong address and jump to garbage. Count your PUSHes and POPs carefully, especially in procedures.
- Common Bug: Unsigned vs Signed Confusion — Using JA/JB (unsigned) when you should use JG/JL (signed), or vice versa, causes logic errors. FFFFh is greater than 0 unsigned (65535 > 0) but less than 0 signed (-1 < 0). Match the jump type to your data interpretation.
- Using DEBUG.COM — DOS DEBUG.COM is a basic command-line debugger. Commands: R (display registers), T (trace/single-step), G (go/run), D (dump memory), U (unassemble), A (assemble), E (enter bytes). It works directly with machine code — no labels, just addresses.
Code example
; Program: Demonstrates common bugs and how to catch them
; Run this in emu8086, single-stepping to see each issue
.MODEL SMALL
.STACK 100h
.DATA
values DB 200, 150, 100, 50, 25
vcount EQU 5
result DW ?
msg DB 'Debug Demo$'
.CODE
MAIN PROC
MOV AX, @DATA
MOV DS, AX
; === Test 1: Unsigned overflow ===
MOV AL, 200
ADD AL, 100 ; AL = 300? No! AL = 44 (300-256)
; Step here and check: AL=2Ch, CF=1
; CF=1 tells you there was a carry out
; === Test 2: Signed overflow ===
MOV AL, 127 ; max positive signed byte
ADD AL, 1 ; AL = 128 = -128 signed!
; Step here: AL=80h, OF=1, SF=1
; OF=1 tells you signed overflow occurred
; === Test 3: Sum array (correct way) ===
LEA SI, values
XOR AX, AX ; clear sum
MOV CX, vcount
JCXZ no_data ; guard!
sum_loop:
MOV BL, [SI] ; load byte
XOR BH, BH ; zero-extend to word
ADD AX, BX ; add to word-size sum
INC SI
LOOP sum_loop
; AX should be 200+150+100+50+25 = 525
; Watch AX grow each iteration
no_data:
MOV [result], AX
; === Test 4: Verify memory ===
; After execution, check memory at 'result'
; Should see: 0D 02 (525 = 020Dh, little-endian)
MOV AH, 4Ch
INT 21h
MAIN ENDP
END MAINLine-by-line walkthrough
- 1. Test 1: MOV AL, 200 / ADD AL, 100 — AL is 8 bits, so 300 wraps to 44 (300 mod 256). CF=1 signals the unsigned overflow
- 2. Test 2: MOV AL, 127 / ADD AL, 1 — 127 is the max positive signed byte. Adding 1 gives 128 (80h), which is -128 in signed. OF=1 signals signed overflow
- 3. JCXZ no_data — guard before the loop. If vcount were 0, this prevents the loop from running 65535 times
- 4. XOR BH, BH — zero-extends BL to BX before adding. Without this, random data in BH corrupts the sum
- 5. ADD AX, BX — accumulates in a 16-bit register to handle sums over 255 (our sum is 525)
- 6. After the loop, result should be 020Dh (525). In memory it appears as 0D 02 due to little-endian byte order
- 7. At each step in emu8086, compare your expected register values with actual values. Any discrepancy is a bug
Spot the bug
; Sum should be 150 (50+50+50)
.DATA
vals DW 50, 50, 50
.CODE
MAIN PROC
MOV CX, 3
LEA SI, vals
XOR AX, AX
add_loop:
ADD AX, [SI]
INC SI
LOOP add_loop
MOV AH, 4Ch
INT 21h
MAIN ENDP
END MAINNeed a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Debugging 8086 Programs (GeeksforGeeks)
- x86 Flags Register (Wikipedia)
- emu8086 Debugging Tutorial (YouTube)
- DEBUG.COM Commands (Wikipedia)