October 22, 2026
MOD_EMUP - 8086 assembly tutorial
Putting this together is more of a refresher course for myself.
My goal is not to write code, but to reverse engineer and understand
existing code. Notice also that I don't call this an x86 tutorial.
This is the ancient "real mode" 16 bit code that ran DOS.
I am using nasm (actually ndisasm). There are various assembly syntax
in use for this sort of code. Nasm moves from right to left.
In other words, the destination is on the left, much like an x=7 assignment.
This is important to bear in mind and get straight.
Registers
There are 4 main registers, A, B, C, and D. They can be used as 16 bit
registers (calling them AX), or split into two 8 bit (AH and AL).
There are five index/pointer registers.
- IP (usually called PC in other machines), which is rarely directly referenced.
- SP (stack pointer)
- BP (the base pointer).
- SI (source index)
- DI (destination index)
The base pointer is usually used to access parameters passed on the stack.
Don't confuse it with BX.
SI and DI are used for string and block operations.
Lastly there are the infernal segment registers. We have 3 of them:
- CS - code segment
- SS - stack segment
- DS - data segment
These are used shifted left by 4 bits (one hex digit), extending the 16 bit
addressing ability to 20 bits in an awkward fashion.
Basic instructions
mov ax, 5
add ax, bx
sub ax, 2
inc ax
dec ax
cmp ax, bx
jmp address
je/jne address
Here are some idioms:
or ax,ax ; set flags
xor ax,ax ; ax = 0
Memory access
Notice in the above that there is no indication of an "immediate value" when
we load the value 5 into the ax register. We use brackets to indicate that the
value is a memory reference, not a value to be manipulated. Like this:
mov ax, 0xb800
mov ds, ax
mov bx, 0x123
mov ds:[bx], cx
Notice that we cannot directly load values into a segment register, we have to
"bounce" them through a regular register like this.
Also we don't usually specify a segment register when we do a MOV.
There are defaults that are used (by default ...).
String instructions
There are 5 of these. They use SI as the source index, DI as the destination index,
and CX as the counter. There is a flag (DF) that determines direction.
- movsb/movsw - move string from [SI] to [DI]
- lodsb/lodsw - copy from [SI] to al/ax
- stosb/stosw - copy from al/ax to [DI]
- cmpsb/cmpsw - subtract [DI] from [SI] and set flags
- scasb/scasw - subtract [DI] from AL/AX and set flags
These are usually prefixed with a "REP" modifier:
- rep - repeat until cx == 0
- repe/repz - repeat while equal/zero, also watches CX
- repne/repnx
- loop - loop until cx decrements to 0
- cld - clear direction (go forward with si)
- std - set direction (go backward with si)
Here is an idiom for clearing a buffer:
cld ; move forward
mov cx, 512
mov al, 0
mov di, offset buffer
rep stosb
Here is an idiom to find the length of a null terminated string:
cld
mov di, offset str
mov al, 0
mov cx, 0xffff
repne scasb
mov ax, 0xffff
sub ax, cx
dec ax
Subroutines
The CALL instruction decrements SP by 2 and places the call address there.
The RET instruction pops that (incrementing SP by 2) and goes to that address.
Note that there are near and far calls. A far call pushes the CS first, then the IP,
so 4 bytes are used on the stack. RET handles a near return, RETF handles a far return.
RET n does a near return, but also adds "n" to the SP to discard that many bytes.
This is used to clean up arguments placed on the stack before the call.
In the code I am studying far calls are not marked as such, but most calls are far calls.
You can look at the opcode
0x9a is a far call
0xe8 is a near call
0xff can be either, a far call should be marked as such
System calls
These are managed by the INT instruction.
The int 0x21 instruction handles DOS system calls.
For int 0x21, the value in AH determines the call
mov dx, msg
mov ah, 0x09
int 21h ; print string (ends with '$')
And to exit --
mov al, 0 ; exit code
mov ah, 0x4c ; exit
int 21h
Have any comments? Questions?
Drop me a line!
Tom's Electronics pages / tom@mmto.org