This chapter discusses the conversion between various numeric formats, including integer to decimal string, integer to hexadecimal string, floating-point to string, hexadecimal string to integer, decimal string to integer, and real string to floating-point. In addition to the basic conversions, this chapter discusses error handling (for string-to-numeric conversions) and performance enhancements. This chapter discusses standard-precision conversions (for 8-, 16-, 32-, and 64-bit integer formats) as well as extended-precision conversions (for example, 128-bit integer and string conversions).
Up to this point, this book has relied upon the C Standard Library to perform numeric I/O (writing numeric data to the display and reading numeric data from the user). However, the C Standard Library doesn’t provide extended-precision numeric I/O facilities (and even 64-bit numeric I/O is questionable; this book has been using a Microsoft extension to printf()
to do 64-bit numeric output). Therefore, it’s time to break down and discuss how to do numeric I/O in assembly language—well, sort of. Because most operating systems support only character or string input and output, we aren’t going to do actual numeric I/O. Instead, we’re going write functions that convert between numeric values and strings, and then do string I/O.
The examples in this section work specifically with 64-bit (non-extended-precision) and 128-bit values, but the algorithms are general and extend to any number of bits.
Converting a numeric value to a hexadecimal string is relatively straightforward. Just take each nibble (4 bits) in the binary representation and convert that to one of the 16 characters “0” through “9” or “A” through “F”. Consider the btoh
function in Listing 9-1 that takes a byte in the AL register and returns the two corresponding characters in AH (HO nibble) and AL (LO nibble).
; btoh - This procedure converts the binary value
; in the AL register to two hexadecimal
; characters and returns those characters
; in the AH (HO nibble) and AL (LO nibble)
; registers.
btoh proc
mov ah, al ; Do HO nibble first
shr ah, 4 ; Move HO nibble to LO
or ah, '0' ; Convert to char
cmp ah, '9' + 1 ; Is it "A" through "F"?
jb AHisGood
; Convert 3Ah to 3Fh to "A" through "F":
add ah, 7
; Process the LO nibble here:
AHisGood: and al, 0Fh ; Strip away HO nibble
or al, '0' ; Convert to char
cmp al, '9' + 1 ; Is it "A" through "F"?
jb ALisGood
; Convert 3Ah to 3Fh to "A" through "F":
add al, 7
ALisGood: ret
btoh endp
Listing 9-1: A function that converts a byte to two hexadecimal characters
You can convert any numeric value in the range 0 to 9 to its corresponding ASCII character by ORing the numeric value with 0 (30h). Unfortunately, this maps numeric values in the range 0Ah through 0Fh to 3Ah through 3Fh. So, the code in Listing 9-1 checks to see if it produces a value greater than 3Ah and adds 7 to produce a final character code in the range 41h to 46h (“A” through “F”).
Once we can convert a single byte to a pair of hexadecimal characters, creating a string, output to the display is straightforward. We can call the btoh
(byte to hex) function for each byte in the number and store the corresponding characters away in a string. Listing 9-2 provides examples of btoStr
(byte to string), wtoStr
(word to string), dtoStr
(double word to string), and qtoStr
(quad word to string) functions.
; Listing 9-2
; Numeric-to-hex string functions.
option casemap:none
nl = 10
.const
ttlStr byte "Listing 9-2", 0
fmtStr1 byte "btoStr: Value=%I64x, string=%s"
byte nl, 0
fmtStr2 byte "wtoStr: Value=%I64x, string=%s"
byte nl, 0
fmtStr3 byte "dtoStr: Value=%I64x, string=%s"
byte nl, 0
fmtStr4 byte "qtoStr: Value=%I64x, string=%s"
byte nl, 0
.data
buffer byte 20 dup (?)
.code
externdef printf:proc
; Return program title to C++ program:
public getTitle
getTitle proc
lea rax, ttlStr
ret
getTitle endp
; btoh - This procedure converts the binary value
; in the AL register to two hexadecimal
; characters and returns those characters
; in the AH (HO nibble) and AL (LO nibble)
; registers.
btoh proc
mov ah, al ; Do HO nibble first
shr ah, 4 ; Move HO nibble to LO
or ah, '0' ; Convert to char
cmp ah, '9' + 1 ; Is it "A" to "F"?
jb AHisGood
; Convert 3Ah through 3Fh to "A" to "F":
add ah, 7
; Process the LO nibble here:
AHisGood: and al, 0Fh ; Strip away HO nibble
or al, '0' ; Convert to char
cmp al, '9' + 1 ; Is it "A" to "F"?
jb ALisGood
; Convert 3Ah through 3Fh to "A" to "F":
add al, 7
ALisGood: ret
btoh endp
; btoStr - Converts the byte in AL to a string of hexadecimal
; characters and stores them at the buffer pointed at
; by RDI. Buffer must have room for at least 3 bytes.
; This function zero-terminates the string.
btoStr proc
push rax
call btoh ; Do conversion here
; Create a zero-terminated string at [RDI] from the
; two characters we converted to hex format:
mov [rdi], ah
mov [rdi + 1], al
mov byte ptr [rdi + 2], 0
pop rax
ret
btoStr endp
; wtoStr - Converts the word in AX to a string of hexadecimal
; characters and stores them at the buffer pointed at
; by RDI. Buffer must have room for at least 5 bytes.
; This function zero-terminates the string.
wtoStr proc
push rdi
push rax ; Note: leaves LO byte at [RSP]
; Use btoStr to convert HO byte to a string:
mov al, ah
call btoStr
mov al, [rsp] ; Get LO byte
add rdi, 2 ; Skip HO chars
call btoStr
pop rax
pop rdi
ret
wtoStr endp
; dtoStr - Converts the dword in EAX to a string of hexadecimal
; characters and stores them at the buffer pointed at
; by RDI. Buffer must have room for at least 9 bytes.
; This function zero-terminates the string.
dtoStr proc
push rdi
push rax ; Note: leaves LO word at [RSP]
; Use wtoStr to convert HO word to a string:
shr eax, 16
call wtoStr
mov ax, [rsp] ; Get LO word
add rdi, 4 ; Skip HO chars
call wtoStr
pop rax
pop rdi
ret
dtoStr endp
; qtoStr - Converts the qword in RAX to a string of hexadecimal
; characters and stores them at the buffer pointed at
; by RDI. Buffer must have room for at least 17 bytes.
; This function zero-terminates the string.
qtoStr proc
push rdi
push rax ; Note: leaves LO dword at [RSP]
; Use dtoStr to convert HO dword to a string:
shr rax, 32
call dtoStr
mov eax, [rsp] ; Get LO dword
add rdi, 8 ; Skip HO chars
call dtoStr
pop rax
pop rdi
ret
qtoStr endp
; Here is the "asmMain" function.
public asmMain
asmMain proc
push rdi
push rbp
mov rbp, rsp
sub rsp, 64 ; Shadow storage
; Because all the (x)toStr functions preserve RDI,
; we need to do the following only once:
lea rdi, buffer
; Demonstrate call to btoStr:
mov al, 0aah
call btoStr
lea rcx, fmtStr1
mov edx, eax
mov r8, rdi
call printf
; Demonstrate call to wtoStr:
mov ax, 0a55ah
call wtoStr
lea rcx, fmtStr2
mov edx, eax
mov r8, rdi
call printf
; Demonstrate call to dtoStr:
mov eax, 0aa55FF00h
call dtoStr
lea rcx, fmtStr3
mov edx, eax
mov r8, rdi
call printf
; Demonstrate call to qtoStr:
mov rax, 1234567890abcdefh
call qtoStr
lea rcx, fmtStr4
mov rdx, rax
mov r8, rdi
call printf
leave
pop rdi
ret ; Returns to caller
asmMain endp
end
Listing 9-2: btoStr
, wtoStr
, dtoStr
, and qtoStr
functions
Here’s the build command and sample output:
C:\>build listing9-2
C:\>echo off
Assembling: listing9-2.asm
c.cpp
C:\>listing9-2
Calling Listing 9-2:
btoStr: Value=aa, string=AA
wtoStr: Value=a55a, string=A55A
dtoStr: Value=aa55ff00, string=AA55FF00
qtoStr: Value=1234567890abcdef, string=1234567890ABCDEF
Listing 9-2 terminated
Each successive function in Listing 9-2 builds on the work done in the previous functions. For example, wtoStr
calls btoStr
twice to convert the 2 bytes in AX to a string of four hexadecimal characters. The code would be faster (but a lot larger) if you were to inline-expand each of these functions wherever the code calls them. If you needed only one of these functions, an inline expansion of any calls it makes would be worth the extra effort.
Here’s a version of qtoStr
with two improvements: inline expansion of the calls to dtoStr
, wtoStr
, and btoStr
, plus the use of a simple table lookup (array access) to do the nibble-to-hex-character conversion (see Chapter 10 for more information on table lookups). The framework for this faster version of qtoStr
appears in Listing 9-3.
; qtoStr - Converts the qword in RAX to a string of hexadecimal
; characters and stores them at the buffer pointed at
; by RDI. Buffer must have room for at least 17 bytes.
; This function zero-terminates the string.
hexChar byte "0123456789ABCDEF"
qtoStr proc
push rdi
push rcx
push rdx
push rax ; Leaves LO dword at [RSP]
lea rcx, hexChar
xor edx, edx ; Zero-extends!
shld rdx, rax, 4
shl rax, 4
mov dl, [rcx][rdx * 1] ; Table lookup
mov [rdi], dl
; Emit bits 56-59:
xor edx, edx
shld rdx, rax, 4
shl rax, 4
mov dl, [rcx][rdx * 1]
mov [rdi + 1], dl
; Emit bits 52-55:
xor edx, edx
shld rdx, rax, 4
shl rax, 4
mov dl, [rcx][rdx * 1]
mov [rdi + 2], dl
.
.
.
Code to emit bits 8-51 was deleted for length reasons.
The code should be obvious if you look at the output
for the other nibbles appearing here.
.
.
.
; Emit bits 4-7:
xor edx, edx
shld rdx, rax, 4
shl rax, 4
mov dl, [rcx][rdx * 1]
mov [rdi + 14], dl
; Emit bits 0-3:
xor edx, edx
shld rdx, rax, 4
shl rax, 4
mov dl, [rcx][rdx * 1]
mov [rdi + 15], dl
; Zero-terminate string:
mov byte ptr [rdi + 16], 0
pop rax
pop rdx
pop rcx
pop rdi
ret
qtoStr endp
Listing 9-3: Faster implementation of qtoStr
Writing a short main program that contains the following loop
lea rdi, buffer
mov rax, 07fffffffh
loopit: call qtoStr
dec eax
jnz loopit
and then using a stopwatch on an old 2012-era 2.6 GHz Intel Core i7 processor, I got the approximate timings for the inline and original versions of qtoStr
:
As you can see, the inline version is significantly (four times) faster, but you probably won’t convert 64-bit numbers to hexadecimal strings often enough to justify the kludgy code of the inline version.
For what it’s worth, you could probably cut the time almost in half by using a much larger table (256 16-bit entries) for the hex characters and convert a whole byte at a time rather than a nibble. This would require half the instructions of the inline version (though the table would be 32 times bigger).
Extended-precision hexadecimal-to-string conversion is easy. It’s simply an extension of the normal hexadecimal conversion routines from the previous section. For example, here’s a 128-bit hexadecimal conversion function:
; otoStr - Converts the oword in RDX:RAX to a string of hexadecimal
; characters and stores them at the buffer pointed at
; by RDI. Buffer must have room for at least 33 bytes.
; This function zero-terminates the string.
otoStr proc
push rdi
push rax ; Note: leaves LO dword at [RSP]
; Use qtoStr to convert each qword to a string:
mov rax, rdx
call qtoStr
mov rax, [rsp] ; Get LO qword
add rdi, 16 ; Skip HO chars
call qtoStr
pop rax
pop rdi
ret
otoStr endp
Decimal output is a little more complicated than hexadecimal output because the HO bits of a binary number affect the LO digits of the decimal representation (this was not true for hexadecimal values, which is why hexadecimal output is so easy). Therefore, we will have to create the decimal representation for a binary number by extracting one decimal digit at a time from the number.
The most common solution for unsigned decimal output is to successively divide the value by 10 until the result becomes 0. The remainder after the first division is a value in the range 0 to 9, and this value corresponds to the LO digit of the decimal number. Successive divisions by 10 (and their corresponding remainder) extract successive digits from the number.
Iterative solutions to this problem generally allocate storage for a string of characters large enough to hold the entire number. Then the code extracts the decimal digits in a loop and places them in the string one by one. At the end of the conversion process, the routine prints the characters in the string in reverse order (remember, the divide algorithm extracts the LO digits first and the HO digits last, the opposite of the way you need to print them).
This section employs a recursive solution because it is a little more elegant. This solution begins by dividing the value by 10 and saving the remainder in a local variable. If the quotient is not 0, the routine recursively calls itself to output any leading digits first. On return from the recursive call (which outputs all the leading digits), the recursive algorithm outputs the digit associated with the remainder to complete the operation. Here’s how the operation works when printing the decimal value 789:
Listing 9-4 implements the recursive algorithm.
; Listing 9-4
; Numeric unsigned integer-to-string function.
option casemap:none
nl = 10
.const
ttlStr byte "Listing 9-4", 0
fmtStr1 byte "utoStr: Value=%I64u, string=%s"
byte nl, 0
.data
buffer byte 24 dup (?)
.code
externdef printf:proc
; Return program title to C++ program:
public getTitle
getTitle proc
lea rax, ttlStr
ret
getTitle endp
; utoStr - Unsigned integer to string.
; Inputs:
; RAX: Unsigned integer to convert.
; RDI: Location to hold string.
; Note: for 64-bit integers, resulting
; string could be as long as 21 bytes
; (including the zero-terminating byte).
utoStr proc
push rax
push rdx
push rdi
; Handle zero specially:
test rax, rax
jnz doConvert
mov byte ptr [rdi], '0'
inc rdi
jmp allDone
doConvert: call rcrsvUtoStr
; Zero-terminate the string and return:
allDone: mov byte ptr [rdi], 0
pop rdi
pop rdx
pop rax
ret
utoStr endp
ten qword 10
; Here's the recursive code that does the
; actual conversion:
rcrsvUtoStr proc
xor rdx, rdx ; Zero-extend RAX -> RDX
div ten
push rdx ; Save output value
test eax, eax ; Quit when RAX is 0
jz allDone
; Recursive call to handle value % 10:
call rcrsvUtoStr
allDone: pop rax ; Retrieve char to print
and al, 0Fh ; Convert to "0" to "9"
or al, '0'
mov byte ptr [rdi], al ; Save in buffer
inc rdi ; Next char position
ret
rcrsvUtoStr endp
; Here is the "asmMain" function.
public asmMain
asmMain proc
push rdi
push rbp
mov rbp, rsp
sub rsp, 56 ; Shadow storage
; Because all the (x)toStr functions preserve RDI,
; we need to do the following only once:
lea rdi, buffer
mov rax, 1234567890
call utoStr
; Print the result:
lea rcx, fmtStr1
mov rdx, rax
mov r8, rdi
call printf
leave
pop rdi
ret ; Returns to caller
asmMain endp
end
Listing 9-4: Unsigned integer-to-string function (recursive)
Here’s the build command and program output:
C:\>build listing9-4
C:\>echo off
Assembling: listing9-4.asm
c.cpp
C:\>listing9-4
Calling Listing 9-4:
utoStr: Value=1234567890, string=1234567890
Listing 9-4 terminated
Unlike hexadecimal output, there really is no need to provide a byte-size, word-size, or dword-size numeric-to-decimal-string conversion function. Simply zero-extending the smaller values to 64 bits is sufficient. Unlike the hexadecimal conversions, there are no leading zeros emitted by the qtoStr
function, so the output is the same for all sizes of variables (64 bits and smaller).
Unlike the hexadecimal conversion (which is very fast to begin with, plus you don’t really call it that often), you will frequently call the integer-to-string conversion function. Because it uses the div
instruction, it can be fairly slow. Fortunately, we can speed it up by using the fist
and fbstp
instructions.
The fbstp
instruction converts the 80-bit floating-point value currently sitting on the top of stack to an 18-digit packed BCD value (using the format appearing in Figure 6-7 in Chapter 6). The fist
instruction allows you to load a 64-bit integer onto the FPU stack. So, by using these two instructions, you can (mostly) convert a 64-bit integer to a packed BCD value, which encodes a single decimal digit per 4 bits. Therefore, you can convert the packed BCD result that fbstp
produces to a character string by using the same algorithm you use for converting hexadecimal numbers to a string.
There is only one catch with using fist
and fbstp
to convert an integer to a string: the Intel packed BCD format (see Figure 6-7 in Chapter 6) supports only 18 digits, whereas a 64-bit integer can have up to 19 digits. Therefore, any fbstp
-based utoStr
function will have to handle that 19th digit as a special case. With all this in mind, Listing 9-5 provides this new version of the utoStr
function.
; Listing 9-5
; Fast unsigned integer-to-string function
; using fist and fbstp.
option casemap:none
nl = 10
.const
ttlStr byte "Listing 9-5", 0
fmtStr1 byte "utoStr: Value=%I64u, string=%s"
byte nl, 0
.data
buffer byte 30 dup (?)
.code
externdef printf:proc
; Return program title to C++ program:
public getTitle
getTitle proc
lea rax, ttlStr
ret
getTitle endp
; utoStr - Unsigned integer to string.
; Inputs:
; RAX: Unsigned integer to convert.
; RDI: Location to hold string.
; Note: for 64-bit integers, resulting
; string could be as long as 21 bytes
; (including the zero-terminating byte).
bigNum qword 1000000000000000000
utoStr proc
push rcx
push rdx
push rdi
push rax
sub rsp, 10
; Quick test for zero to handle that special case:
test rax, rax
jnz not0
mov byte ptr [rdi], '0'
jmp allDone
; The FBSTP instruction supports only 18 digits.
; 64-bit integers can have up to 19 digits.
; Handle that 19th possible digit here:
not0: cmp rax, bigNum
jb lt19Digits
; The number has 19 digits (which can be 0-9).
; Pull off the 19th digit:
xor edx, edx
div bigNum ; 19th digit in AL
mov [rsp + 10], rdx ; Remainder
or al, '0'
mov [rdi], al
inc rdi
; The number to convert is nonzero.
; Use BCD load and store to convert
; the integer to BCD:
lt19Digits: fild qword ptr [rsp + 10]
fbstp tbyte ptr [rsp]
; Begin by skipping over leading zeros in
; the BCD value (max 19 digits, so the most
; significant digit will be in the LO nibble
; of DH).
mov dx, [rsp + 8]
mov rax, [rsp]
mov ecx, 20
jmp testFor0
Skip0s: shld rdx, rax, 4
shl rax, 4
testFor0: dec ecx ; Count digits we've processed
test dh, 0fh ; Because the number is not 0
jz Skip0s ; this always terminates
; At this point the code has encountered
; the first nonzero digit. Convert the remaining
; digits to a string:
cnvrtStr: and dh, 0fh
or dh, '0'
mov [rdi], dh
inc rdi
mov dh, 0
shld rdx, rax, 4
shl rax, 4
dec ecx
jnz cnvrtStr
; Zero-terminate the string and return:
allDone: mov byte ptr [rdi], 0
add rsp, 10
pop rax
pop rdi
pop rdx
pop rcx
ret
utoStr endp
; Here is the "asmMain" function.
public asmMain
asmMain proc
push rbp
mov rbp, rsp
sub rsp, 64 ; Shadow storage
; Because all the (x)toStr functions preserve RDI,
; we need to do the following only once:
lea rdi, buffer
mov rax, 9123456789012345678
call utoStr
lea rcx, fmtStr1
mov rdx, 9123456789012345678
lea r8, buffer
call printf
leave
ret ; Returns to caller
asmMain endp
end
Listing 9-5: A fist
and fbstp
-based utoStr
function
Here’s the build command and sample output from this program:
C:\>build listing9-5
C:\>echo off
Assembling: listing9-5.asm
c.cpp
C:\>listing9-5
Calling Listing 9-5:
utoStr: Value=9123456789012345678, string=9123456789012345678
Listing 9-5 terminated
The program in Listing 9-5 does use a div
instruction, but it executes only once or twice, and only if there are 19 or 20 digits in the number. Therefore, the execution time of this div
instruction will have little overall impact on the speed of the utoStr
function (especially when you consider how often you actually print 19-digit numbers).
I got the following execution times on a 2.6 GHz circa-2012 Core i7 processor:
utoStr
: 108 secondsfist
and fbstp
implementation: 11 secondsClearly, the fist
and fbstp
implementation is the winner.
To convert a signed integer value to a string, you first check to see if the number is negative; if it is, you emit a hyphen (-) character and negate the value. Then you call the utoStr
function to finish the job. Listing 9-6 shows the relevant code.
; itoStr - Signed integer-to-string conversion.
; Inputs:
; RAX - Signed integer to convert.
; RDI - Destination buffer address.
itoStr proc
push rdi
push rax
test rax, rax
jns notNeg
; Number was negative, emit "-" and negate
; value.
mov byte ptr [rdi], '-'
inc rdi
neg rax
; Call utoStr to convert non-negative number:
notNeg: call utoStr
pop rax
pop rdi
ret
itoStr endp
Listing 9-6: Signed integer-to-string conversion
For extended-precision output, the only operation through the entire string-conversion algorithm that requires extended-precision arithmetic is the divide-by-10 operation. Because we are dividing an extended-precision value by a value that easily fits into a quad word, we can use the fast (and easy) extended-precision division algorithm that uses the div
instruction (see “Special Case Form Using div
Instruction” in “Extended-Precision Division” in Chapter 8). Listing 9-7 implements a 128-bit decimal output routine utilizing this technique.
; Listing 9-7
; Extended-precision numeric unsigned
; integer-to-string function.
option casemap:none
nl = 10
.const
ttlStr byte "Listing 9-7", 0
fmtStr1 byte "otoStr(0): string=%s", nl, 0
fmtStr2 byte "otoStr(1234567890): string=%s", nl, 0
fmtStr3 byte "otoStr(2147483648): string=%s", nl, 0
fmtStr4 byte "otoStr(4294967296): string=%s", nl, 0
fmtStr5 byte "otoStr(FFF...FFFF): string=%s", nl, 0
.data
buffer byte 40 dup (?)
b0 oword 0
b1 oword 1234567890
b2 oword 2147483648
b3 oword 4294967296
; Largest oword value
; (decimal=340,282,366,920,938,463,463,374,607,431,768,211,455):
b4 oword 0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFh
.code
externdef printf:proc
; Return program title to C++ program:
public getTitle
getTitle proc
lea rax, ttlStr
ret
getTitle endp
; DivideBy10 - Divides "divisor" by 10 using fast
; extended-precision division algorithm
; that employs the div instruction.
; Returns quotient in "quotient."
; Returns remainder in RAX.
; Trashes RDX.
; RCX - Points at oword dividend and location to
; receive quotient.
ten qword 10
DivideBy10 proc
parm equ <[rcx]>
xor edx, edx ; Zero-extends!
mov rax, parm[8]
div ten
mov parm[8], rax
mov rax, parm
div ten
mov parm, rax
mov eax, edx ; Remainder (always "0" to "9"!)
ret
DivideBy10 endp
; Recursive version of otoStr.
; A separate "shell" procedure calls this so that
; this code does not have to preserve all the registers
; it uses (and DivideBy10 uses) on each recursive call.
; On entry:
; Stack - Contains oword in/out parameter (dividend in/quotient out).
; RDI - Contains location to place output string.
; Note: this function must clean up stack (parameters)
; on return.
rcrsvOtoStr proc
value equ <[rbp + 16]>
remainder equ <[rbp - 8]>
push rbp
mov rbp, rsp
sub rsp, 8
lea rcx, value
call DivideBy10
mov remainder, al
; If the quotient (left in value) is not 0, recursively
; call this routine to output the HO digits.
mov rax, value
or rax, value[8]
jz allDone
mov rax, value[8]
push rax
mov rax, value
push rax
call rcrsvOtoStr
allDone: mov al, remainder
or al, '0'
mov [rdi], al
inc rdi
leave
ret 16 ; Remove parms from stack
rcrsvOtoStr endp
; Nonrecursive shell to the above routine so we don't bother
; saving all the registers on each recursive call.
; On entry:
; RDX:RAX - Contains oword to print.
; RDI - Buffer to hold string (at least 40 bytes).
otostr proc
push rax
push rcx
push rdx
push rdi
; Special-case zero:
test rax, rax
jnz not0
test rdx, rdx
jnz not0
mov byte ptr [rdi], '0'
inc rdi
jmp allDone
not0: push rdx
push rax
call rcrsvOtoStr
; Zero-terminate string before leaving:
allDone: mov byte ptr [rdi], 0
pop rdi
pop rdx
pop rcx
pop rax
ret
otostr endp
; Here is the "asmMain" function.
public asmMain
asmMain proc
push rdi
push rbp
mov rbp, rsp
sub rsp, 56 ; Shadow storage
; Because all the (x)toStr functions preserve RDI,
; we need to do the following only once:
lea rdi, buffer
; Convert b0 to a string and print the result:
mov rax, qword ptr b0
mov rdx, qword ptr b0[8]
call otostr
lea rcx, fmtStr1
lea rdx, buffer
call printf
; Convert b1 to a string and print the result:
mov rax, qword ptr b1
mov rdx, qword ptr b1[8]
call otostr
lea rcx, fmtStr2
lea rdx, buffer
call printf
; Convert b2 to a string and print the result:
mov rax, qword ptr b2
mov rdx, qword ptr b2[8]
call otostr
lea rcx, fmtStr3
lea rdx, buffer
call printf
; Convert b3 to a string and print the result:
mov rax, qword ptr b3
mov rdx, qword ptr b3[8]
call otostr
lea rcx, fmtStr4
lea rdx, buffer
call printf
; Convert b4 to a string and print the result:
mov rax, qword ptr b4
mov rdx, qword ptr b4[8]
call otostr
lea rcx, fmtStr5
lea rdx, buffer
call printf
leave
pop rdi
ret ; Returns to caller
asmMain endp
end
Listing 9-7: 128-bit extended-precision decimal output routine
Here’s the build command and program output:
C:\>build listing9-7
C:\>echo off
Assembling: listing9-7.asm
c.cpp
C:\>listing9-7
Calling Listing 9-7:
otoStr(0): string=0
otoStr(1234567890): string=1234567890
otoStr(2147483648): string=2147483648
otoStr(4294967296): string=4294967296
otoStr(FFF...FFFF):
string=340282366920938463463374607431768211455
Listing 9-7 terminated
Sadly, we cannot use the fbstp
instruction to improve the performance of this algorithm as fbstp
is limited to 80-bit BCD values.
Once you have an extended-precision unsigned decimal output routine, writing an extended-precision signed decimal output routine is easy. The basic algorithm is similar to that for 64-bit integers given earlier:
To check the sign of an extended-precision integer, test the HO bit of the number. To negate a large value, the best solution is probably to subtract that value from 0. Listing 9-8 is a quick version of i128toStr
that uses the otoStr
routine from the previous section.
; i128toStr - Converts a 128-bit signed integer to a string.
; Inputs:
; RDX:RAX - Signed integer to convert.
; RDI - Pointer to buffer to receive string.
i128toStr proc
push rax
push rdx
push rdi
test rdx, rdx ; Is number negative?
jns notNeg
mov byte ptr [rdi], '-'
inc rdi
neg rdx ; 128-bit negation
neg rax
sbb rdx, 0
notNeg: call otostr
pop rdi
pop rdx
pop rax
ret
i128toStr endp
Listing 9-8: 128-bit signed integer-to-string conversion
The code in the previous sections converted signed and unsigned integers to strings by using the minimum number of necessary character positions. To create nicely formatted tables of values, you will need to write functions that provide appropriate padding in front of the string of digits before actually emitting the digits. Once you have the “unformatted” versions of these routines, implementing the formatted versions is easy.
The first step is to write iSize
and uSize
routines that compute the minimum number of character positions needed to display the value. One algorithm to accomplish this is similar to the numeric string conversion routines. In fact, the only difference is that you initialize a counter to 0 upon entry into the routine (for example, the nonrecursive shell routine), and you increment this counter rather than outputting a digit on each recursive call. (Don’t forget to increment the counter inside iSize
if the number is negative; you must allow for the output of the minus sign.) After the calculation is complete, these routines should return the size of the operand in the EAX register.
The only problem is that such a conversion scheme is slow (using recursion and div
is not very fast). As it turns out, a brute-force version that simply compares the integer value against 1, 10, 100, 1000, and so on, works much faster. Here’s the code that will do this:
; uSize - Determines how many character positions it will take
; to hold a 64-bit numeric-to-string conversion.
; Input:
; RAX - Number to check.
; Returns:
; RAX - Number of character positions required.
dig2 qword 10
dig3 qword 100
dig4 qword 1000
dig5 qword 10000
dig6 qword 100000
dig7 qword 1000000
dig8 qword 10000000
dig9 qword 100000000
dig10 qword 1000000000
dig11 qword 10000000000
dig12 qword 100000000000
dig13 qword 1000000000000
dig14 qword 10000000000000
dig15 qword 100000000000000
dig16 qword 1000000000000000
dig17 qword 10000000000000000
dig18 qword 100000000000000000
dig19 qword 1000000000000000000
dig20 qword 10000000000000000000
uSize proc
push rdx
cmp rax, dig10
jae ge10
cmp rax, dig5
jae ge5
mov edx, 4
cmp rax, dig4
jae allDone
dec edx
cmp rax, dig3
jae allDone
dec edx
cmp rax, dig2
jae allDone
dec edx
jmp allDone
ge5: mov edx, 9
cmp rax, dig9
jae allDone
dec edx
cmp rax, dig8
jae allDone
dec edx
cmp rax, dig7
jae allDone
dec edx
cmp rax, dig6
jae allDone
dec edx ; Must be 5
jmp allDone
ge10: cmp rax, dig14
jae ge14
mov edx, 13
cmp rax, dig13
jae allDone
dec edx
cmp rax, dig12
jae allDone
dec edx
cmp rax, dig11
jae allDone
dec edx ; Must be 10
jmp allDone
ge14: mov edx, 20
cmp rax, dig20
jae allDone
dec edx
cmp rax, dig19
jae allDone
dec edx
cmp rax, dig18
jae allDone
dec edx
cmp rax, dig17
jae allDone
dec edx
cmp rax, dig16
jae allDone
dec edx
cmp rax, dig15
jae allDone
dec edx ; Must be 14
allDone: mov rax, rdx ; Return digit count
pop rdx
ret
uSize endp
For signed integers, you can use the following code:
; iSize - Determines the number of print positions required by
; a 64-bit signed integer.
iSize proc
test rax, rax
js isNeg
jmp uSize ; Effectively a call and ret
; If the number is negative, negate it, call uSize,
; and then bump the size up by 1 (for the "-" character):
isNeg: neg rax
call uSize
inc rax
ret
iSize endp
For extended-precision size operations, the brute-force approach quickly becomes unwieldy (64 bits is bad enough). The best solution is to divide your extended-precision value by a power of 10 (say, 1e+18). This will reduce the size of the number by 18 digits. Repeat this process as long as the quotient is greater than 64 bits (keeping track of the number of times you’ve divided the number by 1e+18). When the quotient fits into 64 bits (19 or 20 digits), call the 64-bit uSize
function and add in the number of digits you eliminated with the division operation (18 for each division by 1e+18). The implementation is left to you on this one . . .
Once you have the iSize
and uSize
routines, writing the formatted output routines, utoStrSize
or itoStrSize
, is easy. On initial entry, these routines call the corresponding iSize
or uSize
routine to determine the number of character positions for the number. If the value that the iSize
or uSize
routine returns is greater than the value of the minimum size parameter (passed into utoStrSize
or itoStrSize
), no other formatting is necessary. If the value of the parameter size is greater than the value iSize
or uSize
returns, the program must compute the difference between these two values and emit that many spaces (or other filler characters) to the output string before the numeric conversion. Listing 9-9 shows the utoStrSize
and itoStrSize
functions.
; utoStrSize - Converts an unsigned integer to a formatted string
; having at least "minDigits" character positions.
; If the actual number of digits is smaller than
; "minDigits" then this procedure inserts enough
; "pad" characters to extend the size of the string.
; Inputs:
; RAX - Number to convert to string.
; CL - minDigits (minimum print positions).
; CH - Padding character.
; RDI - Buffer pointer for output string.
utoStrSize proc
push rcx
push rdi
push rax
call uSize ; Get actual number of digits
sub cl, al ; >= the minimum size?
jbe justConvert
; If the minimum size is greater than the number of actual
; digits, we need to emit padding characters here.
; Note that this code used "sub" rather than "cmp" above.
; As a result, CL now contains the number of padding
; characters to emit to the string (CL is always positive
; at this point as negative and zero results would have
; branched to justConvert).
padLoop: mov [rdi], ch
inc rdi
dec cl
jne padLoop
; Okay, any necessary padding characters have already been
; added to the string. Call utoStr to convert the number
; to a string and append to the buffer:
justConvert:
mov rax, [rsp] ; Retrieve original value
call utoStr
pop rax
pop rdi
pop rcx
ret
utoStrSize endp
; itoStrSize - Converts a signed integer to a formatted string
; having at least "minDigits" character positions.
; If the actual number of digits is smaller than
; "minDigits" then this procedure inserts enough
; "pad" characters to extend the size of the string.
; Inputs:
; RAX - Number to convert to string.
; CL - minDigits (minimum print positions).
; CH - Padding character.
; RDI - Buffer pointer for output string.
itoStrSize proc
push rcx
push rdi
push rax
call iSize ; Get actual number of digits
sub cl, al ; >= the minimum size?
jbe justConvert
; If the minimum size is greater than the number of actual
; digits, we need to emit padding characters here.
; Note that this code used "sub" rather than "cmp" above.
; As a result, CL now contains the number of padding
; characters to emit to the string (CL is always positive
; at this point as negative and zero results would have
; branched to justConvert).
padLoop: mov [rdi], ch
inc rdi
dec cl
jne padLoop
; Okay, any necessary padding characters have already been
; added to the string. Call utoStr to convert the number
; to a string and append to the buffer:
justConvert:
mov rax, [rsp] ; Retrieve original value
call itoStr
pop rax
pop rdi
pop rcx
ret
itoStrSize endp
Listing 9-9: Formatted integer-to-string conversion functions
The code appearing thus far in this chapter has dealt with converting integer numeric values to character strings (typically for output to the user). Converting floating-point values to a string is just as important. This section (and its subsections) covers that conversion.
Floating-point values can be converted to strings in one of two forms:
Regardless of the final output format, two distinct operations are needed to convert a value in floating-point form to a character string. First, you must convert the mantissa to an appropriate string of digits. Second, you must convert the exponent to a string of digits.
However, this isn’t a simple case of converting two integer values to a decimal string and concatenating them (with an e between the mantissa and exponent). First of all, the mantissa is not an integer value: it is a fixed-point fractional binary value. Simply treating it as an n-bit binary value (where n is the number of mantissa bits) will almost always result in an incorrect conversion. Second, while the exponent is, more or less, an integer value,1 it represents a power of 2, not a power of 10. Displaying that power of 2 as an integer value is not appropriate for decimal floating-point representation. Dealing with these two issues (fractional mantissa and binary exponent) is the major complication associated with converting a floating-point value to a string.
Though there are three floating-point formats on the x86-64—single-precision (32-bit real4
), double-precision (64-bit real8
), and extended-precision (80-bit real10
)—the x87 FPU automatically converts the real4
and real8
formats to real10
upon loading the value into the FPU. Therefore, by using the x87 FPU for all floating-point arithmetic during the conversion, all we need to do is write code to convert real10
values into string form.
real10
floating-point values have a 64-bit mantissa. This is not a 64-bit integer. Instead, those 64 bits represent a value between 0 and slightly less than 2. (See “IEEE Floating-Point Formats” in Chapter 2 for more details on the IEEE 80-bit floating-point format.) Bit 63 is usually 1. If bit 63 is 0, the mantissa is denormalized, representing numbers between 0 and about 3.65 × 10-4951.
To output the mantissa in decimal form with approximately 18 digits of precision, the trick is to successively multiply or divide the floating-point value by 10 until the number is between 1e+18 and just less than 1e+19 (that is, 9.9999 . . . e+18). Once the exponent is in the appropriate range, the mantissa bits form an 18-digit integer value (no fractional part), which can be converted to a decimal string to obtain the 18 digits that make up the mantissa value (using our friend, the fbstp
instruction). In practice, you would multiply or divide by large powers of 10 to get the value into the range 1e+18 to 1e+19. This is faster (fewer floating-point operations) and more accurate (also because of fewer floating-point operations).
To convert the exponent to an appropriate decimal string, you need to track the number of multiplications or divisions by 10. For each division by 10, add 1 to the decimal exponent value; for each multiplication by 10, subtract 1 from the decimal exponent value. At the end of the process, subtract 18 from the decimal exponent value (as this process produces a value whose exponent is 18) and convert the decimal exponent value to a string.
To convert the exponent to a string of decimal digits, use the following algorithm:
fbstp
instruction will not be able to convert these values to a packed BCD value.
To resolve this issue, the code should explicitly test for values in this range and round them up to 1e+17 (and increment the decimal exponent value, should this happen). In some cases, values could be greater than 1e+19. In such instances, one last division by 10.0 will solve the problem.
fbstp
instruction can convert to a packed BCD value, so the conversion function uses fbstp
to do this conversion.Listing 9-10 provides the (abbreviated) code and data to implement the mantissa-to-string conversion function, FPDigits
. FPDigits
converts the mantissa to a sequence of 18 digits and returns the decimal exponent value in the EAX register. It doesn’t place a decimal point anywhere in the string, nor does it process the exponent at all.
.data
align 4
; TenTo17 - Holds the value 1.0e+17. Used to get a floating-
; point number into the range x.xxxxxxxxxxxxe+17.
TenTo17 real10 1.0e+17
; PotTblN - Hold powers of 10 raised to negative powers of 2.
PotTblN real10 1.0,
1.0e-1,
1.0e-2,
1.0e-4,
1.0e-8,
1.0e-16,
1.0e-32,
1.0e-64,
1.0e-128,
1.0e-256,
1.0e-512,
1.0e-1024,
1.0e-2048,
1.0e-4096
; PotTblP - Hold powers of 10 raised to positive powers of 2.
align 4
PotTblP real10 1.0,
1.0e+1,
1.0e+2,
1.0e+4,
1.0e+8,
1.0e+16,
1.0e+32,
1.0e+64,
1.0e+128,
1.0e+256,
1.0e+512,
1.0e+1024,
1.0e+2048,
1.0e+4096
; ExpTbl - Integer equivalents to the powers
; in the tables above.
align 4
ExpTab dword 0,
1,
2,
4,
8,
16,
32,
64,
128,
256,
512,
1024,
2048,
4096
.
.
.
*************************************************************
; FPDigits - Used to convert a floating-point number on the FPU
; stack (ST(0)) to a string of digits.
; Entry Conditions:
; ST(0) - 80-bit number to convert.
; Note: code requires two free FPU stack elements.
; RDI - Points at array of at least 18 bytes where
; FPDigits stores the output string.
; Exit Conditions:
; RDI - Converted digits are found here.
; RAX - Contains exponent of the number.
; CL - Contains the sign of the mantissa (" " or "-").
; ST(0) - Popped from stack.
*************************************************************
P10TblN equ <real10 ptr [r8]>
P10TblP equ <real10 ptr [r9]>
xTab equ <dword ptr [r10]>
FPDigits proc
push rbx
push rdx
push rsi
push r8
push r9
push r10
; Special case if the number is zero.
ftst
fstsw ax
sahf
jnz fpdNotZero
; The number is zero, output it as a special case.
fstp tbyte ptr [rdi] ; Pop value off FPU stack
mov rax, "00000000"
mov [rdi], rax
mov [rdi + 8], rax
mov [rdi + 16], ax
add rdi, 18
xor edx, edx ; Return an exponent of 0
mov bl, ' ' ; Sign is positive
jmp fpdDone
fpdNotZero:
; If the number is not zero, then fix the sign of the value.
mov bl, ' ' ; Assume it's positive
jnc WasPositive ; Flags set from sahf above
fabs ; Deal only with positive numbers
mov bl, '-' ; Set the sign return result
WasPositive:
; Get the number between 1 and 10 so we can figure out
; what the exponent is. Begin by checking to see if we have
; a positive or negative exponent.
xor edx, edx ; Initialize exponent to 0
fld1
fcomip st(0), st(1)
jbe PosExp
; We've got a value between zero and one, exclusive,
; at this point. That means this number has a negative
; exponent. Multiply the number by an appropriate power
; of 10 until we get it in the range 1 through 10.
mov esi, sizeof PotTblN ; After last element
mov ecx, sizeof ExpTab ; Ditto
lea r8, PotTblN
lea r9, PotTblP
lea r10, ExpTab
CmpNegExp:
sub esi, 10 ; Move to previous element
sub ecx, 4 ; Zeroes HO bytes
jz test1
fld P10TblN[rsi * 1] ; Get current power of 10
fcomip st(0), st(1) ; Compare against NOS
jbe CmpNegExp ; While Table >= value
mov eax, xTab[rcx * 1]
test eax, eax
jz didAllDigits
sub edx, eax
fld P10TblP[rsi * 1]
fmulp
jmp CmpNegExp
; If the remainder is *exactly* 1.0, then we can branch
; on to InRange1_10; otherwise, we still have to multiply
; by 10.0 because we've overshot the mark a bit.
test1:
fld1
fcomip st(0), st(1)
je InRange1_10
didAllDigits:
; If we get to this point, then we've indexed through
; all the elements in the PotTblN and it's time to stop.
fld P10TblP[10] ; 10.0
fmulp
dec edx
jmp InRange1_10
; At this point, we've got a number that is 1 or greater.
; Once again, our task is to get the value between 1 and 10.
PosExp:
mov esi, sizeof PotTblP ; After last element
mov ecx, sizeof ExpTab ; Ditto
lea r9, PotTblP
lea r10, ExpTab
CmpPosExp:
sub esi, 10 ; Move back 1 element in
sub ecx, 4 ; PotTblP and ExpTbl
fld P10TblP[rsi * 1]
fcomip st(0), st(1)
ja CmpPosExp;
mov eax, xTab[rcx * 1]
test eax, eax
jz InRange1_10
add edx, eax
fld P10TblP[rsi * 1]
fdivp
jmp CmpPosExp
InRange1_10:
; Okay, at this point the number is in the range 1 <= x < 10.
; Let's multiply it by 1e+18 to put the most significant digit
; into the 18th print position. Then convert the result to
; a BCD value and store away in memory.
sub rsp, 24 ; Make room for BCD result
fld TenTo17
fmulp
; We need to check the floating-point result to make sure it
; is not outside the range we can legally convert to a BCD
; value.
; Illegal values will be in the range:
; >999,999,999,999,999,999 ... <1,000,000,000,000,000,000
; $403a_de0b_6b3a_763f_ff01 ... $403a_de0b_6b3a_763f_ffff
; Should one of these values appear, round the result up to
; $403a_de0b_6b3a_7640_0000:
fstp real10 ptr [rsp]
cmp word ptr [rsp + 8], 403ah
jne noRounding
cmp dword ptr [rsp + 4], 0de0b6b3ah
jne noRounding
mov eax, [rsp]
cmp eax, 763fff01h
jb noRounding;
cmp eax, 76400000h
jae TooBig
fld TenTo17
inc edx ; Inc exp as this is really 10^18
jmp didRound
; If we get down here, there were problems getting the
; value in the range 1 <= x <= 10 above and we've got a value
; that is 10e+18 or slightly larger. We need to compensate for
; that here.
TooBig:
lea r9, PotTblP
fld real10 ptr [rsp]
fld P10TblP[10] ; /10
fdivp
inc edx ; Adjust exp due to fdiv
jmp didRound
noRounding:
fld real10 ptr [rsp]
didRound:
fbstp tbyte ptr [rsp]
; The data on the stack contains 18 BCD digits. Convert these
; to ASCII characters and store them at the destination location
; pointed at by EDI.
mov ecx, 8
repeatLp:
mov al, byte ptr [rsp + rcx]
shr al, 4 ; Always in the
or al, '0' ; range "0" to "9"
mov [rdi], al
inc rdi
mov al, byte ptr [rsp + rcx]
and al, 0fh
or al, '0'
mov [rdi], al
inc rdi
dec ecx
jns repeatLp
add rsp, 24 ; Remove BCD data from stack
fpdDone:
mov eax, edx ; Return exponent in EAX
mov cl, bl ; Return sign in CL
pop r10
pop r9
pop r8
pop rsi
pop rdx
pop rbx
ret
FPDigits endp
Listing 9-10: Floating-point mantissa-to-string conversion
The FPDigits
function does most of the work needed to convert a floating-point value to a string in decimal notation: it converts the mantissa to a string of digits and provides the exponent in a decimal integer form. Although the decimal format does not explicitly display the exponent value, a procedure that converts the floating-point value to a decimal string will need the (decimal) exponent value to determine where to put the decimal point. Along with a few additional arguments that the caller supplies, it’s relatively easy to take the output from FPDigits
and convert it to an appropriately formatted decimal string of digits.
The final function to write is r10ToStr
, the main function to call when converting a real10
value to a string. This is a formatted output function that translates the binary floating-point value by using standard formatting options to control the output width, the number of positions after the decimal point, and any fill characters to write where digits don’t appear (usually, this is a space). The r10ToStr
function call will need the following arguments:
r10
real10
value to convert to a string (if r10
is a real4
or real8
value, the FPU will automatically convert it to a real10
value when loading it into the FPU).fWidth
decDigits
fWidth
because there must be room for a sign character, at least one digit to the left of the decimal point, and the decimal point. If this value is 0, the conversion routine will not emit a decimal point to the string. This is an unsigned value; if the caller supplies a negative number here, the procedure will treat it as a very large positive value (and will return an error).fill
r10ToStr
produces uses fewer characters than fWidth
, the procedure will right-justify the numeric value in the output string and fill the leftmost characters with this fill
character (which is usually a space character).buffer
maxLength
fWidth
is greater than or equal to this value), then it returns an error.The string output operation has only three real tasks: properly position the decimal point (if present), copy only those digits specified by the fWidth
value, and round the truncated digits into the output digits.
The rounding operation is the most interesting part of the procedure. The r10ToStr
function converts the real10
value to ASCII characters before rounding because it’s easier to round the result after the conversion. So the rounding operation consists of adding 5 to the (ASCII) digit just beyond the least significant displayed digit. If this sum exceeds (the character) 9, the rounding algorithm has to add 1 to the least significant displayed digit. If that sum exceeds 9, the algorithm must subtract (the value) 10 from the character and add 1 to the next least significant digit. This process repeats until reaching the most significant digit or until there is no carry out of a given digit (that is, the sum does not exceed 9). In the (rare) case that rounding bubbles through all the digits (for example, the string is “999999 . . . 9”), then the rounding algorithm has to replace the string with “10000 . . . 0” and increment the decimal exponent by 1.
The algorithm for emitting the string differs for values with negative and non-negative exponents. Negative exponents are probably the easiest to process. Here’s the algorithm for emitting values with a negative exponent:
decDigits
.decDigits
is less than 4, the function sets it to 4 as a default value.3decDigits
is greater than fWidth
, the function emits fWidth "#"
characters to the string and returns.decDigits
is less than fWidth
, then output (fWidth - decDigits)
padding characters (fill
) to the output string.r10
was negative, emit -0.
to the string; otherwise, emit 0.
to the string (with a leading space in front of the 0 if non-negative).0.
or -0.
characters), then the function outputs the specified (fWidth
) characters from the converted digit string. If the width is greater than 21, the function emits all 18 digits from the converted digits and follows it by however many 0 characters are necessary to fill out the field width.If the exponent is positive or 0, the conversion is slightly more complicated. First, the code has to determine the number of character positions required by the result. This is computed as follows:
exponent + 2 + decDigits
+ (0 if decDigits
is 0, 1 otherwise)
The exponent value is the number of digits to the left of the decimal point (minus 1). The 2
component is present because there is always a position for the sign character (space or hyphen) and there is always at least one digit to the left of the decimal point. The decDigits
component adds in the number of digits to appear after the decimal point. Finally, this equation adds in 1 for the dot character if a decimal point is present (that is, if decDigits
is greater than 0).
Once the required width is computed, the function compares this value against the fWidth
value the caller supplies. If the computed value is greater than fWidth
, the function emits fWidth
“#
” characters and returns. Otherwise, it can emit the digits to the output string.
As happens with negative exponents, the code begins by determining whether the number will consume all the character positions in the output string. If not, it computes the difference between fWidth
and the actual number of characters and outputs the fill
character to pad the numeric string. Next, it outputs a space or a hyphen character (depending on the sign of the original value). Then the function outputs the digits to the left of the decimal point (by counting down the exponent
value). If the decDigits
value is nonzero, the function emits the dot character and any digits remaining in the digit string that FPDigits
produced. If the function ever exceeds the 18 digits that FPDigits
produces (either before or after the decimal point), then the function fills the remaining positions with the 0 character. Finally, the function emits the zero-terminating byte for the string and returns to the caller.
Listing 9-11 provides the source code for the r10ToStr
function.
***********************************************************
; r10ToStr - Converts a real10 floating-point number to the
; corresponding string of digits. Note that this
; function always emits the string using decimal
; notation. For scientific notation, use the e10ToBuf
; routine.
; On Entry:
; r10 - real10 value to convert.
; Passed in ST(0).
; fWidth - Field width for the number (note that this
; is an *exact* field width, not a minimum
; field width).
; Passed in EAX (RAX).
; decimalpts - # of digits to display after the decimal pt.
; Passed in EDX (RDX).
; fill - Padding character if the number is smaller
; than the specified field width.
; Passed in CL (RCX).
; buffer - Stores the resulting characters in
; this string.
; Address passed in RDI.
; maxLength - Maximum string length.
; Passed in R8d (R8).
; On Exit:
; Buffer contains the newly formatted string. If the
; formatted value does not fit in the width specified,
; r10ToStr will store "#" characters into this string.
; Carry - Clear if success; set if an exception occurs.
; If width is larger than the maximum length of
; the string specified by buffer, this routine
; will return with the carry set and RAX = -1,
; -2, or -3.
***********************************************************
r10ToStr proc
; Local variables:
fWidth equ <dword ptr [rbp - 8]> ; RAX: uns32
decDigits equ <dword ptr [rbp - 16]> ; RDX: uns32
fill equ <[rbp - 24]> ; CL: char
bufPtr equ <[rbp - 32]> ; RDI: pointer
exponent equ <dword ptr [rbp - 40]> ; uns32
sign equ <byte ptr [rbp - 48]> ; char
digits equ <byte ptr [rbp - 128]> ; char[80]
maxWidth = 64 ; Must be smaller than 80 - 2
push rdi
push rbx
push rcx
push rdx
push rsi
push rax
push rbp
mov rbp, rsp
sub rsp, 128 ; 128 bytes of local vars
; First, make sure the number will fit into the
; specified string.
cmp eax, r8d ; R8d = max length
jae strOverflow
; If the width is zero, raise an exception:
test eax, eax
jz voor ; Value out of range
mov bufPtr, rdi
mov qword ptr decDigits, rdx
mov fill, rcx
mov qword ptr fWidth, rax
; If the width is too big, raise an exception:
cmp eax, maxWidth
ja badWidth
; Okay, do the conversion.
; Begin by processing the mantissa digits:
lea rdi, digits ; Store result here
call FPDigits ; Convert r80 to string
mov exponent, eax ; Save exp result
mov sign, cl ; Save mantissa sign char
; Round the string of digits to the number of significant
; digits we want to display for this number:
cmp eax, 17
jl dontForceWidthZero
xor rax, rax ; If the exp is negative or
; too large, set width to 0
dontForceWidthZero:
mov rbx, rax ; Really just 8 bits
add ebx, decDigits ; Compute rounding position
cmp ebx, 17
jge dontRound ; Don't bother if a big #
; To round the value to the number of significant digits,
; go to the digit just beyond the last one we are considering
; (EAX currently contains the number of decimal positions)
; and add 5 to that digit. Propagate any overflow into the
; remaining digit positions.
inc ebx ; Index + 1 of last sig digit
mov al, digits[rbx * 1] ; Get that digit
add al, 5 ; Round (for example, +0.5)
cmp al, '9'
jbe dontRound
mov digits[rbx * 1], '0' + 10 ; Force to zero
whileDigitGT9: ; (See sub 10 below)
sub digits[rbx * 1], 10 ; Sub out overflow,
dec ebx ; carry, into prev
js hitFirstDigit; ; digit (until 1st
; digit in the #)
inc digits[rbx * 1]
cmp digits[rbx], '9' ; Overflow if > "9"
ja whileDigitGT9
jmp dontRound
hitFirstDigit:
; If we get to this point, then we've hit the first
; digit in the number. So we've got to shift all
; the characters down one position in the string of
; bytes and put a "1" in the first character position.
mov ebx, 17
repeatUntilEBXeq0:
mov al, digits[rbx * 1]
mov digits[rbx * 1 + 1], al
dec ebx
jnz repeatUntilEBXeq0
mov digits, '1'
inc exponent ; Because we added a digit
dontRound:
; Handle positive and negative exponents separately.
mov rdi, bufPtr ; Store the output here
cmp exponent, 0
jge positiveExponent
; Negative exponents:
; Handle values between 0 and 1.0 here (negative exponents
; imply negative powers of 10).
; Compute the number's width. Since this value is between
; 0 and 1, the width calculation is easy: it's just the
; number of decimal positions they've specified plus three
; (since we need to allow room for a leading "-0.").
mov ecx, decDigits
add ecx, 3
cmp ecx, 4
jae minimumWidthIs4
mov ecx, 4 ; Minimum possible width is four
minimumWidthIs4:
cmp ecx, fWidth
ja widthTooBig
; This number will fit in the specified field width,
; so output any necessary leading pad characters.
mov al, fill
mov edx, fWidth
sub edx, ecx
jmp testWhileECXltWidth
whileECXltWidth:
mov [rdi], al
inc rdi
inc ecx
testWhileECXltWidth:
cmp ecx, fWidth
jb whileECXltWidth
; Output " 0." or "-0.", depending on the sign of the number.
mov al, sign
cmp al, '-'
je isMinus
mov al, ' '
isMinus: mov [rdi], al
inc rdi
inc edx
mov word ptr [rdi], '.0'
add rdi, 2
add edx, 2
; Now output the digits after the decimal point:
xor ecx, ecx ; Count the digits in ECX
lea rbx, digits ; Pointer to data to output d
; If the exponent is currently negative, or if
; we've output more than 18 significant digits,
; just output a zero character.
repeatUntilEDXgeWidth:
mov al, '0'
inc exponent
js noMoreOutput
cmp ecx, 18
jge noMoreOutput
mov al, [rbx]
inc ebx
noMoreOutput:
mov [rdi], al
inc rdi
inc ecx
inc edx
cmp edx, fWidth
jb repeatUntilEDXgeWidth
jmp r10BufDone
; If the number's actual width was bigger than the width
; specified by the caller, emit a sequence of "#" characters
; to denote the error.
widthTooBig:
; The number won't fit in the specified field width,
; so fill the string with the "#" character to indicate
; an error.
mov ecx, fWidth
mov al, '#'
fillPound: mov [rdi], al
inc rdi
dec ecx
jnz fillPound
jmp r10BufDone
; Handle numbers with a positive exponent here.
positiveExponent:
; Compute # of digits to the left of the ".".
; This is given by:
; Exponent ; # of digits to left of "."
; + 2 ; Allow for sign and there
; ; is always 1 digit left of "."
; + decimalpts ; Add in digits right of "."
; + 1 ; If there is a decimal point
mov edx, exponent ; Digits to left of "."
add edx, 2 ; 1 digit + sign posn
cmp decDigits, 0
je decPtsIs0
add edx, decDigits ; Digits to right of "."
inc edx ; Make room for the "."
decPtsIs0:
; Make sure the result will fit in the
; specified field width.
cmp edx, fWidth
ja widthTooBig
; If the actual number of print positions
; is fewer than the specified field width,
; output leading pad characters here.
cmp edx, fWidth
jae noFillChars
mov ecx, fWidth
sub ecx, edx
jz noFillChars
mov al, fill
fillChars: mov [rdi], al
inc rdi
dec ecx
jnz fillChars
noFillChars:
; Output the sign character.
mov al, sign
cmp al, '-'
je outputMinus;
mov al, ' '
outputMinus:
mov [rdi], al
inc rdi
; Okay, output the digits for the number here.
xor ecx, ecx ; Counts # of output chars
lea rbx, digits ; Ptr to digits to output
; Calculate the number of digits to output
; before and after the decimal point.
mov edx, decDigits ; Chars after "."
add edx, exponent ; # chars before "."
inc edx ; Always one digit before "."
; If we've output fewer than 18 digits, go ahead
; and output the next digit. Beyond 18 digits,
; output zeros.
repeatUntilEDXeq0:
mov al, '0'
cmp ecx, 18
jnb putChar
mov al, [rbx]
inc rbx
putChar: mov [rdi], al
inc rdi
; If the exponent decrements to zero,
; then output a decimal point.
cmp exponent, 0
jne noDecimalPt
cmp decDigits, 0
je noDecimalPt
mov al, '.'
mov [rdi], al
inc rdi
noDecimalPt:
dec exponent ; Count down to "." output
inc ecx ; # of digits thus far
dec edx ; Total # of digits to output
jnz repeatUntilEDXeq0
; Zero-terminate string and leave:
r10BufDone: mov byte ptr [rdi], 0
leave
clc ; No error
jmp popRet
badWidth: mov rax, -2 ; Illegal width
jmp ErrorExit
strOverflow:
mov rax, -3 ; String overflow
jmp ErrorExit
voor: or rax, -1 ; Range error
ErrorExit: leave
stc ; Error
mov [rsp], rax ; Change RAX on return
popRet: pop rax
pop rsi
pop rdx
pop rcx
pop rbx
pop rdi
ret
r10ToStr endp
Listing 9-11: r10ToStr
conversion function
Converting a floating-point value to exponential (scientific) form is a bit easier than converting it to decimal form. The mantissa always takes the form sx.y where s is a hyphen or a space, x is exactly one decimal digit, and y is one or more decimal digits. The FPDigits
function does almost all the work to create this string. The exponential conversion function needs to output the mantissa string with sign and decimal point characters and then output the decimal exponent for the number. Converting the exponent value (returned as a decimal integer in the EAX register by FPDigits
) to a string is just the numeric-to-decimal string conversion given earlier in this chapter, using different output formatting.
The function this chapter presents allows you to specify the number of digits for the exponent as 1, 2, 3, or 4. If the exponent requires more digits than the caller specifies, the function returns a failure. If it requires fewer digits than the caller specifies, the function pads the exponent with leading 0s. To emulate the typical floating-point conversion forms, specify an exponent size of 2 for single-precision values, 3 for double-precision values, and 4 for extended-precision values.
Listing 9-12 provides a quick-and-dirty function that converts the decimal exponent value to the appropriate string form and emits those characters to a buffer. This function leaves RDI pointing beyond the last exponent digit and doesn’t zero-terminate the string. It’s really just a helper function to output characters for the e10ToStr
function that will appear in the next listing.
*************************************************************
; expToBuf - Unsigned integer to buffer.
; Used to output up to 4-digit exponents.
; Inputs:
; EAX: Unsigned integer to convert.
; ECX: Print width 1-4.
; RDI: Points at buffer.
; FPU: Uses FPU stack.
; Returns:
; RDI: Points at end of buffer.
expToBuf proc
expWidth equ <[rbp + 16]>
exp equ <[rbp + 8]>
bcd equ <[rbp - 16]>
push rdx
push rcx ; At [RBP + 16]
push rax ; At [RBP + 8]
push rbp
mov rbp, rsp
sub rsp, 16
; Verify exponent digit count is in the range 1-4:
cmp rcx, 1
jb badExp
cmp rcx, 4
ja badExp
mov rdx, rcx
; Verify the actual exponent will fit in the number of digits:
cmp rcx, 2
jb oneDigit
je twoDigits
cmp rcx, 3
ja fillZeros ; 4 digits, no error
cmp eax, 1000
jae badExp
jmp fillZeros
oneDigit: cmp eax, 10
jae badExp
jmp fillZeros
twoDigits: cmp eax, 100
jae badExp
; Fill in zeros for exponent:
fillZeros: mov byte ptr [rdi + rcx * 1 - 1], '0'
dec ecx
jnz fillZeros
; Point RDI at the end of the buffer:
lea rdi, [rdi + rdx * 1 - 1]
mov byte ptr [rdi + 1], 0
push rdi ; Save pointer to end
; Quick test for zero to handle that special case:
test eax, eax
jz allDone
; The number to convert is nonzero.
; Use BCD load and store to convert
; the integer to BCD:
fild dword ptr exp ; Get integer value
fbstp tbyte ptr bcd ; Convert to BCD
; Begin by skipping over leading zeros in
; the BCD value (max 10 digits, so the most
; significant digit will be in the HO nibble
; of byte 4).
mov eax, bcd ; Get exponent digits
mov ecx, expWidth ; Number of total digits
OutputExp: mov dl, al
and dl, 0fh
or dl, '0'
mov [rdi], dl
dec rdi
shr ax, 4
jnz OutputExp
; Zero-terminate the string and return:
allDone: pop rdi
leave
pop rax
pop rcx
pop rdx
clc
ret
badExp: leave
pop rax
pop rcx
pop rdx
stc
ret
expToBuf endp
Listing 9-12: Exponent conversion function
The actual e10ToStr
function in Listing 9-13 is similar to the r10ToStr
function. The output of the mantissa is less complex because the form is fixed, but there is a little additional work at the end to output the exponent. Refer back to “Converting a Floating-Point Value to a Decimal String” on page 527 for details on the operation of this code.
***********************************************************
; e10ToStr - Converts a real10 floating-point number to the
; corresponding string of digits. Note that this
; function always emits the string using scientific
; notation; use the r10ToStr routine for decimal notation.
; On Entry:
; e10 - real10 value to convert.
; Passed in ST(0).
; width - Field width for the number (note that this
; is an *exact* field width, not a minimum
; field width).
; Passed in RAX (LO 32 bits).
; fill - Padding character if the number is smaller
; than the specified field width.
; Passed in RCX.
; buffer - e10ToStr stores the resulting characters in
; this buffer (passed in RDI).
; expDigs - Number of exponent digits (2 for real4,
; 3 for real8, and 4 for real10).
; Passed in RDX (LO 8 bits).
; maxLength - Maximum buffer size.
; Passed in R8.
; On Exit:
; RDI - Points at end of converted string.
; Buffer contains the newly formatted string. If the
; formatted value does not fit in the width specified,
; e10ToStr will store "#" characters into this string.
; If there was an error, EAX contains -1, -2, or -3
; denoting the error (value out of range, bad width,
; or string overflow, respectively).
***********************************************************
; Unlike the integer-to-string conversions, this routine
; always right-justifies the number in the specified
; string. Width must be a positive number; negative
; values are illegal (actually, they are treated as
; *really* big positive numbers that will always raise
; a string overflow exception).
***********************************************************
e10ToStr proc
fWidth equ <[rbp - 8]> ; RAX
buffer equ <[rbp - 16]> ; RDI
expDigs equ <[rbp - 24]> ; RDX
rbxSave equ <[rbp - 32]>
rcxSave equ <[rbp - 40]>
rsiSave equ <[rbp - 48]>
Exponent equ <dword ptr [rbp - 52]>
MantSize equ <dword ptr [rbp - 56]>
Sign equ <byte ptr [rbp - 60]>
Digits equ <byte ptr [rbp - 128]>
push rbp
mov rbp, rsp
sub rsp, 128
mov buffer, rdi
mov rsiSave, rsi
mov rcxSave, rcx
mov rbxSave, rbx
mov fWidth, rax
mov expDigs, rdx
cmp eax, r8d
jae strOvfl
mov byte ptr [rdi + rax * 1], 0 ; Zero-terminate str
; First, make sure the width isn't zero.
test eax, eax
jz voor
; Just to be on the safe side, don't allow widths greater
; than 1024:
cmp eax, 1024
ja badWidth
; Okay, do the conversion.
lea rdi, Digits ; Store result string here
call FPDigits ; Convert e80 to digit str
mov Exponent, eax ; Save away exponent result
mov Sign, cl ; Save mantissa sign char
; Verify that there is sufficient room for the mantissa's sign,
; the decimal point, two mantissa digits, the "E", and the
; exponent's sign. Also add in the number of digits required
; by the exponent (2 for real4, 3 for real8, 4 for real10).
; -1.2e+00 :real4
; -1.2e+000 :real8
; -1.2e+0000 :real10
mov ecx, 6 ; Char posns for above chars
add ecx, expDigs ; # of digits for the exp
cmp ecx, fWidth
jbe goodWidth
; Output a sequence of "#...#" chars (to the specified width)
; if the width value is not large enough to hold the
; conversion:
mov ecx, fWidth
mov al, '#'
mov rdi, buffer
fillPound: mov [rdi], al
inc rdi
dec ecx
jnz fillPound
jmp exit_eToBuf
; Okay, the width is sufficient to hold the number; do the
; conversion and output the string here:
goodWidth:
mov ebx, fWidth ; Compute the # of mantissa
sub ebx, ecx ; digits to display
add ebx, 2 ; ECX allows for 2 mant digs
mov MantSize,ebx
; Round the number to the specified number of print positions.
; (Note: since there are a maximum of 18 significant digits,
; don't bother with the rounding if the field width is greater
; than 18 digits.)
cmp ebx, 18
jae noNeedToRound
; To round the value to the number of significant digits,
; go to the digit just beyond the last one we are considering
; (EBX currently contains the number of decimal positions)
; and add 5 to that digit. Propagate any overflow into the
; remaining digit positions.
mov al, Digits[rbx * 1] ; Get least sig digit + 1
add al, 5 ; Round (for example, +0.5)
cmp al, '9'
jbe noNeedToRound
mov Digits[rbx * 1], '9' + 1
jmp whileDigitGT9Test
whileDigitGT9:
; Subtract out overflow and add the carry into the previous
; digit (unless we hit the first digit in the number).
sub Digits[rbx * 1], 10
dec ebx
cmp ebx, 0
jl firstDigitInNumber
inc Digits[rbx * 1]
jmp whileDigitGT9Test
firstDigitInNumber:
; If we get to this point, then we've hit the first
; digit in the number. So we've got to shift all
; the characters down one position in the string of
; bytes and put a "1" in the first character position.
mov ebx, 17
repeatUntilEBXeq0:
mov al, Digits[rbx * 1]
mov Digits[rbx * 1 + 1], al
dec ebx
jnz repeatUntilEBXeq0
mov Digits, '1'
inc Exponent ; Because we added a digit
jmp noNeedToRound
whileDigitGT9Test:
cmp Digits[rbx], '9' ; Overflow if char > "9"
ja whileDigitGT9
noNeedToRound:
; Okay, emit the string at this point. This is pretty easy
; since all we really need to do is copy data from the
; digits array and add an exponent (plus a few other simple chars).
xor ecx, ecx ; Count output mantissa digits
mov rdi, buffer
xor edx, edx ; Count output chars
mov al, Sign
cmp al, '-'
je noMinus
mov al, ' '
noMinus: mov [rdi], al
; Output the first character and a following decimal point
; if there are more than two mantissa digits to output.
mov al, Digits
mov [rdi + 1], al
add rdi, 2
add edx, 2
inc ecx
cmp ecx, MantSize
je noDecPt
mov al, '.'
mov [rdi], al
inc rdi
inc edx
noDecPt:
; Output any remaining mantissa digits here.
; Note that if the caller requests the output of
; more than 18 digits, this routine will output zeros
; for the additional digits.
jmp whileECXltMantSizeTest
whileECXltMantSize:
mov al, '0'
cmp ecx, 18
jae justPut0
mov al, Digits[rcx * 1]
justPut0:
mov [rdi], al
inc rdi
inc ecx
inc edx
whileECXltMantSizeTest:
cmp ecx, MantSize
jb whileECXltMantSize
; Output the exponent:
mov byte ptr [rdi], 'e'
inc rdi
inc edx
mov al, '+'
cmp Exponent, 0
jge noNegExp
mov al, '-'
neg Exponent
noNegExp:
mov [rdi], al
inc rdi
inc edx
mov eax, Exponent
mov ecx, expDigs
call expToBuf
jc error
exit_eToBuf:
mov rsi, rsiSave
mov rcx, rcxSave
mov rbx, rbxSave
mov rax, fWidth
mov rdx, expDigs
leave
clc
ret
strOvfl: mov rax, -3
jmp error
badWidth: mov rax, -2
jmp error
voor: mov rax, -1
error: mov rsi, rsiSave
mov rcx, rcxSave
mov rbx, rbxSave
mov rdx, expDigs
leave
stc
ret
e10ToStr endp
Listing 9-13: e10ToStr
conversion function
The routines converting numeric values to strings, and strings to numeric values, have a couple of fundamental differences. First of all, numeric-to-string conversions generally occur without possibility of error;4 string-to-numeric conversion, on the other hand, must handle the real possibility of errors such as illegal characters and numeric overflow.
A typical numeric input operation consists of reading a string of characters from the user and then translating this string of characters into an internal numeric representation. For example, in C++ a statement like cin >> i32;
reads a line of text from the user and converts a sequence of digits appearing at the beginning of that line of text into a 32-bit signed integer (assuming i32
is a 32-bit int
object). The cin >> i32;
statement skips over certain characters, like leading spaces, in the string that may appear before the actual numeric characters. The input string may also contain additional data beyond the end of the numeric input (for example, it is possible to read two integer values from the same input line), and therefore the input conversion routine must determine where the numeric data ends in the input stream.
Typically, C++ achieves this by looking for a character from a set of delimiter characters. The delimiter character set could be something as simple as “any character that is not a numeric digit” or the set of whitespace characters (space, tab, and so on), and perhaps a few other characters such as a comma (,
) or some other punctuation character. For the sake of example, the code in this section will assume that any leading spaces or tab characters (ASCII code 9) may precede any numeric digits, and the conversion stops on the first nondigit character it encounters. Possible error conditions are as follows:
It will be up to the caller to determine if the numeric string ends with an invalid character (upon return from the function call).
The basic algorithm to convert a string containing decimal digits to a number is the following:
For signed integer input, you use this same algorithm with the following modifications:
-
), set a flag denoting that the number is negative and skip the “-
” character (if the first character is not -
, then clear the flag).Listing 9-14 implements the conversion algorithm.
; Listing 9-14
; String-to-numeric conversion.
option casemap:none
false = 0
true = 1
tab = 9
nl = 10
.const
ttlStr byte "Listing 9-14", 0
fmtStr1 byte "strtou: String='%s'", nl
byte " value=%I64u", nl, 0
fmtStr2 byte "Overflow: String='%s'", nl
byte " value=%I64x", nl, 0
fmtStr3 byte "strtoi: String='%s'", nl
byte " value=%I64i",nl, 0
unexError byte "Unexpected error in program", nl, 0
value1 byte " 1", 0
value2 byte "12 ", 0
value3 byte " 123 ", 0
value4 byte "1234", 0
value5 byte "1234567890123456789", 0
value6 byte "18446744073709551615", 0
OFvalue byte "18446744073709551616", 0
OFvalue2 byte "999999999999999999999", 0
ivalue1 byte " -1", 0
ivalue2 byte "-12 ", 0
ivalue3 byte " -123 ", 0
ivalue4 byte "-1234", 0
ivalue5 byte "-1234567890123456789", 0
ivalue6 byte "-9223372036854775807", 0
OFivalue byte "-9223372036854775808", 0
OFivalue2 byte "-999999999999999999999", 0
.data
buffer byte 30 dup (?)
.code
externdef printf:proc
; Return program title to C++ program:
public getTitle
getTitle proc
lea rax, ttlStr
ret
getTitle endp
; strtou - Converts string data to a 64-bit unsigned integer.
; Input:
; RDI - Pointer to buffer containing string to convert.
; Output:
; RAX - Contains converted string (if success), error code
; if an error occurs.
; RDI - Points at first char beyond end of numeric string.
; If error, RDI's value is restored to original value.
; Caller can check character at [RDI] after a
; successful result to see if the character following
; the numeric digits is a legal numeric delimiter.
; C - (carry flag) Set if error occurs, clear if
; conversion was successful. On error, RAX will
; contain 0 (illegal initial character) or
; 0FFFFFFFFFFFFFFFFh (overflow).
strtou proc
push rdi ; In case we have to restore RDI
push rdx ; Munged by mul
push rcx ; Holds input char
xor edx, edx ; Zero-extends!
xor eax, eax ; Zero-extends!
; The following loop skips over any whitespace (spaces and
; tabs) that appears at the beginning of the string.
dec rdi ; Because of inc below
skipWS: inc rdi
mov cl, [rdi]
cmp cl, ' '
je skipWS
cmp al, tab
je skipWS
; If we don't have a numeric digit at this point,
; return an error.
cmp cl, '0' ; Note: "0" < "1" < ... < "9"
jb badNumber
cmp cl, '9'
ja badNumber
; Okay, the first digit is good. Convert the string
; of digits to numeric form:
convert: and ecx, 0fh ; Convert to numeric in RCX
mul ten ; Accumulator *= 10
jc overflow
add rax, rcx ; Accumulator += digit
jc overflow
inc rdi ; Move on to next character
mov cl, [rdi]
cmp cl, '0'
jb endOfNum
cmp cl, '9'
jbe convert
; If we get to this point, we've successfully converted
; the string to numeric form:
endOfNum: pop rcx
pop rdx
; Because the conversion was successful, this procedure
; leaves RDI pointing at the first character beyond the
; converted digits. As such, we don't restore RDI from
; the stack. Just bump the stack pointer up by 8 bytes
; to throw away RDI's saved value.
add rsp, 8
clc ; Return success in carry flag
ret
; badNumber - Drop down here if the first character in
; the string was not a valid digit.
badNumber: mov rax, 0
pop rcx
pop rdx
pop rdi
stc ; Return error in carry flag
ret
overflow: mov rax, -1 ; 0FFFFFFFFFFFFFFFFh
pop rcx
pop rdx
pop rdi
stc ; Return error in carry flag
ret
ten qword 10
strtou endp
; strtoi - Converts string data to a 64-bit signed integer.
; Input:
; RDI - Pointer to buffer containing string to convert.
; Output:
; RAX - Contains converted string (if success), error code
; if an error occurs.
; RDI - Points at first char beyond end of numeric string.
; If error, RDI's value is restored to original value.
; Caller can check character at [RDI] after a
; successful result to see if the character following
; the numeric digits is a legal numeric delimiter.
; C - (carry flag) Set if error occurs, clear if
; conversion was successful. On error, RAX will
; contain 0 (illegal initial character) or
; 0FFFFFFFFFFFFFFFFh (-1, indicating overflow).
strtoi proc
negFlag equ <byte ptr [rsp]>
push rdi ; In case we have to restore RDI
sub rsp, 8
; Assume we have a non-negative number.
mov negFlag, false
; The following loop skips over any whitespace (spaces and
; tabs) that appears at the beginning of the string.
dec rdi ; Because of inc below
skipWS: inc rdi
mov al, [rdi]
cmp al, ' '
je skipWS
cmp al, tab
je skipWS
; If the first character we've encountered is "-",
; then skip it, but remember that this is a negative
; number.
cmp al, '-'
jne notNeg
mov negFlag, true
inc rdi ; Skip "-"
notNeg: call strtou ; Convert string to integer
jc hadError
; strtou returned success. Check the negative flag and
; negate the input if the flag contains true.
cmp negFlag, true
jne itsPosOr0
cmp rax, tooBig ; Number is too big
ja overflow
neg rax
itsPosOr0: add rsp, 16 ; Success, so don't restore RDI
clc ; Return success in carry flag
ret
; If we have an error, we need to restore RDI from the stack:
overflow: mov rax, -1 ; Indicate overflow
hadError: add rsp, 8 ; Remove locals
pop rdi
stc ; Return error in carry flag
ret
tooBig qword 7fffffffffffffffh
strtoi endp
; Here is the "asmMain" function.
public asmMain
asmMain proc
push rbp
mov rbp, rsp
sub rsp, 64 ; Shadow storage
; Test unsigned conversions:
lea rdi, value1
call strtou
jc UnexpectedError
lea rcx, fmtStr1
lea rdx, value1
mov r8, rax
call printf
lea rdi, value2
call strtou
jc UnexpectedError
lea rcx, fmtStr1
lea rdx, value2
mov r8, rax
call printf
lea rdi, value3
call strtou
jc UnexpectedError
lea rcx, fmtStr1
lea rdx, value3
mov r8, rax
call printf
lea rdi, value4
call strtou
jc UnexpectedError
lea rcx, fmtStr1
lea rdx, value4
mov r8, rax
call printf
lea rdi, value5
call strtou
jc UnexpectedError
lea rcx, fmtStr1
lea rdx, value5
mov r8, rax
call printf
lea rdi, value6
call strtou
jc UnexpectedError
lea rcx, fmtStr1
lea rdx, value6
mov r8, rax
call printf
lea rdi, OFvalue
call strtou
jnc UnexpectedError
test rax, rax ; Nonzero for overflow
jz UnexpectedError
lea rcx, fmtStr2
lea rdx, OFvalue
mov r8, rax
call printf
lea rdi, OFvalue2
call strtou
jnc UnexpectedError
test rax, rax ; Nonzero for overflow
jz UnexpectedError
lea rcx, fmtStr2
lea rdx, OFvalue2
mov r8, rax
call printf
; Test signed conversions:
lea rdi, ivalue1
call strtoi
jc UnexpectedError
lea rcx, fmtStr3
lea rdx, ivalue1
mov r8, rax
call printf
lea rdi, ivalue2
call strtoi
jc UnexpectedError
lea rcx, fmtStr3
lea rdx, ivalue2
mov r8, rax
call printf
lea rdi, ivalue3
call strtoi
jc UnexpectedError
lea rcx, fmtStr3
lea rdx, ivalue3
mov r8, rax
call printf
lea rdi, ivalue4
call strtoi
jc UnexpectedError
lea rcx, fmtStr3
lea rdx, ivalue4
mov r8, rax
call printf
lea rdi, ivalue5
call strtoi
jc UnexpectedError
lea rcx, fmtStr3
lea rdx, ivalue5
mov r8, rax
call printf
lea rdi, ivalue6
call strtoi
jc UnexpectedError
lea rcx, fmtStr3
lea rdx, ivalue6
mov r8, rax
call printf
lea rdi, OFivalue
call strtoi
jnc UnexpectedError
test rax, rax ; Nonzero for overflow
jz UnexpectedError
lea rcx, fmtStr2
lea rdx, OFivalue
mov r8, rax
call printf
lea rdi, OFivalue2
call strtoi
jnc UnexpectedError
test rax, rax ; Nonzero for overflow
jz UnexpectedError
lea rcx, fmtStr2
lea rdx, OFivalue2
mov r8, rax
call printf
jmp allDone
UnexpectedError:
lea rcx, unexError
call printf
allDone: leave
ret ; Returns to caller
asmMain endp
end
Listing 9-14: Numeric-to-string conversions
Here’s the build command and sample output for this program:
C:\>build listing9-14
C:\>echo off
Assembling: listing9-14.asm
c.cpp
C:\>listing9-14
Calling Listing 9-14:
strtou: String=' 1'
value=1
strtou: String='12 '
value=12
strtou: String=' 123 '
value=123
strtou: String='1234'
value=1234
strtou: String='1234567890123456789'
value=1234567890123456789
strtou: String='18446744073709551615'
value=18446744073709551615
Overflow: String='18446744073709551616'
value=ffffffffffffffff
Overflow: String='999999999999999999999'
value=ffffffffffffffff
strtoi: String=' -1'
value=-1
strtoi: String='-12 '
value=-12
strtoi: String=' -123 '
value=-123
strtoi: String='-1234'
value=-1234
strtoi: String='-1234567890123456789'
value=-1234567890123456789
strtoi: String='-9223372036854775807'
value=-9223372036854775807
Overflow: String='-9223372036854775808'
value=ffffffffffffffff
Overflow: String='-999999999999999999999'
value=ffffffffffffffff
Listing 9-14 terminated
For an extended-precision string-to-numeric conversion, you simply modify the strtou
function to have an extend-precision accumulator and then do an extended-precision multiplication by 10 (rather than a standard multiplication).
As was the case for numeric output, hexadecimal input is the easiest numeric input routine to write. The basic algorithm for hexadecimal-string-to-numeric conversion is the following:
Listing 9-15 implements this extended-precision hexadecimal input routine for 64-bit values.
; Listing 9-15
; Hexadecimal string-to-numeric conversion.
option casemap:none
false = 0
true = 1
tab = 9
nl = 10
.const
ttlStr byte "Listing 9-15", 0
fmtStr1 byte "strtoh: String='%s' "
byte "value=%I64x", nl, 0
fmtStr2 byte "Error, RAX=%I64x, str='%s'", nl, 0
fmtStr3 byte "Error, expected overflow: RAX=%I64x, "
byte "str='%s'", nl, 0
fmtStr4 byte "Error, expected bad char: RAX=%I64x, "
byte "str='%s'", nl, 0
hexStr byte "1234567890abcdef", 0
hexStrOVFL byte "1234567890abcdef0", 0
hexStrBAD byte "x123", 0
.code
externdef printf:proc
; Return program title to C++ program:
public getTitle
getTitle proc
lea rax, ttlStr
ret
getTitle endp
; strtoh - Converts string data to a 64-bit unsigned integer.
; Input:
; RDI - Pointer to buffer containing string to convert.
; Output:
; RAX - Contains converted string (if success), error code
; if an error occurs.
; RDI - Points at first char beyond end of hexadecimal string.
; If error, RDI's value is restored to original value.
; Caller can check character at [RDI] after a
; successful result to see if the character following
; the numeric digits is a legal numeric delimiter.
; C - (carry flag) Set if error occurs, clear if
; conversion was successful. On error, RAX will
; contain 0 (illegal initial character) or
; 0FFFFFFFFFFFFFFFFh (overflow).
strtoh proc
push rcx ; Holds input char
push rdx ; Special mask value
push rdi ; In case we have to restore RDI
; This code will use the value in RDX to test and see if overflow
; will occur in RAX when shifting to the left 4 bits:
mov rdx, 0F000000000000000h
xor eax, eax ; Zero out accumulator
; The following loop skips over any whitespace (spaces and
; tabs) that appears at the beginning of the string.
dec rdi ; Because of inc below
skipWS: inc rdi
mov cl, [rdi]
cmp cl, ' '
je skipWS
cmp al, tab
je skipWS
; If we don't have a hexadecimal digit at this point,
; return an error.
cmp cl, '0' ; Note: "0" < "1" < ... < "9"
jb badNumber
cmp cl, '9'
jbe convert
and cl, 5fh ; Cheesy LC -> UC conversion
cmp cl, 'A'
jb badNumber
cmp cl, 'F'
ja badNumber
sub cl, 7 ; Maps 41h to 46h -> 3Ah to 3Fh
; Okay, the first digit is good. Convert the string
; of digits to numeric form:
convert: test rdx, rax ; See if adding in the current
jnz overflow ; digit will cause an overflow
and ecx, 0fh ; Convert to numeric in RCX
; Multiply 64-bit accumulator by 16 and add in new digit:
shl rax, 4
add al, cl ; Never overflows outside LO 4 bits
; Move on to next character:
inc rdi
mov cl, [rdi]
cmp cl, '0'
jb endOfNum
cmp cl, '9'
jbe convert
and cl, 5fh ; Cheesy LC -> UC conversion
cmp cl, 'A'
jb endOfNum
cmp cl, 'F'
ja endOfNum
sub cl, 7 ; Maps 41h to 46h -> 3Ah to 3Fh
jmp convert
; If we get to this point, we've successfully converted
; the string to numeric form:
endOfNum:
; Because the conversion was successful, this procedure
; leaves RDI pointing at the first character beyond the
; converted digits. As such, we don't restore RDI from
; the stack. Just bump the stack pointer up by 8 bytes
; to throw away RDI's saved value.
add rsp, 8 ; Remove original RDI value
pop rdx ; Restore RDX
pop rcx ; Restore RCX
clc ; Return success in carry flag
ret
; badNumber- Drop down here if the first character in
; the string was not a valid digit.
badNumber: xor rax, rax
jmp errorExit
overflow: or rax, -1 ; Return -1 as error on overflow
errorExit: pop rdi ; Restore RDI if an error occurs
pop rdx
pop rcx
stc ; Return error in carry flag
ret
strtoh endp
; Here is the "asmMain" function.
public asmMain
asmMain proc
push rbp
mov rbp, rsp
sub rsp, 64 ; Shadow storage
; Test hexadecimal conversion:
lea rdi, hexStr
call strtoh
jc error
lea rcx, fmtStr1
mov r8, rax
lea rdx, hexStr
call printf
; Test overflow conversion:
lea rdi, hexStrOVFL
call strtoh
jnc unexpected
lea rcx, fmtStr2
mov rdx, rax
mov r8, rdi
call printf
; Test bad character:
lea rdi, hexStrBAD
call strtoh
jnc unexp2
lea rcx, fmtStr2
mov rdx, rax
mov r8, rdi
call printf
jmp allDone
unexpected: lea rcx, fmtStr3
mov rdx, rax
mov r8, rdi
call printf
jmp allDone
unexp2: lea rcx, fmtStr4
mov rdx, rax
mov r8, rdi
call printf
jmp allDone
error: lea rcx, fmtStr2
mov rdx, rax
mov r8, rdi
call printf
allDone: leave
ret ; Returns to caller
asmMain endp
end
Listing 9-15: Hexadecimal string-to-numeric conversion
Here’s the build command and program output:
C:\>build listing9-15
C:\>echo off
Assembling: listing9-15.asm
c.cpp
C:\>listing9-15
Calling Listing 9-15:
strtoh: String='1234567890abcdef' value=1234567890abcdef
Error, RAX=ffffffffffffffff, str='1234567890abcdef0'
Error, RAX=0, str='x123'
Listing 9-15 terminated
For hexadecimal string conversions that handle numbers greater than 64 bits, you have to use an extended-precision shift left by 4 bits. Listing 9-16 demonstrates the necessary modifications to the strtoh
function for a 128-bit conversion.
; strtoh128 - Converts string data to a 128-bit unsigned integer.
; Input:
; RDI - Pointer to buffer containing string to convert.
; Output:
; RDX:RAX - Contains converted string (if success), error code
; if an error occurs.
; RDI - Points at first char beyond end of hex string.
; If error, RDI's value is restored to original value.
; Caller can check character at [RDI] after a
; successful result to see if the character following
; the numeric digits is a legal numeric delimiter.
; C - (carry flag) Set if error occurs, clear if
; conversion was successful. On error, RAX will
; contain 0 (illegal initial character) or
; 0FFFFFFFFFFFFFFFFh (overflow).
strtoh128 proc
push rbx ; Special mask value
push rcx ; Input char to process
push rdi ; In case we have to restore RDI
; This code will use the value in RDX to test and see if overflow
; will occur in RAX when shifting to the left 4 bits:
mov rbx, 0F000000000000000h
xor eax, eax ; Zero out accumulator
xor edx, edx
; The following loop skips over any whitespace (spaces and
; tabs) that appears at the beginning of the string.
dec rdi ; Because of inc below
skipWS: inc rdi
mov cl, [rdi]
cmp cl, ' '
je skipWS
cmp al, tab
je skipWS
; If we don't have a hexadecimal digit at this point,
; return an error.
cmp cl, '0' ; Note: "0" < "1" < ... < "9"
jb badNumber
cmp cl, '9'
jbe convert
and cl, 5fh ; Cheesy LC -> UC conversion
cmp cl, 'A'
jb badNumber
cmp cl, 'F'
ja badNumber
sub cl, 7 ; Maps 41h to 46h -> 3Ah to 3Fh
; Okay, the first digit is good. Convert the string
; of digits to numeric form:
convert: test rdx, rbx ; See if adding in the current
jnz overflow ; digit will cause an overflow
and ecx, 0fh ; Convert to numeric in RCX
; Multiply 64-bit accumulator by 16 and add in new digit:
shld rdx, rax, 4
shl rax, 4
add al, cl ; Never overflows outside LO 4 bits
; Move on to next character:
inc rdi
mov cl, [rdi]
cmp cl, '0'
jb endOfNum
cmp cl, '9'
jbe convert
and cl, 5fh ; Cheesy LC -> UC conversion
cmp cl, 'A'
jb endOfNum
cmp cl, 'F'
ja endOfNum
sub cl, 7 ; Maps 41h to 46h -> 3Ah to 3Fh
jmp convert
; If we get to this point, we've successfully converted
; the string to numeric form:
endOfNum:
; Because the conversion was successful, this procedure
; leaves RDI pointing at the first character beyond the
; converted digits. As such, we don't restore RDI from
; the stack. Just bump the stack pointer up by 8 bytes
; to throw away RDI's saved value.
add rsp, 8 ; Remove original RDI value
pop rcx ; Restore RCX
pop rbx ; Restore RBX
clc ; Return success in carry flag
ret
; badNumber - Drop down here if the first character in
; the string was not a valid digit.
badNumber: xor rax, rax
jmp errorExit
overflow: or rax, -1 ; Return -1 as error on overflow
errorExit: pop rdi ; Restore RDI if an error occurs
pop rcx
pop rbx
stc ; Return error in carry flag
ret
strtoh128 endp
Listing 9-16: 128-bit hexadecimal string-to-numeric conversion
The algorithm for unsigned decimal input is nearly identical to that for hexadecimal input. In fact, the only difference (beyond accepting only decimal digits) is that you multiply the accumulating value by 10 rather than 16 for each input character (in general, the algorithm is the same for any base; just multiply the accumulating value by the input base). Listing 9-17 demonstrates how to write a 64-bit unsigned decimal input routine.
; Listing 9-17
; 64-bit unsigned decimal string-to-numeric conversion.
option casemap:none
false = 0
true = 1
tab = 9
nl = 10
.const
ttlStr byte "Listing 9-17", 0
fmtStr1 byte "strtou: String='%s' value=%I64u", nl, 0
fmtStr2 byte "strtou: error, rax=%d", nl, 0
qStr byte "12345678901234567", 0
.code
externdef printf:proc
; Return program title to C++ program:
public getTitle
getTitle proc
lea rax, ttlStr
ret
getTitle endp
; strtou - Converts string data to a 64-bit unsigned integer.
; Input:
; RDI - Pointer to buffer containing string to convert.
; Output:
; RAX - Contains converted string (if success), error code
; if an error occurs.
; RDI - Points at first char beyond end of numeric string.
; If error, RDI's value is restored to original value.
; Caller can check character at [RDI] after a
; successful result to see if the character following
; the numeric digits is a legal numeric delimiter.
; C - (carry flag) Set if error occurs, clear if
; conversion was successful. On error, RAX will
; contain 0 (illegal initial character) or
; 0FFFFFFFFFFFFFFFFh (overflow).
strtou proc
push rcx ; Holds input char
push rdx ; Save, used for multiplication
push rdi ; In case we have to restore RDI
xor rax, rax ; Zero out accumulator
; The following loop skips over any whitespace (spaces and
; tabs) that appears at the beginning of the string.
dec rdi ; Because of inc below
skipWS: inc rdi
mov cl, [rdi]
cmp cl, ' '
je skipWS
cmp al, tab
je skipWS
; If we don't have a numeric digit at this point,
; return an error.
cmp cl, '0' ; Note: "0" < "1" < ... < "9"
jb badNumber
cmp cl, '9'
ja badNumber
; Okay, the first digit is good. Convert the string
; of digits to numeric form:
convert: and ecx, 0fh ; Convert to numeric in RCX
; Multiple 64-bit accumulator by 10:
mul ten
test rdx, rdx ; Test for overflow
jnz overflow
add rax, rcx
jc overflow
; Move on to next character:
inc rdi
mov cl, [rdi]
cmp cl, '0'
jb endOfNum
cmp cl, '9'
jbe convert
; If we get to this point, we've successfully converted
; the string to numeric form:
endOfNum:
; Because the conversion was successful, this procedure
; leaves RDI pointing at the first character beyond the
; converted digits. As such, we don't restore RDI from
; the stack. Just bump the stack pointer up by 8 bytes
; to throw away RDI's saved value.
add rsp, 8 ; Remove original RDI value
pop rdx
pop rcx ; Restore RCX
clc ; Return success in carry flag
ret
; badNumber - Drop down here if the first character in
; the string was not a valid digit.
badNumber: xor rax, rax
jmp errorExit
overflow: mov rax, -1 ; 0FFFFFFFFFFFFFFFFh
errorExit: pop rdi
pop rdx
pop rcx
stc ; Return error in carry flag
ret
ten qword 10
strtou endp
; Here is the "asmMain" function.
public asmMain
asmMain proc
push rbp
mov rbp, rsp
sub rsp, 64 ; Shadow storage
; Test hexadecimal conversion:
lea rdi, qStr
call strtou
jc error
lea rcx, fmtStr1
mov r8, rax
lea rdx, qStr
call printf
jmp allDone
error: lea rcx, fmtStr2
mov rdx, rax
call printf
allDone: leave
ret ; Returns to caller
asmMain endp
end
Listing 9-17: Unsigned decimal string-to-numeric conversion
Here’s the build command and sample output for the program in Listing 9-17:
C:\>build listing9-17
C:\>echo off
Assembling: listing9-17.asm
c.cpp
C:\>listing9-17
Calling Listing 9-17:
strtou: String='12345678901234567' value=12345678901234567
Listing 9-17 terminated
Is it possible to create a faster function that uses the fbld
(x87 FPU BCD store) instruction? Probably not. The fbstp
instruction was much faster for integer conversions because the standard algorithm used multiple executions of the (very slow) div
instruction. Decimal-to-numeric conversion uses the mul
instruction, which is much faster than div
. Though I haven’t actually tried it, I suspect using fbld
won’t produce faster running code.
The algorithm for (decimal) string-to-numeric conversion is the same regardless of integer size. You read a decimal character, convert it to an integer, multiply the accumulating result by 10, and add in the converted character. The only things that change for larger-than-64-bit values are the multiplication by 10 and addition operations. For example, to convert a string to a 128-bit integer, you would need to be able to multiply a 128-bit value by 10 and add an 8-bit value (zero-extended to 128 bits) to a 128-bit value.
Listing 9-18 demonstrates how to write a 128-bit unsigned decimal input routine. Other than the 128-bit multiplication by 10 and 128-bit addition operations, this code is functionally identical to the 64-bit string to integer conversion.
; strtou128 - Converts string data to a 128-bit unsigned integer.
; Input:
; RDI - Pointer to buffer containing string to convert.
; Output:
; RDX:RAX - Contains converted string (if success), error code
; if an error occurs.
; RDI - Points at first char beyond end of numeric string.
; If error, RDI's value is restored to original value.
; Caller can check character at [RDI] after a
; successful result to see if the character following
; the numeric digits is a legal numeric delimiter.
; C - (carry flag) Set if error occurs, clear if
; conversion was successful. On error, RAX will
; contain 0 (illegal initial character) or
; 0FFFFFFFFFFFFFFFFh (overflow).
strtou128 proc
accumulator equ <[rbp - 16]>
partial equ <[rbp - 24]>
push rcx ; Holds input char
push rdi ; In case we have to restore RDI
push rbp
mov rbp, rsp
sub rsp, 24 ; Accumulate result here
xor edx, edx ; Zero-extends!
mov accumulator, rdx
mov accumulator[8], rdx
; The following loop skips over any whitespace (spaces and
; tabs) that appears at the beginning of the string.
dec rdi ; Because of inc below
skipWS: inc rdi
mov cl, [rdi]
cmp cl, ' '
je skipWS
cmp al, tab
je skipWS
; If we don't have a numeric digit at this point,
; return an error.
cmp cl, '0' ; Note: "0" < "1" < ... < "9"
jb badNumber
cmp cl, '9'
ja badNumber
; Okay, the first digit is good. Convert the string
; of digits to numeric form:
convert: and ecx, 0fh ; Convert to numeric in RCX
; Multiply 128-bit accumulator by 10:
mov rax, accumulator
mul ten
mov accumulator, rax
mov partial, rdx ; Save partial product
mov rax, accumulator[8]
mul ten
jc overflow1
add rax, partial
mov accumulator[8], rax
jc overflow1
; Add in the current character to the 128-bit accumulator:
mov rax, accumulator
add rax, rcx
mov accumulator, rax
mov rax, accumulator[8]
adc rax, 0
mov accumulator[8], rax
jc overflow2
; Move on to next character:
inc rdi
mov cl, [rdi]
cmp cl, '0'
jb endOfNum
cmp cl, '9'
jbe convert
; If we get to this point, we've successfully converted
; the string to numeric form:
endOfNum:
; Because the conversion was successful, this procedure
; leaves RDI pointing at the first character beyond the
; converted digits. As such, we don't restore RDI from
; the stack. Just bump the stack pointer up by 8 bytes
; to throw away RDI's saved value.
mov rax, accumulator
mov rdx, accumulator[8]
leave
add rsp, 8 ; Remove original RDI value
pop rcx ; Restore RCX
clc ; Return success in carry flag
ret
; badNumber - Drop down here if the first character in
; the string was not a valid digit.
badNumber: xor rax, rax
xor rdx, rdx
jmp errorExit
overflow1: mov rax, -1
cqo ; RDX = -1, too
jmp errorExit
overflow2: mov rax, -2 ; 0FFFFFFFFFFFFFFFEh
cqo ; Just to be consistent
errorExit: leave ; Remove accumulator from stack
pop rdi
pop rcx
stc ; Return error in carry flag
ret
ten qword 10
strtou128 endp
Listing 9-18: Extended-precision unsigned decimal input
Once you have an unsigned decimal input routine, writing a signed decimal input routine is easy, as described by the following algorithm:
I’ll leave the actual code implementation as a programming exercise for you.
Converting a string of characters representing a floating-point number to the 80-bit real10
format is slightly easier than the real10
-to-string conversion appearing earlier in this chapter. Because decimal conversion (with no exponent) is a subset of the more general scientific notation conversion, if you can handle scientific notation, you get decimal conversion for free. Beyond that, the basic algorithm is to convert the mantissa characters to a packed BCD form (so the function can use the fbld
instruction to do the string-to-numeric conversion) and then read the (optional) exponent and adjust the real10
exponent accordingly. The algorithm to do the conversion is the following:
+
) or minus (-
) sign character. Skip it if one is present. Set a sign flag to true if the number is negative (false if non-negative).fbld
instruction, and left-justified packed BCD values are always greater than or equal to 1018. Initializing the exponent to –18 accounts for this.e
or E
, then go to step 20.6 Otherwise, skip over the e
or E
character and continue with step 15.+
or -
, skip over it. Set a flag to true if the sign character is -
, and set it to false otherwise (note that this exponent sign flag is different from the mantissa sign flag set earlier in this algorithm).real10
value (using the fbld
instruction).real10
value by 10 raised to the power specified by that bit. For example, if bits 12, 10, and 1 are set, multiply the real10
value by 104096, 101024, and 102.real10
value by 10 raised to the power specified by that bit. For example, if bits 12, 10, and 1 are set, divide the real10
value by 104096, 101024, and 102.Listing 9-19 provides an implementation of this algorithm.
; Listing 9-19
; Real string-to-floating-point conversion.
option casemap:none
false = 0
true = 1
tab = 9
nl = 10
.const
ttlStr byte "Listing 9-19", 0
fmtStr1 byte "strToR10: str='%s', value=%e", nl, 0
fStr1a byte "1.234e56",0
fStr1b byte "-1.234e56",0
fStr1c byte "1.234e-56",0
fStr1d byte "-1.234e-56",0
fStr2a byte "1.23",0
fStr2b byte "-1.23",0
fStr3a byte "1",0
fStr3b byte "-1",0
fStr4a byte "0.1",0
fStr4b byte "-0.1",0
fStr4c byte "0000000.1",0
fStr4d byte "-0000000.1",0
fStr4e byte "0.1000000",0
fStr4f byte "-0.1000000",0
fStr4g byte "0.0000001",0
fStr4h byte "-0.0000001",0
fStr4i byte ".1",0
fStr4j byte "-.1",0
values qword fStr1a, fStr1b, fStr1c, fStr1d,
fStr2a, fStr2b,
fStr3a, fStr3b,
fStr4a, fStr4b, fStr4c, fStr4d,
fStr4e, fStr4f, fStr4g, fStr4h,
fStr4i, fStr4j,
0
align 4
PotTbl real10 1.0e+4096,
1.0e+2048,
1.0e+1024,
1.0e+512,
1.0e+256,
1.0e+128,
1.0e+64,
1.0e+32,
1.0e+16,
1.0e+8,
1.0e+4,
1.0e+2,
1.0e+1,
1.0e+0
.data
r8Val real8 ?
.code
externdef printf:proc
; Return program title to C++ program:
public getTitle
getTitle proc
lea rax, ttlStr
ret
getTitle endp
*********************************************************
; strToR10 - RSI points at a string of characters that represent a
; floating-point value. This routine converts that string
; to the corresponding FP value and leaves the result on
; the top of the FPU stack. On return, ESI points at the
; first character this routine couldn't convert.
; Like the other ATOx routines, this routine raises an
; exception if there is a conversion error or if ESI
; contains NULL.
*********************************************************
strToR10 proc
sign equ <cl>
expSign equ <ch>
DigitStr equ <[rbp - 20]>
BCDValue equ <[rbp - 30]>
rsiSave equ <[rbp - 40]>
push rbp
mov rbp, rsp
sub rsp, 40
push rbx
push rcx
push rdx
push r8
push rax
; Verify that RSI is not NULL.
test rsi, rsi
jz refNULL
; Zero out the DigitStr and BCDValue arrays.
xor rax, rax
mov qword ptr DigitStr, rax
mov qword ptr DigitStr[8], rax
mov dword ptr DigitStr[16], eax
mov qword ptr BCDValue, rax
mov word ptr BCDValue[8], ax
; Skip over any leading space or tab characters in the sequence.
dec rsi
whileDelimLoop:
inc rsi
mov al, [rsi]
cmp al, ' '
je whileDelimLoop
cmp al, tab
je whileDelimLoop
; Check for "+" or "-".
cmp al, '-'
sete sign
je doNextChar
cmp al, '+'
jne notPlus
doNextChar: inc rsi ; Skip the "+" or "-"
mov al, [rsi]
notPlus:
; Initialize EDX with -18 since we have to account
; for BCD conversion (which generates a number * 10^18 by
; default). EDX holds the value's decimal exponent.
mov rdx, -18
; Initialize EBX with 18, which is the number of significant
; digits left to process and it is also the index into the
; DigitStr array.
mov ebx, 18 ; Zero-extends!
; At this point, we're beyond any leading sign character.
; Therefore, the next character must be a decimal digit
; or a decimal point.
mov rsiSave, rsi ; Save to look ahead 1 digit
cmp al, '.'
jne notPeriod
; If the first character is a decimal point, then the
; second character needs to be a decimal digit.
inc rsi
mov al, [rsi]
notPeriod:
cmp al, '0'
jb convError
cmp al, '9'
ja convError
mov rsi, rsiSave ; Go back to orig char
mov al, [rsi]
jmp testWhlAL0
; Eliminate any leading zeros (they do not affect the value or
; the number of significant digits).
whileAL0: inc rsi
mov al, [rsi]
testWhlAL0: cmp al, '0'
je whileAL0
; If we're looking at a decimal point, we need to get rid of the
; zeros immediately after the decimal point since they don't
; count as significant digits. Unlike zeros before the decimal
; point, however, these zeros do affect the number's value as
; we must decrement the current exponent for each such zero.
cmp al, '.'
jne testDigit
inc edx ; Counteract dec below
repeatUntilALnot0:
dec edx
inc rsi
mov al, [rsi]
cmp al, '0'
je repeatUntilALnot0
jmp testDigit2
; If we didn't encounter a decimal point after removing leading
; zeros, then we've got a sequence of digits before a decimal
; point. Process those digits here.
; Each digit to the left of the decimal point increases
; the number by an additional power of 10. Deal with
; that here.
whileADigit:
inc edx
; Save all the significant digits, but ignore any digits
; beyond the 18th digit.
test ebx, ebx
jz Beyond18
mov DigitStr[rbx * 1], al
dec ebx
Beyond18: inc rsi
mov al, [rsi]
testDigit:
sub al, '0'
cmp al, 10
jb whileADigit
cmp al, '.'-'0'
jne testDigit2
inc rsi ; Skip over decimal point
mov al, [rsi]
jmp testDigit2
; Okay, process any digits to the right of the decimal point.
whileDigit2:
test ebx, ebx
jz Beyond18_2
mov DigitStr[rbx * 1], al
dec ebx
Beyond18_2: inc rsi
mov al, [rsi]
testDigit2: sub al, '0'
cmp al, 10
jb whileDigit2
; At this point, we've finished processing the mantissa.
; Now see if there is an exponent we need to deal with.
mov al, [rsi]
cmp al, 'E'
je hasExponent
cmp al, 'e'
jne noExponent
hasExponent:
inc rsi
mov al, [rsi] ; Skip the "E".
cmp al, '-'
sete expSign
je doNextChar_2
cmp al, '+'
jne getExponent;
doNextChar_2:
inc rsi ; Skip "+" or "-"
mov al, [rsi]
; Okay, we're past the "E" and the optional sign at this
; point. We must have at least one decimal digit.
getExponent:
sub al, '0'
cmp al, 10
jae convError
xor ebx, ebx ; Compute exponent value in EBX
ExpLoop: movzx eax, byte ptr [rsi] ; Zero-extends to RAX!
sub al, '0'
cmp al, 10
jae ExpDone
imul ebx, 10
add ebx, eax
inc rsi
jmp ExpLoop
; If the exponent was negative, negate our computed result.
ExpDone:
cmp expSign, false
je noNegExp
neg ebx
noNegExp:
; Add in the BCD adjustment (remember, values in DigitStr, when
; loaded into the FPU, are multiplied by 10^18 by default.
; The value in EDX adjusts for this).
add edx, ebx
noExponent:
; Verify that the exponent is between -4930 and +4930 (which
; is the maximum dynamic range for an 80-bit FP value).
cmp edx, 4930
jg voor ; Value out of range
cmp edx, -4930
jl voor
; Now convert the DigitStr variable (unpacked BCD) to a packed
; BCD value.
mov r8, 8
for9: mov al, DigitStr[r8 * 2 + 2]
shl al, 4
or al, DigitStr[r8 * 2 + 1]
mov BCDValue[r8 * 1], al
dec r8
jns for9
fbld tbyte ptr BCDValue
; Okay, we've got the mantissa into the FPU. Now multiply the
; mantissa by 10 raised to the value of the computed exponent
; (currently in EDX).
; This code uses power of 10 tables to help make the
; computation a little more accurate.
; We want to determine which power of 10 is just less than the
; value of our exponent. The powers of 10 we are checking are
; 10**4096, 10**2048, 10**1024, 10**512, and so on. A slick way to
; do this check is by shifting the bits in the exponent
; to the left. Bit #12 is the 4096 bit. So if this bit is set,
; our exponent is >= 10**4096. If not, check the next bit down
; to see if our exponent >= 10**2048, etc.
mov ebx, -10 ; Initial index into power of 10 table
test edx, edx
jns positiveExponent
; Handle negative exponents here.
neg edx
shl edx, 19 ; Bits 0 to 12 -> 19 to 31
lea r8, PotTbl
whileEDXne0:
add ebx, 10
shl edx, 1
jnc testEDX0
fld real10 ptr [r8][rbx * 1]
fdivp
testEDX0: test edx, edx
jnz whileEDXne0
jmp doMantissaSign
; Handle positive exponents here.
positiveExponent:
lea r8, PotTbl
shl edx, 19 ; Bits 0 to 12 -> 19 to 31
jmp testEDX0_2
whileEDXne0_2:
add ebx, 10
shl edx, 1
jnc testEDX0_2
fld real10 ptr [r8][rbx * 1]
fmulp
testEDX0_2: test edx, edx
jnz whileEDXne0_2
; If the mantissa was negative, negate the result down here.
doMantissaSign:
cmp sign, false
je mantNotNegative
fchs
mantNotNegative:
clc ; Indicate success
jmp Exit
refNULL: mov rax, -3
jmp ErrorExit
convError: mov rax, -2
jmp ErrorExit
voor: mov rax, -1 ; Value out of range
jmp ErrorExit
illChar: mov rax, -4
ErrorExit: stc ; Indicate failure
mov [rsp], rax ; Save error code
Exit: pop rax
pop r8
pop rdx
pop rcx
pop rbx
leave
ret
strToR10 endp
; Here is the "asmMain" function.
public asmMain
asmMain proc
push rbx
push rsi
push rbp
mov rbp, rsp
sub rsp, 64 ; Shadow storage
; Test floating-point conversion:
lea rbx, values
ValuesLp: cmp qword ptr [rbx], 0
je allDone
mov rsi, [rbx]
call strToR10
fstp r8Val
lea rcx, fmtStr1
mov rdx, [rbx]
mov r8, qword ptr r8Val
call printf
add rbx, 8
jmp ValuesLp
allDone: leave
pop rsi
pop rbx
ret ; Returns to caller
asmMain endp
end
Listing 9-19: A strToR10
function
Here’s the build command and sample output for Listing 9-19.
C:\>build listing9-19
C:\>echo off
Assembling: listing9-19.asm
c.cpp
C:\>listing9-19
Calling Listing 9-19:
strToR10: str='1.234e56', value=1.234000e+56
strToR10: str='-1.234e56', value=-1.234000e+56
strToR10: str='1.234e-56', value=1.234000e-56
strToR10: str='-1.234e-56', value=-1.234000e-56
strToR10: str='1.23', value=1.230000e+00
strToR10: str='-1.23', value=-1.230000e+00
strToR10: str='1', value=1.000000e+00
strToR10: str='-1', value=-1.000000e+00
strToR10: str='0.1', value=1.000000e-01
strToR10: str='-0.1', value=-1.000000e-01
strToR10: str='0000000.1', value=1.000000e-01
strToR10: str='-0000000.1', value=-1.000000e-01
strToR10: str='0.1000000', value=1.000000e-01
strToR10: str='-0.1000000', value=-1.000000e-01
strToR10: str='0.0000001', value=1.000000e-07
strToR10: str='-0.0000001', value=-1.000000e-07
strToR10: str='.1', value=1.000000e-01
strToR10: str='-.1', value=-1.000000e-01
Listing 9-19 terminated
Donald Knuth’s The Art of Computer Programming, Volume 2: Seminumerical Algorithms (Addison-Wesley Professional, 1997) contains a lot of useful information about decimal arithmetic and extended-precision arithmetic, though that text is generic and doesn’t describe how to do this in x86 assembly language.
dToStr
produce?qToStr
to write a 128-bit hexadecimal output routine.utoStrSize
function?uSizeToStr
produce if the number requires more print positions than specified by the minDigits
parameter?r10ToStr
function?r10ToStr
produce if the output won’t fit in the string size specified by the fWidth
argument?e10ToStr
function?