Intel x86 Assembly Language & Microarchitecture MS-DOS, TASM/MASM function to print a 16-bit number in binary, quaternary, octal, hex


Example

Print a number in binary, quaternary, octal, hexadecimal and a general power of two

All the bases that are a power of two, like the binary (21), quaternary (22), octal (23), hexadecimal (24) bases, have an integral number of bits per digit1.
Thus to retrieve each digit2 of a numeral we simply break the number intro group of n bits starting from the LSb (the right).
For example for the quaternary base, we break a 16-bit number in groups of two bits. There are 8 of such groups.
Not all power of two bases have an integral number of groups that fits 16 bits; for example, the octal base has 5 groups of 3 bits that account for 3·5 = 15 bits out of 16, leaving a partial group of 1 bit3.

The algorithm is simple, we isolate each group with a shift followed by an AND operation.
This procedure works for every size of the groups or, in other words, for any base power of two.

In order to show the digits in the right order the function start by isolating the most significant group (the leftmost), thereby it is important to know: a) how many bits D a group is and b) the bit position S where the leftmost group starts.
These values are precomputed and stored in carefully crafted constants.

Parameters

The parameters must be pushed on the stack.
Each one is 16-bit wide.
They are shown in order of push.

ParameterDescription
NThe number to convert
BaseThe base to use expressed using the constants BASE2, BASE4, BASE8 and BASE16
Print leading zerosIf zero no non-significant zeros are print, otherwise they are. The number 0 is printed as "0" though

Usage

push 241
push BASE16
push 0
call print_pow2              ;Prints f1

push 241
push BASE16
push 1
call print_pow2              ;Prints 00f1

push 241
push BASE2
push 0
call print_pow2              ;Prints 11110001

Note to TASM users: If you put the constants defined with EQU after the code that uses them, enable multi-pass with the /m flag of TASM or you'll get Forward reference needs override.

Code

;Parameters (in order of push):
;
;number
;base (Use constants below)
;print leading zeros
print_pow2:
 push bp
 mov bp, sp

 push ax
 push bx
 push cx
 push dx
 push si
 push di

 ;Get parameters into the registers

 ;SI = Number (left) to convert
 ;CH = Amount of bits to shift for each digit (D)
 ;CL = Amount od bits to shift the number (S)
 ;BX = Bit mask for a digit

 mov si, WORD PTR [bp+08h]
 mov cx, WORD PTR [bp+06h]            ;CL = D, CH = S

 ;Computes BX = (1 << D)-1
 
 mov bx, 1
 shl bx, cl
 dec bx

 xchg cl, ch              ;CL = S, CH = D

_pp2_convert:
 mov di, si
 shr di, cl
 and di, bx         ;DI = Current digit

 or WORD PTR [bp+04h], di             ;If digit is non zero, [bp+04h] will become non zero
                      ;If [bp+04h] was non zero, result is non zero
 jnz _pp2_print                       ;Simply put, if the result is non zero, we must print the digit

 
 ;Here we have a non significant zero
 ;We should skip it BUT only if it is not the last digit (0 should be printed as "0" not
 ;an empty string)

 test cl, cl
 jnz _pp_continue


_pp2_print:
 ;Convert digit to digital and print it

 
 mov dl, BYTE PTR [DIGITS + di]
 mov ah, 02h
 int 21h


_pp_continue:
 ;Remove digit from the number

 sub cl, ch
jnc _pp2_convert

 pop di
 pop si
 pop dx
 pop cx
 pop bx
 pop ax

 pop bp
 ret 06h

Data

This data must be put in the data segment, the one reached by `DS`.

DIGITS    db    "0123456789abcdef"

;Format for each WORD is S D where S and D are bytes (S the higher one)
;D = Bits per digit  --> log2(BASE)
;S = Initial shift count --> D*[ceil(16/D)-1]

BASE2    EQU    0f01h
BASE4    EQU    0e02h
BASE8    EQU    0f03h
BASE16    EQU    0c04h

NASM porting

To port the code to NASM remove the PTR keyword from memory accesses (e.g. mov si, WORD PTR [bp+08h] becomes mov si, WORD PTR [bp+08h])

Extending the function

The function can be easily extended to any base up to 2255, though each base above 216 will print the same numeral as the number is only 16 bits.

To add a base:

  1. Define a new constant BASEx where x is 2n.
    The lower byte, named D, is D = n.
    The upper byte, named S, is the position, in bits, of the higher group. It can be calculated as S = n · (⌈16/n⌉ - 1).
  2. Add the necessary digits to the string DIGITS.

Example: adding base 32

We have D = 5 and S = 15, so we define BASE32 EQU 0f05h.
We then add sixteen more digits: DIGITS db "0123456789abcdefghijklmnopqrstuv".

As it should be clear, the digits can be changed by editing the DIGITS string.


1 If B is a base, then it has B digits per definition. The number of bits per digit is thus log2(B). For power of two bases this simplifies to log2(2n) = n which is an integer by definition.

2 In this context it is assumed implicitly that the base under consideration is a power of two base 2n.

3 For a base B = 2n to have an integral number of bit groups it must be that n | 16 (n divides 16). Since the only factor in 16 is 2, it must be that n is itself a power of two. So B has the form 22k or equivalently log2(log2(B)) must be an integer.