String Operations

Computers are frequently used to manipulate characters strings as well as numeric data. In data processing applications names, addresses, and so forth must be stored and sometimes rearranged. Text editor and word processor programs must be capable of searching for and moving strings of characters. An assembler must be able to separate assembly language statement elements, identifying those that are reserved mnemonics. Even when computation is primarily numerical, it is often necessary to convert either a character string to an internal numerical format when a number is entered at the keyboard or an internal format to a character string for display purposes.

An 80x86 microprocessor has instructions to manipulate character strings. The same instructions can manipulate strings of doublewords or words. This chapter covers 80x86 instructions that are used to handle strings, with emphasis on character strings. A variety of applications are given, including procedures that are similar to those in some high-level languages and the procedure called by the dtoa macro.

Using String Instructions

Five 80x86 instructions are designed for string manipulation: movs (move string), cmps (compare string), scas (scan string), stos (store string), and lods (load string). The movs instruction is used to copy a string from one memory location to another. The cmps instruction is designed to compare the contents of two strings. The scas instruction can be used to search a string for one particular value. The stos instruction can store a new value in some position of a string. Finally, the lods instruction copies a value out of some position of a string.

A string in the 80x86 architecture refers to a contiguous collection of bytes, words, or doublewords in memory. Strings are commonly defined in a program's data segment using such directives as

 response BYTE 20 DUP (?)
 label1 BYTE 'The results are ', 0
 wordString WORD 50 DUP (?)
 arrayD DWORD 60 DUP (0)

Note that strings and arrays are actually the same except for the way we look at them.

Each string instruction applies to a source string, a destination string, or both. The bytes, words, or doublewords of these strings are processed one at a time by the string instruction. Register indirect addressing is used to locate the individual byte, word, or doubleword elements. The 80x86 instructions access elements of the source string using the address in the source index register ESI. Elements in the destination string are accessed using the address in the destination index register EDI. If you program using a segmented memory model, then you also must know that the source element is in the data segment (at address DS:ESI) while the destination element is in the extra segment (at address ES:EDI). With flat memory model programming, both segment registers contain the same segment number and no distinction between segments.

Since the source and destination addresses of string elements are always given by ESI and EDI, respectively, no operands are needed to identify these locations. Without any operand, however, the assembler cannot tell the size of the string element to be used. For example, just movs by itself could say to move a byte, a word, or a doubleword. The Microsoft Macro Assembler offers two ways around this dilemma. The first method is to use destination and source operands; these are ignored except that MASM notes their type (both operands must be the same type) and uses that element size. The second method is to use special versions of the mnemonics that define the element size-instructions that operate on bytes use a b suffix, word string instructions use a w suffix, and doubleword string instructions use a d suffix. For example, movsb is used to move byte strings, movsw is used to move word strings, and movsd is used to move doubleword strings. Any of these instructions assemble as a movs and none uses an operand since the assembler knows the element size from the mnemonic. This book will use mnemonics with b, w, and d suffixes rather than operands for string instructions.

Although a string instruction operates on only one string element at a time, it always gets ready to operate on the next element. It does this by changing the source index register ESI and/or the destination index register EDI to contain the address of the next element of the string(s). When byte-size elements are being used, the index registers are changed by one; for words, ESI and EDI are changed by two; and for doublewords, the registers are changed by four. The 80x86 can move either forward through a string, from lower to higher addresses, or backward, from higher to lower addresses. The movement direction is determined by the value of the direction flag DF, bit 10 of the EFLAGS register. If DF is set to 1, then the addresses in ESI and EDI are decremented by string instructions, causing right to left string operations. If DF is clear (0), then the values in ESI and EDI are incremented by string instructions, so that strings are processed left to right.

The 80x86 has two instructions whose sole purpose is to reset or set the direction flag DF. The cld instruction clears DF to 0 so that ESI and EDI are incremented by string instructions and strings are processed left to right. The std instruction sets DF to 1 so that strings are processed backward. Neither instruction affects any flag other than DF. Technical data about these instructions appear in Fig. 7.1.

 

Clock Cycles

   

Instruction

386

486

Pentium

Number of Bytes

Opcode

cld

2

2

2

1

FC

std

2

2

2

1

FD


Figure 7.1: cld and std instructions

Finally it is time to present all the details about a string instruction. The move string instruction movs transfers one string element (byte, word, or doubleword) from a source string to a destination string. The source element at address DS:ESI is copied to address ES:EDI. After the string element is copied, both index registers are changed by the element size (1, 2, or 4), incremented if the direction flag DF is 0 or decremented if DF is 1. The movs instruction does not affect any flag. It comes in movsb, movsw, and movsd versions; Fig. 7.2 gives information about each form.

   

Clock Cycles

   

Mnemonic

Element size

386

486

Pentium

Number of Bytes

Opcode

movsb

byte

7

7

4

1

A4

movsw

word

7

7

4

1

A5

movsd

doubleword

7

7

4

1

A5


Figure 7.2: movs instructions (use ESI and EDI)

Figure 7.3 gives an example of a program that uses the movs instruction. The important part of the example is the procedure strcopy. This procedure has two parameters passed on the stack, which give the destination and source addresses of byte or character strings. The source string is assumed to be null-terminated. Procedure strcopy produces an exact copy of the source string at the destination location, terminating the destination string by a null byte.

; test of "strcopy" procedure
; author: R. Detmer revised: 10/97

.386
.MODEL FLAT
ExitProcess PROTO NEAR32 stdcall, dwExitCode:DWORD
INCLUDE io.h ; header file for input/output
cr equ 0dh ; carriage return character
Lf equ 0ah ; line feed

.STACK 4096 ; reserve 4096-byte stack

.DATA ; reserve storage for data
prompt BYTE cr, Lf, "Original string? ",0
stringIn BYTE 80 DUP (?)
display BYTE cr, Lf, "Your string was...", cr, Lf
stringOut BYTE 80 DUP (?)

.CODE
_start: output prompt ; ask for string
 input stringIn, 80 ; get source string
 lea eax, stringOut ; destination address
 push eax ; first parameter
 lea eax, stringIn ; source
 push eax ; second parameter
 call strcopy ; call string copy procedure
 output display ; print result
 INVOKE ExitProcess, 0 ; exit with return code 0

PUBLIC _start ; make entry point public

strcopy PROC NEAR32

; Procedure to copy string until null byte in source is copied.
; It is assumed that destination location is long enough for copy.

; Parameters are passed on the stack:
; (1) address of destination
; (2) address of source
 push ebp ;save base pointer
 mov ebp,esp ;copy stack pointer

 push edi ;save registers and flags
 push esi
 pushf

 mov esi,[ebp+8] ;initial source address
 mov edi,[ebp+12] ;destination
 cld ;clear direction flag
whileNoNull:
 cmp BYTE PTR [esi],0 ;null source byte?
 je endWhileNoNull ;stop copying if null
 movsb ;copy one byte
 jmp whileNoNull ;go check next byte
endWhileNoNull:
 mov BYTE PTR [edi],0 ;terminate destination string

 popf ;restore flags and registers

 pop esi
 pop edi
 pop ebp
 ret 8 ;exit procedure, discarding parameters
strcopy ENDP

 END


Figure 7.3: String copy program

The procedure only uses registers ESI and EDI. It saves each of these and the flag register on the stack so that the procedure will return with them unchanged. The index registers ESI and EDI must be initialized to the addresses of the first string bytes to be processed. The values for ESI and EDI are the arguments that were pushed on the stack. The direction flag is cleared for left-to-right copying.

After initialization, the procedure executes the following pseudocode design:

while next source byte is not null
 copy source byte to destination;
 increment source index;
 increment destination index;
end while;
put null byte at end of destination string;

To check whether the next source byte is null, the statement

 cmp BYTE PTR [esi],0 ; null source byte?

is used. Recall that the notation [esi] indicates register indirect addressing, so that the element at the address in ESI is used, that is, the current byte of the source string. The operator BYTE PTR is necessary since MASM cannot tell from the operands [esi] and 0 whether byte, word, or doubleword comparison is needed. Copying the source byte and incrementing both index registers is accomplished by the movsb instruction. Finally,

 mov BYTE PTR [edi],0 ; terminate destination string

serves to move a null byte to the end of the destination string since EDI was incremented after the last byte of the source was copied to the destination. Again, the operator BYTE PTR tells MASM that the destination is a byte rather than a word or doubleword.

The program to test strcopy simply inputs a string from the keyboard, calls strcopy to copy it somewhere else, and finally displays the string copy. The most interesting part of the code is the collection of instructions needed to call the procedure. The arguments are not removed from the stack since the procedure does that job.

Normally the source string for a movs instruction does not overlap the destination string. However, occasionally this is useful. Suppose that you want to initialize a 80 character-long string at starSlash with the pattern */, repeated 40 times. The following code can do this task.

 starSlash BYTE 80 DUP (?)
 ...
 mov starSlash, '*' ; first *
 mov starSlash+1, '/' ; first /
 lea esi, starSlash ; source address
 lea edi, starSlash+2 ; destination
 cld ; process left to right
 mov ecx, 78 ; characters to copy
 forCount: movsb ; copy next character
 loop forCount ; repeat

In this example, the first time movsb is executed, a * from the first string position is copied to the third position. The next iteration, a / is copied from the second to the fourth position. The third time, a * is copied from the third to the fifth position, and so on. The next section introduces an easier way to repeat a movs instruction.

Exercises 7.1

  1. What will be the output of the following program?

    .386
    .MODEL FLAT
    ExitProcess PROTO NEAR32 stdcall, dwExitCode:DWORD
    INCLUDE io.h ; header file for input/output
    cr equ 0dh ; carriage return character
    Lf equ 0ah ; line feed
    
    .STACK 4096 ; reserve 4096-byte stack
    .DATA ; global data
    string BYTE 'ABCDEFGHIJ'
     BYTE cr, Lf, 0
    .CODE
    setup1 PROC NEAR32
     lea esi, string ; beginning of string
     lea edi, string+5 ; address of 'F'
     cld ; forward movement
     ret
    setup1 ENDP
    
    _start: call setup1 ; set source, destination, direction
     movsb ; move 4 characters
     movsb
     movsb
     movsb
     output string ; display modified string
     INVOKE ExitProcess, 0 ; exit with return code 0
    PUBLIC _start ; make entry point public
     END
    
  2. Repeat Problem 1, replacing the procedure setup1 by

    setup2 PROC NEAR32
     lea esi, string ; beginning of string
     lea edi, string+2 ; address of 'C'
     cld ; forward movement
     ret
    setup2 ENDP
    
  3. Repeat Problem 1, replacing the procedure setup1 by

    setup3 PROC NEAR32
     lea esi, string+9 ; end of string
     lea edi, string+4 ; address of 'E'
     std ; backward movement
     ret
    setup3 ENDP
    
  4. Repeat Problem 1, replacing the procedure setup1 by

    setup4 PROC NEAR32
     lea esi, string+9 ; end of string
     lea edi, string+7 ; address of 'H'
     std ; backward movement
     ret
    setup4 ENDP
    

Programming Exercises 7.1

  1. Write a program that copies strings read in one at a time from the keyboard into a large storage area for later processing. Specifically, use the input macro to input a string, then copy the string to the first of a 1024 byte block of storage that has been reserved in the data segment. (Recall that the input macro produces a null-terminated string.) Follow the string by a carriage return and a linefeed character in this storage area. Repeat the process with additional strings, copying each subsequent string to the storage area so that it immediately follows the linefeed after the last string. Exit the loop when the first character of the source string is $-do not copy this last string to the storage area. Do, however, place a null byte after the linefeed of the last string in the storage area. Finally, use the output macro to display all the characters in the data area. The result should be the strings that were entered, one per line.

Repeat Prefixes and More String Instructions

Each 80x86 string instruction operates on one string element at a time. However, the 80x86 architecture includes three repeat prefixes that change the string instructions into versions that repeat automatically either for a fixed number of iterations or until some condition is satisfied. The three repeat prefixes actually correspond to two different single-byte codes; these are not themselves instructions, but supplement machine codes for the primitive string instructions, making new instructions.

Figure 7.4 shows two program fragments, each of which copies a fixed number of characters from sourceStr to destStr. The number of characters is loaded into the ECX register from count. The code in part (a) uses a loop. Since the count of characters might be zero, the loop is guarded by a jecxz instruction. The body of the loop uses movsb to copy one character at a time. The loop instruction takes care of counting loop iterations. The program fragment in part (b) is functionally equivalent to the one in part (a). After the count is copied into ECX, it uses the repeat prefix rep with a movsb instruction; the rep movsb instruction does the same thing as the last four lines in part (a).

 lea esi, sourceStr ; source string
 lea edi, destStr ; destination
 cld ; forward movement
 mov ecx, count ; count of characters to copy
 jecxz endCopy ; skip loop if count is zero
 copy: movsb ; move 1 character
 loop copy ; decrement count and continue
 endCopy:

 (a) movsbiterated in a loop


 lea esi, sourceStr ; source string
 lea edi, destStr ; destination
 cld ; forward movement
 mov ecx, count ; count of characters to copy
 rep movsb ; move characters

 (b) repeat prefix with movsb


Figure 7.4: Copying a fixed number of characters of a string

The rep prefix is normally used with the movs instructions and with the stos instruction (discussed below). It causes the following design to be executed:

while count in ECX > 0 loop
 perform primitive instruction;
 decrement ECX by 1;
end while;

Note that this is a while loop. The primitive instruction is not executed at all if ECX contains zero. It is not necessary to guard a repeated string instruction as it often is with an ordinary for loop implemented with the loop instruction.

The other two repeat prefixes are repe, with equivalent mnemonic repz, and repne, which is the same as repnz. The mnemonic repe stands for "repeat while equal" and repz stands for "repeat while zero." Similarly repne and repnz mean "repeat while not equal" and "repeat while not zero," respectively. Each of these repeat prefixes is appropriate for use with the two string instructions cmps and scas, which affect the zero flag ZF.

The names of these mnemonics partially describe their actions. Each instruction works the same as rep, iterating a primitive instruction while ECX is not zero. However, each also examines ZF after the string instruction is executed. The repe and repz continue iterating while ZF=1, as it would be following a comparison where two operands were equal. The repne and repnz continue iterating while ZF=0, as it would be following a comparison where two operands were different. Repeat prefixes themselves do not affect any flag. The three repeat prefixes are summarized in Fig. 7.5. Note that rep and repz (repe) generate exactly the same code.

Mnemonic

Loop while

Number of bytes

Opcode

rep

ECX>0

1

F3

repz/repe

ECX>0 and ZF=1

1

F3

repnz/repne

ECX>0 and ZF=0

1

F2


Figure 7.5: Repeat prefixes

The repz and repnz prefixes do not quite produce true while loops with the conditions shown in Fig. 7.5. The value in ECX is checked prior to the first iteration of the primitive instruction, as it should be with a while loop. However, ZF is not checked until after the primitive instruction is executed. In practice, this is very convenient since the instruction is skipped for a zero count, but the programmer does not have to do anything special to initialize ZF prior to repeated instructions.

Figure 7.6 shows how the repeat prefix rep combines with the movs instructions. In the clock cycles columns, there is a "set up" time plus a time for each iteration. The n in the table represents the number of iterations, so that, for example, a rep movsb takes 33 (13+4×5) clock cycles on a Pentium to move five bytes. (The table entries are not strictly accurate since there are special timings for the 486 and Pentium when n=0 or n=1.)

 

Element

Clock Cycles

Number

 

Mnemonic

size

386

486

Pentium

of bytes

Opcode

rep movsb

byte

7+4n

12+3n

13+4n

2

F3 A4

rep movsw

word

       

F3 A5

rep movsd

doubleword

       

F3 A5


Figure 7.6: rep movs instructions

The cmps instructions, summarized in Fig. 7.7, compare elements of source and destination strings. Chapter 5 explained how a cmp instruction subtracts two operands and sets flags based on the difference. Similarly, cmps subtracts two string elements and sets flags based on the difference; neither operand is changed. If a cmps instruction is used in a loop, it is appropriate to follow cmps by almost any of the conditional jump instructions, depending on the design being implemented.

 

Element

Clock Cycles

Number

 

Mnemonic

size

386

486

Pentium

of bytes

Opcode

cmpsb

byte

10

8

5

1

A6

cmpsw

word

       

A7

cmpsd

doubleword

       

A7

repe cmpsb

byte

5+9n

7+7n

9+4n

2

F3 A6

repe cmpsw

word

       

F3 A7

repe cmpsd

doubleword

       

F3 A7

repne cmpsb

byte

5+9n

7+7n

9+4n

2

F2 A6

repne cmpsw

word

       

F2 A7

repne cmpsd

doubleword

       

F2 A7


Figure 7.7: cmps instructions

Repeat prefixes are often used with cmps instructions. In fact, for the task of finding if two strings are identical, the repe prefix is a perfect companion for cmps. Figure 7.7 summarizes all the cmps instructions, including repeated ones. Again, the timings are not strictly accurate for 486 and Pentium CPUs; for rep cmps there are special timings when n=0.

It is often necessary to search for one string embedded in another. Suppose that the task at hand is to find the position, if any, at which the string at key appears in the string at target. One simple algorithm to do this is

position := 1;
while position < (targetLength - keyLength + 1) loop
 if key matches the substring of target starting at position
 then
 report success;
 exit process;
 end if;
 add 1 to position;
end while;
report failure;

This algorithm checks to see if the key string matches the portion of the target string starting at each possible position. Using 80x86 registers, checking for one match can be done as follows:

ESI := address of key;
EDI := address of target + position - 1;
ECX := length of key;

forever loop
 if ECX = 0 then exit loop; end if;
 compare [ESI] and [EDI] setting ZF;
 increment ESI;
 increment EDI;
 decrement ECX;
 if ZF = 0 then exit loop; end if;
end loop;

if ZF = 1
then
 match was found;
end if;

The forever loop is exactly what is done by the repeated string instruction repe cmpsb. Since the loop is terminated when either ECX = 0 or when ZF = 0, it is necessary to be sure that the last pair of characters compared were the same; this is the reason for the extra if structure at the end of the design. Figure 7.8 shows a complete program that implements this design.

; program to search for one string embedded in another
; author: R. Detmer revised: 10/97

.386
.MODEL FLAT

ExitProcess PROTO NEAR32 stdcall, dwExitCode:DWORD
INCLUDE io.h

cr EQU 0dh ; carriage return character
Lf EQU 0ah ; linefeed character

.STACK 4096 ; reserve 4096-byte stack

.DATA
prompt1 BYTE "String to search? ", 0
prompt2 BYTE cr, Lf, "Key to search for? ", 0
target BYTE 80 DUP (?)
key BYTE 80 DUP (?)
trgtLength DWORD ?
keyLength DWORD ?
lastPosn DWORD ?
failure BYTE cr,Lf,Lf,"The key does not appear in the string.",cr,Lf,0
success BYTE cr,Lf,Lf,'The key appears at position'
position BYTE 11 DUP (?)
 BYTE " in the string.", cr, Lf, 0

PUBLIC _start ; make entry point public
.CODE

_start: output prompt1 ; ask for
 input target,80 ; and input target string
 lea eax, target ; find length of string
 push eax ; length parameter
 call strlen
 mov trgtLength,eax ; save length of target
 output prompt2 ; ask for
 input key,80 ; and input key string
 lea eax, key ; find length of string
 push eax ; length parameter
 call strlen
 mov keyLength,eax ; save length of key

; calculate last position of target to check
 mov eax,trgtLength
 sub eax,keyLength
 inc eax ; trgtLength − keyLength + 1
 mov lastPosn, eax
 cld ; left to right comparison

 mov eax,1 ; starting position
whilePosn: cmp eax,lastPosn ; position <= last_posn?
 jnle endWhilePosn ; exit if past last position
 lea esi,target ; address of target string
 add esi,eax ; add position
 dec esi ; address of position to check
 lea edi,key ; address of key
 mov ecx,keyLength ; number of positions to check
 repe cmpsb ; check
 jz found ; exit on success
 inc eax ; increment position
 jmp whilePosn ; repeat
endWhilePosn:
 output failure ; the search failed
 jmp quit ; exit
found: dtoa position,eax ; convert position to ASCII
 output success ; search succeeded
quit:
 INVOKE ExitProcess, 0 ; exit with return code 0
strlen PROC NEAR32
; find length of string whose address is passed on stack
; length returned in EAX
 push ebp ; establish stack frame
 mov ebp, esp
 pushf ; save flags
 push ebx ; and EBX
 sub eax, eax ; length := 0
 mov ebx, [ebp+8] ; address of string
whileChar: cmp BYTE PTR [ebx], 0 ; null byte?
 je endWhileChar ; exit if so
 inc eax ; increment length
 inc ebx ; point at next character
 jmp whileChar ; repeat
endWhileChar:
 pop ebx ; restore registers and flags
 popf
 pop ebp
 ret 4 ; return, discarding parameter
strlen ENDP

 END


Figure 7.8: String search program

The scan string instruction scas is used to scan a string for the presence or absence of a particular string element. The string that is examined is a destination string; that is, the address of the element being examined is in the destination index register EDI. With a scasb instruction, the element searched for is the byte in the AL register; with a scasw, it is the word in the AX register; and with a scasd, it is the doubleword in the EAX register. The scasb, scasw, and scasd instructions use no operand since the mnemonics tell the element size. Figure 7.9 summarizes the scas instructions; as with the previous repeated instructions, there are special timings for n=0 on the 486 and Pentium.

 

Element

Clock Cycles

Number

 

Mnemonic

size

386

486

Pentium

of bytes

Opcode

scasb

byte

7

6

4

1

AE

scasw

word

       

AF

scasd

doubleword

       

AF

repe scasb

byte

5+8n

7+5n

9+4n

2

F3 AE

repe scasw

word

       

F3 AF

repe scasd

doubleword

       

F3 AF

repne scasb

byte

5+8n

7+5n

9+4n

2

F2 AE

repne scasw

word

       

F2 AF

repne scasd

doubleword

       

F2 AF


Figure 7.9: scas instructions (use EDI)

The program shown in Fig. 7.10 inputs a string and a character and uses repne scasb to locate the position of the first occurrence of the character in the string. It then displays the part of the string from the character to the end. The length of the string is calculated using the strlen procedure that previously appeared in Fig. 7.8; this time we assume that strlen is separately assembled. The lea instruction is used to load the offset of the string to be searched and cld ensures a forward search.

; Program to locate a character within a string.
; The string is displayed from the character to the end.
; author: R. Detmer revised: 10/97

.386
.MODEL FLAT

ExitProcess PROTO NEAR32 stdcall, dwExitCode:DWORD
INCLUDE io.h
EXTRN strlen:NEAR32
PUBLIC _start

cr EQU 0dh ; carriage return character
Lf EQU 0ah ; linefeed character

.STACK 4096 ; reserve 4096-byte stack

.DATA
prompt1 BYTE "String? ", 0
prompt2 BYTE cr, Lf, Lf, "Character? ", 0
string BYTE 80 DUP (?)
char BYTE 5 DUP (?)
label1 BYTE cr, Lf, Lf, "The rest of the string is ---", 0
crlf BYTE cr, Lf, 0

.CODE
_start: output prompt1 ; prompt for string
 input string,80 ; get string
 lea eax, string ; find length of string
 push eax ; length parameter
 call strlen
 inc ecx ; include null in string length
 mov ecx, eax ; save length of string

 output prompt2 ; prompt for character
 input char,5 ; get character
 mov al, char ; character to AL

 lea edi, string ; offset of string
 cld ; forward movement
 repne scasb ; scan while character not found
 dec edi ; back up to null or matching character

 output label1 ; print label
 output [edi] ; output string
 output crlf ; skip to new line

 INVOKE ExitProcess, 0 ; exit with return code 0
 END


Figure 7.10: Program to find character in string

After the search, the destination index EDI will be one greater than desired since a string instruction always increments index registers whether or not flags were set. If the search succeeded, EDI will contain the address of the character following the one that matched with AL, or the address of the character after the end of the string if ECX was decremented to zero. The dec edi instruction takes care of both cases, backing up to the position of the matching character if there was one, or to the null byte at the end of the string otherwise. The string length was incremented so that the null character would be included in the search. The output macro displays the last portion of the string, whose address is in EDI.

The store string instruction stos copies a byte, a word, or a doubleword from AL, AX, or EAX to an element of a destination string. A stos instruction affects no flag, so that when it is repeated with rep, it copies the same value into consecutive positions of a string. For example, the following code will store spaces in the first 30 bytes of string.

 mov ecx,30 ; 30 bytes
 mov al, ' ' ; character to store
 lea edi, string ; address of string
 cld ; forward direction
 rep stosb ; store spaces

Information about the stos instructions is in Fig. 7.11. As with previous repeated string instructions, the 80486 and Pentium have special timings when n=0.

 

Element

Clock Cycles

Number

 

Mnemonic

size

386

486

Pentium

of bytes

Opcode

stosb

byte

4

5

3

1

AA

stosw

word

       

AB

stosd

doubleword

       

AB

rep stosb

byte

5+5n

7+4n

9n

2

F3 A6

rep stosw

word

       

F3 A7

rep stosd

doubleword

       

F3 A7


Figure 7.11: stos instructions (use EDI)

The load string instruction lods is the final string instruction. This instruction copies a source string element to the AL, AX, or EAX register, depending on the string element size. A lods instruction sets no flag. It is possible to use a rep prefix with lods but it is not helpful-all values except for the last string element would be replaced as successive values were copied to the destination register. A lods instruction is useful in a loop set up with other instructions, making it possible to easily process string elements one at a time. The lods instructions are summarized in Fig. 7.12. Repeated versions are not included since they are not used.

 

Element

Clock Cycles

Number

 

Mnemonic

size

386

486

Pentium

of bytes

Opcode

lodsb

byte

5

5

2

1

AC

lodsw

word

       

AD

lodsd

doubleword

       

AD


Figure 7.12: lods instructions (use ESI)

Exercises 7.2

For each exercise below, assume that the data segment contains

source BYTE "brown"
dest BYTE "brine"
  1. Suppose that the following instructions are executed:

    lea esi, source
    lea edi, dest
    cld
    mov ecx, 5
    repne cmpsb
    

    Assuming that ESI starts at 00010000 and EDI starts at 00010005, what will be the values stored in ESI and EDI following the repne cmpsb instruction? What will be stored in ECX?

  2. Suppose that the following instructions are executed:

    lea esi, source
    lea edi, dest
    cld
    mov ecx, 5
    repe cmpsb
    

    Assuming that ESI starts at 00010000 and EDI starts at 00010005, what will be the values stored in ESI and EDI following the repe cmpsb instruction? What will be stored in ECX?

  3. Suppose that the following instructions are executed:

    mov al, 'w'
    lea edi, dest
    cld
    mov ecx, 5
    repe scasb
    

    Assuming that EDI starts at 00010005, what will be the value stored in EDI following the repe scasb instruction? What will be stored in ECX?

  4. Suppose that the following instructions are executed:

    mov al, 'n'
    lea edi, dest
    cld
    mov ecx, 5
    repne scasb
    

    Assuming that EDI starts at 00010005, what will be the value stored in EDI following the repne scasb instruction? What will be stored in ECX?

  5. Suppose that the following instructions are executed:

    mov al, '*'
    lea edi, dest
    cld
    mov ecx, 5
    rep stosb
    

    Assuming that EDI starts at 00010005, what will be the value stored in EDI following the rep stosb instruction? What will be stored in ECX? What will be stored in the destination string?

  6. Suppose that the following instructions are executed:

     lea esi, source
     lea edi, dest
     cld
     mov ecx, 5
    for6: lodsb
     inc al
     stosb
     loop for6
    endFor6:
    

    Assuming that ESI starts at 00010000 and EDI starts at 00010005, what will be the values stored in ESI and EDI following the for loop? What will be stored in ECX? What will be stored in the destination string?

  7. Suppose that the following instructions are executed:

    lea esi, source
    lea edi, dest
    cld
    mov ecx, 3
    rep movsb
    

    Assuming that ESI starts at 00010000 and EDI starts at 00010005, what will be the values stored in ESI and EDI following the rep movsb instruction? What will be stored in ECX? What will be stored in the destination string?

  8. Suppose that the following instructions are executed:

    lea esi, source+4
    lea edi, dest+4
    std
    mov ecx, 3
    rep movsb
    

    Assuming that ESI starts at 00010010 and EDI starts at 00010015, what will be the values stored in ESI and EDI following the rep movsb instruction? What will be stored in ECX? What will be stored in the destination string?

Programming Exercises 7.2

  1. Write a NEAR32 procedure index to find the position of the first occurrence of a character in a null-terminated string. Specifically, the procedure must have two parameters: (1) a character and (2) the address of a string in the data segment. Use the stack to pass the parameters: For the character, use an entire word with the character in the low-order byte. Use the EAX register to return the position of the character within the string; return zero if the character is not found. No other register should be altered. Procedure index will not remove parameters from the stack.
  2. Write a NEAR32 procedure append that will append one null-terminated string to the end of another. Specifically, the procedure must have two parameters: (1) the address of string1 in the data segment and (2) the address of string2 in the data segment. Use the stack to pass the parameters. The procedure should copy the characters of string2 to the end of string1 with the first character of string2 replacing the null byte at the end of string1, and so on. (Warning: In the data section, enough space must be reserved after the null byte of the first string to hold the characters from the second string.) All registers used by the procedure should be saved and restored. Procedure append will not remove parameters from the stack.
  3. Write a complete program that prompts for and inputs a person's name in the "LastName, FirstName" format and builds a new string with the name in the format "FirstName LastName." A comma and a space separate the names originally and there is no character except the null following FirstName; only a space separates the names in the new string. After you generate the new string in memory, display it.
  4. Write a complete program which prompts for and inputs a person's name in the "LastName, FirstName" format and builds a new string with the name in the format "FirstName LastName." One or more spaces separate the names originally and there may be spaces following FirstName. Only a single space separates the names in the new string. After you generate the new string in memory, display it.
  5. Write a complete program that prompts for and inputs a string and a single character. Construct a new string that is identical to the old one except that it is shortened by removing each occurrence of the character. After you generate the new string in memory, display it.
  6. Write a complete program that prompts for and inputs a sentence and a single word. Construct a new sentence that is identical to the old one except that it is shortened by removing each occurrence of the word. After you generate the new sentence in memory, display it.
  7. Write a complete program that prompts for and inputs a sentence and two words. Construct a new sentence that is identical to the old one except that each occurrence of the first word is replaced by the second word. After you generate the new sentence in memory, display it.

Character Translation

Sometimes character data are available in one format but need to be in another format for processing. One instance of this occurs when characters are transmitted between two computer systems, one normally using ASCII character codes and the other normally using EBCDIC character codes. Another time character codes need to be altered is to transmit them to a device that cannot process all possible codes; it is sometimes easier to replace the unsuitable codes by acceptable codes than to delete them entirely.

The 80x86 instruction set includes the xlat instruction to translate one character to another character. In combination with other string-processing instructions, it can easily translate all the characters in a string.

The xlat instruction requires only one byte of object code, the opcode D7. It takes five clock cycles to execute on an 80386, and four clock cycles on an 80486 or a Pentium. Prior to execution, the character to be translated is in the AL register. The instruction works by using a translation table in the data segment to look up the translation of the byte in AL. This translation table normally contains 256 bytes of data, one for each possible 8-bit value in AL. The byte at offset zero in the table-the first byte-is the character to which 00 is translated. The byte at offset one is the character to which 01 is translated. In general xlat uses the character being translated as an offset into the table, and the byte at that offset then replaces the character in AL.

The xlat instruction has no operand. The EBX register must contain the address of the translation table.

Figure 7.13 illustrates a short program which translates each character of string in place; that is, it replaces each character by its translation using the original location in memory. The heart of the program is the translation table and the sequence of instructions

; Translate uppercase letters to lowercase; don't change lower
; case letters and digits. Translate other characters to spaces.
; author: R. Detmer revised: 10/97

.386
.MODEL FLAT

ExitProcess PROTO NEAR32 stdcall, dwExitCode:DWORD
INCLUDE io.h
PUBLIC _start
cr EQU 0dh ; carriage return character
Lf EQU 0ah ; linefeed character

.STACK 4096 ; reserve 4096-byte stack

.DATA
string BYTE 'This is a #!$& STRING',0
strLength EQU $ - string - 1
label1 BYTE 'Original string ->',0
label2 BYTE cr, Lf, 'Translated string ->',0
crlf BYTE cr, Lf, 0
table BYTE 48 DUP (' '), '0123456789', 7 DUP (' ')
 BYTE 'abcdefghijklmnopqrstuvwxyz', 6 DUP (' ')
 BYTE 'abcdefghijklmnopqrstuvwxyz', 133 DUP (' ')

.CODE
_start: output label1 ; display original string
 output string
 output crlf
 mov ecx, strLength ; string length
 lea ebx, table ; address of translation table
 lea esi, string ; address of string
 lea edi, string ; destination also string
forIndex: lodsb ; copy next character to AL
 xlat ; translate character
 stosb ; copy character back into string
 loop forIndex ; repeat for all characters

 output label2 ; display altered string
 output string
 output crlf

 INVOKE ExitProcess, 0
 END


Figure 7.13: Translation program

 mov ecx, strLength ; string length
 lea ebx, table ; address of translation table
 lea esi, string ; address of string
 lea edi, string ; destination also string
forIndex: lodsb ; copy next character to AL
 xlat ; translate character
 stosb ; copy character back into string
 loop forIndex ; repeat for all characters

These instructions implement a for loop with the design

for index := 1 to stringLength loop
 load source character into AL;
 translate character in AL;
 copy character in AL to destination;
end for;

One new feature in this program is the use of the location counter symbol $. Recall that the assembler calculates addresses as if they start at 00000000, and increments a counter every time it generates bytes of object code. The dollar sign symbol refers to the value of this counter at the time it is encountered in assembly. In this particular program, it will be at the address just beyond the null byte of the string. Since the symbol string actually references its address, the expression string - $ is the length of string, including the null byte. The value equated to strLength is string - $ - 1, which excludes the null byte. The assembly process will be discussed more in Chapter 9.

Each ASCII code is translated to another ASCII code by this program. Uppercase letters are translated to lowercase, lowercase letters and digits are unchanged, and all other characters are translated to spaces. Construction of such a table involves looking at a table of ASCII codes (see Appendix A). For this program the translation table is defined by

table BYTE 48 DUP (' '), '0123456789', 7 DUP (' ')
 BYTE 'abcdefghijklmnopqrstuvwxyz', 6 DUP (' ')
 BYTE 'abcdefghijklmnopqrstuvwxyz', 133 DUP (' ')

Careful counting will show that exactly 256 bytes are defined. Recall that a BYTE directive stores the ASCII code of each character operand. Each of the first 48 bytes of the table will contain the ASCII code for a space (i.e., blank), 2016. Therefore if the code in the AL register represents any of the first 48 ASCII characters-a control character, or one of the printable characters from 2016 (space) to 2F16 (/)-it will be translated to a space.

Note that it is legal to translate a character to itself. Indeed, this is what will happen for digits; the ASCII codes 3016 to 3916 for digits 0 through 9 appear at offsets 3016 to 3916. The codes for the seven characters : through @ are next in an ASCII chart; each of these will be translated to a space. The next ASCII characters are the uppercase letters and the next entries in the table are codes for the lowercase letters. For example, the table contains 6116 at offset 4116, so an uppercase A (ASCII code 4116) will be translated to a lower case a (ASCII code 6116). The next six blanks are at the offsets 9116 ([) through 9616 (‘), so that each of these characters is translated to a blank. The ASCII code for each lowercase letter is assembled at an offset equal to its value, so each lowercase letter is translated to itself. Finally, the translation table contains 133 ASCII codes for blanks; these are the destinations for {, |, }, ~, DEL, and each of the 128 bit patterns starting with a one, none of them codes for ASCII characters.

Figure 7.14 shows the output of the program in Fig. 7.13. Notice that "strange" characters are not deleted, they are replaced by blanks.

 Original string ->This is a #!$& STRING

 Translated string ->this is a string


Figure 7.14: Output from translation program

Exercises 7.3

  1. Here is a partial hexadecimal/EBCDIC conversion table:

    81 a

    C1 A

    40 space

    82 b

    C2 B

    4B .

    83 c

    C3 C

    6B ,

    84 d

    C4 D

     

    85 e

    C5 E

    F0 0

    86 f

    C6 F

    F1 1

    87 g

    C7 G

    F2 2

    88 h

    C8 H

    F3 3

    89 i

    C9 I

    F4 4

    91 j

    D1 J

    F5 5

    92 k

    D2 K

    F6 6

    93 l

    D3 L

    F7 7

    94 m

    D4 M

    F8 8

    95 n

    D5 N

    F9 9

    96 o

    D6 O

     

    97 p

    D7 P

     

    98 q

    D8 Q

     

    99 r

    D9 R

     

    A2 s

    E2 S

     

    A3 t

    E3 T

     

    A4 u

    E4 U

     

    A5 v

    E5 V

     

    A6 w

    E6 W

     

    A7 X

    E7 X

     

    A8 y

    E8 Y

     

    A9 z

    E9 Z

     

    Give a translation table that would be suitable for xlat translation of EBCDIC codes for letters, digits, space, period, and comma to the corresponding ASCII codes, translating every other EBCDIC code to a null character.

  2. Give a translation table that would be suitable for xlat translation of ASCII codes for lowercase letters to the corresponding uppercase letters, leaving all other characters unchanged.
  3. Here is an alternative to the xlat instruction.

    movzx eax, al ; clear high order bits in EAX
    mov al, [ebx+eax] ; copy new character from table to AL
    

    Given that [ebx+eax] references the memory byte at the address that is the sum of the contents of EBX and EAX, explain why this pair of instructions is equivalent to a single xlat instruction.

Programming Exercises 7.3

  1. In the United States, decimal numbers are written with a decimal point separating the integral part from the fractional part and with commas every three positions to the left of the decimal point. In many European countries, decimal numbers are written with the roles of commas and decimal points reversed. For example, the number 1,234,567.89 would be written 1.234.567,89. Write a program that will interchange commas and periods, translating a string of characters representing either format of number to the other format. Use the xlat instruction with a translation table that translates a period to a comma, a comma to a period, each digit to itself, and any other character to a space. Prompt for and input the number to be translated. Translate the string. Display the new number format with an appropriate label.

Converting a 2 s Complement Integer to an ASCII String

The dtoa and itoa macros have been used to convert 2’s complement integers to strings of ASCII characters for output. The code for these operations is similar. In this section we examine the slightly shorter code for itoa.

The itoa macro expands into the following sequence of instructions.

 push ebx ; save EBX
 mov bx, source
 push bx ; source parameter
 lea ebx, dest ; destination address
 push ebx ; destination parameter
 call itoaproc ; call itoaproc(source,dest)
 pop ebx ; restore EBX

These instructions call procedure itoaproc after pushing the source value and the destination address on the stack. The actual source and destination are used in the expanded macro, not the names source and dest. So that the user does not need to worry about any register contents being altered, EBX is initially saved on the stack and is restored at the end of the sequence. The parameters are removed from the stack by procedure itoaproc since the alternative add esp,6 following the call instruction potentially changes the flags.

The real work of 2’s complement integer to ASCII conversion is done by the procedure itoaproc. The assembled version of this procedure is contained in the file IO.OBJ. The source code from file IO.ASM is shown in Fig. 7.15. The procedure begins by saving all of the registers that it alters on the stack; the flag register is also saved so that the procedure call to itoaproc will not change flag settings. The flag register and other registers are restored immediately before returning from the procedure.

; itoaproc(source, dest)
; convert integer (source) to string of 6 characters at destination address

itoaproc PROC NEAR32
 push ebp ; save base pointer
 mov ebp, esp ; establish stack frame
 push eax ; Save registers
 push ebx ; used by
 push ecx ; procedure
 push edx
 push edi
 pushf ; save flags

 mov ax, [ebp+12] ; first parameter (source integer)
 mov edi, [ebp+8] ; second parameter (dest address)
ifSpecial: cmp ax,8000h ; special case −32,768?
 jne EndIfSpecial ; if not, then normal case
 mov BYTE PTR [edi],'-' ; manually put in ASCII codes
 mov BYTE PTR [edi+1],'3' ; for −32,768
 mov BYTE PTR [edi+2],'2'
 mov BYTE PTR [edi+3],'7'
 mov BYTE PTR [edi+4],'6'
 mov BYTE PTR [edi+5],'8'
 jmp ExitIToA ; done with special case
EndIfSpecial:

 mov dx, ax ; save source number

 mov al,' ' ; put blanks in
 mov ecx,5 ; first five
 cld ; bytes of
 rep stosb ; destination field

 mov ax, dx ; copy source number
 mov cl,' ' ; default sign (blank for +)
IfNeg: cmp ax,0 ; check sign of number
 jge EndIfNeg ; skip if not negative
 mov cl,'-' ; sign for negative number
 neg ax ; number in AX now >= 0
EndIfNeg:

 mov bx,10 ; divisor
WhileMore: mov dx,0 ; extend number to doubleword
 div bx ; divide by 10
 add dl,30h ; convert remainder to character
 mov [edi],dl ; put character in string
 dec edi ; move forward to next position
 cmp ax,0 ; check quotient
 jnz WhileMore ; continue if quotient not zero

 mov [edi],cl ; insert blank or "-" for sign
ExitIToA: popf ; restore flags and registers
 pop edi
 pop edx
 pop ecx
 pop ebx
 pop eax
 pop ebp
 ret 6 ;exit, discarding parameters
itoaproc ENDP


Figure 7.15: Integer to ASCII conversion procedure

The basic idea of the procedure is to build a string of characters right to left by repeatedly dividing the number by 10, using the remainder to determine the rightmostcharacter. For instance, dividing the number 2895 (0B4F16) by 10 gives a remainder of 5 and a quotient of 289 (012116), the last digit of the number and a new number with which to repeat the process. This scheme works nicely for positive numbers, but a negative number must be changed to its absolute value before starting the division loop. To complicate things further, the bit pattern 800016 represents the negative number −32,76810, but +32,768 cannot be represented in 2’s complement form in a 16-bit word.

After standard entry code, the value parameter is copied to AX and the destination address to EDI. The procedure then checks for the special case 800016. If this is the value, then the ASCII codes for 32768 are moved one at a time to the destination, using the fact that the destination address is in EDI. The location for the minus sign is in the EDI register, so register indirect addressing can be used to put this character in the correct memory byte. The location for the character 3 is one byte beyond the address contained in EDI; this address is referenced by [edi+1]. The remaining four characters are similarly put in place, and the procedure is exited.

The next step of the procedure is to put five leading blanks in the six-byte-long destination field. The procedure does this with a rep stosb, which uses EDI to point to successive bytes in destination field. Note that EDI is left pointing at the last byte of the destination field.

The procedure next stores the correct "sign" in the CL register. A blank is used for a number greater than or equal to zero, and minus character (−) is used for a negative number. A negative number is also negated, giving its absolute value for subsequent processing.

Finally the main idea is executed. The divisor 10 is placed in the BX register. The non-negative number is extended to a doubleword by moving zeros to DX. Division by 10 in BX gives a remainder from 0 to 9 in DX, the last decimal digit of the number. This is converted to the corresponding ASCII code by adding 3016; recall that the ASCII codes for digits 0 through 9 are 3016 through 3916. A mov using register indirect addressing puts the character in place in the destination string, and EDI is decremented to point at the next position to the left.

This process is repeated until the quotient is zero. Finally the "sign" stored in CL (blank or –) is copied to the immediate left of the last code for a digit. Other positions to the left, if any, were previously filled with blanks.

Exercises 7.4

  1. Why does itoaproc use a destination string six bytes long?
  2. Suppose that negative numbers are not changed before the division loop of itoaproc begins and that an idiv instruction is used rather than a div instruction in this loop. Recall that when a negative number is divided by a positive number, both quotient and remainder will be negative. For instance, −1273 = 10*(−127) + (−3). How could the rest of the division loop be modified to produce the correct ASCII codes for both positive and negative numbers?

Programming Exercises 7.4

  1. Rewrite itoaproc, adding a length parameter. Specifically, the new itoaNew will be a NEAR32 procedure with three parameters, passed on the stack:

    1. the 2’s complement number to convert to ASCII characters (a word)
    2. the address of the ASCII string (a doubleword)
    3. the desired length of the ASCII string (a word)

    The number will be converted to a string of ASCII characters starting at the offset in the data segment. Do not use a blank in front of a positive number. If the length is less than or equal to the actual number of characters needed to display the number, fill the entire field with pound signs (#). If the length is larger than needed, pad with extra spaces to the left of the number. The procedure will remove parameters from the stack and must modify no register.

  2. Write a NEAR32 procedure hexString that converts a 32-bit integer to a string of exactly eight characters representing its value as a hexadecimal number. (That is, the output characters will be 0–9 and A–F, with no blanks.) The procedure will have two parameters, passed on the stack:

    1. the number
    2. the address of the destination string

    The procedure will remove parameters from the stack and must modify no register. (The remainder upon division by 16 produces a decimal value corresponding to the rightmost hex digit.)

  3. Write a NEAR32 procedure binaryString that converts a 32-bit integer to a string of exactly 32 characters representing its value as a binary number. The procedure will have two parameters, passed on the stack:

    1. the number
    2. the address of the destination string

    The procedure will remove parameters from the stack and must modify no register. (The remainder upon division by 2 gives the rightmost bit.)

Other Architectures CISC versus RISC Designs

Early digital computers had very simple instruction sets. When designers began to use microcode to implement instructions in the 1960s, it became possible to have much more complex instructions. At the same time high-level programming languages were becoming popular, but language compilers were fairly primitive. This made it desirable to have machine language statements that almost directly implemented high-level language statements, increasing the pressure to produce computer architectures with many complex instructions.

The Intel 80x86 machines use complex instruction set computer (CISC) designs. Instructions such as the string instructions discussed in this chapter would never have appeared in early computers. CISC machines also have a variety of memory addressing modes, and the 80x86 family is typical in this respect, although you have only seen a few of its modes so far. Often CISC instructions take several clock cycles to execute.

Reduced instruction set computer (RISC) designs began to appear in the 1980s. These machines have relatively few instructions and few memory addressing modes. Their instructions are so simple that any one can be executed in a single clock cycle. As compiler technology improved, it became possible to produce efficient code for RISC machines. Of course, it often takes many more instructions to implement a given high-level language statement on a RISC than on a CISC machine, but the overall operation is often faster because of the speed with which individual instructions execute.

In RISC architectures, instructions are all the same format; that is, the same number of bytes are encoded in a common pattern. This is not the case with CISC architectures. If the 80x86 chips were RISC designs, then this book would have no questions asking "How many clock cycles?" or "How many bytes?" When we look at the many 80x86 instruction formats in Chapter 9, you may wish for the simplicity of a RISC machine.

One unusual feature of many RISC designs is a relatively large collection of registers (sometimes over 500), of which only a small number (often 32) are visible at one time. Registers are used to pass parameters to procedures, and the registers that are used to store arguments in the calling program overlap the registers that are used to receive the parameter values in the procedure. This provides a simple but very efficient method of communication between a calling program and a procedure.

There are proponents of both CICS and RISC designs. At this point in time it is not obvious that one is clearly better than the other. However, the popular Intel 80x86 and Motorola 680x0 families are both CISC designs, so we will be dealing with CISC systems at least in the near future.

Summary

The word string refers to a collection of consecutive bytes, words, or doublewords in memory. The 80x86 instruction set includes five instructions for operating on strings: movs (to move or copy a string from a source to a destination location), cmps (to compare two strings), scas (to scan a string for a particular element), stos (to store a given value in a string), and lods (to copy a string element into EAX, AX, or AL). Each of these has mnemonic forms ending with b, w, or d to give the size of the string element.

A string instruction operates on one string element at a time. When a source string is involved, the source index register ESI contains the address of the string element. When a destination string is involved, the destination index register EDI contains the address of the string element. An index register is incremented or decremented after the string element is accessed, depending on whether the direction flag DF is reset to zero or set to one; the cld and std instructions are used to give the direction flag a desired value.

Repeat prefixes rep, repe (repz), and repne (repnz) are used with some string instructions to cause them to repeat automatically. The number of times to execute a primitive instruction is placed in the ECX register. The conditional repeat forms use the count in ECX but will also terminate instruction execution if the zero flag gets a certain value; these are appropriate for use with the cmps and scas instructions that set or reset ZF.

The xlat instruction is used to translate the characters of a string. It requires a 256-byte-long translation table that starts with the destination byte to which the source byte 00 is translated and ends with the destination byte to which the source byte FF is translated. The xlat instruction can be used for such applications as changing ASCII codes to EBCDIC codes or for changing the case of letters within a given character coding system.

The itoa macro expands to code that calls a procedure itoaproc. Basically this procedure works by repeatedly dividing a non-negative number by 10 and using the remainder to get the rightmost character of the destination string.

The 80x86 chips are examples of complex instruction set computer (CISC) architecture. They include many complex instructions and offer many different addressing modes. Reduced instruction set computer (RISC) architectures implement fewer and simpler instructions and have more limited addressing options. Even though RISC computers take more instructions to accomplish a task, they are usually quite fast since they execute their simple instructions very rapidly.



Introduction to 80x86 Assembly Language and Computer Architecture
Introduction to 80x86 Assembly Language and Computer Architecture
ISBN: 0763772232
EAN: 2147483647
Year: 2000
Pages: 113

Flylib.com © 2008-2020.
If you may any questions please contact us: flylib@qtcs.net