In this post, I write about the development of a service procedure which
converts hexadecimal values into readable ASCII. I had to do such a conversion
many times in the past, and I had to do it here once more, so I paused for a
moment and I decided to develop a procedure that I could use from here on
every time such a task recurred again.
write_ASCII1 is the new cast of
CREG_STR
(convert register in string) where
out_a_nibble
and
nibble_to_ASCII
are directly fused in the main code. In the post "Hand made linking", I
created the procedure "convert register in string" in order to demonstrate how
to perform the static-linking by hands so it was useful, at that time, to keep
out_a_nibble
and
nibble_to_ASCII as separate procedures.
[...]
;234567890123456789012345678901234567890123456789012345678901234567890123456789
;-------10--------20--------30--------40--------50--------60--------70-------79
;##############################################################################
; WRTASCII: WRiTe ASCII
;
; Copyright (C) 2020 - Michele Musci
; Distributed under the GNU Affero General Public License version 3.
; See https://www.gnu.org/licenses/agpl-3.0.txt
;
; This procedure converts the content of BX into the corresponding ASCII code
; and store it in the memory location pointed by ES:[DI].
; CX is used to control the portion of BX to be converted in ASCII.
; EXAMPLE: BX: CX: ES:[DI]:
; 75AE 0000 ""
; 75AE 0001 "E"
; 75AE 0002 "AE"
; 75AE 0003 "5AE"
; 75AE >= 4 "75AE"
;
; INPUT: BX -> WORD to be converted in ASCII.
; CX -> number of nibbles to be converted.
; ES:[DI] -> memory address to save the ASCII equivalent
; of the content of BX.
; OUTPUT: ---
; REG. USAGE: AX, CX, DX, DI
; THIS CALLS: ---
;
; Build it with command:
; debug < show_str.npp > show_str_dbg.npp
;##############################################################################
;
; In case CX = 0x00, the procedure has to do nothing, so I prefer to return
; immediately to the caller even before beginning to push the registers
; on the stack.
;------------------------------------------------------------------------------
jcxz 7c3e ; JumpCXZero immediate_end: --->
;
;------------------------------------------------------------------------------
; Start of procedure: initial setup.
;------------------------------------------------------------------------------
push ax ; Preserve used registers.
push cx ;
push dx ;
push di ;
;
cld ; Clear Direction Flag to set string operation
; with auto increment.
;------------------------------------------------------------------------------
; check and fix CX if necessary
; IF (CX >= 4)
;------------------------------------------------------------------------------
cmp cx, 4 ; flags = CX - 0x 04
; Unsigned comparison:
jbe 7c1f ; JumpBeloworEqual endif_CX: --->
;
;
;-------------------------------
; THEN...IF (CX >= 4)
;-------------------------------
mov cx, 4 ; Limit CX to 0x 04
;
;
;-------------------------------
; END...IF (CX >= 4)
;-------------------------------
;
; endif_CX: <---
;
;------------------------------------------------------------------------------
;
; loop_start: <---
;
;------------------------------------------------------------------------------
; Select the nibble
;------------------------------------------------------------------------------
mov ax, bx ; it copies the value in AX.
mov dx, cx ; store the value of CX to resume it before loop.
; The selection of the nibble destroys CX.
;
; SHR instruction must use cl register:
; before shift | after shift
; -------------+--------------
; 75A[E] | 75A[E] --> 0 bits shift
; 75[A]E | 075[A] --> 4 bits shift
; 7[5]AE | 007[5] --> 8 bits shift
; [7]5AE | 000[7] --> 12 bits shift
;
; Relationship table for the value of shift:
; index | value of shift
; x | y = f(x)
; ------+----------
; 4 | 12
; 3 | 8
; 2 | 4
; 1 | 0
; y = 4*(x-1)
;
; Relationship formula for the value of shift:
; y = 4*(x-1) --> CL = 4*(CL - 1) desired target
dec cl ; CL = CL - 1 --> CL = (CL - 1) step 1
shl cl, 1 ; CL = 2*CL --> CL = 2*(CL - 1) step 2
shl cl, 1 ; CL = 2*CL --> CL = 2*2*(CL - 1) final step
;
shr ax, cl ; Select the nibble.
and al, 0f ; Mask remaining bits in AL (we can ignore AH).
;
;
;------------------------------------------------------------------------------
; convert AL into ASCII
;------------------------------------------------------------------------------
add al, 30 ; AL get increased by the ASCII value for '0'
; 0x30 = '0' ... 0x39 = '9'
;
;
;-------------------------------
; IF (AL > 0x39)
;-------------------------------
cmp al, 39 ; flag = AL - 0x39
; 0x39 = '9' in ASCII, so if AL is now anywhere
; above 0x39 then we are in hex range from 0xA
; to 0xF. If so we have to add 0x07
; to the calculation for conversion.
; Unsigned comparison:
jbe 7c35 ; JumpBelowOrEqual endif_AL: --->
;
;
;-------------------------------
; THEN...IF (AL > 0x39)
;-------------------------------
add al, 7 ;
;
;
;---------------------------------------
; END...IF (AL > 0x39)
;---------------------------------------
;
;endif_AL: <---
;
;---------------------------------------
; Store ASCII to string in memory
;---------------------------------------
stosb ; save the ASCII in the string
; al contains already the ASCII code to be stored
; es:[di] = al; di = di + 1
;
;
;-------------------------------
; NEXT cx <= 4, to 0, dec cx
;-------------------------------
mov cx, dx ; Restore the counter for CX before loop.
loop 7c1f ; loop loop_start: --->
; dec cx
; jnz
;
;
;------------------------------------------------------------------------------
; END of MAIN
;------------------------------------------------------------------------------
pop di ; Restore extra used registers.
pop dx
pop cx
pop ax
;
; immediate_end: <---
;
ret
[...]
write_ASCII converts a portion of the content of BX in ASCII. CX controls how
many nibbles are to be converted. Since there are no more than four nibbles in
BX, then CX can span from one to four. I decided to implement a small check
about the value of CX and I did it in two stages. I placed the first stage of
control for CX at the beginning so that if CX was zero, nothing had to
happen, keeping the execution very lean. In case CX was greater than zero, then I
started with the execution of the procedure preserving immediately only the
registers that were going to be used. I would like you to observe that I had
not yet fixed my style definitively about which register I had to preserve and
which not. In this case, I was preserving all registers that changed and I had
not any sort of standardized opening and closing of procedure where all
registers went on and off the stack regardless of their effective usage in the
code. I didn't yet know what was right or wrong, the experience still had to
teach me. With the second stage of control for CX, I limited the maximum value
to be less or equal to four. After this point, I think that you can recognize the
execution core being the merge of
out_a_nibble
and
nibble_to_ASCII.
As you can see, I was passing the required parameters for the procedure thru
registers. More in general, I should talk about the calling convention and
what I understood about that topic. As far as I knew, there is no standard
enforced by the architecture of the x86 CPU but two major possibilities and
several hybrid strategies built upon them in between. At the two extremes,
either one can pass all parameters thru registers or pass all parameters thru
stack. I don't know yet what is the best for me, but I have to admit of being
heavily influenced by the calling convention used in the interrupts. In the
case of interrupts, one has to pass the arguments thru registers. I am used to
interrupts as these are the very basic services I ever called in assembly so
it seemed to me to be natural to keep this convention. At the same time, I am
aware that interrupts are quite simple services so passing parameters thru
registers can be more than enough. Maybe one day I will develop some procedure
of a certain complexity which requires some more parameters than available
registers, in which case I will have to push and pass parameters thru the
stack.
Comments
Post a Comment