简体   繁体   中英

In assembly why can't a hexadecimal number be printed without shifting?

This code is taken from an online example. Let's say I have the variable I want to print in my DL.

DISPLAY_HEX PROC NEAR
    MOV BL,DL   

    MOV BH,0    
    MOV CL,4    

    SHL BX,CL   
    MOV DL,BH   

    CALL ONE_DIGIT  

    MOV CL,4    
    SHR BL,CL   
    MOV DL,BL   

    CALL ONE_DIGIT  

    RET     
DISPLAY_HEX ENDP


ONE_DIGIT PROC NEAR

    CMP DL,9    
    JA LETTER   

    ADD DL,48
    JMP NEXT    

LETTER: ADD DL, 'A'-10  

NEXT:   MOV AH,02H  
    INT 21H 

END:    RET     
ONE_DIGIT ENDP

Why the shifts? Can't it be printed like a decimal? Also, why is both SHR and SHL being used here?

In base16 (hex) you've got 16 possible digits ( 0..F ), so it takes exactly 4 bits to represent one hexadecimal digit (log2(16) == 4). Here I'm talking about digits in the sense of the values ( 0..F , or 0..15 in base10), not the ASCII characters.

So one byte can hold two hexadecimal digits. Let's say that DL holds the following bits: XXXXYYYY (where each X and Y is either a binary 0 or 1).

First the 16-bit register BX is shifted 4 bits to the left. BX consists of BL (least significant byte) and BH (most significant byte). BH has been set to 0, and BL contains the input, so prior to the shift BX will contain the bits 00000000XXXXYYYY . And after the shift it will contain 0000XXXXYYYY0000 .
Then the most significant byte of BX (ie BH , which now contains 0000XXXX ) is moved to DL , converted to a character, and printed.

For the second part BL , which now contains YYYY0000 is shifted 4 bits to the right, resulting in 0000YYYY . And then that value is converted to a character and printed.

Your code is massively overcomplicated, no wonder it was confusing. It's getting the high nibble of DL by left-shifting BX, so the two nibbles are split into BH and BL, but the nibble in BL is in the top 4 bytes.

You need one shift to get the high 4 bits down to the bottom of a register.

Using AND to keep only the low 4 bits would be easier, and much faster on a real 8086 (where each count of a shift took a clock cycle, unlike modern CPUs with barrel-shifter ALUs that can do arbitrary shifts in 1 clock cycle).

This is a simpler and easier-to-understand implementation, which is also more compact and thus faster and better on a real 8086.

; Input in DL
; clobbers AX, CL, DX
DISPLAY_HEX PROC NEAR
    mov  dh, dl          ; save a copy for later

    mov  cl, 4
    shr  dl, cl          ; extract the high nibble into an 8-bit integer

    CALL ONE_DIGIT  

    and  dh, 0Fh         ; clear the high nibble, keeping only the low nibble
    mov  dl, dh          ; and pass it to our function
    CALL ONE_DIGIT  

    RET     

DISPLAY_HEX ENDP

To save code size, mov dl, 0Fh / and dl, dh would be only 2 bytes per instruction, instead of 3 for and dh, 0Fh , and it takes the mov off the critical path for out-of-order execution.

The implementation of ONE_DIGIT also has needlessly-complex branching. (Often you'd implement nibble -> ASCII with a lookup table, but it only needs one branch, not two. Running an extra add is cheaper than an extra jmp .)

; input: DL = 0..15 integer
; output: AL = ASCII hex char written
; clobbers = AH    
ONE_DIGIT PROC NEAR
    CMP   DL,9    
    JBE   DIGIT   

    ADD   DL, 'A'-10  - '0'
DIGIT:
    ADD   DL, '0'

    MOV AH,02H  
    INT 21H      ; write char to stdout, return in AL.  Checks for ^c/break
    RET
ONE_DIGIT ENDP

We could have done ADD dx, '00' (and changed the cmp ) to operate on both nibbles at once (after isolating them each in DH and DL).

We could also hoist the MOV AH,02H , because int 21h / ah=2 doesn't modify AH. .

If we cared about performance, we'd create a 2-character string and use one int 21h system call to print both digits at once, though.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM