8086 Assembly Language Tutorial
8086 Assembly Language Tutorial
Tutorial Index :
Section 1. Introduction to Assembler and Assembly Language___________________3
Section 1.1. Generation of Language_________________________________________3
Section 1.2. Assembly Language and Assembler________________________________3
Section 1.3. Introduction to 8086 CPU internal architecture______________________4
Section 1.4. Memory segmentation___________________________________________5
Section 2. First Step in Assembly program__________________________________5
Section 3. Basic structure of assembly program______________________________6
Section 3.1. Basic structure of Assembly program______________________________6
Section 3.2. Basic structure of Data Segment___________________________________8
Section 3.3. Basic structure of Code segment___________________________________9
Section 3.4. Simple Commands in the Code Segment___________________________11
Section 3.4.1. Data Movement___________________________________________________11
Section 3.4.2. Arithmetic Operations______________________________________________13
Section [Link]. ADD and SUB function_________________________________________13
Section [Link]. MUL and DIV function__________________________________________14
Section [Link]. Other arithmetic operations_______________________________________15
Section 3.4.3. Simple Control flow________________________________________________15
Section [Link]. Unconditional Jump____________________________________________15
Section [Link]. CALL procedure_______________________________________________16
Section [Link]. Conditional Jump______________________________________________17
Section [Link]. Looping______________________________________________________18
Section 3.4.4. Logic Control_____________________________________________________18
Section [Link]. Simple Logic Function__________________________________________18
Section [Link]. Relational Operators____________________________________________19
Section [Link]. Rotation & shifting_____________________________________________19
Section 3.4.5. Simple interrupts (I/O)______________________________________________20
Section 3.5. Basic structure of the Stack Segment______________________________22
Section 4. Macro Processing_____________________________________________23
Section 4.1. Macro Definition_______________________________________________23
Section 4.2. Local Directives_______________________________________________25
Section 4.3. Nested Macro_________________________________________________25
Section 4.4. Special Directives______________________________________________26
Section 4.4.1. Repetition Directives_______________________________________________26
Section 4.4.2. Conditional Directives______________________________________________27
Section 4.5. INCLUDE Directive____________________________________________29
1. Low level language, e.g. Machine Language & Assembly Language. Low level languages are
closely related to the internal architecture of the computer system. For each single action of the
computer, a corresponding program line must be written.
2. High level language, e.g. C, C++, COBOL, etc.
Machine language, or first-generation language, is a set of instructions that can be directly executed
by a computer system. Each instruction is composed of an operation code (OPCODE), and an operand which
defines the function that the computer must perform. They are written at the most basic level of the computer
operation, as a series of 1s and 0s. However, it is difficult to understand and time-consuming for
programmers.
CODE_SEG SEGMENT
MAIN PROC FAR
ASSUME CS:CODE_SEG, DS:DATA_SEG
MOV AX, DATA_SEG
MOV DS, AX
MOV CX,0100h
START: MOV AH,2h
MOV DL,2Ah
MOV ABC,0Ah
INT 21h
MOV AH,4Ch
INT 21h
MAIN ENDP
CODE_SEG ENDS
DATA_SEG SEGMENT
ABC DB 0Bh
DATA_SEG ENDS
END MAIN
Figure 1 Simple Assembly Program
An assembler is a language translator used to convert assembly language into machine code.
Assembler accepts as input a program whose instructions are essentially in one to one correspondence with
those of machine language, with symbolic names used for operation codes and operands. It produces as output
1
8088 refers to the model of the processor used. For example, 80386, 80486, 80586 are the model of the processor. It is designed in late
1970s.
*** Page 3 ***
J 4 8086 Assembly Language Tutorial
a machine-language program in main storage for execution. It is not necessary to find the meaning or work
done in the assembly program. The purpose of the assembler is nothing but the translation.
3. Other Registers
There are two main types of other registers. One is instruction pointer (IP) and the other is
the status register. The instruction pointer contains the address of the currently executing instruction.
A 16-bit register provides a pointer into the current code segment.
The status register holds 16-bit information, which includes overflow register (O), sign
register (N), zero register (Z) & carry register (C).
O is set to 1 if overflow value (signed) is generated. Cleared otherwise.
N is set to 1 if negative result generated. Cleared otherwise.
Z is set to 1 if result is zero. Cleared otherwise.
C is set to 1 if carry bit (unsigned) is generated. Cleared otherwise.
P is set to 1 if parity bit is even. Cleared otherwise.
O N Z P C
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
Figure 3 Internal construction of Status register
Writing the assembly program includes the tracing of the data & address in the memory. Here, we
introduce the memory segmentation.
The first 1MB spaces of memory, from address 0 2 to FFFFFh, is the entire address space of the 8086/
8088 microprocessors. The data addresses to locations are limited in this range.
2
h means in hexadecimal form.
*** Page 5 ***
J 6 8086 Assembly Language Tutorial
All the above segments are not necessarily located one by one in the memory. They can be located
far away from each other, as shown in figure 6. Furthermore, an assembly program is free to allocate less than
3 segments. For example, only 1 code segment & 1 data segment is accepted with the warning message.
1750:0001 DS
3230:0001
SS
Segment Definition
All programs consist of one or more segments. Of course, while your programs is running, the
segment registers point at the currently active segments. Segment, in the assembly language source file, are
defined with the following instructions:
<Segment Name> segment (<align>) (<combine>) (<class>)
<code>
<Segment Name> ends
where
<Segment Name> is the segment name you designed,
<align> indicates the boundary on which the segment is to begin,
<combine> indicates whether to combine with other segments in linking,
<class> is used as identifier for the linker to combine various segments.
Segment Registers
*** Page 6 ***
J 7 8086 Assembly Language Tutorial
When MS-DOS begins execution of your program, it initializes two segment registers. It points CS at
the code segment containing your main program and it points SS at your stack segment. From that point
forward, you are responsible for maintaining the segment registers yourself.
To access the data correctly in your program, you should copy the address from the actual data
segment to the data segment register, as shown below:
MOV AX, DATA_SEG
MOV DS, AX
Let’s explain each term in the syntax of the segment.
Segment Names
The segment directive requires a label in the label field. This label is the segment’s name. The
assembler will use the segment name to obtain the address of a segment. You must also specify the segment’s
name in the label field of the ENDS directive that ends the segment.
Segments normally load into memory in the order that they appear in your source file. If you write
the code segment after the data segment, the data segment would load into memory before the code segment.
Align Type
The align parameter is one of the following words: byte, word, para, or page. These keywords
instruct the assembler, linker, and DOS to load the segment on a byte, word, paragraph or page boundary. The
align parameter is optional. If one of the above keywords does not appear as a parameter to the segment
directive, the default alignment is paragraph (16 bytes).
Aligning a segment on a byte boundary loads the segment into memory starting at the first available
byte after the last segment. Aligning on a word boundary will start the segment at the first byte with an even
address after the last segment.
Combine Type
The combine type controls the order of the segment. To specify the combine type you use one of the
keywords: PUBLIC, STACK or NONE. The PUBLIC and STACK combine types essentially perform the
same operation. They will combine all the segments with the same name, and join into a single contiguous
segment. The main difference between PUBLIC and STACK is that STACK is used in the stack segment
while PUBLIC is used in the code segment and data segment.
Class Type
The final operand to the segment directive is usually the class type. The class type specifies the
ordering of segments that do not have the same segment name. This operand consists of a symbol enclosed by
apostrophes ‘ ‘. Generally, you should use the following names: CODE (for code segment), DATA (for data
segment) and STACK (for stack segment).
The following will be the typical three segments:
CODE_SEG SEGMENT PARA PUBLIC ‘CODE’
: :
CODE_SEG ENDS
DATA_SEG SEGMENT PARA PUBLIC ‘DATA’
: :
DATA_SEG ENDS
STACK_SEG SEGMENT PARA STACK ‘STACK’
DW 1024 dup (?)
STACK_SEG ENDS
END
Figure 7 Typical Segment Definitions
Assume Directive
How does the assembler know which segment is the data segment, and which one is stack segment?
The segment directives don’t tell you what type of segment it happens to be in the program. When you specify
the segment in your program, not only you must tell the CPU that a segment is a data segment, but also you
must tell the assembler where and when the segment is a data segment. The assume directive provides this
information to the assembler.
The assume directive takes the following form:
*** Page 7 ***
J 8 8086 Assembly Language Tutorial
ASSUME CS:<code segment name>, DS:<data segment name>, SS:<stack segment name>
The assume directives tells the assembler that you have loaded the specified segment register(s) with
the segment addresses of the special value. Note that this directive does not modify any of the segment
registers, it simply tells the assembler to assume the segment registers are pointing at certain segments in the
program.
END directive
The END directive terminates an assembly language source file. In addition to telling the assembler
that it has reached the end of the assembly language source file, the end directive operand <entry point> tells
the MS-DOS where to transfer the control when the program begins execution, as shown by the following
syntax:
END <entry point>
If you write separate assembly and you’re linking together several different object codes, only one
module can have a main program. Likewise, only one module should specify the starting location of the
program. Others will leave blank.
For the above example, the variable V1 locates at location DS:0, V21 locates at location DS:1, V22
locates at location DS:2, V3 locates at location DS:4, etc. The question mark ‘?’ tells the assembler that the
variable should be left uninitalized when it loads into the memory, i.e. just leave space for that variable. You
may specify any initial value for the variable before or after (replace the “?”) the execution of the program.
Number Base
To differentiate between numbers in the various bases, you can use a suffix character. If you
terminate a number with a ‘b’ or ‘B’, then the assembler assumes that it is a binary number. If it contains any
digits other than 0 or 1 will generate an error. A suffix ‘D’ or ‘d’ assumes the number is a decimal number,
while a suffix ‘H’ or ‘h’ will select the hexadecimal radix.
All integer constants must begin with a decimal digit, including hexadecimal constants. To represent
the number ‘ABCD’, you must specify as ‘0ABCDh’. The assembler requires the leading decimal digit so that
it can differentiate between symbols and numeric constants.
If you do not specify the suffix after the number, the assembler will use the current default radix, i.e.
decimal radix. Therefore, you can specify the values without using ‘D’ character.
Array
In the high-level programming, it is commonly to use the arrays. Abstractly, an array is an aggregate
data type whose members are all the same type. Selection of the member from the array is by an integer
index. For example, A[2] selects the third element from array A in C language.
3
All string must be ended with “$” notation, otherwise the assembler will output the unpredictable result.
*** Page 8 ***
J 9 8086 Assembly Language Tutorial
There are three factors controlling the array. The base address of an array is the address of the first
element on the array and always appears in the lowest memory location. The index is the position of the
member that retrieves the specified element. The element size is the size of the element in the memory.
For a single dimension array, the address of the specified element can be calculated as:
Address = Base Address + (Index Element Size)
In the assembly language, an array is defined as
<arrayname> DB | DW <size> dup (<element>)
Array name is the name of the array variable. The <size> dup (<element>) tells the assembler to
duplicate the object with the size defined in <size>. For example, in Figure 7, 10 dup (?) duplicate the array
V3 10 times with a byte size for each element. With a ? in the “( )”, 10 uninitialized value will be defined. If a
value, say 1, is defined in the ( ), 10 byte size array with value 1 will be assigned.
In order to design an array with all different value, you can use the following syntax
<arrayname> DB | DW value1, value2, value3, ……valuen
This form allocates n variables of DB or DW type. It initializes the first item to value1, the second
item to value2, etc. For example,
Integers DB 0, 1, 2, 3, 4
initialize the five-element array, with the values from 0 to 4 respectively.
Consider the following declaration:
Strange DW 256 dup (0, 1, 2, 3)
The array Strange has 1024 elements. The n dup (XXX) operand tells the assembler to duplicate
XXX n times, not creating an array with n elements. If XXX contains k elements, then the dup operator will
create an array with size kn. The values in the above example will be 0 1 2 3 0 1 2 3 ….
To access the element in the array, the address formula will be used.
For the base address, you can use the name of the array. The element size is the number of bytes for
each array element. If the object is an array of the bytes, the element size is 1, while it will be 2 if the object is
an array of the word length.
The assembly code for accessing the array will be
[<arrayname>+<index>*<element size>]
The square box tells the assembler to resolve the entire element to the address inside the box. For
example, to denote the array SUMMER[2] with word size in assembly, you can use it as:
[SUMMER+3*2]
String
To declare the string, you can use the following syntax
<stringname> DB ‘<string>$’
The <stringname> is the name of the string, which defined inside the ‘’ or “ “. If you want to place an
‘ inside a string, you must place a pair of ‘ next to each other, e.g.
‘I’’m fine, thanks’
or by using the other characters as the string delimiter:
“I’m fine, thanks”
A string will appear as an array of ASCII number for each character. With the size of the ASCII is a
byte size, we will only use DB to define the string.
Label
The label field is an optional field containing a symbolic label for the current statement. Labels are
used in assembly language as to mark the lines which can be jumped by the other instruction (as GOTO
statement). In general, you should begin your labels in column one with the syntax:
<label name> :
A symbol is associated with some particular value. This value can be an offset within a segment, a
constant, a string, etc. A symbolic name consists of a sequence of letters, digits, and special characters, with
the following restrictions:
1. First character must be alphabetic letter or special character like ‘$’. No numeric digit is
allowed. Only ‘$’ or ‘?’ characters are not allowed (as these two characters have meanings).
2. It cannot be reserved words.
3. The maximum length for the characters is 31.
4. It is not case-sensitive, which is different from C. It will treat the upper and lower case
alphabetic equivalently.
Mnemonic
A mnemonic is an instruction name. The mnemonic field contains an assembler instruction.
Instructions are divided into three classes: machine instructions, assembler directives and pseudo opcodes.
Machine instructions are assembler mnemonics that corresponds to the actual instructions. The assembler
directives are special instructions that provide the information to the assembler but not generate any code. All
instructions introduced in section 3.1 are assembler directives. They are only message to the assembler,
nothing else. A pseudo-opcode is a message to the assembler, but it will emit the object code bytes. For
example, DB and DW in data segment in section 3.2 are the pseudo-opcode. These instructions will emit the
bytes or words of the data specified by their operands but they are not true instructions in the assembly
language.
Operand
The operand field contains the operands, or parameters, for the instruction specified in the mnemonic
field. Operands never appear on lines by themselves. The type and number of the operands depend entirely on
the specific instruction.
Comment
The comment field allows you to annotate each line of source code in your program. When the
assembler is processing a line of text with beginning of the notation “;”, it completely ignores everything on
the source line following a semi-colon. You can also have a comment on the line by itself.
Procedure Definition
In the whole code segment you must define at least one procedure segment as:
<procedure name> PROC {FAR | NEAR}
: : ;***your (main/ sub) program here***
<procedure name> ENDP
Procedure name, as function name of the subroutine in high level language, must be unique. Proc
means the procedure begins, while endp tells the assembler the procedure ends. Far refers the procedure can
be called outside, while Near refers the procedure can be called only in this procedure. As you can see, the
definition of a procedure looks like definition of a segment. One difference is that procedure name must be a
unique identifier within your program. Your code calls this procedure using this name. This topic will be
further discussed in the CALL section.
Immediate data reference & Register data reference (Direct data reference)
Immediate data reference consists of an immediate mode source and a destination location.
Data value directly move to a defined register. From the example “MOV AX, 004Ch”, it means that
the hexadecimal number 004C immediately move to AX (accumulator long) register.
Register data reference includes data moving from one register or memory location to another
register. From the example, “MOV AX, BX”, it means that the content of the BX register moved to
the AX register.
We can also represent this mode into another way. The instruction
MOV AL, DS:[8088h]
Loads the byte at memory location 8088h (in data segment) to the AL register. Likewise, the
instruction
MOV DS:[1234h], DL
stores the value from the DL register to the memory location 1234h.
Likewise, to access the location in the code segment 1234h you would use
MOV AX, CS:[1234h]
Of course, you can use the name of the memory location as the source. For example,
MOV AX, [CON]
means that the content of variable CON will be copied to AX. The CPU, as similar as above, will
resolve the [CON] as an address first.
In order to remember all the above memory addressing modes, the following syntax can help you to
do this.
MOV <destination>, <DISP | [BX | BP] | [SI | DI] >
There are totally three terms in the source field: DISP, [BX | BP] and [SI | DI]. You can choose one
term, two terms or three terms. For example, choose DISP from column one, nothing from column two and
[DI] from column three getting
MOV AL, DISP [DI]
Moreover, the generic of the move instruction takes three different assembly language forms:
MOV <register>, <memory>
MOV <memory>, <register>
MOV <register>, <register>
Note that at least one of the operands is always general purpose register.
Finally, if the effective address calculation produces a value greater than FFFFh, the CPU ignores the
overflow and the result wraps around back to zero. For example, if BX contains 10h, the instruction
MOV AL, FFFFh [BX]
will load the AL register from location DS: 0Fh, but not DS: 1000Fh.
XCHG instruction
The XCHG (exchange) instruction swaps two values. The general form is
XCHG <operand1>, <operand2>
There are two specific forms of this instruction on the 8086 machine:
XCHG <register>, <memory>
XCHG <register>, <register>
Since the 8086 often provides shorter and faster versions of instructions that use the AX register, you
should try to arrange your computations so that the CPU can use the AX register as much as possible.
However, both the operands must be the same size, and the XCHG instruction does not modify any status
flags in the status register.
During the ADD operation, although there is a carry bit, it would not be added into the result.
However, the carry flags (CF) will be set to 1. When ADC operation performed, it will add three elements,
including AX contents, W21 and CF, whatever CF related to that calculation or not.
The SUB instruction is similar to ADD instruction. Note that the subtraction is not commutative.
4
Since it is overflow for AL, the value would store in the whole AX register.
*** Page 13 ***
J 14 8086 Assembly Language Tutorial
The ADD and SUB instruction will affect the status registers. They will set the overflow flag to be 1
if signed overflow/ underflow occurs. They will set the sign flag if the result is negative. They will set the
zero flag if the result is zero, and set the carry flag if an unsigned overflow occurs.
The first MUL B2 will treat 80h as +128, whereas IMUL will treat 80h as -128 (2's complement).
After MUL executes, since 12864 = 8192 which is 2000h, therefore AX = 2000. However, when IMUL
executes, since -12864 = -8192 which is E000h, therefore AX = E000.
Division shares the same properties with multiplication. The syntax are:
DIV {source} ; AL = AX / source …. AH or AX = DX:AX / source…DX
IDIV {source} ; AL = AX / source …. AH or AX = DX:AX / source…DX
which takes the form:
DIV {<register> | <memory>}
IDIV {<register> | <memory>}
DIV handles unsigned data while IDIV handles signed division. The basic operations can be divided
into two types: byte into word or word into double word. In the byte into word operation, the dividend will be
in the AX register. The remainder will put in AH while the quotient will put in AL. In the word into double
word operation, the most significance dividend will be in the DX register while the least significance
dividend will be in the AX register. The remainder will put in DX while the quotient will put in AX.
You cannot simply divide the 8-bit value by another 8-bit value. If the denominator is an eight-bit
value, the numerator must be a 16-bit value. You can extend that 8-bit value into 16 bit and loaded it into a
16-bit register (e.g., AX), and perform the division.
Furthermore, if the dividend and divisor have the same sign, DIV and IDIV generate same results.
However, if they are different in their sign, DIV generates a positive quotient while IDIV generates a negative
quotient.
We summarized the MUL and DIV operation into the following table.
Instruction Multiplier Multiplicand in Product in
MUL CL CL (byte) AL AX
MUL BX BX (word) AX DX AX
Instruction Divisor Dividend in Quotient in Remainder in
DIV CL CL (byte) AX AL AH
DIV BX BX (word) DX AX AX DX
Table 1 Summary of MUL and DIV operation
Note that you can use any general purpose register. For example, if you use
JMP AX
It is roughly equivalent to
MOV IP, AX
Some forms of memory addressing, unfortunately, do not intrinsically specify a size. For example,
JMP [BX]
cannot tell us the size of the variable (for far or near jump?). To solve the ambiguity, you will need to
use a type coercion operator.
Coercion Operator
There are times when you would probably like to treat a byte variable as a word, or treat a word as a
double word (addressing). Temporarily changing the type of a label for some particular occurrence is called
coercion, as shown below:
<type> PTR <expression>
Type is any of byte, word, dword, near, far, or other types. Expression is any general expression on
that is the address of some object. The coercion operator returns an expression with the same value as
expression, but with the type specified by type. For example,
JMP word ptr [BX]
refers to the size of the BX is a word size. For example, the following will jump to the different code
segment address with address ABC:
JMP dword ptr CS:[ABC]
From the example, we notice that if the subroutine A to be called within the code segment, PROC
NEAR can be defined for the procedure. However, when we want to call the procedure outside the code
segment, e.g. B, PROC FAR must be defined. After finishing all the procedure and returning to the program
after CALL function, RET tells the assembler return back to the original program. Otherwise, the program
would continue to execute with unpredictable results. Note that the above program is not valid in executing
the FAR call procedure. For more details, please refer the subroutine section.
INC AX
NEXTSTAT: : :
Figure 14 Example of using conditional jump
Use the opposite branch to skip over the instructions you want to execute if the condition is true.
Always use the opposite branch rule given earlier to select the opposite branch, otherwise you may use more
than one conditional jump to perform one jumping.
6
XOR is Exclusive OR, which will set the bit zero if both bits are equal (0 0 or 1 1).
*** Page 18 ***
J 19 8086 Assembly Language Tutorial
it will return FFh. The use of the relational operators will be discussed in the conditional directive
section.
C 0 0 C
SHL/ SAL SHR
C C
ROL ROR
C C
RCL RCR
0 C
SAR
For 8088 and 8086 CPUs, the number of bits to be shifted or rotated is either 1 or in CL. If the
number of bits shifted is larger than 1, the source operand must be initialized in CL. For example, if AH =
10101101 and 3 bit right shifting is needed, the following must be followed. 7
MOV CL, 3
SHR AH, CL
7
The command, for example, “SHL AH, 3” is incorrect unless the constant is 1.
*** Page 19 ***
J 20 8086 Assembly Language Tutorial
Since shifting an integer value to the left one position is equivalent to multiplying that value by two
(2h), use the shift left instruction for multiplication by powers of two.
For example, if the following calculation is done:
810 1010 (= 8010) where 810 = 000010002
It is noticed that 8 10 = 8 (8+2) = 8 (23+21) = 8 23+ 8 21
10 10
Therefore, when we do the following procedure,
MOV AH, 8h ; store the value of AH = 24d
MOV AL, AH ; store the value of AL = 24d
MOV CL, 3 ; left shifting the AH by 3
SHL AH, CL
SHL AL, 1 ; left shifting the AL by 1
ADD AH, AL ; Add the value to perform the multiplication
Figure 18 Multiplication and Shifting Example
The keyboard and screen interrupt is another good example. Interrupt 21h provides some service for
keyboard and screen handling, with different service number. To choose a particular operation, you load the
service number into the AH register before executing the 21h. The following table lists some typical service
numbers.
Service No. Explanation
01h Keyboard input with echo
02h Display output
07h Keyboard input without echo (no check for Ctrl-C)
*** Page 20 ***
J 21 8086 Assembly Language Tutorial
For example, to read a character from a keyboard buffer with displaying the character on the screen,
you would use the following code:
MOV AH, 01h
INT 21h
Figure 20 Input Service interrupt
The AL register will store the standard ASCII code shown on the screen. With the service number
08h, it acts as service number 02h, except that no output will be shown on the screen. With the service
number 07h, it acts as service number 08h, except that it will ignore Ctrl-C key.
Another example is to print a character or string on the screen. In order to print the character, use the
output Service 02h. The operation would display a character read from the DL register (ASCII format). For
displaying the string, use the output Service 09h. The operation would display a string, defined in the data
segment, with the effective address at DX register. For example, if we want to print the string “HELLO”, the
following procedure must be follow:
MOV AH, 09h
LEA DX, NAME
INT 21h
: :
DATA_SEG SEGMENT
NAME DB “HELLO$”
DATA_SEG ENDS
Figure 21 Output Service interrupt
LEA (load effective address) instruction is used to prepare the pointer values. It takes the form:
LEA <destination>, <source>
which takes the form
LEA <register>, <memory>
It loads the specified general purpose register with the effective address of the specified memory
location. The effective address is the final memory address obtained after all addressing mode computations.
For example,
LEA AX, 3 [BX]
loads the value of BX plus 3 into the AX register.
For the string case, it will load the address of the first character (ASCII form) in the register (DX) to
enable the string operation. Furthermore, it will display characters until it finds the “$” sign.
bp
sp
When a program is loaded into the memory, a stack is created. Stack segment register ( SS) initialized
the stack segment. Stack pointer (SP) will point to the top of the stack. The base register (BP) is used to
access the elements inside the stack without popping out the top elements. When using SP and BP, the
referencing pointer is SS.
There are two basic instructions for stack manipulation. They are push and pop, and the syntax are:
Figure 23 Stack operation
PUSH {<register> | <memory>}
POP {<register> | <memory>}
Push Direction
ss
next element
to be placed
sp top of stack
PUSH will decrease the stack pointer (SP) by 2 before placing the data in the stack. POP is an
operation which copies the top element to the register before increase the stack pointer by 2 9.
Notice three things about the manipulation of the stack. First, it is always in the stack segment.
Second, the stack grows down in memory, i.e. as you push the values onto the stack the CPU stores them into
successively lower (smaller) memory locations. Finally, the SP (stack pointer) always contains the address of
the value on the top of the stack.
All pushes and pops are 16-bit operations. There is no way to push a single 8-bit value onto the stack.
To push an 8-bit value you should load it into 16-bit register first, with the
higher order byte being 0. Figure 24 Stack push implementation
8
Reserve space for interrupt execution, procedure call and own use.
9
It implies that it will only PUSH and POP a word size data. (16 bit)
*** Page 22 ***
J 23 8086 Assembly Language Tutorial
do not exist when your assembly program is running. The purpose of these statements is to control which
statements the assembler assembles into your final execution file. The macro directives let you emit repetitive
sequences of instructions to an assembly language file like high level language procedures and loops, but
without any running overheads.
The assembler has facilities that programmers use to define the macros. A specific macro name is
defined for the macro along with the assembly instructions that the macro is to generate. Next, the instructions
are defined within the macro.
A macro definition appears before any defined segment, except if your macro is a data definition, put
it in data segment. The syntax is:
<Macro name> MACRO (arg1,arg2,....)
::::: ;***your macro here***
ENDM
The name of the macro is defined before the MARCO directive. It should be a valid and unique
symbol in the source file. You will use this identifier to expand the macro (as the calling subroutine). The
arguments are the values you specify when you expand the macro (as the formal and actual parameters in the
calling subroutine). The MACRO directive tells the assembler that the following instructions up to ENDM
are to be part of the macro definition. The ENDM tells the assembler the end of the macro definition.
During the expansion, the assembler will expand every occurrence of the macro in the code segment.
It will replace the macro name by the macro body at the destination address, and the formal arguments inside
macro body by the supplied arguments. In this case, no run-time overhead is needed. The only thing the
assembler done is a simple substitution followed by normal assembling.
Note that the assembler does not immediately assemble the instructions between the MACRO and
ENDM directives when the assembler encounters the macro. Instead, the assembler stores the text
corresponding to the macro into a special table (called the symbol table). The assembler inserts these
instructions into your program when the assembler expands the macro. For example,
CLS MACRO
MOV AH, 06h
MOV AL, 00h
MOV BH, 07h
MOV CX, 0000h
MOV DH, 18h
MOV DL, 50h
INT 10h
ENDM
Figure 25 Macro Definition of clearing Screen
How to call the macro procedure? In the code segment, use the symbol to call the macro procedure.
In the above example, if you want to call the CLS macro, just write CLS in the code segment. When you do
this, the assembler will insert the statements between the MACRO and ENDM directives into your code at
the point of the macro invocation.
If arguments are needed, formal parameters and actual parameters (argument) are defined in the
macro definition and the calling macro respectively. The assembler will substitute the actual parameters
appearing as operands for the formal parameters appearing in the macro definition. The assembler does a
straight textual substitution only. For example, if we want to print the message with different message in
different cases, the following example can be followed:
PRTMSG MACRO MSG ;/*** printing message ***/
MOV AH, 09h
LEA DX, MSG
INT 21h
ENDM
CODE_SEG ENDS
In some instances using macros can save a considerable amount of typing in the program. For
example, if you want to clear the screen many times, you may use a lot of code for only one purpose in case
of no macro. A large number of same statements are duplicated in the program. Writing the macro can
simplify your program, and easy to write.
Since the assembler does a textual substitution for macro parameters when expanding the macro,
there are times when a macro expansion might not produce the results you expected. For example, look at the
following statement:
MSG A * 5
As you can see, the calling to the MSG macro with text can lead to the problem. The assembler will
automatically convert a text object A passed to the macro.
If we call the macro procedure as:
MSG <A*5>
the assembler will automatically convert the text object A*5 to the macro. In order to evaluate the
value A*5 expression, the following should be done:
MSG %A*5
It will evaluate the expression “A*5” and convert the resulting numeric value to a text value
consisting of the digits that represent the value before the expansion.
The macro, on the other hand, does not emit any code when processing the statements between the
MACRO and ENDM directives. However, upon encountering macro in the mnemonic field:
1. The assembler will assemble every statement between the MACRO and ENDM directives
2. Emit the code to the output execution file (*.exe).
At the running time, the CPU executes these instructions without the procedure overhead.
The execution of a macro expansion is usually faster than the execution time of the same code
implemented with a procedure. Furthermore, to call a macro, you simply specify the macro name as though it
were an instruction or directive. To call a procedure, you need to use the CALL instruction.
The local label definition should be defined after the macro directives. During the expansion, the
assembler will assign the different labels in the program.
Suppose that you have another macro named DISPLY that uses the service 02 in the AH register to
display a character:
DISPLY MACRO CHAR
MOV DL, CHAR
MOV AH, 02h
INT 21h
ENDM
Figure 30 Example of Display macro
Now, you can change the above macro into the following nested macro:
DISPLY MACRO CHAR
MOV DL, CHAR
DOSINT 02h
ENDM
Figure 31 Example of Display macro
REPT directive
The syntax of the REPT directives is:
REPT <expression>
<statements>
ENDM
Expression must be a numeric expression that evaluates to an unsigned constant. The repeat directive
duplicates all the statements between REPT and ENDM with the number of times indicated in the expression,
for example,
MAKEWORD MACRO N
REPT N
DW 1024
ENDM
ENDM
Figure 32 Example of REPT directives
When a macro MAKEWORD 3 is called, the loop will repeat 3 times, each time emitting the code of
"DW 1024" after the expansion. Note that the REPT loop executes at assembly time, not at run time. REPT is
not a mechanism for creating loops within the program, it is only used for replicating sections of code within
your program.
IRP directive
Another form of the repeat macro is the IRP macro. The IRP (Indefinite Repeat) operation will cause
a repeat of block of instruction up to the ENDM. The syntax is:
IRP <<parameter>, <arguments>>
<statements>
ENDM
The “< >” brackets are required around the items in the parameter and arguments. The IRP directive
replicates the instructions between IRP and ENDM once for each item appearing in the argument.
Furthermore, for each iteration, the first symbol in the parameter is assigned the value of the successive items
from the second parameter. For example,
IRP N, <1,2,3,4>
DB N
ENDM
Figure 33 Indefinite Repeat Example
IRPC directive
The third form of the loop macro is the IRPC macro. It differs from the IRP macro in that it repeats a
loop the number of times specified by the length of a character string rather than by the number of the
operands present. Here is the general syntax:
IRPC <<parameter>, <string argument>>
<statements>
ENDM
10
Ignore this part forward for CSC Minor Students
*** Page 26 ***
J 27 8086 Assembly Language Tutorial
The statements in the loop repeat once for each character in the string operand. The angle brackets “<
>” must appear around the string, for example,
IRPC N, <1234>
DB N
ENDM
Figure 34 Indefinite Repeat Example
The assembler will generate a block of the code for each character in the string argument. After the
expansion, the assembler will generate DB 1, DB 2, DB 3 and DB 4. The arguments can be any number of
legal symbols, string, numeric, or arithmetic constants.
IF Directive
The assembly language supports a number of the conditional directives. Conditional directives IF are
most useful within a macro definition. Every IF directives must have a matching ENDIF to terminate the
tested condition. One optional ELSE may provide an alternative condition. The syntax is:
IF <condition>
<sequence of statements>
<ELSE> ;optional
<sequence of statements>
ENDIF
Omission of ENDIF causes an error message "Undetermined conditional". The assembler evaluates
the condition. If it is a non-zero value (true), the assembler will assemble the statements between the if and
else directives (or endif, if the else is not present). If the expression evaluates to zero (false) and an else
section is present, the assembler will assemble the statements between the else and the endif directive. If the
else section is not present and expression evaluates to false, the assembler will not assemble any of the code
between the if and endif directives.
The important thing to remember is that the condition has to be an expression (it can be the relational
operation, e.g., EQ, LT, etc.) that the assembler can evaluate at assembly time, i.e., it must evaluate to a
constant. For example, if you want to assemble the first set of code for A = 0, otherwise to assembler the
second set of code, you could use the following statements:
IF A EQ 0
MOV AX, A ;<first set>
ELSE
MOV BX, A ;<second set>
ENDIF
Figure 35 Example of using IF directives
IFE Directive
The IFE directive is used exactly like the IF directive except it assembles the code after the IFE
directive only if the expression evaluates to zero (false), rather than non-zero (true), i.e. reverse case of IF
directive.
IFB, IFNB
The IFB and IFNB directives, useful mainly in macros, check to see if an operand is blank (IFB) or
not blank (IFNB), which is in the form:
IFB <<condition>>
<sequence of statements>
<ELSE> ;optional
<sequence of statements>
ENDIF
The IFB works in an opposite manner to IFB, i.e. it would assemble the statements above that IFB
does not and vice versa. Note that “< >” is needed in the IFB statement.
For example, if we use IFNB (if not blank), all INT 21h requests require a service in the AH register,
whereas some requests also require a value in the DX register. The following macro, DOSINT, uses IFNB to
test for a nonblank argument for the DX.
DOSINT MACRO SERVICE, ADDRESS
MOV AH, SERVICE
IFNB <ADDRESS>
LEA DX, ADDRESS
ENDIF
INT 21h
ENDM
Figure 36 Example of IF directives
If DOSINT 01h is called, the assembler will generate only three program lines, which is:
MOV AH, 01h
INT 21h
Figure 37 Example of IF directives after expansion of missing parameter
If DOSINT 09h, MSG is called, the assembler will generate the following program lines:
MOV AH, 09h
LEA DX, MSG
INT 21h
Figure 38 Example of IF directives after the expansion
<sequence of statements>
<ELSE> ;optional
<sequence of statements>
ENDIF
where XXX is the assembly code. Note that “< >” is required.
EXITM directive
The EXITM directive immediately terminates the expansion of a macro, exactly as ENDM. But why
EXITM? The answer is the conditional assembly. Conditional assembly can be used to conditionally execute
the EXITM directive in certain condition, such as
COUNTER MACRO COUNT
IF COUNT EQ 0
: :
EXITM
ENDIF
ADD AX, COUNT
ENDM
Figure 39 Example of EXITM
Although there are possible ways to module your program into the procedures, you may need to pass
the data from the main program or return the data from the procedure. When you pass parameters, it depends
on the size and the number of the parameters. There are several ways to pass the parameters to the procedure.
How can we do if common data is used in both main program and the subprograms? A common
requirement is to process data in one assembly module that is defined in another assembly module. We
modify the above example in the following one, and noted the changes is that we move the content of A and B
to AX and BX in the subprogram.
*** Page 30 ***
J 31 8086 Assembly Language Tutorial
; Main program
EXTRN ADDING1:FAR
PUBLIC A, B
CODE_SEG SEGMENT PARA PUBLIC
MAIN1 PROC FAR
ASSUME CS:CODE_SEG,DS:DATA_SEG,SS:STACK_SEG
MOV AX, DATA_SEG
MOV DS, AX
CALL ADDING1
MOV AH, 4Ch
INT 21h
MAIN1 ENDP
CODE_SEG ENDS
Note that there are two main changes in the above example. To begin with, the main program MAIN
defines the data A and B as PUBLIC. The data segment is also defined with the PUBLIC attribute. In the
code segments, the PUBLIC attribute will cause the linker to combine the two logical code segments into one
physical code segment.
Next, the subprogram ADDING defines A and B as EXTRN, and both as WORD size. This
definition informs the assembler as to the length of the one word. The assembler can now generate to correct
operation code for the MOV instructions, but the linker will have to complete the operands.
The main program and the subprogram may define any other data items, but only those defined as
PUBLIC and EXTRN are known in common.
The reason why the ADDING subprogram can refer to the main program's data is because it does not
change the address in the DS register, which still points to the main program data segment. However,
programs are not always simple, and subprograms often have to define their own data as well as refer the data
in the calling program.
The next example shows the variation of the data definition. Both the data is defined in both data
segments.
; Main program
EXTRN ADDING2:FAR
PUBLIC A
CODE_SEG SEGMENT PARA
MAIN2 PROC FAR
ASSUME CS:CODE_SEG,DS:DATA_SEG,SS:STACK_SEG
The main difference between this example and the above example is the definition of the data. Data
A is defined in the main program while data B is defined in the sub-program. In the subprogram, it has to get
the data A first while the DS register still contains the address of the main program data segment address. The
subprogram then pushes the DS on the stack and loads the address of its own data segment. The subprogram
now can get its own data segment.
Furthermore, you can use the PUBLIC in both the data segment. In this case, the linker combines
them and need not to push and pop the DS because the programs use the same data segment and DS address.
PUSH B
CALL ADDING3
MOV AH, 4Ch
INT 21h
MAIN3 ENDP
CODE_SEG ENDS
Take a look at the stack after the execution of the MOV BP, SP in
the subprogram ADDING3, and look like the right hand figure:
1. A push loaded data A (0005) onto the stack first.
2. A push loaded data B (0007) onto the stack in the main
program.
3. CALL function in the main program pushed the content
of the CS onto the stack, e.g., 1234.
4. CALL function in the main program pushed the content
of the IP onto the stack, e.g., 0035.
The called program requires use of the BP, say 0000, to access the
parameters in the stack. Its first action is to save the contents of the BP for Figure 44 The contents of the stack
the calling program by pushing it onto the stack. The program then inserts the
contents of the SP into the BP because the BP is usable as an index register. Since the BP now also contains
the SP pointer, which points the top of the stack, now data A is in the stack at (BP+8) and data B is at (BP+6).
The routine transfers these values from the stack to the AX and BX, and performs the addition.
Before returning to the calling program, the routine pops the BP (returning the base pointer address),
which increments the SP by 2. The last instruction, RET, is a far return to the calling program that performs
the following:
1. Pops the word now at the top of the stack to the IP and increase the SP by 2.
2. Pops the word now at the top of the stack onto the CS and increase the SP by 2.
3. Because of the two passed parameters (A and B) in the stack, the RET instruction is coded
as RET 4. The 4, known as a pop value, contains the number of bytes in the passed
parameters. The RET operation also adds the pop value to the SP, correcting it so that it
points to the bottom of the stack.
In effect, because the parameters in the stack are no longer required, the operation discards the value
A and B in the stack and returns correctly to the calling program. Note that the POP and RET operation
increment the SP but no any erasing in the content of the stack.
When saving other registers onto the stack, always make sure that you save and set up BP before
pushing the other registers. If you push the other registers before setting up the BP, the offsets into the stack
will change. That means that the two statement
PUSH BP
MOV BP, SP
instructions should be the first two instructions in any subroutine when passing the parameters by
stack.
The SUB SP, 6 instruction makes room for three words on the stack. You can allocate
three local variables in these three words. You can reference these three variables by indexing off the BP
register ([bp-2], [bp-4], [bp-6]) using negative offsets. Upon reaching that statement, you can use the memory
between BP and SP as the temporary storage of the local variables.
The example uses the matching instruction:
ADD SP, 6
at the end of the procedure to delocate the local storage. The value you add to the stack pointer must
exactly match the value you subtract when allocating this storage. If these two values don’t match, the stack
pointer upon entry to the routine will not match the stack pointer upon exit, this is like pushing or popping too
many items inside the procedure.
Unlike parameters, you can allocate local variables in any order. As long as you are consistent with
your location assignments, you can allocate them in any way you choose.
1. Similar as in Assembly Language, when a C program links to the assembly program, EXTRN
and PUBLIC must be defined in the main and subprogram respectively. In the C program, the
syntax of the EXTRN is:
extern <program function>
For example, if a function called _ADDING (which is an assembly language procedure)
to be called by a C program with parameters a and b with both integer type, the following
must be written before the main program in C:
extern int _ADDING (int, int);
The extern statement must be defined at the beginning of the main program.
2. If you are programming in C, all external labels should start with an underscore ‘_’ character in
assembly. The C compilers automatically prefix an underscore to all function and external
variable names when they’re used in C code, so you only need to attend to underscores in your
assembler code. You must be sure that all assembler references to C functions and variables
begin with underscores, and you must begin all assembler functions and variables that are made
public and referenced by C with underscores. Furthermore, you must name the code segment as
_TEXT, and all calls from C to assembly will be near procedure.
3. The assembler is normally insensitive to case when handling symbolic names, making no
distinction between uppercase and lowercase letters. Since C is case sensitive, it’s desirable to
have assembler be case sensitive, at least for those symbols that are shared between assembler
and C.
4. It’s important that your assembler EXTRN statements that declare external C variables specify
the right size for those variables. The correspondence between C and assembler types is as
follows: Byte size in assembly:
Byte size in assembly: unsigned char, char
Word size in assembly: unsigned short, short, unsigned int, int
5. C will pass parameters onto the stack. Before calling a function, C first pushes the parameters to
that function onto the stack, starting with the rightmost parameter and ending with the leftmost
parameter. The C function call:
Testing (a, b, 5);
compiles to
MOV AX, 5
PUSH AX
PUSH B
PUSH A
CALL _Testing
Figure 47 Conversion of C function to assembly function
You can see that the rightmost parameter, 5, begin pushed first, then B, and finally A.
6. As far as C is concerned, C can do anything as long as they preserve the registers. However, SI
and DI are special cases, since they are used by C as register variables. If register variables are
enabled in the C module calling your assembler function, you must preserve SI and DI. It is a
good practice that you should push them on entry and pop them when exit.
7. A C callable assembler function must return a value in AX, just like other functions. In general,
a 8-bit or 16-bit value is returned in AX register, while the 32-bit values are returned in DX:AX
two-word form.
The typical procedure for accessing the two passed parameters is done as the following example.
C program:
#include <stdio.h>
#include <stdlib.h>
extern int adding (int, int);
int main ( )
{
int a, b;
: : :
ADDING (a, b);
: : :
}
Assembly Program:
PUBLIC _ADDING
CODE_SEG SEGMENT PARA PUBLIC ‘CODE’
_ADDING PROC NEAR
ASSUME CS: CODE_SEG
PUSH BP
MOV BP, SP
MOV A, [BP+4]
MOV B, [BP+6]
PUSH SI
PUSH DS
: :
POP DS
POP SI
POP BP
MOV AX, 0
RET 4
_ADDING ENDP
CODE_SEG ENDS
END _ADDING
Figure 48 Example of Linking from C to Assembly Program
One case in which you may wish to call a C function from assembler is when you need to perform
complex calculations. This is especially true when mixed integer type and floating-point calculations are
involved. There is nothing different from calling to assembly program, but note that:
1. Define the extern function (procedure calling to C) in both C and assembly program. In the
assembly procedure, if you want to call the C function outside the procedure, the definition of
the extern will be
EXTRN _adding: proc
where proc means the procedure.
2. Push all necessary parameters before calling C function.
3. Pop all necessary parameters after calling C function.
4. The return parameter will be in the AX register.
To call a BIOS or DOS interrupt, an interrupt instruction will be used in the assembly program.
Given an interrupt value, the instruction INT will transfer the control to one of the 256 different interrupt
handlers. The interrupt vector table holds the address of these interrupt handlers. We will introduce different
applications of BIOS and DOS interrupt in the keyboard and screen processing.
To begin with, the system needs to know the maximum length of the input data. The purpose is to
warn the user who input too many characters. Secondly, the system must know how many characters the user
inputs. Therefore, the label NAMESTRING needs three parameters to define an input string. The first
parameter denotes the maximum length of input string, the second parameter denotes the actual number of
input string, and the third parameter declares the space for input string.
To request the input string in code segment, apply the DOS interrupt with service 0Ah in the AH.
Load the address of the parameter list into the DX and issue the INT 21h. Such interrupt will wait for the user
to enter the characters and check whether they are exceeding the maximum or not. This operation will echo
the entered characters Pressing the ENTER key (ASCII code: 0Dh) will tell the system the end of an entry. 11
All the characters are interpreted as the ASCII code (with ENTER key). Therefore, the string
definition must be DB.
11
Enter Key will count in the maximum length of the string. However, it will not count in the actual length of the string.
*** Page 37 ***
J 38 8086 Assembly Language Tutorial
where 0Dh is the enter key and 0Ah is the line-feed control.
For example,
MOV AH, 06h
MOV AL, 00h
MOV BH, 07h
MOV CX, 0000h
MOV DH, 18h
MOV DL, 50h
INT 10h
Figure 51 Screen clearing example
7 6 5 4 3 2 1 0
blinking bit
Foreground color bits
Background color bits
BH specifies the color of the resulting screen. The most-significant bit represents blinking, the next 3
bits represents background color, while the least-significant 4 bits represents foreground color. Inserting a
different value to different parameters causes the windowing effect function. For example,
MOV AH, 06h
MOV AL, 05h
MOV BH, 61h
MOV CX, 0A1Ch
MOV DH, 0Eh
MOV DL, 34h
INT 10h
Figure 53 Windows effect example
would create a window at the center of the screen with its own parameters.
After the above execution, DH will contain the row number and DL will contain the column number
of the cursor position.
The character will be read in the AL register with the ASCII representation.
we cannot know what value of AX will be at any time. Bugs in the ISR are very difficult to find, because such
bugs often affect the execution of unrelated code. The solution to this problem, of course, is to make sure you
preserve all registers you use in the ISR. One possible method to preserve the registers is to save and restore
all register a procedure modifies.
Writing the ISR is only the first step to implementing an interrupt handler. You must also initialize
the interrupt vector table entry with the address of your ISR. There is two common way to accomplish this:
store the address directly in the interrupt vector table or call DOS and let DOS do the job for you.
Storing the address yourself is an easy task. All you need to do is loading a segment register with
zero and store the four-byte address at the appropriate offset within that segment. The following code
sequence initializes the entry for interrupt 255 with the address of the SimpleISR routine in the above
example.
MOV AX, 0
MOV ES, AX
CLI
LEA WORD PTR ES:[0FFh*4], [SimpleISR]
MOV WORD PTR ES:[0FFh*4+2], CS
Figure 58 Example of storing the address by yourself
The CLI instruction tells the assembler to prevent any interrupt from this point, and the STI
instruction allows any interrupt raised, and the syntax is
CLI
STI
Perhaps a better way to initialize an interrupt vector is to use the DOS Set Interrupt Vector call.
Calling DOS interrupt with AH equal to 25h provides this function. This call expects an interrupt number in
the AL register and the address of the interrupt service routine in DS: DX, where DS stores the segment
address of the interrupt and DX stores the address. The call to DOS that would accomplish the same thing as
the code above is
MOV AX, 2523h
MOV DX, CS ;assume SimpleISR in code segment
MOV DS, DX
LEA DX, SimpleISR
INT 21h
MOV AX, DATA_SEG; restore data segment in DS
MOV DS, AX
Figure 59 Example of storing the address by dos interrupt
Although this code sequence is a little more complex than putting the data directly into the interrupt
vector table, it is safer. Many programs monitor changes made to the interrupt vector table through DOS. If
you call DOS to change an interrupt vector table entry, those programs will become aware of your changes.
Generally, it is a very bad idea to patch the interrupt vector and not restore the original entry after
your program terminates. Well behaved programs always save the previous value of an interrupt vector table
entry and restore this value before termination. The following code sequences demonstrate how to do this. At
the beginning of your program, you should save the old interrupt address. The DOS interrupt with service
number 35h in AH will perform this. This interrupt will save the interrupt number, defined in AL, in ES: BX
as segment: offset address. For example,
MOV AX, 3523h
INT 21h
MOV [OLDOFFSET], BX
MOV [OLDSEGMENT], ES
Figure 60 Load the address of the interrupt
The above example shows how to save the interrupt address (segment: offset) of INT 10h (Video
interrupt). Defining AH = 35h, AL = 23h, the segment address will be stored in ES and the offset address will
be stored in BX after the interrupt. We will save all these address in the two variables.
Before exit the program, you should restore the interrupt vector entries, for example:
MOV DS, CS: [OLDSEGMENT]
MOV DX, CS: [OLDOFFSET]
MOV AX, 2523h
INT 21h
The above interrupt only responds to the corresponding interrupt. For example, the interrupt may
raised if Ctrl-C key is pressed (INT 23h). After your ISR has defined, that interrupt service routine only
responds to the Ctrl-C key, which is exactly the same as the original interrupt. Here is the reference of other
interrupts that can be changed to your ISR.
Interrupt No. Interrupt raised when......
00h Divide overflow error
09h Keyboard input
08h/ 1Ch Every 1/18.2 sec (0.055sec, 5/91sec)
23h Ctrl-C key is pressed
70h Every 940ms
Table 5 Interrupt service code
This code will pass along your original ISR. The OLDOFFSET variable must be defined in the code
segment if you use this technique to transfer the control to the original ISR.
Reentrancy problem
A minor problem develops with developing ISRs, what happens if you enable interrupts while in an
ISR and a second interrupt from the same device comes along? This would interrupt the ISR and then reenter
the ISR from the beginning. Many applications do not behave properly under these conditions. The easiest
way to prevent such an occurrence is to turn off the interrupts while executing code in a critical section, i.e.
using CLI instruction.
Another problem is the DOS interrupt problem. The internal data structure of DOS is designed so that
is is non-reenterable. It is because the DOS is for a single user, and single program system. It is not possible to
active the DOS interrupt when it has activated. Therefore, you can only include the INT 21h in the ISR only if
we know that INT 21h has not been called. Generally, it is suggested that you should not to call INT 21h in
the ISR.
Section 8. Examples
Example 1
Question: Write a program to satisfy the following requirement:
1. Read until 10 small alphabetic letter entered.
2. Convert the 10 small letter to capital letter and output these letters.
Algorithm:
1. Display the input message and set the counter SI.
2. Read the characters
3. Check whether it is small alphabetic letter.
*** Page 42 ***
J 43 8086 Assembly Language Tutorial
4. If it is correct, change that letter to capital letter by adding -20h to its ASCII. Save the ASCII in
the array MSG [SI]12.
5. Check whether the loop ends (10). If the loop is not finished, or the input is incorrect, repeat step
2 until it is finished.
6. Display the capital letter list.
Program:
;*** BEGIN OF THE PROGRAM ***
;*** DEFINE THE VARIABLES ***
CODE_SEG SEGMENT
BEGIN PROC FAR
ASSUME CS:CODE_SEG,DS:DATA_SEG,SS:STACK_SEG
START: MOV AX,DATA_SEG
MOV DS,AX
;*** DISPLAY THE MESSAGE 1 ***
MOV AH,09h
LEA DX,MESSAGE1
INT 21h
;*** READ THE CHARACTERS ***
READ_CH: MOV AH,01h
INT 21h
;*** JUMP IF THE CHARACTERS < 'a' ***
CMP AL,61h
JL READ_CH
;*** CORRECT IF THE CHARACTERS < 'z' ***
CMP AL,7Bh
JL CORRECT
JMP READ_CH
;*** CHANGE THE SAMLL LETTER TO CAPITAL LETTER ***
CORRECT: ADD AL,-20h
;*** CHECK THE CHARACTERS READ ***
MOV [MSG+SI],AL
ADD SI,1
CMP SI,10
JZ WRITE_MSG
JMP READ_CH
;*** ECHO THE STRING READ ***
WRITE_MSG: MOV AH,09h
LEA DX,MESSAGE2
INT 21h
LEA DX,MSG
INT 21h
;*** END OF PROGRAM ***
END_SEG: MOV AH,4Ch
INT 21h
BEGIN ENDP
CODE_SEG ENDS
;*** DATA SEGMENT ***
DATA_SEG SEGMENT
MESSAGE1 DB "Enter the 10 characters :$"
MESSAGE2 DB " ;Corresponding CORRECT characters in capital letter :$"
MSG DB 10 DUP(?)
END_MSG DB '$'
DATA_SEG ENDS
;*** STACK SEGMENT ***
STACK_SEG SEGMENT STACK
DW 10 DUP (?)
STACK_SEG ENDS
END BEGIN
Example 2
Question: Write an assembly program to do the following:
1. Read from the keyboard a positive decimal integer of at most 4 digits (0-9999). Convert it to
decimal integer.
2. Using stack, display the integer as string.
Algorithm:
1. Read the characters. Check whether the user enters the return input (ASCII for return key is 0Dh)
or enter the digits.
12
In assembly program, to implement the array, use [MSG+SI] (as MSG [SI] in C). It will act as a list.
*** Page 43 ***
J 44 8086 Assembly Language Tutorial
ADD COUNT,-1
CMP COUNT,0
JG WRITE_MSG
;*** END OF PROGRAM ***
END_SEG: MOV AH,4Ch
INT 21h
BEGIN ENDP
CODE_SEG ENDS
;*** STACK SEGMENT ***
STACK_SEG SEGMENT STACK
DW 40 DUP(?)
STACK_SEG ENDS
;*** DATA SEGMENT ***
DATA_SEG SEGMENT
MESSAGE1 DB "Enter digits (max 4):$"
MESSAGE2 DB "The output is:$"
SUM DW 0
COUNT DB 0
ADDER DB 0
DATA_SEG ENDS
END BEGIN
Example 3
Question: Write an assembly program to do the quick sort.
Algorithm: Please refer any reference on recursive calling (C programming textbook, P. 245)
Program for C Version: (Copy from C programming textbook, P. 245)
#include<stdio.h>
#define SWAP(x,y) {int z=(x);(x)=(y);(y)=(z);}
main()
{
int a[100], n;
: :
rquick(a,0,n-1);
: :
}
void rquick(int a[], int lo, int hi)
{
int low, high, pivot;
low=lo;
high=hi;
if (low<high)
{
pivot=a[high];
do {
while (low<high && a[low]<=pivot)
low++;
while (low<high && a[high]>=pivot)
high--;
if (low<high)
SWAP(a[low],a[high]);
} while (low<high)
SWAP(a[low],a[hi]);
rquick(a,[Link]-1);
rquick(a,low+1,hi);
}
}
CMP [SI], AX
JL NLOOP
;*** HIGH-- ***
MOV AX, [BP-4]
SUB AX, 1
MOV [BP-4], AX
SUB SI, 2
JMP WHILE2
;*** IF LOW<HIGH ***
NLOOP: MOV AX, [BP-4]
CMP [BP-2], AX
JGE CHECKING
;*** SWAP(A[LOW],A[HIGH]) ***
PUSH AX
MOV AX, [BX]
XCHG AX, [SI]
MOV [BX], AX
POP AX
;*** WHILE (LOW<HIGH) ***
CHECKING: MOV AX, [BP-4]
CMP [BP-2], AX
JL WHILE1
;*** COMPUTE ADDRESS OF A[HI], PUT IN CX ***
LEA CX, A
ADD CX, [BP+4]
ADD CX, [BP+4]
;*** SWAP(A[LOW],A[HI]) ***
MOV AX, [BX]
PUSH SI
MOV SI,CX
XCHG AX, [SI]
POP SI
MOV [BX], AX
;*** call Recursive 1 ***
MOV AX, [BP-2]
DEC AX
PUSH [BP+6]
PUSH AX
CALL SORT
;*** call Recursive 2 ***
MOV AX, [BP-2]
INC AX
PUSH AX
PUSH [BP+4]
CALL SORT
;*** EXIT and RET ***
ELOOP: ADD SP, 6
POP BP
RET 4
SORT ENDP
CODE_SEG ENDS
;*** STACK SEGMENT ***
STACK_SEG SEGMENT STACK 'STACK'
DW 1024 DUP(?)
STACK_SEG ENDS
;*** DATA SEGMENT ***
DATA_SEG SEGMENT PARA 'DATA'
A DW 5,2,3,4,1
DATA_SEG ENDS
END BEGIN
Example 4
Question: Write a program which displays another message when interrupt raised.
Algorithm: 1. Store the old ISR address, and define new ones.
2. Display the endless message.
3. Define the new ISR. Execute the ISR by pressing Ctrl_C key.
4. Accept "Y" to terminate the endless message.
5. Restore the old ISR.
Program:
;********** MACRO DEFINITION **********
PRINT MACRO MSG
MOV AH, 09h
With Debug:
Segment Definition:
With:
ASSUME CS:<code segment name>, DS:<data segment name>, SS:<stack segment name>
Program Ending:
Data Segment:
Data Definition:
Code Segment:
Label Definition:
<label name> :
Procedure Definition:
Data Movement:
Arithmetic Operation:
LOOP <label>
Logic Operation:
Interrupts:
Stack Segment:
Stack Definition:
Stack Manipulation:
Macro Definition:
Directives:
REPT <expression>
<statements>
ENDM
*** Page 51 ***
J 52 8086 Assembly Language Tutorial
Calling Sequences: