Some Size Optimization

TAD/Hugi

Introduction

This article describes a small (and hopefully useful) little utility which most coders need from time to time, the 'raw binary file-2-asm source' thing. Some ppl might find it useful for another reason, it's only 72 bytes in length. It demonstrates some widely known size optimization tricks to achieve this size. No doubt it could be even smaller.... (I'm betting that a certain person with an '.NL' email address will "chew some bytes".)

Anyway, let's get on with the code!!

Hey, your comments suck!

Yeah, but not as much as my code (heheh)..

Next to some instructions in the comments field I've written a [n] number, these parts will be more fully explained later. Heres the entire source!

	     .MODEL TINY		     ;  [1] 
	.CODE
	ORG	256
go:	mov	ax, 823Dh		;  [2] 
	mul	si
	mov	bl, [si-80h]		;  [3] 
	mov	[si+bx-7Fh], al 	;  [4] mark end of filename 
lp:	int	21h			;  [5] open the file/print string 
	jc	bye+1
bye:	mov	dx, 01C3h		;  [6] load at [DS:01C3] hex 
	mov	ah, 3Fh
	mov	cl, 8			;  [7] 
	mov	bl, 5			;  [8] (BX)=file handle 
	int	21h			;      load upto 8 bytes 
	xchg	ax, cx
	jcxz	bye+1			;  [9] read 0 bytes (EOF) ? 
	lea	di, txt+5
	mov	si, dx
prt:	mov	al, '0'
	stosb
	lodsb				;      get byte to convert 
	aam	16			; [10] AH=hi nibble, AL=lo nibble 
hex:	xchg	ah, al
	cmp	al, 10
	sbb	al, 69h
	das				; [11] convert(AL) into ASCII 
	stosb
	xor	ch, cl
	jnz	hex			; [12] toggle z/nz (loop 2x) 
	mov	ax, ',h'
	stosw				;      add ',h' 
	loop	prt
	mov	ax, 0924h		; [13] (AL)=char '$', (AH)=func 9 
	dec	di
	stosb				; [14] mark end with '$' 
	mov	dl,(txt-go)		; [15] 
	jmp	lp
txt	db	13,10,'db',9		;      new-line string 
	END	go 

Those pesky numbers

Okay, the comments were optimized a little too much in the above... Let me explain the reasons for doing some of those strange things.

	     .MODEL TINY		     ;  [1] 
	.CODE
	ORG	256 

First part of any optimized program: Compile it as a .COM program. It avoids all the .EXE header, the relocations and the register-setup of a normal .EXE program. The register values when a .COM program starts are:

	  CS = DS = ES = SS		  ; = code segment (16 bit, 64Kb) 
     AX=0000  BX=0000  CX=00FF	DX=CS
     SI=0100  DI=FFFE  BP=09xx	SP=FFFE
     IP=0100

     DF=0  IF=1 

Anyone on the Hugi size coding compo mailing list will instantly recognize the above... pity that some debuggers like TD don't..

go:     mov     ax, 823Dh		     ;  [2] 
	mul	si 

The strange looking instruction pair above makes (AX)=3D00h and (DX)=0082h but only takes 5 bytes instead of the usual 6 which two "MOV reg16,xxxx" instructions take up. The following instructions do exactly the same thing in the same number of bytes, but the MUL method makes (AL)=00 for free.

go:     mov     dx, 0082h		     ;  [2]
	mov	ah, 3Dh

The following marks the end of the filename on the command-line with a 00 byte terminator, without it DOS will fail to open the file.

	     mov     bl, [si-80h]	     ;  [3]
	mov	[si+bx-7Fh], al 	;  [4] mark end of filename

It's the same as this, but 2 bytes smaller..

	     mov     bl, [0080h]	      ;	[3]
	mov	[bx+0081h], al		 ;  [4] mark end of filename

I've used (SI) as a base address because it saves 1 byte in each of the above lines of code. An 8-bit displacement takes 1 byte, whereas a 16-bit address obviously takes 2 bytes. The (SI) register has a default value of 0100 hex (256 decimal).

lp:     int     21h		     ;  [5] open the file/print string

The above INT 21h instruction is used twice, once to open the file and once at the end to print the string using function AH=09h.

bye:    mov     dx, 01C3h		     ;  [6] load at [DS:01C3] hex

The above address 01C3 hex has a special purpose, the low-byte (C3 hex) is used as a hidden RET instruction. This is a technique called 'Overlapping Opcodes' or 'Hidden Opcodes'. The idea is simple, take a multi-byte instruction and then jump into the middle of it. The opcode bytes for the "MOV DX,01C3h" are BA C3 01 hex, so by jumping past the 1st byte we hit C3 hex which is a "RET" instruction.

The high-byte (01 hex) has a special purpose too. The 'txt' string shares the same high-byte address, so this saves another byte over a normal "LEA DX,txt" or "MOV DX,xxxx" instruction.

	     mov     cl, 8		     ;  [7]

Assume that (CH)=00, this saves 1 byte over a "MOV CX,0008" instruction.

	     mov     bl, 5		     ;  [8]

This is a very naughty method. It assumes the file handle for the first opened file is 5 (which under certain conditions it ain't!!). I should have really saved the file-handle given back by the (AH)=3D open function and used this, but hey, this saved a few bytes on the INT 21h.

	     xchg    ax, cx
	jcxz	bye+1			;  [9] read 0 bytes (EOF) ?

If he hit the EOF (End-Of-File) the (CX) register will be 0, otherwise it will have a number from 1 to 8 denoting the number of bytes read in.

	     aam     16 		     ; [10] AH=hi nibble, AL=lo nibble

The very useful BCD instructions!!! (Ah, my favourite.) The above splits the byte in the (AL) register into two parts. The high-nibble (bits 7..4) is placed in (AH) and the low-nibble (bits 3..0) is placed in (AL). Which is very useful for hex-2-ascii routines.

	     AH = AL div 16	    AL = AL mod 16

Check out st0ne's nice article about the other BCD instructions in Hugi 17, well worth the read.

	     xor     ch, cl
	jnz	hex			; [12] toggle z/nz (loop 2x)

Because (CX) is always between 1 and 8 the (CH) register can be used as nice loop counter with a repeat of two (once for each nibble) by toggling it with the value in the (CL) register. You can of course use NOT, XOR or even CMC to perform a similar task depending on the loop itself.

The XOR instruction also clears the CF (carry-flag) which is important because the INT 21h is used to open the file and to print the string.

	     mov     ax, 0924h		     ; [13] (AL)=char '$', (AH)=func 9

The above loads two 8-bits registers at the same time. This saves 1 byte over a "MOV AH,09h" and "MOV AL,24h" combination.

	     dec     di
	stosb				; [14] mark end with '$'

Mark the end of the current line buffer with '$' (hex 24) as needed by the stoopid Int 21h, Function 9 call. The above saves 1 byte over a "MOV [DI-1],AL" instruction.

	     mov     dl,(txt-go)	     ; [15]

The (DH) register is already 01 hex, so only the low-byte of the address needs to be loaded here. This saves 1 byte over a normal "MOV DX,xxxx".

How to use it

Almost forgot. First compile it as a .COM program.

	     TASM  raw2
	TLINK /t  raw2 

Then give it a filename on the command-line and redirect its screen output to any file you wish. I normally use the '.DB' extension, coz I'm too thick to think up anything more imaginative, also because using .ASM and .INC as a file-extension is far too dangerous... e.g.:

	     RAW2 mydata.dat  > mydata.db 

Final thought.

Oh well, that's all folks... I know it's not a ground-breaking article, but someone might find it useful. There has been so few articles about size optimization (or 'size coding' as Adok likes to call it) in the past, that I thought one was needed.

You should find the 'full' source code in the bonus pack together with a safer version which does not assume the file-handle is 5.

Happy optimizing...

TAD/Hugi