Here are some simple but very powerful library routines, primarily
concerned with screen output. They all follow the same conventions:
- Routines preserve all registers that they are not specified to return.
- The direction flag (DF) should always be clear before calling.
All code is presented in MASM format. I do not use very many of the
functions of this assembler so it should be trivial to assemble these under
a different one. I do, however, use OPTION SCOPED, this means that labels
within a PROC block are local to that PROC block (a double colon suffixed
label is given global scope though).
First come the primitive routines. These are responsible for the actual
output and simply call DOS to do it. The name for this sort of thing is
called a 'wrapper' function. It does nothing in itself except afford a
particular interface to an application. If all your access to the OS is in
a small number of logical wrapper functions then porting your code to other
systems becomes a lot easier.
;pstrcx- write CX characters to stdout
; uses DOS function 040h
;
;entry: DS:SI=string address
; CX=length of string
;
;exit: (no parameters are returned)
pstrcx PROC NEAR
;assume that DOS can't handle a zero-byte write
;(I don't trust those M$ programmers)
JCXZ don
PUSH AX
PUSH BX
MOV AH,040h
MOV BX,1 ;stdout is handle #1
XCHG DX,SI
INT 021h
XCHG DX,SI
POP BX
POP AX
don: RET
pstrcx ENDP
Note the use of XCHG. XCHG is an extremely useful instruction indeed,
even though there are those who wish to see it's death along with all
those other "horrible, odd-ball, x86 specific". XCHG in essence performs
two operations simultaneously, which is hideously useful considering
they are both MOV's, also if one of the registers is AX (or EAX in 32-bit
code) you get a lovely 1 byte instruction bonus.
XCHG is in fact the real instruction hiding behind the psuedo-op NOP.
If you look at the opcode for a NOP, it is 090h, this is actually the
encoding for XCHG AX,AX, which since it has no effect on the machine state
whatsoever (except of course IP+=1) is ideally suited for this.
I haven't looked back since adding putch to my library. I used to use
the sequence:-
MOV DL,<char>; MOV AH,2; INT 021h
Not only is the putch method much cleaner and more flexible it is
also saving bytes! Of course the pay-back is that this method adds clocks.
However if you think about it the wasted clocks are meaningless really.
Sending characters one at a time to stdout is rather like spelling out
a dictate to your secretary letter-by-letter. In a case where you want
more MIPS you should be looking at your higher level algorithm and not
the output routine, an INT takes a vast amount of time anyway...
;putch- write single character to stdout
; uses DOS function 02h
;
;entry: AL=character to write
;
;exit: (no parameters are returned)
putch PROC NEAR
PUSH DX
XCHG DX,AX
MOV AH,2
INT 021h
XCHG DX,AX
POP DX
RET
putch ENDP
Not hot on speed this strlen, it was written to be compact. You can
if you wish write MUCH faster code than this. I believe X-Bios2 presented
something along these lines in a previous APJ. However, the most important
thing here is certainly not speed, and again if you wanted speed on string
handling so badly, you should really not use asciiz at all; it was never
designed for that.
;strlen- return length of asciiz string
;
;entry: DS:SI=address of asciiz string
;
;exit: CX=length of string
strlen PROC NEAR
PUSH AX
XOR CX,CX
DEC CX
lop: INC CX
LODSB
CMP AL,1
JNC lop
SBB SI,CX
POP AX
RET
strlen ENDP
Now, already, we start getting serious payback for being so good.
The code virtually writes itself.....
;pstr- write asciiz string to stdout
;
;entry: DS:SI=address of asciiz string
;
;exit: (no parameters are returned)
pstr PROC NEAR
PUSH CX
CALL NEAR PTR strlen
CALL NEAR PTR pstrcx
POP CX
RET
pstr ENDP
;pstrcr- write asciiz string to stdout with appended newline
;
;entry: DS:SI=address of asciiz string
;
;exit: (no parameters are returned)
pstrcr PROC NEAR
CALL NEAR PTR pstr
JMP NEAR PTR outcr
pstrcr ENDP
;outcr- write newline to stdout
;
;entry: (no entry parameters)
;
;exit: (no parameters are returned)
outcr PROC NEAR
PUSH AX
MOV AL,0Dh;CALL NEAR PTR putch
MOV AL,0Ah;CALL NEAR PTR putch
POP AX
RET
outcr ENDP
;pchn- write repeated character to stdout
;
;entry: AL=character
; CX=repetitions (0 is valid and does nothing)
;
;exit: (no parameters are returned)
pchn PROC NEAR
JCXZ don
PUSH CX
lop: CALL NEAR PTR putch
LOOP lop
POP CX
don: RET
pchn ENDP
;pstrlcl- output string DS:SI left justified in a field
; of CL spaces
;
; if the field width is smaller than the string length
; then the string is simply output
;
;entry: DS:SI=asciiz string
; CL=field width
;
;exit: (all registers preserved)
pstrlcl PROC NEAR
PUSH AX
PUSH CX
CALL NEAR PTR pstr
MOV CH,0
XCHG CX,AX
CALL NEAR PTR strlen
SUB AX,CX
JNA SHORT don
XCHG CX,AX
MOV AL,020h
CALL NEAR PTR pchn
don: POP CX
POP AX
RET
pstrlcl ENDP
Note the use of JNA. If you look at the logic for the JNA branch
(not many people seem to do this) you find that it branches iff
CF=1 OR ZF=1, hence after the SUB if the result goes <=0
You may notice that all the routine names are <= 8 chars. The reason
for this being that you can save each one as a seperate file, giving it
the name of the routine. This allows easy reference but has a drawback
or two:
(i) you have to remember the dependencies when you INCLUDE them
(ii) you end up with a LOT of files
So far I haven't found either of these 'drawbacks' to be a serious
problem.
I will be referring back to routines a lot in future articles; whenever
routines are required I will state it and the code shall have a list of
INCLUDE's for the routines to be included. In this manner it will be possible
to present quite untrivial programs within a reasonable amount of space.
|