IN THIS CHAPTER, YOU WILL LEARN
- To use string instructions for various applications.
- The concept and use of procedures and call instructions.
- To distinguish macros from procedures and learn to write and use macros.
- To write programs to convert between the commonly used number formats.
- Signed number arithmetic using 8086 instructions.
- To write programs using the high level language constructs of MASM.
4.1 | String Instructions
The 8086 has a set of instructions for handling blocks of data in the form of bytes or words. They are called ‘string’ instructions. A string is an array of data of the same type – for example, a character string or a byte string. Table 4.1 gives the list of string instructions/prefixes which are used in string manipulations. The usefulness of string instructions can be seen when in the memory data has to be moved, searched or compared in blocks.
Table 4.1 | List of Instructions/Prefixes Used in String Operations
Consider the case of 100 (say) words or bytes in a particular memory area that is to be moved to another memory area. This can very well be done using pointer registers and looping using a counter. However, this whole process can be automated with lesser number of instructions if we use string instructions. Similarly, we may need to compare two blocks of data for equality or search a data block for a particular data. In all these cases, string instructions make our task easier and our code shorter. However, before using these instructions, we have to include a few initialization steps in our code.
Pre-requisites for Using String Instructions
- Two segments are to be defined, i.e., the data segment and the extra segment. This means that the corresponding segment registers DS and ES have to be initialized and used. The data segment is the source segment and the extra segment is the destination segment.
- The SI registers and DI registers should act as pointers to the data segment and extra segment, respectively. This means that, initially, SI should contain the address (offset) of the first location in the data segment. Similarly, DI should contain the address (offset) of the first location in the extra segment.
- There is a control flag called the direction flag which is used exclusively for string operations. Its purpose is that in string operations, if the flag is set, the pointer registers get automatically decremented, and if reset, the reverse occurs. So, whenever string instructions are being used, the direction flag (DF) should be set or reset depending on the direction the addresses are to be modified after each operation.
- The counter CX should be loaded with the count of the number of operations required.
4.1.1 | The MOVS Instruction
Example 4.1 shows the use of the string instruction MOVSB with the necessary pre-requisites incorporated. Let us examine how this has been done. The problem in hand is to transfer 10 bytes (the character ‘*’) from an area of memory designated as DATA1 to an area named DATA2. Thus, the source has to be the data segment, and the destination the extra segment. How do we incorporate the extra segment?
If we use the full segment model, we can define two segments – the data segment (DAT) and the extra segment (EXTR). See how it is done. When both the segments registers are defined, the segments get defined. Then, the program instructions cause the data in the data segment to be copied to the extra segment. Example 4.1a uses the full segment model for this.
In Example 4.1a, two segments DAT and EXTR are defined and two segment registers DS and ES are initialized. The addresses SI and DI point to the source data and destination data addresses, and DF is cleared for auto incrementing the pointer registers.
Now let us do the same program using the simplified model. The small model can have only one data segment, so how can we have an ‘extra segment’? The solution is to have both data areas in the data segment itself, but after the data segment is defined, copy the value of DS into ES – this makes the assembler believe that the extra segment is available – though it is the same as the data segment. In this example, the destination data area is chosen to be at org 0200H. This is done just to space the source and destination data areas and is not mandatory. Thus, the first prerequisite of having both the data and extra segments is satisfied.
Next, the SI register is made to point to the address DATA1 and the DI register to DATA2. Then, the counter register is loaded with 10, the count of data transfer operations required. The instruction CLD is used to clear the direction flag (DF). This causes the value of SI and DI to be incremented after each move operation. Here, the data is in the form of bytes. Hence, SI and DI are incremented only by one each time. The string instruction used here is MOVSB (prefixed by REP).This causes the data in the location pointed by SI to be moved to the location pointed by DI. After each such move operation, two actions occur. One is that CX decrements by 1 and the other is that the pointer registers are incremented. This sequence continues until CX = 0. Example 4.1a and 4.1b illustrate these steps.
For the above program, if the data involved is in the form of words, the only change would be to replace the line REP MOVSB with REP MOVSW. In this case, CLD will cause SI and DI to be incremented by 2.
4.1.2 | The CMPS Instruction
Now, let us use the next string instruction – CMPSB/CMPSW. This instruction is for string comparison, byte by byte or word by word, as the case may be. The conditional flags are modified according to the result of the comparison. String comparison has to be accompanied by the use of the conditional REPE/REPZ prefix. Since string comparison checks only for equality, the Zero flag is made use of automatically. Example 4.2 compares two character strings (of length six) which are saved in the data segment. One of two messages is to be displayed depending on whether WRD2 is the same as WRD1.
Here, SI and DI point to the two character strings, CX stores the count and the direction flag is cleared for auto incrementing the address pointers. REPE is used as the prefix for comparing the string bytes. REPE CMPSB means that the corresponding string bytes of the source and destination are compared as long as they are equal. Equality also implies that the Zero flag is set. The comparison is stopped as soon as an inequality is encountered. One of the following two conditions cause the comparison loop to be exited:
- An inequality is seen i.e., the Zero flag gets reset.
- Normal exiting when CX = 0.
In the example, the two character strings are shown to be different, which causes the message ‘NOT SAME’ to be printed. If WRD1 and WRD2 are the same, the message ‘SAME’ is printed.
4.1.3 | The SCAS Instruction
The SCAS instruction scans a byte or word string to ascertain the presence of a specific byte or word. This specific data is loaded into AL or AX. The string which is to be scanned has to be in the extra segment and is to be pointed by DI. This is mandatory and cannot be overridden. Example 4.3 illustrates a typical scenario for the use of this instruction. Suppose that an ASCII string labeled LIST is stored in memory. The program searches the string for the presence of the ASCII character ‘S’. DI addresses the string to be searched. The string to be searched is placed in the Extra segment, by making the ES have the same value as DS. Thus, the extra segment is the same as the data segment. In this example, the SCASB instruction has an REPNE prefix (repeat while not equal) prefix. The REPNE prefix causes the SCASB instruction to repeat until either the CX register reaches 0 or until an equal condition is indicated (ZF = 1) as the outcome of the SCASB instruction’s comparison operation. The scanning of the string continues as long as ‘S’ is not found in the string. If ‘S’ is found, the search is terminated immediately.
In the above, a variation (from the previous examples) has been used. DI has been made to point to the last character of the string, and the direction flag is set, such that the address auto decrements after each scanning operation. Refer to Example 3.18, where the same problem has been done without using string instructions.
4.1.4 | The STOS and LODS Instructions
The STOS instruction is the mnemonic for ‘storing’ a string in memory. As such, we need to define a memory area in which ‘storing’ is to be done. This memory area is defined to be the extra segment, and it is addressed by DI as it is the destination segment. The data to be stored is placed in the AL or AX register. An area in memory can be filled with the required data using this instruction. Example 4.4 illustrates the use of the STOS instruction. Here, AX is loaded with 0001, and this is moved to a location named AREA, which has been defined as an array of 50 words. This array is filled with the word 0001 by the STOSB instruction, using the REP prefix and the counter CX initialized with 50. Since STOS is specified for the extra segment, ES register has to be initialized. Notice that, as done earlier, a data segment is first defined (by the.DATA directive) and the DS register content is then moved to ES. Note also that the memory AREA is addressed using the DI register. SI register cannot be used.
It is obvious that the STOS instruction is used to fill up an area in memory with the same data.
The last of the string instructions is LODS. This is an instruction for ‘loading’. Loading always means the act of taking data from memory and putting it into a register. Here, the source memory is the data segment and the pointer to it is SI. The data segment is the source segment and the destination register is the AL or AX register. There is no sense in using the REP prefix for the LODS instruction as data can be loaded to AL/AX only once.
4.2 | Procedures
In high level languages (i.e., C, C++), you might have come across ‘functions’. A function is a program which does a specific task. When this task is to be done repeatedly, the function is used again and again. When a ‘main’ program considers this as a subsidiary task, the function (or sub routine) is ‘called’ whenever its service is required. This is applicable to assembly language programming also. Borrowing Intel’s terminology, we shall, however, call it a ‘procedure’.
Figure 4.1 shows two cases. One is the case of a main program calling many different procedures. The second is a program calling the same procedure repeatedly. Thus, there is a main program which can call a procedure anywhere in its body. The former is the ‘calling program’ and the latter is the ‘called program’. The main program is in a particular code segment and the procedure may be written in the same code segment (in which case it is a ‘near’ procedure), or it may be in a different segment (‘far’ procedure).
Figure 4.1 | a Main program calling different procedures b Main program calling the same procedure repeatedly
Let us now see the sequence of actions taken by a processor when using a procedure. For now, let us assume that the procedure is a ‘near’ procedure. In the course of the action of execution of the main program, a CALL instruction is encountered. This signals that a procedure written elsewhere has to be executed. This is essentially a branching operation, as the normal program execution sequence is disturbed. At the time the CALL instruction is being executed, the IP (instruction pointer) will be pointing to the next instruction in the main program. The steps taken by the processor automatically are:
- It saves the current IP content on the stack (this is the ‘return’ address for coming back to the main program after executing the procedure).
- The CALL destination (specified in the CALL instruction) will be the address of the procedure. The IP is now loaded with this address and execution proceeds from that location.
- The procedure is executed until a RET (return) instruction in the procedure is encountered.
- Then, the old value of the IP is retrieved from the stack and control returns to the main program at the return address.
Figure 4.2 shows a CALL encountered in a main program. Then, the procedure MULT is taken up for execution. At the end of the procedure, there is the RET instruction, executing which causes control to return to the main program. The instruction after the CALL instruction will now be taken up.
Figure 4.2 | Call and return
4.2.1 | Writing a Procedure
Like any assembler, MASM also has specifications for writing a procedure. The procedure should begin with the procedure name followed by the directive PROC. We also use the directives NEAR or FAR which specify the ‘type’ of the procedure. The procedure should end with the procedure name and ENDP. Example 4.5 shows a program which enters 10 single digit numbers through the keyboard, finds their squares and stores the squares in memory. Only byte locations have been allocated in memory for the squares as the square of the highest single digit number is 81, which will fit into a byte space. In the procedure, the register assigned for the product is AX. However, the product will occupy only AL. Hence, only the value in AL is moved to memory.
Now, let us examine the salient features of this program.
- A procedure named SQUARE is used to calculate the squares.
- Entering data from the keyboard and storing the squares in memory is done in the main program.
- The procedure is in the same code segment. Hence, it is designated as near.
- The procedure ends with a RET (return) instruction which takes control back to the main program.
- The .EXIT statement is the last statement in the main program. We know that it gets converted to the instructions for returning control to DOS. Thus, it is the last instruction which gets executed.
- The last line in the code is the END directive which tells the assembler to stop reading.
- We know that the stack segment is necessary for the working of a procedure. For now, we are not defining any stack segment but using the stack defined automatically by DOS. When we need more stack size, we will define our own stack.
In Example 4.5, the square of the numbers has been calculated and saved in memory. It may be interesting to display them on the monitor. This can be done by referring to Example 3.22. The program for converting any hexadecimal number to binary and displaying can be made another procedure and called here.
4.2.2 | Call and Return Instructions
Now that we have seen the general format and structure of a program with procedures, let us examine the CALL instruction in greater detail. This instruction is obviously a ‘branch’ instruction because it causes control to move to a different address (called the target or destination), which may be in the same or a different code segment.
126.96.36.199 | Intrasegment or ‘Near’ Call
i) Direct CALL
Usage: CALL label (see Fig. 4.3).
Figure 4.3 | Format of the direct near CALL instruction
The direct call is like a direct jump instruction and is three bytes long. It is relative and the destination can be −32,768 bytes to +32,767 bytes from the address of the instruction following the call (this will be the current content of the IP). This means that the offset can be a 16-bit signed number. When this call is executed, the new value of IP = old IP + offset, where the second and third bytes of the instruction give the offset. The assembler must calculate the offset during its first pass. Example 4.5 uses this type of the call instruction – the direct near call.
CALL NEW_WAY ;using the label, the assembler calculates the offset
CALL MULTI ;calls a procedure named MULTI
ii) Indirect CALL
Usage: CALL reg16, CALL [reg16]
In this case, the destination is specified in a 16-bit register or in a memory location pointed by a register. This is not a ‘relative’ call. The content of the referred register or memory location is loaded into IP for using the procedure.
The following program illustrates the use of an indirect near call. Example 4.6 illustrates the use of the CALL register instruction to call procedures that begin at offset addresses ENT and DISP. (These calls could also call the procedures directly as CALL ENT and CALL DISP.) The offset address ENT is placed in BX and DISP in SI, and then the CALL BX and CALL SI instructions call these procedures. The ENT procedure allows keyboard entry with echo. If an upper case letter is entered by ENT, the calling program converts into lowercase and the DISP procedure displays the corresponding lowercase letter.
188.8.131.52 | Intersegment or Far Call
Direct Far Call
A far call is an intersegment call, which means that the destination address is in a different code segment. This will be a 5-byte instruction, the first byte being the opcode, the second and third bytes being the new value of IP and the fourth and fifth being the new values of CS. This is not a relative call. When the procedure is called, the IP and CS values are replaced by the corresponding values in the call instruction as shown in the Fig. 4.4.
Figure 4.4 | Format of the far jump instruction
Indirect Far Call
For an indirect call, the destination address is not in the instruction – rather, it is in a register or memory. For a far call, four bytes are needed to specify a destination. Obviously, a register cannot specify it. Hence, the four bytes needed to specify a destination are stored in memory and pointed by a register. As an example, CALL DWORD PTR [SI] can be a far call instruction. [SI] and [SI + 1] gives the new value of IP, and [SI + 2] and [SI + 3] gives the new value of CS.
Far calls and procedures are specified using the ‘far’ directive when defining the procedure. In Chapter 5, we will see how the EXTRN directive is used in this context.
4.2.3 | The RET Instruction
i) Usage: RET
When a procedure is called, the current value of IP is pushed on to the stack. This naturally means that when the procedure has ended, control should go back to the main program at the point where it had branched off. This means that the value of IP (and CS for a ‘far’ call) will have to be retrieved. This is done by the ‘return’ instruction which is the last instruction in the procedure. The execution of RET causes the return address to be popped from the stack to IP or IP and CS. (Whenever a procedure is defined, it is known whether it is a far or near procedure. From this, it is determined whether the stack has saved just the old IP value or both IP and CS.)
ii) Usage: RET n
This is another form of the RET instruction. This adds the number ‘n’ to the stack pointer (SP) after the return address has been popped off the stack on return from a procedure. We will see the use of this in Example 4.10.
4.2.4 | The Use of the Stack in Procedure Calls
The stack is used in procedure calls for saving the return address. There is another context when a stack is necessary. The main program may be using a number of general-purpose registers. Since the number of registers is limited, what happens if the procedure too needs a few of those registers? If the procedure uses the same registers, obviously their content will be changed. To avoid this problem, the contents of these registers (including the flag register) may be pushed on to the stack before calling the procedure and popped back from stack on returning from the procedure. This allows both the main program as well as the procedure the use of the same registers without losing their content. It is up to the programmer to write these push and pop instructions (either in the main program or in the procedure). The only point to remember would be to push and pop in reverse order. However, now MASM 6.x has a construct ‘USES’ which automatically pushes the specified registers onto the stack. The popping will be automatic on returning from the procedure. Example 4.7a illustrates this.
Now see Example 4.7b. The main program is not shown. What is illustrated is that it uses registers DX and CX. These registers are used in the procedure MULTI as well. The USES construct at the start of the procedure will be converted to PUSH DX and PUSH CX when the procedure is called and to POP CX and POP DX when returning from the procedure. This can be verified by ‘debugging’ the program or by using the directive.list all. Then, the list file will show the listing of the procedure as shown below. Note the push and pop DX and CX which we have not written specifically in the program but have been caused by the USES construct.
4.2.5 | Passing Parameters To and From Procedures
When procedures are called, they have to be given data on which to work on, and then these procedures will have to return results to the calling program. There are various ways of doing this. One way would be to place it in registers which can be accessed by the main program as well as the procedure. Another way would be to place data in memory and access it by name or by using pointers. Still another method is to use the stack for this. Data or addresses made available to both the calling and called programs are called ‘parameters’ and now we will discuss various ways of ‘passing parameters’ to and from procedures.
184.108.40.206 | Passing Parameters Through Registers
In this case, data is placed in registers by the main program, and these registers are used by the procedure. Example 4.8 is a program which calculates and places in memory the Nth term of an arithmetic progression. The formula for the Nth term of an A.P. is A + (N - 1) D, where A is the first term and D is the common difference. Here, the values of A, N and D are placed in the data segment from where the main program takes them, loads them into registers and passes the values to the procedure. The procedure does the computation and passes the result to the main program through register BX.
220.127.116.11 | Passing Parameters Through Memory
In Example 4.9, the data which is used by the procedure is accessed from memory through the pointer register BX, and data is put back in memory in the same way. Example 4.9 is a program for arranging a set of numbers (stored in memory) in descending order. The method involves comparing numbers pair-wise and repeating this N - 1 times if there are N numbers to be sorted. The steps are:
- Suppose the numbers are 6, 8, 34, 0 and 67. The first two numbers are compared. If the first number is greater than the second, no change is made to the ordering. Otherwise, exchange them such that the bigger number comes first in the pair. For our set, now the ordering becomes 8, 6, 34, 0 and 67.
- Next, the second and third numbers (i.e., 6 and 34) are compared. The new ordering is 8, 34, 6, 0 and 67.
- This is repeated N - 1 times, so that due to pair-wise ordering, the bigger number comes first in the new ordering of pairs. In Example 4.9, the procedure one_set does this.
- This sequence of steps is repeated N - 1 times, such that we get all the numbers sorted in descending order, replacing the unsorted array.
In the above program, nine numbers (bytes) are stored in the data segment and sorted. Any number of bytes can be sorted this way, limited only by the maximum value of N. For sorting in ascending order, only one instruction in the procedure need be changed (which one?). After sorting is done, if we write a procedure for converting hexadecimal numbers to ASCII, the sorted numbers can be displayed as well.
18.104.22.168 | Passing Parameters Through the Stack
There may be reasons where the stack, which is an area in the memory, can be used to store data. The procedure can take data from the stack for its computation. Example 4.10 is a simple example which also elaborates how the register BP is used to access data in the stack. Here, four words which are in the data segment are pushed on to the stack by the main program. The procedure accesses the data using the indexing features of BP. Remember that BP is a register associated (by default) with the stack segment and that BP cannot be used without an offset. Example 4.10 calculates (A + B) - (E + D) where A, B, E and D are words stored in data memory. The result is also to be saved in the data segment.
Note We do not use C as a label because ‘C’ is a reserved word in MASM.
Notes The salient features of the program are:
- A stack of 100 bytes has been defined in the beginning.
- The push instructions are in the main program.
- In the procedure, the SP value is copied to BP.
- The data in the stack is accessed by adding offsets to BP.
- The return instruction is RET 8. This causes SP to have the value it had before the four PUSH operations are executed. Otherwise, what would happen is that each time this program is run, the stack pointer decrements by 8 bytes (for the four PUSH operations). Thus, the stack size gets reduced by that much for each procedure call which will finally cause the stack size to reduce to zero, thus causing system crash.
- RET 8 adds 8 to SP after returning from the procedure. In effect, it erases the data that had been pushed on to the stack.
Refer to Fig. 4.5. Let us say that before the first PUSH, the value of SP = 0120H. This figure shows the content of the stack after the four PUSH operations. The address of SP now is 0118H. When the CALL is executed, SP = 0116 H, because the content of IP is pushed on the stack. Before returning from the procedure, SP = 0116 H.RET 8 causes IP to be popped. So, SP = 0118 H. Adding 8 to this value of SP causes SP to become 0120 H again. It is important to be very clear about stack operations if you plan to use the stack. Any wrong alteration of the stack can cause a system crash.
Figure 4.5 | Stack operation for the PUSH and RET 8 instructions
Stack Overflow and Underflow
SP can have a maximum value of FFFFH. For each PUSH operation, the SP value decrements by 2, and in the limiting case, it can go to SP = 0000.
Any PUSH operation beyond this will cause a ‘stack overflow’. This creates a condition when there is no space in the stack for new data.
Stack underflow is the other case when POP operations cause SP to have values beyond the defined top of stack.
4.3 | Macros
The name of our assembler is ‘Macro assembler’. As such, we see that it is expected to be able to handle an item called ‘macros’. What is a macro? It is like an opcode – when used, it executes. A macro, when called by name, executes the instructions that are listed under that name. Thus, essentially, a macro is a short hand notation for a number of instruction lines. It essentially makes assembly language coding more readable and helps to avoid repetitive coding.
Usually, a macro, like a procedure, is defined for performing a specific function. However, the ‘overheads’ involved in invoking a procedure are not incurred here. A procedure call causes pushing and popping of addresses/data in stack. A macro when invoked just expands the code by putting in all the instructions corresponding to the called macro. It does not have to ‘call’ and ‘return’. Thus, when we write our code with macros, it may look small, but when assembling the code, each macro is replaced by the full set of instructions it consists of. Thus, we can say that macros execute faster but the assembled code takes more memory. This is because a procedure is written only once in memory, but the macro statements are written as part of the code every time the macro is invoked.
4.3.1 | Writing a Macro
Whenever we have a set of code lines that we may use frequently, it could be convenient to make a macro out of it. A macro has the following format:
Let us think of a simple task that could be defined as a macro. If we frequently want to enter data through the keyboard, it could be written as a macro.
This macro does not have parameters to be passed to it. Another similar macro is for displaying a character as shown below.
When writing programs using macros, make sure the macros are defined before they are used. Example 4.11 is a simple illustration of these principles.
When we run the above program, we get the pressed key displayed twice. Why?
Now, let us define and use a macro using parameters.
The above is a macro which displays the string whose address is loaded into DX. The parameter is ‘STRINGDAT’. It is also called a dummy variable. When the macro is invoked, the actual name of the string to be displayed may be used. Example 4.12 illustrates these ideas.
In the above program, the DISPLAY macro is first defined. Then, the macro is invoked twice with two different parameters MESG1 and MESG2. Note that the macro not only displays a string but also has instructions for displaying new line and carriage return. This ensures that after displaying one line the cursor moves to the left side of the next line. Thus, in Example 4.12, the two messages are displayed on separate lines.
4.3.2 | Using the ‘Local’ Directive in Macros
Whenever macros use labels, they should be declared as local to the macro. Otherwise, if the same macro is used many times, they will create assembly errors, as the label will correspond to different addresses each time. So, just after the macro name is declared, the labels must be written preceded by the word ‘LOCAL’. Any number of labels can be declared in this way.
It would be a good idea for you to examine the list file of the following program to see how the label ‘AGAIN’ is being managed.
Example 4.13 shows such a case. Two macros have been defined – STAR and NEW. The former contains a label AGAIN which has been declared as LOCAL. This macro causes the character ‘*’ to be printed on a line N times. N is a parameter whose value is passed to this macro, when called. The macro NEW just moves the cursor to the left side of the next line. The output of Example 4.13 is as shown below.
4.4 | Number Format Conversions
We know that computers do all their calculations using binary arithmetic, but we are accustomed to doing calculations in decimal form. We see numbers printed out in the decimal form and enter data using the keyboard as decimal numbers. Our conclusion obviously is that though arithmetic data is processed mostly in binary form by computers, there are methods to convert binary data to forms better suited for display and understanding. Also, if we want to process numeric data in a format other than binary, that should also be possible.
As such, let us examine some of the number formats frequently used. Two of the most widely used formats are BCD and ASCII. BCD is ‘binary coded decimal’ – two versions of which are ‘packed BCD’ and unpacked BCD. As an example of the former, a decimal number 56 (say) is written as four bit binary packed in a single byte as 0101 0110. Unpacked BCD means using a whole byte to represent a decimal number. For example, 56 is written as two bytes – 0000 0101(5) 0000 0110(6). Another popular and very important number format is the ASCII format. It represents a decimal digit in a byte. Thus, 6 is 0011 0110 or 36H. The decimal digits from 0 to 9 have their ASCII representation as 30H, 31H … 39H.
Some useful conversions are between the following different number formats:
- Packed BCD to unpacked BCD.
- Unpacked BCD to packed BCD.
- Unpacked BCD to ASCII.
- Binary to ASCII.
- ASCII to binary.
Note Number format conversions have been discussed in detail in Chapter 0. If you have doubts regarding such conversions, refer to Section 0.6.
4.4.1 | Packed BCD to Unpacked BCD Conversion
This has been done in Example 3.25 for a 2-digit packed BCD number. The exact method of conversion is detailed there along with the example. We will now see how an eight-byte packed BCD is unpacked, converted to ASCII and displayed.
In Example 4.14, we see that an eight-digit decimal number is represented in packed BCD, such that each decimal digit has a four-bit representation. This is stored as a double word (four bytes). However, when accessing it for processing, one byte each of this double word is moved to AL and processed. The steps are:
- A procedure BCD_TO converts packed BCD to unpacked BCD.
- This procedure saves CX and AX as they are used in the procedure.
- To convert to ASCII, two unpacked BCD bytes are put in a word and logically ORed with 3030H.
- After the conversion of the 8 bytes is over, we find that the ASCII values are in memory with the most significant digit in the highest memory pointed by BX. The instructions from the label STRT onwards are for displaying the ASCII digits.
Converting from ASCII to packed BCD is just a reverse of all these processes.
4.4.2 | BCD Calculations
DAA – Decimal adjust AL after addition.
For BCD, none of the digits should have a value greater than 9, but we know that a nibble can contain a value of 0FH. This causes problems when BCD numbers are added. To sort out this problem, there is a specific instruction DAA which stands for ‘Decimal Adjust Accumulator’. This assumes that the sum of the BCD addition is in AL.
The need for this instruction is because direct addition of BCD numbers causes errors, and a correction is usually needed. The details of the correction done are discussed in Section 0.7.2. Now, see the following example which adds two multi byte BCD numbers to get the result in BCD itself. The two BCD numbers are stored as double words (DD).
Note When they are stored thus, the lowest byte of the number will be in the lowest address. Now, BX and SI point to the BCD numbers to be added. For adding, only one byte each of the numbers is accessed and added. The sum is stored in the byte location SUM. Here too, the LSB of the sum will be in the lowest address.
On adding 4 BCD bytes, there is a possibility of a carry, which corresponds to the 5th BCD byte of the sum. To preserve this final carry, AH register is used as shown. The sum of the given two numbers is 0139559823.
DAS – Decimal adjust AL after subtraction.
For BCD subtraction, there is an instruction DAS which operates on the data in AL. Refer Section 0.7.5 for the intricacies of BCD subtraction. DAS is used (like DAA) to make the necessary corrections. The use of this instruction is left to you.
4.5 | Ascii Operations
The processor has a number of instructions relating to the processing of ASCII numbers.
4.5.1 | Ascii Addition
AAA – ASCII Adjust AL after addition
This instruction is used after addition of two ASCII numbers. It checks the lower nibble of AL for one of the two conditions:
- whether it is within A and F.
- whether the auxiliary carry flag (AF) is set.
Depending on this, it performs the required corrections. See the three cases shown.
Add ‘5’ and ‘4’, get the sum in AL.
i) Case 1
In this case, the lower nibble is not within A and F, and the AC flag is not set, i.e., AF = 0. Then, the instruction just resorts to making the upper nibble of AL to 0. Thus, if the AAA instruction is used after the addition, the sum becomes 09, i.e., 0000 1001.To convert it back to ASCII, OR it with 30H.
We get 39H in AL, which is the ASCII code of 9. Also, AH = 0.
Add ‘7’ and ‘6’, get the sum in AL.
In this case, the right most nibble is greater than 9 – within A and F – and it is no longer representing a decimal digit. Hence, 6 is added to the right most nibble (like in BCD correction, the number 6 is added because that is the difference between decimal and hexadecimal); it also clears the upper nibble of AL, sets CF and AF, and adds 1 to AH. Thus, if the AAA instruction is used after the addition, the sum in AL is now 03 and AH = 01 (if AH had been cleared earlier).
The AX register will contain 3133H after this, which is the ASCII value of 13. These facts must be taken into consideration when adding multibyte ASCII numbers.
Add ‘9’and ‘8’, get the sum in AL.
Here, the right-most nibble is not within A and F, but AF = 1. Hence, 6 is added to the right most nibble, and it also clears the upper nibble of AL, sets CF and AF, and adds 1 to AH. Thus, if the AAA instruction is used after the addition, AL = 7, AH = 1, CF = 1 and AF = 1. In all cases, AAA clears the upper nibble of AL. These facts must be taken into consideration when adding multibyte ASCII numbers.
4.5.2 | Ascii Subtraction
AAS – ASCII adjust AL after subtraction.
This function is similar to the AAA instruction. After the subtraction of two ASCII numbers, AAS checks the lower nibble of AL. If this number has a value between A and F, or if the AF flag is set, 6 is subtracted from AL. Also, 1 is subtracted from AH, and AF and CF are set. In all cases, the upper nibble of AL is cleared.
Subtract ‘5’ from ‘8’.
If AAS is used after this subtraction, the content AL is 03.
Subtract ‘8’ from ‘5’.
After AAS, AH = FFH, AL = 07.
Actually, the answer should be −3 but we get FF07, which is the ten’s complement 10 − 3 = 7.
Let us subtract ‘4’ from ‘15’. We get the ASCII value of ‘11’ in the AX register with the following program.
Now, it is up to you to find out what happens when ‘9’ is subtracted from ‘4’. Also, note that the instructions AAA and AAS can be used after adding or subtracting unpacked BCD numbers as well.
4.5.3 | Multiplication and Division
AAM – ASCII adjust AX after multiplication.
For multiplication and division of ASCII numbers, we need to convert them to unpacked BCD. Let us deal with ASCII multiplication first. In the following program, the numbers to be multiplied are 6 and 9. The product has to be 54. The ASCII numbers are first unpacked and multiplied. After that, the AAM instruction is used. This causes the product to be in the unpacked BCD format. If the result of multiplication (MUL BL) is compared with the result of AAM, it is obvious that AAM accomplishes the conversion by dividing AX by 10. Following this, if ORing with 3030H is done, the ASCII value of the product is available in AX. In the program, this value is copied to CX and displayed using the DISP macro.
Note The most significant digit is displayed first.
AAD – ASCII adjust AX before division.
This instruction works by converting the unpacked BCD in AX to binary (hex) before division. It multiplies AH by 10, adds the product to AL and clears AH. This is the hex representation of the BCD number in AX. After division, we find the quotient in AL and the remainder in AH. See the following program segment.
4.6 | Conversions for Computations and Display/Entry
We have seen various number formats so far, but by now it should be obvious that using BCD and ASCII numbers for computation is cumbersome, especially when large numbers are involved. A more convenient format for all arithmetic calculations is the binary format, which is compactly written in hexadecimal. Another point is that, once the computation is done, the natural format for display is the ASCII format. For displaying, we convert the binary number to unpacked BCD and then to ASCII. When entering data through the keyboard also, the ASCII number format is used. All this implies the necessity of converting binary numbers to ASCII and vice versa.
4.6.1 | Converting Ascii Numbers to Binary Form
Let us see how we convert a decimal number (say 6754) to binary form.
This is the algorithm to be used for conversion. Let us write a program which does this. Here, the multiplicands 1, 10, 100 and 1000 are stored in the data segment. The binary value also is finally stored in data memory.
The above program is a direct application of the algorithm mentioned. We get the converted binary value in the word location BINAR. We can extend this for more number of digits too.
4.6.2 | Converting Binary Numbers to ascii Form
The method for this is repetitive division. This has been done in Example 3.22 for a 16-bit binary number.
4.7 | Signed Number Arithmetic
In all our discussions and problems, whenever we dealt with numbers, we assumed the numbers to be unsigned. Now, let us bring signed numbers also into our domain.
Negative numbers are represented in the two’s complement format. When the data length is one byte, the maximum value of decimal numbers it can represent is −128 to +127. With 16 bits, this is from −32,768 to +32,767. If the result of any operation exceeds this word size, the overflow flag is set. This indicates that our result has to be re-interpreted and corrected. The conditions under which the overflow flag is set can be stated as:
- When there is a carry from D6 to D7, or a carry from D7 outwards, but not both. This is for byte operations.
- For word operations, when there is a carry from bit D14 to D15, or a carry from D15 outwards, but not both.
Let us see a few cases of signed number operations.
For the above program in which +34 (22H) and −23 (E9H) are decimal numbers, the calculation in binary form:
Thus, the sum is 0BH (11 in decimal), which is the right sum. CF = 1, OF = 0, SF = 0.
The answer of this computation is 96H (−106), which is correct.
In both the above problems, the overflow flag is found cleared, which indicates that the signed number computation has produced the right result. Now, see the next program.
In the above case, the overflow flag will be set, because there has been an overflow from bit D6 to D7, and the sign bit is set. Even though two positive numbers have been added, the sign bit indicates a negative sum. Thus, AFH has to be thought of as a negative number, which is not correct. The problem is that the sum is +175, which is too large to fit in an 8-bit register. Since the overflow flag is set, it indicates that the register AL has overflowed the size allotted for signed operations in 8-bit format.
Understanding that the problem that has caused the error is one of inadequacy of the size of the destination, it can be solved by extending the bit size of the operands. This can be done by sign extending a byte to a word and a word to a double word. The 8086 has two specific instructions for it.
i) CBW – convert byte to word.
This instruction works on the data in AL. It copies the sign bit (D7) of AL to all the bits of AH, Thus, the byte in AL is extended to a word in AX.
ii) CWD – convert word to double word.
This instruction works on the data in AX. It copies the sign bit (D15) of AX to all the bits of DX. Thus, DX - AX is the new double word. Let us now use these instructions to avoid overflow in the case of signed number operations.
In this case, the overflow flag is not set and hence the sum in AX is considered to be correct. The sign bit also indicates a positive number.
Note In a general case, we are not sure if the result of adding/subtracting signed numbers will exceed the register size allowed for it. To accommodate for a correction as in Example 4.21, the instruction JO (Jump on overflow) can be used. The program can be written in a way that after the addition or subtraction (as the case may be) operation, the JO instruction tests the overflow flag and the CBW or CWD instruction is used only if the overflow flag is found to be set. Then, re-computation using the extended word or double word can be done.
4.7.1 | Comparison of Signed Numbers
Comparison of two unsigned numbers is based on their numerical value only, and the sign does not come into the picture. For example, 7 is greater than 3 numerically. However, when these numbers are signed, the picture changes. Think of the numbers −7 and −3. We now say that −3 is greater than −7. With this picture, we use the terms ‘above’ and ‘below’ for unsigned numbers, and ‘greater than’ and ‘less than’ for signed numbers. The mnemonics used and the flags used also are different.
In Chapter 3, Table 3.2 lists out a number of conditional jump instructions catering to unsigned number operations. Now, let us have a look at the jump instructions for signed number operations. See Table 4.2.
Table 4.2 | Conditional Jump Instructions Catering to Signed Numbers
The list in Table 4.2 shows the conditions of the sign, overflow and zero flags and undoubtedly causes a certain amount of confusion. Let us test a program for the various situations possible and check the condition of these flags.
The result of each comparison of the above program will help to clear the confusion.
- When the destination is greater than the source, OF = SF.
- When the destination is less than the source, OF ! = OF.
- When the destination is equal to the source, ZF = 1.
Example 4.23 finds the smallest of three signed numbers and stores it in a memory location. It compares the first number with the second number. If the first number is less, it is compared with the third number. Otherwise, the second number is compared with the third number and finally the smallest number is identified and stored in memory. In this case, the negative number −90 is found to be the smallest number.
4.7.2 | Signed Multiplication and Division
There are two instructions to be used for signed multiplication and division.
All that was discussed about the MUL instruction (Section 3.4.4) holds true in the case of IMUL too. The only difference is that since the operands are signed, the product also is signed. Another point is that when the product is not large enough to use all the registers allocated for it, the sign bit is copied to the upper register (AH or DX as the case may be) and CF = OF = 0.
The signed division operation is similar to unsigned division, as discussed in Section 3.4.5, except that the operands and result are signed. Because of this, if the quotient register is AL, the value of the quotient is to be between −128 and +127. If the quotient register is to be AX, this value is to be between −32,768 and +32,767. If this is violated, a ‘divide overflow’ error message is displayed on the screen.
Now, let us use the signed multiply and divide instructions in a program. Let us compute (XY—Z2)/L and store the quotient alone. The values of X, Y, Z and L are put in the data segment as shown in the program. For multiplication, a procedure named MULT is defined and used.
In this program, the operands are positive as well as negative numbers. Let us make a few observations.
- With the values of X, Y, Z and L as given in the program, the product is −511 (FE01H) and the quotient is +56, which is found in AL as 38H.
- If the divisor is now changed to +9, the quotient is −56, which is found in AL as C8H.
- If the divisor is changed to −3, we get a message divide overflow error and the program execution is terminated. This is because the quotient (+ 170) is too large to fit in AL. Remember that the largest positive number that AL can hold is +127 only.
4.7.3 | Arithmetic Shift
A shift operation that makes a difference in the result depending on the sign of the operand is arithmetic shifting. There are two instructions pertaining to this.
i) SAR destination, count – Shift right arithmetic.
This is similar to the shift right logical (SHR) discussion in Section 3.5.2, but here, when shifting right, the sign bit is filled into the MSB. This instruction can be used to divide a signed number by 2 for one shifting each.
ii) SAL destination, count – Shift left arithmetic.
This is the same as shift left logical (SHL). As shifting left does not involve any sign bit, logical and arithmetic shifting are the same. The two mnemonics (SAL and SHL) mean the same and can be used interchangeably.
4.8 | Programming Using High Level Language Constructs
MASM has become easier and very convenient to use ever since it was appended with a number of high level language constructs. Such constructs are available only for MASM 6.0 and higher versions. They are designated as ‘dot commands’ as they are preceded by a dot. These constructs make MASM a ‘high level assembler’. A few of these constructs are listed below:
- IF, ELSEIF and ELSE … ENDIF
- REPEAT … UNTIL
- WHILE … ENDW
- REPEAT … UNTILCXZ
Except the last one, these constructs function as they do in a high level language (for example, C and C++). For the last construct, the loop repeats until register CX = 0.
Now, we will endeavor to use these constructs in a few interesting programs. Example 4.25 is a simple program which takes in data through the keyboard (without echo) and verifies three conditions.
- Is the value of the number between 0 and 5?
- Is the value of the number between 6 and 9?
- Is a key pressed ‘not a number’?
These three questions are resolved using IF, ELSEIF and ELSE statements and appropriate messages are displayed. The symbol ‘&&’ is used for ‘and’ just as in a high level language.
Let us see what exactly these high level language constructs are. Obviously, they get converted to assembly language statements. We use the .LISTALL directive to find out. Example 4.25b shows the listing for the code segment alone up to the .ENDIF statement.
We find the equivalent assembly code for the high level language constructs, which in this case are compare and jump instructions.
Next, we will do a program using the WHILE … ENDW construct. Example 4.26 shows how the WHILE statement is used to repeat execution of a loop while a condition is satisfied and to exit the loop when the condition no longer holds.
As long as CX is not equal to zero, the loop repeats. The end of the loop is specified with a .ENDW construct. In this program, a string consisting of all the letters of the alphabet, stored in location starting from DAT, is to be reversed and displayed. After the reversed string is stored in location DAT1 onwards, it is appended with a ‘$’ so that it can be displayed with the INT 21H function number 9. The message REVERSED STRING: is displayed before the reversed string is displayed.
The output of the above program is as shown below.
REVERSED STRING: ZYXWVUTSRQPONMLKJIHGFEDCBA
For both the above problems, the high level language constructs are written indented to the left. This is done only to improve the readability of the program.
The above problem can be worked out using the REPEAT … UNTIL construct. The loop is repeated until CX = 0. CX becoming zero is the stopping condition for the loop.
There is also a UNTILCXZ construct available that can be used in this example. The UNTILCXZ instruction uses the CX register as a counter to repeat a loop a fixed number of times. This example can be rewritten using a REPEAT UNTILCXZ loop also.
Now, let us see a case of nested loops. Example 4.27 shows the case of an IF loop nested within a repeat–until loop. In this, a character is to be searched within a string and the number of occurrences of that character in the string is to be found out. To make the program inter-active for the user, messages asking for the character and the string are displayed using DOS INT 21H function 9. Both the character and the string are entered through the keyboard. The string is terminated when the ‘enter’ key is pressed. This corresponds to the ‘carriage return’ character 0DH and this is the termination condition of the repeat until loop.
The output of the Example 4.27 for a string and the character ‘t’, entered through the keyboard, is shown below.
ENTER THE CHARACTER: t
ENTER THE STRING: tyu8iolhttttfeerttt
NUMBER OF OCCURENCES: 8
Now that we have seen the use of the high level language constructs, it is obvious that many of our previous programs can be re-written using these constructs.
KEY POINTS OF THIS CHAPTER
- String instructions are used to simplify coding when bulk data is moved, scanned and compared.
- Then, SI and DI registers are used as pointers to the data segment and extra segment, and the direction flag is used for auto increment/decrement.
- The conditional/unconditional REP prefix is used for repetitive string operations.
- Procedures are subprograms which are ‘called’ by the main program for performing specialized operations.
- Procedures may be intra-segment or inter-segment and this is clarified when defining a procedure by using the ‘near’ or ‘far’ prefix.
- Parameters to and from procedures may be passed via registers, memory or stack.
- Macros are functionally similar to procedures but act in a different way.
- Number format conversions are necessary as BCD, ASCII and binary numbers are used frequently.
- 8086 has specialized instructions for handling ASCII and BCD number operations.
- Signed number arithmetic involves the use of special instructions.
- The overflow flag is very important in signed operations.
- The high level language constructs of MASM have made assembly language programming easier.
- What is the role of the direction flag in string instructions?
- What is the difference in the functioning of the instructions STD and CLD?
- How does the instruction CLD function in the case of the following two instructions?
- REP MOVSW
- Which is the pointer register to be used when using the extra segment in string operations?
- Why does the LODS instruction not use the REP prefix?
- Do the following instructions function differently? In what way?
- REPNE CMPSB
- REPE CMPSB
- For REPNZ CMPSB, what are the conditions under which the loop is exited?
- Distinguish between a near and a far call.
- What is the difference in the functioning of the RET and RET n instructions?
- What is meant by the term ‘parameter passing’ in the context of procedures?
- What is the benefit of using macros?
- Compare procedures and macros.
- How are local variables in a macro taken care of?
- What is a dummy argument?
- Explain how the DAA instruction functions.
- Explain the action of the following instructions:
- Write down the steps required for the following conversions:
- binary to ASCII,
- ASCII to binary.
- When is the CWD instruction used?
- Is it justified to call MASM a high level assembler?
- Write a program to move 100 words from ‘FROM’ to ‘TO’ which are two areas in the data segment separated by a 200-byte space.
- Write a program to scan the name SAHABUDDIN and replace S by B and B by H.
- Store a password in memory. Enter another password through the keyboard and verify if it matches the stored password. The password entered should not be displayed as such, but each letter should be displayed as ‘*’.
- Write a string program for an application which requires the pointer registers to be auto decremented.
- Write a program to search for a word in a block of N words.
- Write a program which contains the following macros:
- for calculating the Fibonacci series for N,
- for entering the value of N,
- for displaying the numbers.
- Do the above problem using procedures.
- Display the factorial of three numbers. Solve the problem using macros as well as procedures.
- Obtain the list file of the above program in both cases and observe the differences.
- Enter a string of characters through the keyboard. Save it in memory. Verify if the string is a palindrome.
- Using macros/procedures, write a program to add 2 BCD numbers entered through the keyboard and display their sum.
- Write a program where nested macros are used.
- There are 16 ASCII numbers stored in consecutive locations in memory. Convert this to an eight-bit packed BCD number.
- Write a program to add two ten-byte BCD numbers. The sum is to be displayed.
- Two BCD numbers 4093H and 2986H are stored in memory. Subtract the second number from the first. Use the DAS instruction.
- Write a program to add and display two ASCII numbers ‘5678’ and ‘3498’.
- Divide ‘90’ by ‘9’. Use AAD and necessary programming steps.
- Divide ‘3476’ by ‘5’. Display the quotient alone.
- Use AAM to help in converting a 16-bit binary number in AX to a four-digit ASCII character. Hint Divide the content of AX-DX by 100, then use AAM.
- Write a program which checks the number of ASCII digits present in the given number, and converts it to the corresponding binary equivalent.
- Write a program to convert binary numbers to ASCII format for display. The program should have the following features:
- Display the number.
- Convert the ASCII number and store it in memory.
- A procedure, DISPLAY should access the memory to display the numbers.
- There are 10 signed bytes stored in memory, some of which are negative and some of which are positive. Find the largest number of the lot.
- Re-write Example 4.26 using REPEAT … UNTIL and REPEAT … UNTILCXZ constructs.
- Re-write Example 4.27 using .CONTINUE and .BREAK constructs along with .IF and .REPEAT constructs.