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.

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:

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. These are usually prefixed with a "REP" modifier: 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