9
Numeric Conversion

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).

9.1 Converting Numeric Values to Strings

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.

9.1.1 Converting Numeric Values to Hexadecimal Strings

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).

9.1.2 Converting Extended-Precision Hexadecimal Values to Strings

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

9.1.3 Converting Unsigned Decimal Values to Strings

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:

  1. Divide 789 by 10. The quotient is 78, and the remainder is 9.
  2. Save the remainder (9) in a local variable and recursively call the routine with the quotient.
  3. Recursive entry 1: Divide 78 by 10. The quotient is 7, and the remainder is 8.
  4. Save the remainder (8) in a local variable and recursively call the routine with the quotient.
  5. Recursive entry 2: Divide 7 by 10. The quotient is 0, and the remainder is 7.
  6. Save the remainder (7) in a local variable. Because the quotient is 0, don’t call the routine recursively.
  7. Output the remainder value saved in the local variable (7). Return to the caller (recursive entry 1).
  8. Return to recursive entry 1: Output the remainder value saved in the local variable in recursive entry 1 (8). Return to the caller (original invocation of the procedure).
  9. Original invocation: Output the remainder value saved in the local variable in the original call (9). Return to the original caller of the output routine.

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:

Clearly, the fist and fbstp implementation is the winner.

9.1.4 Converting Signed Integer Values to Strings

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

9.1.5 Converting Extended-Precision Unsigned Integers to Strings

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.

9.1.6 Converting Extended-Precision Signed Decimal Values to Strings

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:

  1. Check the sign of the number.
  2. If it is positive, call the unsigned output routine to print it. If the number is negative, print a minus sign. Then negate the number and call the unsigned output routine to print it.

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

9.1.7 Formatted Conversions

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

9.1.8 Converting Floating-Point Values to Strings

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.

9.1.8.1 Converting Floating-Point Exponents

To convert the exponent to a string of decimal digits, use the following algorithm:

  1. If the number is 0.0, directly produce the mantissa output string of “ 000000000000000000” (notice the space at the beginning of the string).
  2. Initialize the decimal exponent to 0.
  3. If the exponent is negative, emit a hyphen (-) character and negate the value; if it is positive, emit a space character.
  4. If the value of the (possibly negated) exponent is less than 1.0, go to step 8.
  5. Positive exponents: Compare the number against successively smaller powers of 10, starting with 10+4096, then 10+2048, then 10+1024, then . . . , then 100. After each comparison, if the current value is greater than the power of 10, divide by that power of 10 and add the power of 10 exponent (4096, 2048, . . . , 0) to the decimal exponent value.
  6. Repeat step 5 until the exponent is 0 (that is, the value is in the range 1.0 ≤ value < 10.0).
  7. Go to step 10.
  8. Negative exponents: Compare the number against successful larger powers of 10 starting with 10-4096, then 10-2048, then 10-1024, then . . . , then 100. After each comparison, if the current value is less than the power of 10, divide by that power of 10 and subtract the power of 10 exponent (4096, 2048, . . . , 0) from the decimal exponent value.
  9. Repeat step 8 until the exponent is 0 (that is, the value is in the range 1.0 ≤ value < 10.0).
  10. Certain legitimate floating-point values are too large to represent with 18 digits (for example, 9,223,372,036,854,775,807 fits into 63 bits but requires more than 18 significant digits to represent). Specifically, values in the range 403A_DE0B_6B3A_763F_FF01h to 403A_DE0B_6B3A_763F_FFFFh are greater than 999,999,999,999,999,999 but still fit within a 64-bit mantissa. The 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.

  11. At this point, the floating-point value is a reasonable number that the fbstp instruction can convert to a packed BCD value, so the conversion function uses fbstp to do this conversion.
  12. Finally, convert the packed BCD value to a string of ASCII characters using an operation converting numeric values to hexadecimal (BCD) to strings (see “Converting Unsigned Decimal Values to Strings” on page 500 and Listing 9-5).

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

9.1.8.2 Converting a Floating-Point Value to a Decimal String

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

  1. The 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

  1. The field width. This is the total number of character positions that the string will consume. This count includes room for a sign (which could be a space or a hyphen) but does not include space for a zero-terminating byte for the string. The field width must be greater than 0 and less than or equal to 1024.

decDigits

  1. The number of digits to the right of the decimal point. This value must be at least 3 less than 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

  1. The fill character. If the numeric string that 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

  1. A buffer to receive the numeric string.

maxLength

  1. The size of the buffer (including the zero-terminating byte). If the conversion routine attempts to create a string larger than this value (meaning 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:

  1. The function begins by adding 3 to decDigits.
  2. If decDigits is less than 4, the function sets it to 4 as a default value.3
  3. If decDigits is greater than fWidth, the function emits fWidth "#" characters to the string and returns.
  4. If decDigits is less than fWidth, then output (fWidth - decDigits) padding characters (fill) to the output string.
  5. If 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).
  6. Next, output the digits from the converted number. If the field width is less than 21 (18 digits plus the 3 leading 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.
  7. Finally, the function zero-terminates the string and returns.

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

9.1.8.3 Converting a Floating-Point Value to Exponential Form

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

9.2 String-to-Numeric Conversion Routines

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).

9.2.1 Converting Decimal Strings to Integers

The basic algorithm to convert a string containing decimal digits to a number is the following:

  1. Initialize an accumulator variable to 0.
  2. Skip any leading spaces or tabs in the string.
  3. Fetch the first character after the spaces or tabs.
  4. If the character is not a numeric digit, return an error. If the character is a numeric digit, fall through to step 5.
  5. Convert the numeric character to a numeric value (using AND 0Fh).
  6. Set the accumulator = (accumulator × 10) + current numeric value.
  7. If overflow occurs, return and report an error. If no overflow occurs, fall through to step 8.
  8. Fetch the next character from the string.
  9. If the character is a numeric digit, go back to step 5, else fall through to step 10.
  10. Return success, with accumulator containing the converted value.

For signed integer input, you use this same algorithm with the following modifications:

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).

9.2.2 Converting Hexadecimal Strings to Numeric Form

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:

  1. Initialize an extended-precision accumulator value to 0.
  2. For each input character that is a valid hexadecimal digit, repeat steps 3 through 6; drop down to step 7 when it is not a valid hexadecimal digit.
  3. Convert the hexadecimal character to a value in the range 0 to 15 (0h to 0Fh).
  4. If the HO 4 bits of the extended-precision accumulator value are nonzero, raise an exception.
  5. Multiply the current extended-precision value by 16 (that is, shift left 4 bits).
  6. Add the converted hexadecimal digit value to the accumulator.
  7. Check the current input character to ensure it is a valid delimiter. Raise an exception if it is not.

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

9.2.3 Converting Unsigned Decimal Strings to Integers

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.

9.2.4 Conversion of Extended-Precision String to Unsigned Integer

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

9.2.5 Conversion of Extended-Precision Signed Decimal String to Integer

Once you have an unsigned decimal input routine, writing a signed decimal input routine is easy, as described by the following algorithm:

  1. Consume any delimiter characters at the beginning of the input stream.
  2. If the next input character is a minus sign, consume this character and set a flag noting that the number is negative; else just drop down to step 3.
  3. Call the unsigned decimal input routine to convert the rest of the string to an integer.
  4. Check the return result to make sure its HO bit is clear. Raise a value out of range exception if the HO bit of the result is set.
  5. If the code encountered a minus sign in step 2, negate the result.

I’ll leave the actual code implementation as a programming exercise for you.

9.2.6 Conversion of Real String to Floating-Point

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:

  1. Begin by stripping away any leading space or tab characters (and any other delimiters).
  2. Check for a leading plus (+) 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).
  3. Initialize an exponent value to –18. The algorithm will create a left-justified packed BCD value from the mantissa digits in the string to provide to the fbld instruction, and left-justified packed BCD values are always greater than or equal to 1018. Initializing the exponent to –18 accounts for this.
  4. Initialize a significant-digit-counter variable that counts the number of significant digits processed thus far to 18.
  5. If the number begins with any leading zeros, skip over them (do not change the exponent or significant digit counters for leading zeros to the left of the decimal point).
  6. If the scan encounters a decimal point after processing any leading zeros, go to step 11; else fall through to step 7.
  7. For each nonzero digit to the left of the decimal point, if the significant digit counter is not zero, insert the nonzero digit into a “digit string” array at the position specified by the significant digit counter (minus 1).5 Note that this will insert the characters into the string in a reversed position.
  8. For each digit to the left of the decimal point, increment the exponent value (originally initialized to –18) by 1.
  9. If the significant digit counter is not zero, decrement the significant digit counter (this will also provide the index into the digit string array).
  10. If the first nondigit encountered is not a decimal point, skip to step 14.
  11. Skip over the decimal point character.
  12. For each digit encountered to the right of the decimal point, continue adding the digits (in reverse order) to the digit string array as long as the significant digit counter is not zero. If the significant digit counter is greater than zero, decrement it. Also, decrement the exponent value.
  13. If the algorithm hasn’t encountered at least one decimal digit by this point, report an illegal character exception and return.
  14. If the current character is not e or E, then go to step 20.6 Otherwise, skip over the e or E character and continue with step 15.
  15. If the next character is + 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).
  16. If the next character is not a decimal digit, report an error.
  17. Convert the string of digits (starting with the current decimal digit character) to an integer.
  18. Add the converted integer to the exponent value (which was initialized to –18 at the start of this algorithm).
  19. If the exponent value is outside the range –4930 to +4930, report an out-of-range exception.
  20. Convert the digit string array of characters to an 18-digit (9-byte) packed BCD value by stripping the HO 4 bits of each character, merging pairs of characters into a single byte (by shifting the odd-indexed byte to the left 4 bits and logically ORing with the even-indexed byte of each pair), and then setting the HO (10th) byte to 0.
  21. Convert the packed BCD value to a real10 value (using the fbld instruction).
  22. Take the absolute value of the exponent (though preserve the sign of the exponent). This value will be 13 bits or less (4096 has bit 12 set, so 4930 or less will have some combination of bits 0 to 13 set to 1, with all other bits 0).
  23. If the exponent was positive, then for each set bit in the exponent, multiply the current 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.
  24. If the exponent was negative, then for each set bit in the exponent, divide the current 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.
  25. If the mantissa is negative (the first sign flag set at the beginning of the algorithm), then negate the floating-point number.

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

9.3 For More Information

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.

9.4 Test Yourself

  1. What is the code that will convert an 8-bit hexadecimal value in AL into two hexadecimal digits (in AH and AL)?
  2. How many hexadecimal digits will dToStr produce?
  3. Explain how to use qToStr to write a 128-bit hexadecimal output routine.
  4. What instruction should you use to produce the fastest 64-bit decimal-to-string conversion function?
  5. How do you write a signed decimal-to-string conversion if you’re given a function that does an unsigned decimal-to-string conversion?
  6. What are the parameters for the utoStrSize function?
  7. What string will uSizeToStr produce if the number requires more print positions than specified by the minDigits parameter?
  8. What are the parameters for the r10ToStr function?
  9. What string will r10ToStr produce if the output won’t fit in the string size specified by the fWidth argument?
  10. What are the arguments to the e10ToStr function?
  11. What is a delimiter character?
  12. What are two possible errors that could occur during a string-to-numeric conversion?