write_ASCII

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.

write_ASCII.npp
[...] ;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