Computing: DOS, OS/2 & Windows Programming

16-bit assembly programming using NASM.

The Netwide Assembler (NASM) is an an assembler for the x86 CPU architecture portable to nearly every modern platform, and with code generation for many platforms old and new. The good news for DOS nostalgics is that the NASM team continues to support DOS, so the latest version of NASM is available for this operating system. You can download NASM from the developers' website; be sure to pick the (latest) release for DOS.

The intention of this tutorial is primarily to show how to use NASM to build 16-bit real mode and protected mode programs on DOS. As the tutorial includes general information about assembly programming, and some largely commented sample programs, it may also be seen as a starting point for assembly newbies to create their own programs (without really being an introduction to the NASM assembly programming language). The program samples have been build and tested on FreeDOS 1.3 RC5, using NASM 2.16.0.1. The tutorial should also apply to MS-DOS or other DOS operating systems. To note, that the protected mode executables, build here, also run on the first Windows releases and in Command Prompt of the following ones (at least until Windows 2000 included). Use the following link to download the source code of the sample programs.

The NASM download is a ZIP archive (in my case: nasm-2.16.01-dos.zip). I unpacked it on my Windows 10 and created an ISO to get the files onto my FreeDOS VMware virtual machine. On FreeDOS, I created the directory C:\NASM and copied all files from the CD to there. The screenshot shows the content of the NASM directory. The assembler executable is called NASM.EXE.

NASM on FreeDOS: Files extracted from the NASM download archive

Before starting to try out the assembler, here some important facts to know about assembly programming:

These long theoretical considerations may be annoying, but I think that you really have to know and understand these basic concepts if you want to create assembly programs...

On a x86 32-bit DOS system, there are three kinds of executables that we can create with NASM:

Building 16-bit real mode programs.

Real mode, also called real address mode, is an operating mode of all x86-compatible CPUs. The mode gets its name from the fact that addresses in real mode always correspond to real locations in memory. Real mode is characterized by a 20-bit segmented memory address space (giving 1 MB of addressable memory, that the real mode program has to share with the programs and drivers already there) and unlimited direct software access to all addressable memory, I/O addresses and peripheral hardware. Real mode provides no support for memory protection, multitasking, or code privilege levels. For details, have a look at the following Wikipedia article.

Here is the code of HELLO1.ASM, a simple real mode "Hello World" program.

org 100h
section .text
start:
        ; Display message calling the DOS write-string function
        mov     dx, hello
        mov     ah, 9
        int     0x21
        ; Terminate the program calling the DOS exit function
        mov     ax, 0x4c00
        int     0x21
section .data
hello   db     'hello, world', 13, 10, '$'
section .bss
        ; Put uninitialised data here

Some general notes concerning this code:

NASM.EXE has a whole bunch of command line parameters. Two of them are of particular interest for us. First of all, we have to indicate the format of the output produced by the assembler (flat-form binary in our case). This is done by using -f bin. With bin, the output actually is a real mode executable rather than an object file and to create a file with .com extension, we'll have to indicate the name of the assembler output, using -o <filename>.com. The command to assemble HELLO1.ASM in order to create a 16bit real mode DOS executable is as follows:
   nasm -f bin hello1.asm -o hello1.com

The screenshot below shows the execution of this command (no output if there aren't any errors or warnings), a directory listing with the source and executable (note that HELLO1.COM is only 27 bytes!), and the execution of the program (displaying the text "hello, world").

NASM on FreeDOS: Creating and running a 16-bit real mode 'Hello World' program

Building 16-bit protected mode programs.

Protected mode, also called protected virtual address mode, is an operational mode of x86-compatible CPUs that allows system software to use features such as segmentation, virtual memory, paging and safe multi-tasking designed in order to increase an operating system's control over application software. For details, have a look at the following Wikipedia article.

Here is HELLO2.ASM, the protected mode version of our "Hello World" program from above.

segment code
..start:
        ; Initialization
        mov     ax, data
        mov     ds, ax
        mov     ax, stack
        mov     ss, ax
        mov     sp, stacktop
        ; Display message calling the DOS write-string function
        mov     dx, hello
        mov     ah, 9
        int     0x21
        ; Terminate the program calling the DOS exit function
        mov     ax, 0x4c00
        int     0x21
segment data
hello   db      'hello, world', 13, 10, '$'
segment stack   stack
        resb    64
stacktop:

Comparing with the code of our program with bin output, we note the following differences:

As said above, to create our 16-bit protected mode .exe file, we'll have to tell the assembler to create a Microsoft OMF object file. This is done, by setting the command line parameter -f obj. No need to use the -o parameter in this case: the default name of the output file is identical to the assembler source file name, with an extension of .obj. So, for the program HELLO2.ASM, the command to assemble it is as follows:
   nasm -f obj hello2.asm

As a difference with before, the assembler output is object code that cannot be executed without being treated by a linker. The NASM download archive doesn't include a linker, so we'll have to find one somewhere else.

There are several 16-bit linkers for DOS available free of charge. The most commonly used are probably ALINK and QLINK. Both require the presence of a DPMI host. An alternative (used by myself when creating the samples in this tutorial), is to use the linker included with MASM. I actually use the one included with MASM 6.11, that you can download from the WinWorld website. For your convenience, I placed a copy of LINK.EXE on my site.

To link our HELLO2.OBJ file in order to create an executable called HELLO2.EXE, run the command
   link hello2.obj,hello2.exe
where you can omit the second parameter, because first, it's the default used by the linker and second, you'll be asked for the name (and some other info when you link the object).

The screenshot below shows the linkage of HELLO2.OBJ (as you can see, Run File is automatically set to HELLO2.EXE; just hit ENTER to accept the default, as you can do for the other items asked), a directory listing with source, object and executable file (note that HELLO2.EXE is lots bigger in size than the real mode HELLO1.COM created before), and finally the execution of HELLO2.EXE.

NASM on FreeDOS: Linking and running a 16-bit protected mode 'Hello World' program

Note: I think that you haven't to worry about the real mode warning issued by LINK.EXE. If you want to prevent it, start your FreeDOS system using HIMEMX.EXE (+ JEMM386.EXE) instead of JEMMEX.EXE as memory driver (add a menu option to your FDCONFIG.SYS, if you don't already have...).

Running NASM using a custom batch file.

The aim of DOS batch files is to make life easier, defining several DOS commands in a file, that you can run as a shell script. My NASM.BAT has for objective the creation of either a real mode, or a protected mode executable, starting from an assembly source file <filename>.asm. The batch file awaits 2 command line parameters: the first one to define what kind of executable you want to create (-r for a real mode program, -p for a protected mode program), the second one being the name of the assembly source file without file extension. The executable produced will be called <filename>.com, or <filename>.exe, depending on the first parameter. Here is the content of my file:

@echo off
if "%1"=="-r" set _MODE=real
if "%1"=="-p" set _MODE=protected
set _PROG=%2
if "%_MODE%"=="" goto NoMode
if "%_PROG%"=="" goto NoProg
if "%_MODE%"=="protected" goto Prot
:Real
if exist %_PROG%.com del %_PROG%.com
c:\nasm\nasm.exe -f bin %_PROG%.asm -o %_PROG%.com
goto End
:Prot
if exist %_PROG%.obj del %_PROG%.obj
if exist %_PROG%.exe del %_PROG%.exe
c:\nasm\nasm.exe -f obj %_PROG%.asm
if not exist %_PROG%.obj goto End
c:\masm611\bin\link.exe %_PROG%.obj,%_PROG%.exe
goto End
:NoMode
echo Invalid or missing mode
goto Usage
:NoProg
echo Missing file name
:Usage
echo Usage: nasm -r|-p asm-file-name
:End
set _MODE=
set _PROG=

Note: The paths c:\nasm\nasm.exe and c:\masm611\bin\link.exe correspond to where NASM and MASM were installed on my system. You'll eventually have to adapt them (or add the directories containing the files to your PATH environment variable, or copy the two files to your c:\freedos\bin directory).

Using interrupts to call DOS functions.

Interrupts are signals generated by the computer every time a certain event occurs. Hardware interrupts are triggered by hardware devices such as the network or sound card. Software interrupts may be triggered from within an assembly program using the INT instruction. The interrupts are numbered from 0x00 to 0xFF and each have a special function (most are unused) and their effect is that the normal program execution is suspended, and the control being transferred to a so-called interrupt handler. This a portion of code to be executed when this specific interrupt occurs. As an example, interrupt 09h (IRQ1) is a keyboard interrupt. It is called each time a key on the keyboard is pressed and the corresponding handler (part of the BIOS) reads the value of the key pressed and puts into the keyboard buffer. Beside this, some special keys or key combinations induce some supplementary action, as for example Shift+PrtScr that triggers interrupt 05h (sending the screen content to the printer), or CTRL+ALT+DEL that makes a direct call to address 0FFFFh:00h (reboot of the system).

Here an overview of some of the x86 interrupts:

InterruptsDescription
00h - 07hProcessor interrupts
08h - 0FhPeripherals interrupts (IRQ0 to IRQ7)
10hVideo interface management
13hDisk access interface management
16hKeyboard interface management
17hPrinter interface management
1BhCTRL+Break keys pressed on the keyboard
20hTermination of a program
21hDOS services (API)
24hCritical error
27hTermination of TSR
2AhNetwork interface management
33hMouse interface management
40hFloppy drive interface management
42hDefault video management
5ChNetBIOS management
70h - 77hIRQ8 to IRQ15 (IRQ AT/286)
80hCall to the Linux kernel (API)

MS-DOS (as other DOS systems, including FreeDOS) provides many common services through interrupt 21h. Entire books have been written about the variety of functions available. Here just a description of six of them. The general way to proceed (for the functions described) is the following:

  1. Load the data or its starting address (if there is any data) into the DL resp. DX register.
  2. Load the function code into the AH or AX register (depending on it's 1 or 2 bytes).
  3. Execute the INT 21h instruction to call the DOS function.
  4. Access the return data (if there is any) in the AL register resp. from the memory area that we indicated when calling the function.

The DOS exit function terminates the program that calls it and returns to the DOS prompt. Here how to call it:
   mov  ax, 4c00h
   int  21h
No data to pass to the function, nor any data returned. Just load AX with the function code 4c00h. We have this code in our two "Hello World" programs, and we will also have it in the other samples.

The DOS write-character function writes a character to the screen. Here how to call it:
   mov  dl, ...
   mov  ax, 02h
   int  21h
The DL register has to contain the character (i.e. its ASCII code) to be displayed. We can load the character as a constant, from another register or from memory (cf. further down in the text); there is no data returned.

The DOS write-string function writes a string (sequence of characters) to the screen. Here how to call it:
   mov  dx, <address>
   mov  ah, 09h
   int  21h
The DL register has to contain the address of the first character (i.e. its ASCII code) to be displayed. The display on the screen starts with the character at this address and continues with the characters at the successive memory locations until the dollar symbol ($) is found (this character not being displayed). After the display of the last character, the cursor stays in the same line at its current position, except if the last two characters were the ASCII codes 0Dh (13) followed by 0AH (10), which code for a carriage return + linefeed (new line indicator on DOS and Windows systems). There is no data return. We have now the knowledge to understand, how the display in the two "Hello World" programs works: mov dx, hello loads the DX register with the address referred to by the variable "hello", i.e. the character "h" of the "hello, world" string. This "h" and the following characters are displayed on the screen; display ends with the character preceding the dollar symbol. As the last two characters send to the screen were 0Dh followed by 0Ah, a carriage-return + linefeed is performed and the cursor is moved to the beginning of the next line.

The DOS read-character function reads a character from the keyboard. Here how to call it:
   mov  ah, 01h
   int  21h
This function has no input data; the return data is the ASCII code of the character pressed on the keyboard in the AL register. Besides reading the character, the character is also echoed (displayed onto the screen).

The DOS read-character (no echo) function also reads a character from the keyboard. Here how to call it:
   mov  ah, 08h
   int  21h
The function works the same way as the one described before, except that the character is not displayed onto the screen.

The DOS read-string function reads a string (sequence of characters terminated by the ENTER key) from the keyboard. Here how to call it:
   mov  dx, <address>
   mov  ah, 0ah
   int  21h
This is a little bit more complicated... The DX register has to be loaded with the address of the buffer, where the string (sequence of ASCII codes) will be placed. However, first we have to indicate the maximum string length the input may have (1 to 255). This value has to be stored by the programmer at the address passed as input to the function (start of the buffer, offset +0) and must equal the maximum number of characters that the user may enter plus 1 (the carriage-return code 0Dh will be returned as last character). As we don't know how many characters the user actually enters, the actual string length has to be returned by the function. It will be stored at the memory location immediately following the buffer start (offset +1). To note that the count returned only equals the string data length (the carriage-return will be stored in the buffer, but it will not be included in the count of characters entered). Finally, as the two first locations of the buffer are used for the counters, the string data entered starts at offset +2.

To illustrate how to use the DOS read-string function, lets write a "Hello User" program. We first ask the user for their name, then send the greeting "Hello <name>!" to the screen. Here is the code of the HELLO3.ASM sample:

segment code
..start:
        ; Initialization
        mov     ax, data
        mov     ds, ax
        mov     ax, stack
        mov     ss, ax
        mov     sp, stacktop
        ; Ask for name (calling the DOS write-string function)
        mov     dx, qname
        mov     ah, 9
        int     0x21
        ; Get name from keyboard buffer (calling the DOS read-string function)
        mov     dx, buffer
        mov     ah, 0x0a
        int     0x21
        ; Display "hello" message (calling the DOS write-string function)
        lea     esi, [buffer + 2]
        mov     cl, [buffer + 1]
        lea     edi, [hname]
copychar:
        mov     bl, [esi]
        mov     [edi], bl
        inc     esi
        inc     edi
        dec     cl
        test    cl, cl
        jnz     copychar
        mov     byte [edi], '!'
        mov     byte [edi + 1], 13
        mov     byte [edi + 2], 10
        mov     byte [edi + 3], '$'
        mov     dx, hello
        mov     ah, 9
        int     0x21
        ; Terminate the program (calling the DOS exit function)
        mov     ax, 0x4c00
        int     0x21
segment data
maxlen  equ     20
buffer  db      maxlen + 1
        resb    maxlen + 2
qname   db      'What is your name? ', '$'
hello   db      13, 10, 'Hello '
hname   resb    maxlen
        resb    4
segment stack   stack
        resb    64
stacktop:

Lets begin by having a look at the data segment. The pseudo-instruction EQU declares a constant. Assembly constants are as in higher-order programming languages: they can't be changed by the program and the assembler simply replaces all occurrences of them by the value they have been assigned. Our constant maxlen = 20 is the maximum length of the user name (an arbitrarily chosen value; if we don't want to set a length limit, we can use maxlen = 255).

"qname" references the memory area where we have stored the string to ask the user for their name. Be sure not to forget the terminal dollar character if you use interrupt 21h to display a string on the screen (just try out what happens if you do...)! Note that the string is not followed by the characters 0Dh 0Ah; there will be no carriage-return + linefeed, the cursor will stay at the position after the last character displayed (and it's here that the name entered by the user will be displayed).

"hello" references the memory area for our greeting message. This area starts with some initialized data (a carriage-return + linefeed and the string "Hello "), followed by 20 bytes reserved for the name entered by the user and 4 further bytes of uninitialized space. Do you have an idea what they are for?

"buffer" references the memory area that will be used for the user input. As we saw when describing the DOS read-string function, the first byte of this area has to contain the maximum number of characters that the user may enter; in our case the length of the name + the carriage-return = maxlen + 1 (21). The remaining space of the buffer will be filled in by the DOS function. Remember that the first byte of the return data is the actual string length, and that the carriage-return is returned as last character. So, the length of the area of uninitialized data that we have to reserve is 1 + (maxlen + 1) = maxlen + 2 (22).

Now, the code segment. After the initialization that has to be done in order to create an OBJ output file, we display the "What is your name? " string on the screen. This is done by loading the address of the first byte of the string ("qname") into DX, loading AH with 0Ah, the code of the DOS read-string function and calling INT 21h. We then want the user to enter their name. As we saw before, in order to use INT 21h, DX has to be loaded with the memory address of the first byte of the buffer area ("buffer") and AH has to be loaded with 0Ah, the code of the DOS write-string function. Program execution is suspended until the user hits the ENTER key (hitting CTRL+C = CTRL+Break will trigger interrupt 1Bh, the program will be aborted and the system will return to the DOS prompt). When the program execution resumes, our buffer will have been filled with the function return data. In order to display the greeting message, we'll have to copy the user name from the buffer to the memory area referred to by the variable "hname". This copy will be done byte by byte. As we will use the DOS write-string function to display the greeting, we'll have to add at least one character (the dollar symbol) to the output string. I will explain the code of all this data movement in the next section. The display itself is done as before (DX having to be loaded with address of the first byte of the greeting message, i.e. "hello"). Finally, we terminate the program by calling the DOS exit function.

CPU registers and memory addresses.

To speed up the processor operations, the processor includes some internal memory storage locations, called registers. The registers store data elements for processing without having to access the memory. A x86 processor has a whole bunch of registers and I will only mention some of them here. If you are serious about assembly programming, you might want to have a look at NASM Assembly - Registers at the tutorialspoint website for details.

There are four 32-bit data registers that are used for arithmetic, logical, and other operations. These 32-bit registers can be used in three ways:

For most operations, you can use any of the 4 (resp. 8) registers as you want. However, each of them has also specific functions. AX is the primary accumulator; it is used in input/output and most arithmetic instructions. For example, in multiplication operation (assembly instruction with a single operand), one of the multiplication operands has always to be stored in EAX, AX or AL (according to the size of the operand). BX is called the base register; it is the only general-purpose register which may be used for indirect addressing. For example, the instruction MOV [BX], AX causes the contents of AX to be stored in the memory location whose address is given in BX. CX is known as the count register, as the ECX, CX registers store the loop count in iterative operations (looping, shift and rotate, and string instructions). DX is known as the data register. It is used in input/output operations, and also (together with AX) for multiplication and division operations involving large values.

x86 data registers

The 32-bit index registers, ESI and EDI, and their 16-bit rightmost portions SI and DI, are mostly used for indexed addressing. E.g.: the instruction MOV BL, [ESI + NUMLEN + 3] causes the contents of the memory location defined by the address given by the value in ESI + the value of the constant "numlen" + 1 to be stored in register BL. In string operations, SI is used as source index, and DI is used as destination index.

The status register, or flags register, is a collection of 1-bit values which reflect the current state of the processor and the results of recent operations. Many instructions change the status of the flags and some other conditional instructions test the value of the flags to take the control flow to another location. Here are some common flag bits:

FlagDescription
CarryThis flag is set if the last arithmetic operation ended with a leftover carry bit coming off the left end of the result. This signals an overflow on unsigned numbers.
OverflowThis flag is set if the last arithmetic operation caused a signed overflow. For example, after adding 0001h to 7FFFh, resulting in 8000h; read as two's complement numbers, this corresponds to adding 1 to 32767 and ending up with -32768.
ZeroThis flag is set if the last computation had a zero result. After a comparison, this indicates that the values compared were equal (since their difference was zero).
SignThis flag is set if the last computation had a negative result (a 1 in the leftmost bit).

Most of the time you will not have to deal with the flags register explicitly; instead, you will execute one of the conditional branch instructions, Jcc, where cc is a mnemonic condition code, as in the following table:

CodesDescription
C, or NCCarry resp. no carry
O, or NOOverflow resp. no overflow
Z, or NZZero resp. not zero
E, or NEEqual resp. not equal
L, or NL; LE, or NLELess resp. not less; less or equal resp. not less or equal
G, or NG; GE, or NGEGreater resp. not greater; greater or equal resp. not greater or equal
S, or NSSign resp. no sign (i.e. negative resp. not negative)

Most assembly language instructions require operands to be processed. An operand address provides the location, where the data to be processed is stored. When an instruction requires two operands, the first operand is generally the destination, which contains data in a register or memory location and the second operand is the source. The source contains either the data to be delivered itself (immediate addressing) or the address (in a register or in memory) of the data. Generally, the source data remains unaltered after the operation. There are three basic modes of addressing: register addressing, immediate addressing, and memory addressing.

In the register addressing mode, the operand is contained in a register. Depending upon the instruction, the register may be the first operand, the second operand or both. As processing data between registers does not involve memory, it provides fastest processing of data. Some examples:
   mov  dx, qname
   mov  ah, 9
   add  al, bl
   add  bl, al
In the first two examples (from our HELLO3.ASM program), the register is the first operand, i.e. a destination; the address referred to by the variable "qname" is moved to the DX register, and the constant value 9 is moved to AH. In the third and fourth examples, both operands are registers; in both cases the values in registers AL and BL are added. In the third example, the result is stored in AL, in the fourth example, it is stored in BL.

In the immediate addressing mode, the operand is a constant value or an expression. When an instruction with two operands uses immediate addressing, the first operand is normally a register, and the second operand is an immediate constant. If labels are used, labels declared with EQU indicate a value, labels declared otherwise refer to an address. The first two examples above use immediate addressing. In the first example, the immediate operand is the address referred to by "qname", in the second example, the immediate operand is the number 9.

In the memory addressing mode, the operand is a value stored in memory. The memory operand may be the first operand, or the second operand, but not both (the other operand normally being a register). Operands specified in a memory-addressing mode require access to the main memory, usually to the data segment. As a result, they tend to be slower than either of the two previous addressing modes.

To locate a data item in the data segment, we need two components: the data segment start address and an offset value within the segment. The start address of the segment is typically found in the special purpose DS register. The offset value is often called the effective address.

There are various memory-addressing modes, differing in the way how the offset value of the data is specified. In the direct addressing mode, the offset value is specified directly as part of the instruction. In an assembly language program, this value is usually indicated by the variable name of the data item. The assembler will translate this name into its associated offset value during the assembly process. To facilitate this translation, the assembler maintains a symbol table. The symbol table stores the offset values of all variables in the assembly language program. Examples from our HELLO3.ASM program:
   lea  edi, [hname]
   mov  cl, [buffer + 1]
   lea  esi, [buffer + 2]
The first example loads the effective address of the variable "hname" into the EDI register. The second example loads the effective address corresponding to the memory location referred to by the variable "buffer" + 1 byte, i.e. the memory address located at an offset of +1 with respect to "buffer". If you consider what I said concerning the keyboard buffer, the instruction MOV CL, [BUFFER + 1] loads the CL register with the number of characters actually entered by the user. The third example loads the ESI register with the effective address immediately following the one in example 2 (memory location with offset +2 with respect to "buffer"), that actually is the memory location containing the first character entered by the user. Important: As a difference with what you may read on some Internet sites or in some assembly manuals, with NASM, the usage of square brackets with effective addresses is mandatory!

In the indirect addressing mode, the operand stored in memory is not coded (as address or variable) in the assembly instruction, but the operand's address is loaded into an index register (ESI, EDI, SI, DI; you can also use EBX and BX) and this register is used in the instruction to designate the memory operand. As this operand actually is the value stored at the address contained in the index register and not the value in the index register, the index register has to be put between square brackets. Examples from HELLO3.ASM:
   mov  bl, [esi]
   mov  [edi], bl
In the first example, the content of the address contained in ESI is moved into the BL register (as BL is an 8-bit register, 1 byte will be moved). In the second example, the content of BL is moved into the address contained in EDI. With the two instructions together, we actually move one byte of data from one memory location to another. And in our sample program, by incrementing the address in ESI and EDI and repeating the move, we can copy the name entered by the user from the keyboard buffer area to the "hname" area in our output string.

I said above that immediate addressing is normally used with a register as first operand, and that in memory addressing mode, one of the operands is normally a register. NASM also allows to use a constant value with memory addressing. However, this presents a problem. If a register is involved, we know the size of the second operand, actually equal to the size of the register. Thus, moving a constant to DL will move one byte, moving it to DX will move 2 bytes. But if the destination is a memory address? How does the assembler know how many bytes we want to move to this, and possibly the following addresses? It can't know it for sure, and that's why we explicitly tell it by using a type specifier. These specifiers are: BYTE (1 byte), WORD (2 bytes), DWORD (4 bytes), QWORD (8 bytes), and TBYTE (10 bytes). Examples from our HELLO3.ASM program:
   mov  byte [edi + 1], 13
   mov  byte [edi + 2], 10
   mov  byte [edi + 3], '$'
In all three examples, we move one single byte. In fact, there are three bytes that are moved to 3 successive memory locations. Do you recognize the byte sequence? Carriage-return + linefeed, followed by a dollar symbol to terminate the memory area containing a string that will be written to the screen using DOS interrupt 21h.

A final point, that has to be discussed before we return to our HELLO3.ASM program, is the way, how data is stored in memory. Data has essentially 4 sources: user input via the keyboard, constants and declared variables in the assembly program, result of some computation (and data read from a file). Keyboard input always produces ASCII codes (and screen output always has to be ASCII codes), independently if a character, string or number is entered. In an assembly program, a character value is stored as an ASCII character, a string as a sequence of ASCII characters, stored at successive memory addresses. All integer numbers, used in an assembly program, are stored in hexadecimal format, independently if they are written as hexadecimal, decimal or otherwise (except for a string representation). Integer computations, like arithmetic or logical operations normally await hexadecimal numbers as input, and generate as result a hexadecimal number. If the integers have to be treated as signed or not depends on the programmer. Floating point numbers have their own format. Some examples from our HELLO3.ASM program):
   maxlen  equ  20
   buffer  db   maxlen + 1
   hello   db   13, 10, 'Hello '
The first example doesn't store anything in memory; the constant will be replaced by its value: 20 -> 14h.
In the second example the value 21 -> 15h will be stored at address "buffer" (more exactly with an offset +"buffer" with respect to the content of the DS register).
The third example stores a sequence of bytes at address "hello" and following: 13 -> 0Dh, 10 -> 0Ah, Hello + space -> 48h 65h 6Ch 6Ch 6Fh 20h.

Things become a little bit more complicated when storing multibyte data. This applies when moving the content of 16- or 32-bit registers to memory, or declaring variables with DW, DD, etc. In fact, there are two ways to store this data, two completely different byte-ordering schemes possible: Either, the processor stores the most significant byte before the least significant one, or it stores it after it. In both cases, the memory area has to be referred to by specifying the lowest memory address. With a word (2 bytes) of data, the MSB is stored at this address and the LSB at the following one, in the case of CPUs that use big-endian byte ordering. In the case of CPUs that use little-endian byte ordering (as do x86 CPUs), it's the LSB that is stored at the lowest address (the one referred to by the variable), and the MSB that is stored at the following one.

Lets see an example. Consider the following code:
   num  resb  2
        mov  ax, 25159
        mov  [num], ax
25159 decimal is 6247h hexadecimal, so a word-size number will be stored at memory locations num and num+1. With little-endian byte ordering, the LSB is stored at the lowest address (the one referred to by the variable). Thus, our data in memory will be as follows: byte 47h stored at address num and byte 62h stored at address num+1.

Here an example with a negative number.:
   num  dw  -29255
-29255 decimal is 8DB9h hexadecimal, and will be stored as follows: byte B9h stored at address num and byte 8Dh stored at address num+1.

And an example with a double-word.:
   num  dd  542803535
542803535 decimal is the 32-bit hexadecimal 205A864Fh hexadecimal, and will be stored as: 4Fh at address num, 86h at num+1, 5Ah at num+2, and 20h at num+3.

So, we have now all the necessary knowledge to understand how, in our HELLO3.ASM program, the name entered by the user is moved to the corresponding area within the greeting output string. We will not use the x86 string instructions, but will do the move byte by byte. First we initialize our copy routine with the code
   lea  esi, [buffer + 2]
   lea  edi, [hname]
   mov  cl, [buffer + 1]
We load the index register ESI with the address of the first character of the name entered by the user (remember that the keyboard input returned by the DOS read-string function starts within the buffer at the offset +2), and the index register EDI with the address within the output string area where the first character of the name has to be placed. As we will copy the name byte by byte, we need a loop counter. Here, we use the register CL, that we initialize with the length of the name (that has been stored into the buffer area at offset +1 by the DOS read-string function).

Note: Maybe you wonder what's about this LEA instruction used here. LEA means "load effective address" and is nothing else than a special kind of MOV. In fact, both instructions do exactly the same, however, when using MOV, the move is done during assembly time, whereas when using LEA, the move is done during runtime. This allows to use instructions like, for example, LEA EBX,[array + ESI] to load EBX with the address of an element of "array", whose index is in the ESI register. Note, that when using a register's content to compute an address, you cannot use MOV. The instruction MOV EBX,[array + ESI] is not permitted, because at assembly time, the value in ESI might not be known. Why the second operand of the LEA instruction is placed between square brackets, no idea. It seems not logical to me, as it's an address (and not the content at an address) that is loaded. But, it's this way that you find it in NASM manuals and NASM source code examples.

Here is the code of the loop that copies the name:
   copychar:
     mov   bl, [esi]
     mov   [edi], bl
     inc   esi
     inc   edi
     dec   cl
     test  cl, cl
     jnz   copychar
We copy the byte from the source address (stored in ESI) to the destination address (stored in EDI). Then we increment the two index registers, that thus point to the next address within the source resp. destination area. For each byte copied, we decrement the counter (BL register). The loop is terminated when the counter is zero (the TEST instruction used here does a logic AND of its two operators, without changing the destination operand, but setting the flags; the zero flag will only be set here if the value in CL is 0).

The following lines of HELLO3.ASM are easy to understand. We add some further characters to the output string (an exclamation mark and a carriage-return + linefeed), then we add, and this is really important as you should know, the dollar symbol to terminate the output string (note the usage of the type specifier BYTE in these moves of an immediate addressing mode operand to a memory location), and finally we load DX with the address of the output string (and AH with function code 09h) to display the greeting message, calling the DOS write-string function.

Introduction to ASCII arithmetic.

As, besides showing how to use NASM on DOS, the purpose of this tutorial also is to give people, who want to learn the assembly programming language, some basic knowledge in order to create their own assembly programs, it's obvious that it has to contain some details concerning numbers and how to process them. I said above that all keyboard input and screen output is in ASCII format. I also said that the arithmetic and logical expressions normally use numbers in hexadecimal format. One way to work with integers would thus be to convert the input operands from ASCII to hexadecimal numbers, to perform the calculations and to convert the results from hexadecimal numbers to ASCII in order to display them onto the screen. This way to proceed is not covered in this tutorial. If you seriously preview to write assembly language programs performing calculations, you should download one of the assembly books available on the Internet; there are chances that you'll find the code of such conversion routines in most of them.

Instead of converting the numbers from ASCII to hexadecimal number and vice-versa, the tutorial shows how you can do calculations on integers as they are when entered from the keyboard, i.e. integers in ASCII representation. The x86 processor includes instructions to perform ASCII arithmetic operations. In short (and without discussing the details how this actually works), it's using the ASCII codes of two integer digits as operands, using the standard addition, subtraction. multiplication, or division operators to calculate the result, that then has to be adjusted using the corresponding ASCII adjust instruction (that will give a result in ASCII representation, ready to be displayed on the screen).

The following table shows the x86 arithmetic instructions and the corresponding ASCII adjust instructions.

OperationArithmetic
instruction
ASCII adjust instruction
AdditionADD, ADCAAA (ASCII adjust after addition)
SubtractionSUB, SBBAAS (ASCII adjust after subtraction)
MultiplicationMULAAM (ASCII adjust after multiplication)
DivisionDIVAAD (ASCII adjust before division)

ASCII addition: As the ASCII adjust instructions require the usage of the AX register, a typical ASCII addition (with 2 1-digit operands) consists in the following:

  1. Making sure that the AH register is cleared.
  2. Moving the first operand to AL.
  3. Adding the second operand (register, immediate, or memory addressing) to AL (result in AL).
  4. Doing the ASCII adjust after addition (result as unpacked BCD representation in AX).
  5. Converting the unpacked BCD to ASCII (for example, using OR AX, 3030h).
The result is two bytes in size, the MSB either being 30h or 31h (ASCII codes of characters 0 resp. 1). In this latter case, there had been a carry: the carry flag is set and can be used with the ADC (add with carry) instruction in order to perform ASCII addition on multibyte operands, using a loop (cf. next section).

Example:
   sub  ah, ah      ; clear AH
   mov  al, ’6’     ; AL = 36h
   add  al, ’7’     ; AL = 36h + 37h = 6Dh
   aaa              ; ax = 0103h
   or   ax, 3030h   ; AX = 3133h

Note: Unpacked BCD (binary coded decimal) is an integer representation, where each digit of the decimal number is represented by the corresponding hexadecimal number. Examples for a 2-byte unpacked BCD: 8 = 0008h, 12 = 0102h.

ASCII subtraction: It works the same way as ASCII addition. If there is a borrow, the carry flag is set and can be used with the SBB (subtract with borrow) instruction in order to perform ASCII subtractions on multibyte operands, using a loop.

Example:
   sub  ah, ah      ; clear AH
   mov  al, ’9’     ; AL = 39h
   add  al, ’3’     ; AL = 39h - 33h = 06h
   aaa              ; AX = 0006h
   or   ax, 3030h   ; AX = 3036h

If you do these calculations for the operation 3 - 9, the AAS instruction will give the result AX = FF04h and after conversion from BCD to ASCII, AL = 34h, ASCII code for the character '4'. The number is recognized as negative, as the AAS instruction has set the carry flag. The result itself is obviously not what it should be. So, another way has to be used to perform subtractions with a negative result. On the other hand, the result obtained is useful with multibyte operands subtractions with a borrow. In the subtraction 53 - 29, for example, the first loop iteration gives 3 - 9 -> AL = 34h (cf. above) and the second one (we use SBB and the carry has been set because of the borrow) gives 5 - 2 - 1 -> AL = 32h. The result of the operation is thus 3234h. These are the ASCII codes for '2' and '4', and 24 is well the correct result of the subtraction.

ASCII multiplication: The AAM instruction is used to adjust the result of a MUL instruction. Note that MUL has only one operand; for byte multiplication, the other operand must be a value in the AL register, the result is a word that will be stored in AX (in the case of a word multiplication, one operand must be in AX; the result will be a double word with its MSW stored in DX and its LSW stored in AX). Not necessary here to clear the AH register. On the other hand, multiplication should not be performed on ASCII numbers; use unpacked BCD operands instead. This means that for two numbers entered from the keyboard, we'll have to convert them from ASCII to unpacked BCD: 30h -> 00h, 31h -> 01h, ... We'll have to mask off the upper four bits of the number in ASCII representation (what may be done by an AND operation of the register containing the operand and 0Fh).

Multiplication example:
   mov  al, ’3’     ; AL = 33h (ASCII)
   mov  bl, ’9’     ; BL = 39h (ASCII)
   and  al, 0fh     ; AL = 03h (BCD)
   and  bl, 0fh     ; BL = 09h (BCD)
   mul  bl          ; AX = 03h * 09h = 001Bh
   aam              ; AX = 0207h
   or   ax, 3030h   ; AX = 3237h

So, we have now all the necessary knowledge to understand the CALC.ASM sample. The program allows to add, subtract and multiply two 1-digit positive integers (no calculation if the subtraction result is negative). The user is asked for an operation string of the form <operand1><operator><operand2>, where <operand1> and <operand2> are numbers from 0 to 9 (no validity check done), and <operator> is one of the symbols '+', '-', and '*'. Here is the code:

segment code
..start:
        ; Initialization
        mov     ax, data
        mov     ds, ax
        mov     ax, stack
        mov     ss, ax
        mov     sp, stacktop
        ; Display title
        mov     dx, stitle
        mov     ah, 9
        int     21h
        ; Ask for operation
        mov     dx, sop
        mov     ah, 9
        int     21h
        ; Get operation from keyboard buffer
        mov     dx, buffer
        mov     ah, 0ah
        int     21h
        mov     al, [buffer + 2]              ; first operand
        mov     ah, 0
        mov     bl, [buffer + 4]              ; second operand
        mov     cl, [buffer + 3]              ; operator
        ; Branch depending on operator
        cmp     cl, '+'
        je      addition
        cmp     cl, '-'
        je      subtraction
        cmp     cl, '*'
        je      multiplication
        ; Display invalid operator message
        mov     dx, serr
        mov     ah, 9
        int     0x21
        jmp     exit
        ; Do addition
addition:
        add     al, bl
        aaa
        jmp     result
        ; Do subtraction
subtraction:
        cmp     al, bl
        jl      negative
        sub     al, bl
        aas
        jmp     result
negative:
        mov     dx, sneg
        mov     ah, 9
        int     21h
        jmp     exit
        ; Do multiplication
multiplication:
        and     al, 0fh
        and     bl, 0fh
        mul     bl
        aam
        ; Display result
result:
        or      ax, 3030h
        mov     [res], ah
        mov     [res + 1], al
        mov     dx, sresult
        mov     ah, 9
        int     21h
        ; Terminate the program
exit:
        mov     ax, 0x4c00
        int     21h
segment data
stitle  db      'Addition, subtraction, and multiplication', 13, 10
        db      'of two 1-digit positive integers', 13, 10, '$'
sop     db      'Operation ? ', '$'
serr    db      13, 10, 'Unknown operator!', 13, 10, '$'
sneg    db      13, 10, 'Result is negative', 13, 10, '$'
buffer  db      4
        resb    5
sresult db      13, 10, 'Result = '
res     resb    2
        db      13, 10, '$'
segment stack   stack
        resb    64
stacktop:

I think that the code of this program shouldn't be to difficult to understand. We read the operation from the keyboard and move the first operand to AL, the second operand to BL and the operator to CL (remember that user input in the buffer starts with offset +2). We then check which operator has to to be used. The instruction CMP compares two values and sets the flags according to the comparison result; here we use the JE (jump if equal) instruction to branch to the label corresponding to the operation that we want to perform. If the operator is unknown, we display an error message. Addition and multiplication are done as explained above. The same for subtraction, but only after having tested if the result is well positive; we compare the two operands using CMP and use a JL (jump if less) to branch to the "negative" label (display of the message "Result is negative" instead of doing the subtraction). All 3 operations lead to the label "result" where we convert the BCD value in AX to ASCII, then move the operation result to the corresponding area within the output string. Note that we don't move AX as a whole, but move AL and AH separately; this is necessary because the little-endian byte ordering used by the x86 CPUs would inverse the two bytes! And finally we display the result string calling the DOS write-string function.

The sample program CALC2.ASM included in the download archive is identical to CALC.ASM, except that user input, calculation and the display of the result is placed within a loop. This allows to do successive calculations without leaving the program. The simplest way to leave the loop (and then terminate the program) is to do so if the user hasn't entered any operation (just hit the ENTER key). This can, for example be done by the following code ("exit" being the label referring to the address where the program termination code starts)
   mov  al, [buffer + 1]
   cmp  al, 0
   je   exit
All we have to do is to check if the number of characters entered by the user (this value has been placed into the keyboard buffer at offset +1 by the DOS read-string function) is zero and if so exit the loop and terminate the program.

Here is the code of sample CALC3.ASM, which is a further extension of our simple calculation programs: If a subtraction has a negative result, instead of displaying a message, we do the calculation and display the negative result.

segment code
..start:
        ; Initialization
        mov     ax, data
        mov     ds, ax
        mov     ax, stack
        mov     ss, ax
        mov     sp, stacktop
        ; Display title
        mov     dx, stitle
        mov     ah, 9
        int     21h
        ; Ask for operation
loop:
        mov     dx, sop
        mov     ah, 9
        int     21h
        ; Read operation into keyboard buffer
        mov     dx, buffer
        mov     ah, 0ah
        int     21h
        ; Exit loop if no entry
        mov     al, [buffer + 1]
        cmp     al, 0
        je      exit
        ; Get operands and operator from keyboard buffer
        mov     al, [buffer + 2]              ; first operand
        mov     ah, 0
        mov     bl, [buffer + 4]              ; second operand
        mov     cl, [buffer + 3]              ; operator
        ; Branch depending on operator
        cmp     cl, '+'
        je      addition
        cmp     cl, '-'
        je      subtraction
        cmp     cl, '*'
        je      multiplication
        ; Display invalid operator message
        mov     dx, serr
        mov     ah, 9
        int     0x21
        jmp     loop
        ; Do addition
addition:
        mov     byte [res], ' '
        add     al, bl
        aaa
        jmp     result
        ; Do subtraction
subtraction:
        mov     byte [res], ' '
        cmp     al, bl
        jge     subtract
        mov     cl, al
        mov     al, bl
        mov     bl, cl
        mov     byte [res], '-'
subtract:
        sub     al, bl
        aas
        jmp     result
        ; Do multiplication
multiplication:
        mov     byte [res], ' '
        and     al, 0fh
        and     bl, 0fh
        mul     bl
        aam
        ; Display result
result:
        or      ax, 3030h
        mov     [res + 1], ah
        mov     [res + 2], al
        mov     dx, sresult
        mov     ah, 9
        int     21h
        jmp     loop
        ; Terminate the program
exit:
        mov     ax, 0x4c00
        int     21h
segment data
stitle  db      'Addition, subtraction, and multiplication', 13, 10
        db      'of two 1-digit positive integers', 13, 10, '$'
sop     db      'Operation ? ', '$'
serr    db      13, 10, 'Unknown operator!', 13, 10, '$'
buffer  db      4
        resb    5
sresult db      13, 10, 'Result = '
res     resb    3
        db      13, 10, '$'
segment stack   stack
        resb    64
stacktop:

Lets have a look at the code. First of all, our output number now has a length of 3 characters: the sign plus two number digits; so, we have to reserve 3 bytes for the variable "res" (vs. 2 bytes in CALC.ASM). The first part of the code is identical to the one in CALC.ASM, except that I use a loop for successive calculations, as described for CALC2.ASM. Addition and multiplication are the same as in CALC.ASM, except that we have to consider the sign of the result; always positive, we move a space to "res" offset +0. The code for the subtraction is different, of course. Doing calculations with negative numbers does not necessarily mean that we have to use negative hexadecimals. As in this case, where the subtraction a - b, with a < b, can be calculated as b - a (and moving a minus sign to the first byte of the result area). And that's what's done in this program: I check if the first operand is less than the second one. If no, I just do the subtraction (I assumed the result to be positive and moved a space to the "sign location" before doing the compare); if yes, I simply swap the operands (and move a '-' to the "sign location"), then jump to the standard subtraction code. The display of the result is the same as in CALC.ASM, but the moves have to be adapted. Because of the sign, the MSB has now to be moved to a location with offset +1, the LSB to a location with offset +2 (vs. offset +0 and +1 in CALC.ASM).

Multibyte integer arithmetic.

Being able to do arithmetic operations on 1-digit numbers doesn't take us really far, so some hints concerning arithmetic operations with integers with more than 1 digit should not be missing in the tutorial.

The sample ADD6.ASM asks for two 6-digit positive integers and outputs their sum (as a 7-digit positive integer) onto the screen. As for the programs before, there is no numeric validity check, and the user has to enter the full 6 digits (i.e. has to enter the leading zeros), and the result is always displayed as a 7-digit number (i.e. is displayed with the leading zeros). It should not be to difficult to adapt the program for subtraction; multiplication would probably need a bigger effort. One nice thing in this program: The definition of a constant with the number length (number of digits) and the usage of this constant to calculate offsets (instead of using numeric literals) allows to adapt the code for the addition of two positive integers of any size (less than 256 digits, of course) by changing one single line in the source. Here is the code:

segment code
..start:
        ; Initialization
        mov     ax, data
        mov     ds, ax
        mov     ax, stack
        mov     ss, ax
        mov     sp, stacktop
        ; Ask for first number
loop:
        mov     dx, snum1
        mov     ah, 09h
        int     21h
        ; Read first number
        mov     dx, num1
        mov     ah, 0ah
        int     21h
        mov     al, [num1 + 1]
        cmp     al, 0
        je      exit                        ; exit the loop if no input
        ; Ask for second number
        mov     dx, snum2
        mov     ah, 09h
        int     21h
        ; Read second number
        mov     dx, num2
        mov     ah, 0ah
        int     21h
        ; Do the addition
        lea     esi, [num1 + numlen + 1]
        lea     edi, [res + numlen]
        mov     cl, numlen
        clc
        pushf
digit:
        mov     al, [esi]                   ; first operand
        mov     ah, 0
        mov     bl, [esi + numlen + 3]      ; second operand
        popf
        adc     al, bl
        aaa
        pushf
        or      ax, 3030h
        mov     [edi], al
        dec     esi
        dec     edi
        dec     cl
        jnz     digit
        mov     [edi], ah
        popf
        ; Display result
        mov     dx, sresult
        mov     ah, 09h
        int     21h
        jmp     loop
        ; Terminate the program
exit:
        mov     ax, 0x4c00
        int     21h
segment data
numlen  equ     6
stitle  db      13, 10, 'Addition, of two 6-digit positive integers', '$'
snum1   db      13, 10, 'First number ? ', '$'
snum2   db      13, 10, 'Second number ? ', '$'
sresult db      13, 10, 'Result = '
res     resb    numlen + 1
        db      13, 10, '$'
num1    db      numlen + 1
        resb    numlen + 2
num2    db      numlen + 1
        resb    numlen + 2
segment stack   stack
        resb    64
stacktop:

The program consists of a loop that reads the two operands, does the addition and displays the result. The loop (and the program) is terminated if the user enters no data (just hits ENTER) when asked for the first operand. The 2 operands are read into the two keyboard input buffers "num1" and "num2". The value at offset +1 in the "num1" buffer area is the length of the first operand read; testing if this value is 0 allows to exit the loop if the user didn't enter any data for the first operand.

Doing a multibyte addition is performing a loop, where each iteration does the addition of two 1-digit operands (this is what we did in the program samples before), starting with the LSB and continuing until all digits have been processed. The important thing here is to consider the possible carry: if a 1-digit operands addition produces a carry, it has to be added when adding the following two 1-digit operands. The obvious problem that occurs here is that the operations after the addition can (and will) modify the carry flag, and we loose its value. This means that after a 1-digit operands addition has been done, we have to save the carry flag, in order to be able to add it when performing the following 1-digit operands addition. The method used here is probably the easiest way to save the value of any flag: push the flag register onto the stack and pop it back when you need the flag values from before.

Lets have a look at the loop initialization code:
   lea  esi, [num1 + numlen + 1]
   lea  edi, [res + numlen]
   mov  cl, numlen
   clc
   pushf
We load ESI with the address of the first operand (using offsets, we will also use ESI to point to the second operand), and EDI with the address of the addition result. CL is loaded with the loop counter variable, i.e. the number of the operands' digits. To do the multibyte addition, we must proceed from LSB to MSB, so starting with the last digit of the operands and writing the corresponding sum digit to the last digit location of the result area. With the first digit located at offset +0, the nth digit is located at an offset of +(n-1). As the operands have numlen digits and the first digit in the "num1" buffer has an offset of +2, the address of the last digit of operand 1 is given by num1 + 2 + (numlen - 1) = num1 + numlen + 1. The result area is one digit longer than the operands' length (numlem + 1), so the address of the last digit of the result is given by res + [(numlen + 1) - 1] = res + numlen.
The last two lines of the code above concern the carry. As within the loop we pop the flags register before doing the 1-digit operands addition, we have to push it onto the stack here; this is done using the instruction PUSHF. To be sure that the carry is zero for the first addition, we perform a CLC (clear carry) before pushing the register.

The addition of the two 1-digit operands is similar as in the programs before (as we have to consider the carry of the preceding addition, we must use the instruction ADC instead of ADD): We load one operand into AL, the other in BL, add the two (+ the carry) with the result in AL, then perform the AAA and OR AX, 3030h, and finally store AL to its correct position within the "res" variable area. The address of the first operand's digit is contained in ESI, the corresponding result digit's address in EDI. Concerning the address of the second operand's digit, we will compute it by adding a given offset to the address of the first operand's digit (content of ESI). If you look at the data segment, you can see that the two numbers' buffer areas succeed to each other, the buffer of the first operand ending with 1 extra location for the carriage-return, the buffer of the second operand beginning with two extra locations for the maximum resp. actual string length. Beside the extra locations, the offset depends on the operands' size (number of digits), declared by the constant "numlen". The second operand's address is thus given by ESI + numlen + 1 + 2 = ESI + numlen + 3.

The carry, that we need to consider in our ADC instruction is the one set (or not) by AAA, thus it is immediately after this instruction that we have to save it (using PUSHF to push the flags register onto the stack). And to use this carry with the ADC of the next digit, we restore it with a POPF immediately before the addition instruction.

Finally, we have to point the indexes to the next operand's and result's digit; this is done by decrementing the values in ESI and EDI (decrementing, because we add the digits from LSB to MSB). We also decrement the loop counter (number of digits that remain to process). And we continue looping until this counter is zero.

Two things that have to be done when the loop is terminated. First, we haven't yet considered the carry of the MSB digits addition (case where the result is greater that 999999). As we saw when discussing the 1-digit operands ASCII addition, the addition result will be in AX, so AH will contain the correct value (30h or 31h), if there has been a carry or not. Thus, we can use the instruction MOV [EDI], AH to fill in the MSB of the result (EDI having been decremented after the last addition points now to the first location within the result's memory area, i.e. the location where we want to store AH).

A final sample to terminate this tutorial. The program FIBO.ASM doesn't contain any new assembly language elements, but it is interesting, because it solves a problem that is stated in lots of programming language manuals: the calculation of the Fibonacci series. These are a series of numbers defined by the function f(n) = n-2 + n-1, with f(0) = 0 and f(1) = 1. FIBO.ASM calculates and displays the first 20 numbers of the series, as shown on the screenshot below.

NASM on FreeDOS: Execution of a NASM program that calculates the Fibonacci series

I think that with the knowledge that you have acquired when working through this tutorial, it should not be to difficult to understand the code of FIBO.ASM, shown here, without further explanations.

segment code
..start:
        ; Initialization
        mov     ax, data
        mov     ds, ax
        mov     ax, stack
        mov     ss, ax
        mov     sp, stacktop
        ; Calculate Fibonacci series
        lea     esi, [series + 3]
        mov     ch, count - 2
loop:
        mov     cl, 4
        clc
        pushf
fdigit:
        mov     al, [esi]
        mov     ah, 0
        mov     bl, [esi + 4]
        popf
        adc     al, bl
        aaa
        pushf
        or      ax, 3030h
        mov     [esi + 8], al
        dec     esi
        dec     cl
        jnz     fdigit
        popf
        add     esi, 8
        dec     ch
        jnz     loop
        ; Display the series
        lea     esi, [series]
        mov     ch, count
display:
        mov     cl, 4
        lea     edi, [sfib]
ddigit:
        mov     bl, [esi]
        mov     [edi], bl
        inc     esi
        inc     edi
        dec     cl
        jnz     ddigit
        mov     dx, sfib
        mov     ah, 9
        int     0x21
        dec     ch
        jnz     display
        ; Terminate the program
exit:
        mov     ax, 0x4c00
        int     21h
segment data
count   equ     20
series  db      '0000'
        db      '0001'
        times count - 2 resb 4
sfib    resb    4
        db      13, 10, '$'
segment stack   stack
        resb    64
stacktop:

I hope that following this tutorial, you got everything you need to create 16-bit real mode and 16-bit protected mode assembly programs on DOS using NASM, and that the sample programs shown with my explanations and comments has given you a base for developing your own assembly programs. If you are serious in 16-bit assembly programming, the manual Introduction to Assembly Programming - For Pentium and RISC Processors, Sivarama P. Dandamudi, © 2005, 1998 Springer Science+Business Media, Inc. might be really helpful. It contains a comprehensive introduction to basic computer organization, the Pentium Processor, and the x86 assembly language, including a lots of code and sample programs, that you can use as a base for your own assembly projects. The manual is available as PDF document on the Internet...


If you find this text helpful, please, support me and this website by signing my guestbook.