shorter code to print a 16bit number in 8086

36 views
Skip to first unread message

luser...@nospicedham.gmail.com

unread,
Jul 30, 2021, 1:13:00 AM7/30/21
to
I wrote this machine code to trace the LIT word in my forth interpreter.
Is it possible to tighten up this code? It's trying to print a 16bit signed
integer (ignoring INT_MIN) left to right with no leading zeros.

This is assembly code, but I hope you'll forgive my ideosyncratic syntax.
The operands map left to right to the reg and reg/mem fields into the
mod-reg-reg/mem byte and the opcodes target the "to" form (direction
field = 1), eg. in "MOV(,R,BX,AX)", BX is the dest, AX is the source, the
R is needed to select register-to-register mode (the mod field). The empty
first argument is for tweaks to the opcode which could be F (clear the
direction field by subtracting 2) or BYTE (clear the word field by subtracting 1).

It tests each digit for zero where it jumps over the int 10h call except
the last digit.

It occurs to me that the DIV instruction has a mod field. So that means
the divisor could be register indirect, right? like through SI or BX, maybe?
Then it could probably be rolled up in a loop, right? Sorry if I'm jumping
the gun asking for help before exerting the requisite effort.

CODE(lit, lit, LODS, PUSH(AX)

#ifdef TRACE

, MOV(,R,BX,AX), OR(,R,BX,BX), JGE,7, MOVI(AX,0x0E00+'-'), INT(10), NEG(R,BX),

MOV(,R,AX,BX), XOR(,R,DX,DX), MOVI(CX,10000), DIV(R,CX),

OR(,R,AX,AX), JZ,6, MOVBI(AH,0x0E), ADDAL(48), INT(10),

MOV(,R,AX,DX), XOR(,R,DX,DX), MOVI(CX,1000), DIV(R,CX),

OR(,R,AX,AX), JZ,6, MOVBI(AH,0x0E), ADDAL(48), INT(10),

MOV(,R,AX,DX), MOVBI(CL,100), XOR(BYTE,R,CH,CH), BYTE+DIV(R,CL),

MOV(BYTE,R,DL,AH),

OR(BYTE,R,AL,AL), JZ,6, MOVBI(AH,0x0E), ADDAL(48), INT(10),

MOV(BYTE,R,AL,DL), XOR(BYTE,R,AH,AH), MOVBI(CL,10), BYTE+DIV(R,CL),

MOV(BYTE,R,DL,AH),

OR(BYTE,R,AL,AL), JZ,6, MOVBI(AH,0x0E), ADDAL(48), INT(10),

MOVBI(AH,0x0E), MOV(BYTE,R,AL,DL), ADDAL(48), INT(10),

MOVI(AX,0x0E00+' '), INT(10)

#endif

)/**/

Kerr-Mudd, John

unread,
Jul 30, 2021, 8:58:47 AM7/30/21
to
I think I got the jist of it;

here's some std asm code from earlier, rejigged for int 0x10 - it requires 5 free stack positions.
uses ax,bx,cx,dx - 36 bytes

PrtNum: ; ax to con using int10 fn 0E: no leading spaces or zeros but show '-'
test ax,ax ; Is it positive?
jge NotNeg
push ax
mov ax,0x0E00+'-'
int 0x10 ; bios prtc
pop ax
neg ax ; show as positive: hibit set only for 0x8000!#
NotNeg:
mov bx,10 ; decimal
xor cx,cx ; Digit count (5 max)
NextDigit:
inc cx ; prt at least 1 digit (i.e. '0')
xor dx,dx ; clear prev, cant cwd for #8000
div bx
push dx ; Remainder is the digit
test ax,ax ; Is it zero yet?
jnz NextDigit
PutDigit:
pop ax ; get digit (000x)
add ax,0x0E+'0' ; digit to display value, prtfn in ah
int 0x10 ; bios prtc
loop PutDigit







--
Bah, and indeed Humbug.

wolfgang kern

unread,
Jul 30, 2021, 8:58:50 AM7/30/21
to
On 30.07.2021 07:11, luser...@nospicedham.gmail.com wrote:
> I wrote this machine code to trace the LIT word in my forth interpreter.
> Is it possible to tighten up this code? It's trying to print a 16bit signed
> integer (ignoring INT_MIN) left to right with no leading zeros.

I'm sure that thjis could be made much faster and shorter too :)

> This is assembly code, but I hope you'll forgive my ideosyncratic syntax.
> The operands map left to right to the reg and reg/mem fields into the
> mod-reg-reg/mem byte and the opcodes target the "to" form (direction
> field = 1), eg. in "MOV(,R,BX,AX)", BX is the dest, AX is the source, the
> R is needed to select register-to-register mode (the mod field). The empty
> first argument is for tweaks to the opcode which could be F (clear the
> direction field by subtracting 2) or BYTE (clear the word field by subtracting 1).
>
> It tests each digit for zero where it jumps over the int 10h call except
> the last digit.

to speed it up you can use reciprocal MUL instead if DIV
tried to fall through instead of jump over ?
better would be a buffer and only a single write to screen.

> It occurs to me that the DIV instruction has a mod field. So that means
> the divisor could be register indirect, right? like through SI or BX, maybe?
> Then it could probably be rolled up in a loop, right? Sorry if I'm jumping
> the gun asking for help before exerting the requisite effort.

don't you use them already? DIV(R,CX)


> CODE(lit, lit, LODS, PUSH(AX)
>
> #ifdef TRACE
>
> , MOV(,R,BX,AX), OR(,R,BX,BX), JGE,7, MOVI(AX,0x0E00+'-'), INT(10), NEG(R,BX),
>
> MOV(,R,AX,BX), XOR(,R,DX,DX), MOVI(CX,10000), DIV(R,CX),
>
> OR(,R,AX,AX), JZ,6, MOVBI(AH,0x0E), ADDAL(48), INT(10),
>
> MOV(,R,AX,DX), XOR(,R,DX,DX), MOVI(CX,1000), DIV(R,CX),
>
> OR(,R,AX,AX), JZ,6, MOVBI(AH,0x0E), ADDAL(48), INT(10),
>
> MOV(,R,AX,DX), MOVBI(CL,100), XOR(BYTE,R,CH,CH), BYTE+DIV(R,CL),
>
> MOV(BYTE,R,DL,AH),
>
> OR(BYTE,R,AL,AL), JZ,6, MOVBI(AH,0x0E), ADDAL(48), INT(10),
>
> MOV(BYTE,R,AL,DL), XOR(BYTE,R,AH,AH), MOVBI(CL,10), BYTE+DIV(R,CL),
>
> MOV(BYTE,R,DL,AH),
>
> OR(BYTE,R,AL,AL), JZ,6, MOVBI(AH,0x0E), ADDAL(48), INT(10),
>
> MOVBI(AH,0x0E), MOV(BYTE,R,AL,DL), ADDAL(48), INT(10),
>
> MOVI(AX,0x0E00+' '), INT(10)
>
> #endif

I'm not familiar with this syntax, but I see many equal lines before the
too many INT 10.

F7 F0 ...F7 F7 and F6 F7..F6 F7 are indeed DIV by reg
same for the IDIV groups F7 F8...
__
wolfgang

R.Wieser

unread,
Jul 30, 2021, 11:29:16 AM7/30/21
to
luser,

> It's trying to print a 16bit signed integer (ignoring INT_MIN)
> left to right with no leading zeros.
...
> It tests each digit for zero where it jumps over the int 10h call
> except the last digit.

Those two do not match. And as your code seem to be written according to
the second ...
Try printing the value 3002 (or something else with embedded zeroes) to see
what I mean.

> Is it possible to tighten up this code?

I'm not sure what "tighten up" is ment to be read as. Smaller ? Faster ?
In the first case, how many bytes is it now ?

My own, erstwhile "trick" was, while doing the same as you (dividing by the
largest divisor first), to load a register with the divisor and than call a
small routine doing the division and printing.

But in the light of your "skip leading zeroes" you could do worse than to
look at what John posted, as that method automatically discards leading
zeroes - it simply stops as soon as the remainder becomes zero. I'm not so
sure about if that "loop" at the end will work though, as the INT 0x10 could
easily trash cx..

> It occurs to me that the DIV instruction has a mod field. So that
> means the divisor could be register indirect, right? like through SI
> or BX, maybe?

Yes, would work. And that same list can than also be used to skip leading
zeroes.

> Sorry if I'm jumping the gun asking for help before exerting the
> requisite effort.

I can imagine that you ask, but I do not quite understand why you did not
test your own code first ...

Regards,
Rudy Wieser



Kerr-Mudd, John

unread,
Jul 30, 2021, 11:59:21 AM7/30/21
to
On Fri, 30 Jul 2021 13:52:44 +0100
"Kerr-Mudd, John" <ad...@nospicedham.127.0.0.1> wrote:

> On Thu, 29 Jul 2021 22:11:00 -0700 (PDT)
> "luser...@nospicedham.gmail.com" <luser...@nospicedham.gmail.com> wrote:
>
> > I wrote this machine code to trace the LIT word in my forth interpreter.
> > Is it possible to tighten up this code? It's trying to print a 16bit signed
> > integer (ignoring INT_MIN) left to right with no leading zeros.
> >
>
[]
> >
> I think I got the jist of it;
>
> here's some std asm code from earlier, rejigged for int 0x10 - it requires 5 free stack positions.
> uses ax,bx,cx,dx - 36 bytes

; corrected version - 37

>
> PrtNum: ; ax to con using int10 fn 0E: no leading spaces or zeros but show '-'
> test ax,ax ; Is it positive?
> jge NotNeg
> push ax
> mov ax,0x0E00+'-'
> int 0x10 ; bios prtc
> pop ax
> neg ax ; show as positive: hibit set only for 0x8000!#
> NotNeg:
> mov bx,10 ; decimal
> xor cx,cx ; Digit count (5 max)
> NextDigit:
> inc cx ; prt at least 1 digit (i.e. '0')
> xor dx,dx ; clear prev, cant cwd for #8000
> div bx
> push dx ; Remainder is the digit
> test ax,ax ; Is it zero yet?
> jnz NextDigit
> PutDigit:
> pop ax ; get digit (000x)

> add ax,0x0E+'0' ; digit to display value, prtfn in ah
I didn't test! Make that line:
add ax,0x0E00+'0' ; digit to display value, prtfn in ah

Kerr-Mudd, John

unread,
Jul 30, 2021, 2:59:54 PM7/30/21
to
On Fri, 30 Jul 2021 17:26:59 +0200
"R.Wieser" <add...@nospicedham.not.available> wrote:

> luser,
>
> > It's trying to print a 16bit signed integer (ignoring INT_MIN)
> > left to right with no leading zeros.
> ...
> > It tests each digit for zero where it jumps over the int 10h call
> > except the last digit.
>
> Those two do not match. And as your code seem to be written according to
> the second ...
> Try printing the value 3002 (or something else with embedded zeroes) to see
> what I mean.
>
> > Is it possible to tighten up this code?
>
> I'm not sure what "tighten up" is ment to be read as. Smaller ? Faster ?
> In the first case, how many bytes is it now ?
>
> My own, erstwhile "trick" was, while doing the same as you (dividing by the
> largest divisor first), to load a register with the divisor and than call a
> small routine doing the division and printing.
>
> But in the light of your "skip leading zeroes" you could do worse than to
> look at what John posted, as that method automatically discards leading
> zeroes - it simply stops as soon as the remainder becomes zero. I'm not so
> sure about if that "loop" at the end will work though, as the INT 0x10 could
> easily trash cx..
>
Seems to be OK on this "DOS" VDM under XP - but yes, the int 10 was put in for the OP - originally the code put to a buffer at di, with a single print (ah=9, Int 21) at the end.

> > It occurs to me that the DIV instruction has a mod field. So that
> > means the divisor could be register indirect, right? like through SI
> > or BX, maybe?
>
> Yes, would work. And that same list can than also be used to skip leading
> zeroes.
>
> > Sorry if I'm jumping the gun asking for help before exerting the
> > requisite effort.
>
> I can imagine that you ask, but I do not quite understand why you did not
> test your own code first ...
>
> Regards,
> Rudy Wieser
>
>
>


R.Wieser

unread,
Jul 30, 2021, 3:30:01 PM7/30/21
to
John,

>> sure about if that "loop" at the end will work though, as the INT 0x10
>> could easily trash cx..
>>
> Seems to be OK on this "DOS" VDM under XP - but yes, the int 10 was
> put in for the OP - originally the code put to a buffer at di, with a
> single
> print (ah=9, Int 21) at the end.

Yes, it also works here (XP Pro sp3). But its not something I will bet my
life on - because the specs say that a(ny) function call (including INTs)
may trash AX thru DX at will.

I know its hard to take, but the extra two bytes for a push/pop cx around
that last INT 0x10 are really needed. :-)

Regards,
Rudy Wieser


Rod Pemberton

unread,
Jul 30, 2021, 6:33:43 PM7/30/21
to
On Thu, 29 Jul 2021 22:11:00 -0700 (PDT)
"luser...@nospicedham.gmail.com" <luser...@nospicedham.gmail.com>
wrote:


Sorry Frank, the first part of this may be a off-topic,
but I haven't been reading or posting to comp.lang.forth
in a while. So, I can understand if the OP posts here
with some Forth-ish assembly ...

> I wrote this machine code to trace the LIT word in
> my forth interpreter.

LIT is usually very simple and just stores an integer from
the interpreter's input stream into the current Forth word.


In C, my LIT primitive - the low-level function which goes
with the LIT dictionary entry - is:

void lit__(void)
{
push((cell_t)*ip);
ip++;
}

This dereferences/fetches what's at the Forth interpreter's
instruction pointer IP, stores that value on the parameter/data
stack, and then increments the IP.


I have LIT in the initial dictionary in C code (which I
use instead of assembly), because LIT is needed prior to
loading a high-level Forth dictionary. LIT is not a
standardized Forth word (subroutine).


You should be able to construct a LIT from your COMPILE
definition. E.g., just drop the , (comma) or equivalent
Forth word like COMPILE, from the end of the definition:

: COMPILE R> DUP CELL+ >R @ , ;
: LIT R> DUP CELL+ >R @ ;

For my personal Forth interpreter, the LIT is high-level
version of LIT equivalent to the low-level C code above.

I.e., the R> (r-from) captures the interpreter's saved IP,
DUPlicates the IP, increments one copy of the IP to skip
over the value, places the adjusted IP onto the return stack
via >R (to-r), fetches the value from the original IP onto
the parameter/data stack. That is the LIT value.

> Is it possible to tighten up this code? It's trying to
> print a 16bit signed integer (ignoring INT_MIN) left to
> right with no leading zeros.

Well, I really didn't look at the Forth-ish assembly,
but my suggestion to make your life easier would be
to print the decimal(?) "16bit signed integer" instead
in hexadecimal as a "16bit UN-signed integer".


--
...

Frank Kotler

unread,
Jul 30, 2021, 6:51:10 PM7/30/21
to
On 07/30/2021 06:01 AM, Rod Pemberton wrote:
> On Thu, 29 Jul 2021 22:11:00 -0700 (PDT)
> "luser...@nospicedham.gmail.com" <luser...@nospicedham.gmail.com>
> wrote:
>
>
> Sorry Frank,
Sorry Rod for the delay in posting = you keep changing your address so
you're not in the "whitelist" (name changed soon to "karenlist" so it's
not racist)
Try to stay on topic everyone... but it's bot as if we're swamped with
traffic...

Best,
Frank
[moderator]

luser...@nospicedham.gmail.com

unread,
Aug 7, 2021, 1:11:32 AM8/7/21
to
On Friday, July 30, 2021 at 12:13:00 AM UTC-5, luser...@nospicedham.gmail.com wrote:
> I wrote this machine code to trace the LIT word in my forth interpreter.
> Is it possible to tighten up this code? It's trying to print a 16bit signed
> integer (ignoring INT_MIN) left to right with no leading zeros.
>
> This is assembly code, but I hope you'll forgive my ideosyncratic syntax.
> The operands map left to right to the reg and reg/mem fields into the
> mod-reg-reg/mem byte and the opcodes target the "to" form (direction
> field = 1), eg. in "MOV(,R,BX,AX)", BX is the dest, AX is the source, the
> R is needed to select register-to-register mode (the mod field). The empty
> first argument is for tweaks to the opcode which could be F (clear the
> direction field by subtracting 2) or BYTE (clear the word field by subtracting 1).
>
> It tests each digit for zero where it jumps over the int 10h call except
> the last digit.
>
> It occurs to me that the DIV instruction has a mod field. So that means
> the divisor could be register indirect, right? like through SI or BX, maybe?
> Then it could probably be rolled up in a loop, right? Sorry if I'm jumping
> the gun asking for help before exerting the requisite effort.
>

Thanks for the help, everyone! Thanks to Rudy for pointing out that my
algorithm was malformed. I had tested it (sort of, I ran it and saw that
there was output), but I didn't check it against known values and so
didn't see the problem of suppressing inner zeros. (the numbers it
printed did appear weird, but I didn't properly consider and valuate
that clue).

John's code is so sweet and simple, I am envious of the ability to write
in such a way. I translated it to my syntax and then tried a few of the
variations alluded to earlier, like using BX to point to a table of divisors.
But I haven't actually implemented the LOOP instruction in the emulator
and the syntax I have for using labels in machine code hasn't been
incorporated into the emulator at all yet. So I haven't tested any of the
following code except to run it through CPP to check that macro expansion
works.

First I just translated John's code to my syntax (now with labels) and
lo and behold, it's super short and sweet. Exactly what I asked for.

CODE(lit, (LIT, LODS, PUSH(AX))
#ifdef TRACE
, (PrtNum, TEST(R,AX,AX), JGE, NotNeg-_, \
PUSH(AX), MOVAX(0xE00+'-'), INT(10), POP(AX), NEG(R,AX)),
(NotNeg, MOVBX(10), XOR(,R,CX,CX)),
(NextDigit, INC(CX), XOR(DX,DX), DIV(BX), PUSH(DX), TEST(AX,AX), JNZ, NextDigit-_),
(PutDigit, POP(AX), ADDAX(0xE00+'0'), INT(10), LOOP, PutDigit-_)
#endif
)

First I tried doing it with a table of divisors and unrolled code to divide by each
word in the table.

CODE(lit, (LIT, LODS, PUSH(AX))
#ifdef TRACE
, (stub, sJMP, CODE-_),
(divisors, DW(10000), DW(1000), DW(100), DW(10)),
(CODE, MOVBX(divisors), XOR(,R,DX,DX),
DIV(Z,BX_), ADDAX(0xE00+'0'), INT(10)),
(L1, MOV(,R,AX,DX), DIV(B,BX_),2, ADDAX(0xE000+'0'), INT(10),
MOV(,R,AX,DX), DIV(B,BX_),4, ADDAX(0xE000+'0'), INT(10)),
(L2, MOV(,R,AX,DX), DIV(B,BX_),6, ADDAX(0xE000+'0'), INT(10),
MOD(,R,AX,DX), ADDAX(0xE00+'0'), INT(10))
#endif
)

Which is longer, uglier, replete with repetition.

Then I tried doing it with a table of divisors and a loop to run through left-to-right.

CODE(lit, (LIT, LODS, PUSH(AX))
#ifdef TRACE
, (tracelit, MOVBX(divisors), XOR(,R,DX,DX), MOVCX(5)),
(digit, DIV(Z,BX_), ADDAX(0xE00+'0'), INT(10),
MOV(,R,AX,DX), ADDI(BX,2), LOOP, digit-_,
NEXT)
(divisors, DW(10000), DW(1000), DW(100), DW(10), DW(1)),
#endif
)

where NEXT is a macro for jumping to the next Forth word, pointed to by SI.
That's pretty short, but it treats all numbers as unsigned and makes
no attempt to suppress leading zeros.

Then I tried without using a table and calling a procedure for each digit.

CODE(lit, (LIT, LODS, PUSH(AX))
#ifdef TRACE
, (tracelit, XOR(,R,DX,DX), MOVCX(5),
TEST(R,AX,AX), JGE,notneg,
MOVI(AX,0xE00+'-'), INT(10), NEG(R,AX), JNO, notneg,
INC(R,DX)),
(notneg, MOVI(BX,10000), DIV(R,BX), CALL, print,
MOVI(BX,1000), MOV(,R,AX,DX), DIV(R,BX), CALL, print,
MOVI(BX,100), MOV(,R,AX,DX), DIV(R,BX), CALL, print,
MOVI(BX,10), MOV(,R,AX,DX), DIV(R,BX), CALL, print,
MOV(,R,AX,DX), CALL, print,
NEXT),
(print, ADDAX(0xE00'0'), INT(10), RET)
#endif
)

It's pretty cool that adding labels also lets me have local procedures,
but I feel like the call instruction vs, the ADD and INT instructions
doesn't really save much and is slower and longer on the screen.
But maybe all that just means that I should be using a DOS string
printing function instead of a character printing function.

And also, Rod was right and this whole effort was a wholy
unnecessary X/Y problem. And if I just add a high-level LIT word,
then I can call U. to print the number be done with this snipe hunt.

On a side note, I stumbled across the source on github for DOS 1.25
and 2.0 and I'm browsing ASM.ASM from 1.25 to get ideas about how to
make an assembler with a more common syntax.

Reply all
Reply to author
Forward
0 new messages