8086 Assembly Program to Add Two 16-bit Numbers

Adding two 16-bit numbers is one of the first real programs every assembly language student writes — and for good reason. It touches every foundational concept at once: segment registers, data declarations, arithmetic instructions, result storage, and program termination. This post walks through a working implementation in three assembler environments: MASM/TASM (the classic DOS toolchain), emu8086 (the popular Windows emulator used in college labs), and NASM (the modern open-source assembler). All examples are tested and produce verifiable output.

Prerequisites: Basic familiarity with what a register is and how hexadecimal notation works. If you are new to 8086 assembly, read Understanding DW and DB in 8086 Assembly first.


The Problem: Adding 0202h and 0408h

We want to add two 16-bit values — 0202h and 0408h — and store the result in a third memory variable. The expected result is 060Ah (decimal 1546). The 8086 CPU has no single instruction that adds two memory operands directly; at least one operand must pass through a register. This constraint shapes the structure of every version below.

A quick sanity check before you even open the assembler: 0202h = 514 decimal, 0408h = 1032 decimal, and 514 + 1032 = 1546 = 060Ah. Always verify your expected output mentally first — it saves a lot of confused staring at the debugger later.


Version 1 — MASM / TASM (Classic DOS Toolchain)

This is the canonical 8086 addition program assembled with Microsoft MASM or Borland TASM on a real DOS machine (or DOSBox). The full session output — including the assembler, linker, and debugger — is shown in the Output section below.

; Data Segment

data segment
    a dw 0202h      ; 16-bit variable 'a' initialized with 0202h
    b dw 0408h      ; 16-bit variable 'b' initialized with 0408h
    c dw ?          ; 16-bit variable 'c' to store the result
data ends

; Code Segment

code segment
assume cs:code, ds:data
start:
    mov ax, data    ; Load the address of the data segment into AX
    mov ds, ax      ; Initialize the data segment register

    mov ax, a       ; Load value of 'a' into AX
    mov bx, b       ; Load value of 'b' into BX
    add ax, bx      ; Perform addition (AX = AX + BX)

    mov c, ax       ; Store the result in 'c'

    int 3           ; Halt program execution for debugging

code ends
end start           ; Mark the end of the program

16-bit addition in 8086: It’s like learning a new language, except the language is binary and the grammar is brutal 😉



Understanding the code:

Data Segment

  • a dw 0202h: This line declares a 16-bit word variable named a and initializes it with the hexadecimal value 0202.
  • b dw 0408h: Similarly, this declares a 16-bit word variable b and initializes it with the hexadecimal value 0408.
  • c dw ?: This declares a 16-bit word variable c but does not initialize it. It will store the result of the addition operation.
💡 Side note — dw vs db: A very common beginner slip is writing db (define byte) instead of dw (define word) for 16-bit variables. db reserves only 1 byte (8 bits); loading a 16-bit value into it corrupts whatever sits next to it in memory. For 16-bit numbers, always use dw.

Code Segment

  • mov ax,data: This instruction moves the address of the data segment into the AX register.
  • mov ds,ax: This sets the Data Segment (DS) register to the value in AX, effectively making the data segment accessible for data operations.
  • mov ax,a: This moves the value of a (0202h) into the AX register.
  • mov bx,b: This moves the value of b (0408h) into the BX register.
  • add ax,bx: This adds the contents of BX to the contents of AX. The result (060Ah) is stored in AX.
  • mov c,ax: This moves the result from AX to the variable c.
  • int 3: This is a breakpoint instruction that causes the program to halt, allowing for debugging and inspection of registers and memory.
💡 Side note — what does int actually mean? INT stands for interrupt — it is a software instruction that hands control over to a specific interrupt handler. INT 3 triggers interrupt vector 3, which the debugger has set up as a breakpoint handler. INT 21h triggers DOS interrupt 33 (21 in hex), which provides DOS services like file I/O and program exit. They are completely different handlers; the number after int is just the interrupt vector index.
⚠️ Common mistake — skipping the DS initialization: Many students delete the mov ax, data / mov ds, ax pair thinking it is boilerplate. It is not. Without it, DS still points to the PSP (Program Segment Prefix), not your data segment. Any subsequent memory access to a, b, or c will read garbage or crash. This two-line setup is mandatory in EXE programs.

Flowchart

Overall Functionality

The code performs the following steps:

  1. Initialization: Sets up the data segment and initializes variables a and b.
  2. Addition: Loads the values of a and b into registers AX and BX, respectively. Adds the two values and stores the result in AX.
  3. Result Storage: Stores the result from AX into the variable c.
  4. Program Termination: Halts the program execution using the int 3 instruction.

In essence, this code adds two 16-bit hexadecimal numbers (0202h and 0408h) and stores the result (060Ah) in the variable c.


Output

C:TASM>masm an16add.asm
Microsoft (R) Macro Assembler Version 5.00
Copyright (C) Microsoft Corp 1981-1985, 1987.  All rights reserved.
 
Object filename [an16add.OBJ]:
Source listing  [NUL.LST]:
Cross-reference [NUL.CRF]:
 
  50402 + 450254 Bytes symbol space free
 
      0 Warning Errors
      0 Severe  Errors
 
C:TASM>link an16add.obj
 
Microsoft (R) Overlay Linker  Version 3.60
Copyright (C) Microsoft Corp 1983-1987.  All rights reserved.
 
Run File [AN16ADD.EXE]:
List File [NUL.MAP]:
Libraries [.LIB]:
LINK : warning L4021: no stack segment
 
C:TASM>debug an16add.exe
-g
 
AX=060A  BX=0408  CX=0022  DX=0000  SP=0000  BP=0000  SI=0000  DI=0000
DS=0B97  ES=0B87  SS=0B97  CS=0B98  IP=0011   NV UP EI PL NZ NA PE NC
0B98:0011 CC            INT     3
-d 0B97:0000
0B97:0000  02 02 08 04 0A 06 00 00-00 00 00 00 00 00 00 00   ................
0B97:0010  B8 97 0B 8E D8 A1 00 00-8B 1E 02 00 03 C3 A3 04   ................
0B97:0020  00 CC 86 72 FF 77 15 8A-86 70 FF 2A E4 50 B8 FD   ...r.w...p.*.P..
0B97:0030  05 50 FF 36 24 21 E8 77-63 83 C4 06 FF 36 24 21   .P.6$!.wc....6$!
0B97:0040  B8 0A 00 50 E8 47 5E 83-C4 04 5E 8B E5 5D C3 90   ...P.G^...^..].
0B97:0050  55 8B EC 81 EC 84 00 C4-5E 04 26 80 7F 0A 00 74   U.......^.&....t
0B97:0060  3E 8B 46 08 8B 56 0A 89-46 FC 89 56 FE C4 5E FC   >.F..V..F..V..^.
0B97:0070  26 8A 47 0C 2A E4 40 50-8B C3 05 0C 00 52 50 E8   &.G.*[email protected].
-q
 
C:TASM>

The key line to look for is AX=060A — that confirms the addition produced the correct result. BX=0408 shows the second operand is still intact in BX after the ADD, which is expected since ADD only modifies the destination register.

💡 Side note — the "no stack segment" warning: The linker warning L4021: no stack segment is harmless for simple programs like this. It just means we did not declare a stack segment explicitly. DOS allocates a default stack, so the program runs fine. You can safely ignore this warning in lab assignments unless the program uses recursion or heavy procedure calls.

Understanding the Memory Dump

The memory dump provided by the -d 0B97:0000 command displays the contents of memory starting from the DS (data segment) base address, which is 0B97:0000 in this case.

Here is the relevant portion of the memory:

0B97:0000  02 02 08 04 0A 06 00 00-00 00 00 00 00 00 00 00

Here, 0A 06 is the result of a + b = 0202h + 0408h = 060Ah, stored in c. In the memory dump, values are stored in little-endian format, meaning the least significant byte appears first. Hence the result 060A is stored as 0A 06 in memory.

⚠️ Common mistake — misreading little-endian output: This trips up almost everyone at least once. When you see 0A 06 in the memory dump and expect 060A, the instinct is to think the program is wrong. It is not. The 8086 always stores the low byte first. Read the two bytes in reverse order — 06 is the high byte, 0A is the low byte — and you get back 060Ah. Every x86 processor from the original 8086 to a modern Intel Core uses this same byte order.

Version 2 — emu8086 (Windows Emulator)

emu8086 is the assembler and emulator combination most commonly used in undergraduate computer organization labs. It accepts MASM-compatible syntax but provides a built-in IDE, step-by-step execution, and a register/memory viewer — making it ideal for learning. The program below uses emu8086’s #make_COM# directive to produce a simpler COM-format executable, which skips the separate linking step entirely.

Tested with: emu8086 v4.08 on Windows 10 (also works on Windows 11 via compatibility mode).

; emu8086 version -- 8086 Assembly Program to Add Two 16-bit Numbers
; Assemble as COM file using: #make_COM#

#make_COM#

org 100h            ; COM programs begin at offset 100h in memory

; --- Code ---
start:
    mov ax, a       ; Load first operand into AX
    mov bx, b       ; Load second operand into BX
    add ax, bx      ; AX = AX + BX  (result: 060Ah)
    mov c, ax       ; Store result into variable c

    ; Terminate program cleanly via DOS interrupt
    mov ax, 4c00h   ; AH = 4Ch (terminate), AL = 00h (exit code 0)
    int 21h         ; Call DOS to exit

; --- Data declarations ---
a dw 0202h          ; First operand: 0202h
b dw 0408h          ; Second operand: 0408h
c dw 0000h          ; Result variable, initialized to 0
⚠️ Common mistake — using int 21h alone without setting AH: INT 21h is a multi-function DOS interrupt. The value in AH selects which DOS service to call. If you write int 21h without first loading AH = 4Ch, DOS reads whatever junk happens to be in AH and calls a completely different function — sometimes printing garbage to the screen, sometimes doing nothing at all. The pattern mov ax, 4c00h sets both AH = 4Ch (exit) and AL = 00h (exit code) in one instruction.

How to Run in emu8086

  1. Open emu8086 and paste the code into the editor.
  2. Click Compile and Run (or press F5).
  3. After execution stops, open the Memory panel and navigate to the address of variable c. You should see bytes 0A 06 — that is 060Ah in little-endian order.
  4. Alternatively, open the Registers panel immediately after the add instruction executes. The AX register will show 060A.

Why no mov ax, data / mov ds, ax in this version? In a COM program, the entire file — code and data — loads into a single 64 KB segment. DOS sets all four segment registers (CS, DS, ES, SS) to the same value before handing control to offset 100h. Since DS already points to the correct segment, the data variables are immediately addressable without any extra setup.


Version 3 — NASM (Modern Open-Source Assembler)

NASM (Netwide Assembler) is the assembler of choice for systems programming, OS development, and competitive programming on Linux. Its syntax differs from MASM in one critical way: memory operands must be enclosed in square brackets. Writing mov ax, a in NASM moves the offset address of a into AX, not the value at that address. To dereference the pointer you must write mov ax, [a].

The version below targets a 16-bit DOS COM executable assembled with NASM on Linux (via DOSBox for execution) or on Windows with DOSBox-X.

Tested with: NASM 2.16.01, executed in DOSBox 0.74-3.

; NASM version -- 8086 Assembly Program to Add Two 16-bit Numbers
; Assemble:  nasm -f bin add16.asm -o add16.com
; Run:       Place add16.com in DOSBox and execute it

bits 16             ; Produce 16-bit machine code
org  100h           ; COM files load at offset 100h

; --- Code ---
start:
    mov ax, [a]     ; Load first operand from memory into AX  (0202h)
    mov bx, [b]     ; Load second operand from memory into BX (0408h)
    add ax, bx      ; AX = AX + BX  (result: 060Ah)
    mov [c], ax     ; Write result back to memory variable c

    ; Exit cleanly via DOS interrupt 21h, function 4Ch
    mov ax, 4c00h   ; AH = 4Ch (exit program), AL = 00h (return code)
    int 21h

; --- Data (placed after code in COM layout) ---
a dw 0202h          ; First 16-bit operand
b dw 0408h          ; Second 16-bit operand
c dw 0000h          ; Result storage
⚠️ The most common NASM mistake — missing brackets: If you come from MASM or emu8086, the instinct is to write mov ax, a. In NASM that is legal — but it loads the address (offset) of a into AX, not the value stored there. Your program will assemble without errors, AX will contain some small offset number like 000Ch, and you will spend a long time wondering why the result is wrong. The fix is always mov ax, [a]. When in doubt in NASM, add the brackets.

Build and Run Steps

# Assemble to flat binary COM format
nasm -f bin add16.asm -o add16.com

# Verify the binary size (should be tiny -- about 14 bytes of code + 6 bytes of data)
ls -l add16.com

# Run inside DOSBox (mount current directory as C:)
# Inside DOSBox:
#   mount c .
#   c:
#   add16.com

Register State After Execution (All Three Versions)

Regardless of which assembler you use, the CPU register state after the add instruction is identical — because all three programs execute the same machine-code sequence on the same 8086-compatible processor model.

RegisterValueMeaning
AX060ASum of 0202h + 0408h (the result)
BX0408Second operand — unchanged after ADD
CF (Carry Flag)0No carry — result fits in 16 bits
ZF (Zero Flag)0Result is not zero
SF (Sign Flag)0Result is positive
OF (Overflow Flag)0No signed overflow

The Carry Flag is the one to watch in any addition program. Here it stays clear (0) because 060Ah is well within the 16-bit unsigned range (0 to FFFFh). This program does not check or handle carry — if you swap in two large values that sum beyond FFFFh, the result in c will silently wrap around and the carry will be lost. That is a separate problem handled in 8086 Assembly Program to Add Two 16-bit Numbers with Carry Handling.


Frequently Asked Questions

What exactly does int mean in int 3 and int 21h?

INT stands for interrupt. It is a CPU instruction that suspends the current program and jumps to a handler routine whose address is stored in the interrupt vector table — a lookup table at the very start of memory. The number after INT is just the index into that table. INT 3 jumps to vector 3, which debuggers like DOS DEBUG pre-configure as a breakpoint handler. INT 21h jumps to vector 33 (21 hex), which DOS sets up as its general-purpose service dispatcher — the function called depends on the value in AH when the interrupt fires. They have nothing in common beyond both being interrupt instructions.

Can I use .data instead of data segment … data ends?

It depends on the assembler. NASM uses section .data. In emu8086 and MASM, the simplified segment directive .data is supported as a shorthand — it is equivalent to data segment … data ends and automatically sets up the assume. However, the simplified form also requires .model small (or another memory model) at the top of the file, which the explicit segment form does not need. For lab assignments using the full segment syntax as shown in Version 1, stick with the explicit form to avoid confusion between the two styles.

What happens if I add two numbers whose sum exceeds FFFFh?

The 16-bit result wraps around (modulo 65536) and the Carry Flag (CF) is set to 1. For example, FFFFh + 0001h produces 0000h in AX with CF = 1. This program does not read CF after the addition, so the carry information is silently discarded and c will hold the wrapped-around value, not the true sum. If your operands might produce a sum larger than FFFFh, you need to check CF immediately after ADD and store it separately — typically using the ADC (add with carry) or JC (jump if carry) instruction. The full carry-handling version is covered at 8086 Assembly Program to Add Two 16-bit Numbers with Carry Handling.


Conclusion

Adding two 16-bit numbers in 8086 assembly is a three-step pattern you will use in almost every program you write: load operands into registers, perform the operation, store the result back to memory. That pattern does not change across MASM, emu8086, or NASM — only the syntactic details do. The mistakes that cost the most time are almost always the same ones: forgetting to initialize DS in EXE programs, misreading little-endian byte order in the memory dump, and dropping the brackets in NASM. Knowing those pitfalls going in puts you well ahead.


See Also

25 thoughts on “8086 Assembly Program to Add Two 16-bit Numbers”

    1. DW stands for define word. When we require 16 bit date, we use DW.
      DB stands for define byte. When 8bit data is required, we use DW.

    2. These are assembler directives
      dw = define word
      db = define byte

      used to specify the size of the data variable

    1. int 3 and int 21h both works fine, after all int 3 is used to terminate statements above it and int 21h is used to terminate whole program

Leave a Reply

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