=========================================================================== BBS: The Beta Connection Date: 06-03-93 (00:08) Number: 680 From: CHRIS PRIEDE Refer#: NONE To: ALL Recvd: NO Subj: BASM TUT01 (1/4) Conf: (232) T_Pascal_R --------------------------------------------------------------------------- No matter what HLL you use -- Pascal, C, COBOL or some other language -- there is still place for assembly in your programs. Corresponding directly to the native language of your computer, it combines unlimited control with unmatched efficiency. Since Borland added the built-in assembler in TP 6.0, enhancing Pascal programs with some assembly has become much easier than before. However, I have read many books and found the BASM sections in Turbo Pascal books are inadequate and frequently contain errors. There are very good assembly books available, but they focus on writing assembly-only programs using standalone assemblers. Considering this, I decided to write a text file -- assembly language lessons for TP programmers, mostly using BASM. When I asked our host Guy for his opinion on this idea, he aproved and suggested I post sections weekly in this conference. A large part of this tutorial will be dedicated not to assembly language itself, but tasks that often require it: writing interrupt handlers and TSRs, accessing hardware directly. I will try to post new sections weekly, if my schedule permits. Questions, suggestions and criticism are very welcome. You will need a copy of TP 6.0 or later. Turbo Debugger, TASM or MASM could be useful, but are not required. - I - To get started, we will take a Pascal routine, convert it to assembly (using BASM) and see if we can beat the compiler at code generation. We will use a simple integer square root function; it is not something you would need often (unless you are writing graphics routines), but serves well as an example and only requires simple data movement and arithmetic instructions. function ISqr(I: word): word; var Root, LRoot: word; begin Root := 1; repeat LRoot := Root; Root := ((I div Root) + Root) div 2; until ((integer(Root - LRoot) <= 1) and (integer(Root - LRoot) >= -1)); ISqr := Root; end; It is based on a well-known formula (name has escaped my memory). The loop usually continues until Root and LRoot are equal, but that might never happen with integers because fraction is truncated. Our version loops until difference is less than 1, resulting in almost correctly rounded result. The number of iterations required to find square root of N never exceeds (ln N) +1, which means our function will find the square root of any valid argument in 12 or less iterations. Now, let's convert this to assembly. One major improvement we can make is to place both temporary variables in registers. CPU can access registers much faster than memory. AX and DX are needed for division, so we will assign Root to register CX and LRoot -- to BX: function ISqr(I: word): word; assembler; asm mov cx, 1 { Root := 1 } @@1: { loop start label } mov bx, cx { LRoot := Root } mov ax, I {< < } sub dx, dx {< AX := (I div Root) < } div cx {< < Root := ((I div Root) } add ax, cx { AX := AX + Root < + Root) div 2 } shr ax, 1 { AX := AX div 2 < } mov cx, ax { Root := AX < } sub ax, bx { AX := Root - LRoot } cmp ax, 1 { Compare AX to 1... } jg @@1 { Greater than 1 -- continue loop } cmp ax, -1 { Compare AX to -1... } jl @@1 { Less than -1 -- continue loop } mov ax, cx { ISqr := Root -- return result in AX } end; Simple statements translate to one instruction, while complex expressions have to be broken down in smaller steps. Notice we compute expression (Root - LRoot) only once, although it's result is tested twice. As you will see shortly, Turbo Pascal is not smart enough yet to take advantage of this: compiled Pascal code would subtract twice. (continued in next message...) --- * D.W.'s TOOLBOX, Atlanta GA, 404-471-6636 * PostLink(tm) v1.05 DWTOOLBOX (#1035) : RelayNet(tm) =========================================================================== BBS: The Beta Connection Date: 06-03-93 (00:10) Number: 681 From: CHRIS PRIEDE Refer#: NONE To: ALL Recvd: NO Subj: BASM TUT01 (2/4) Conf: (232) T_Pascal_R --------------------------------------------------------------------------- Function result is left in AX register. TP expects function return values in the following registers: Char, Byte : AL Word, Integer : AX LongInt, pointers : DX:AX (low order word/offset in AX, high order word/segment in DX) Let's go through this line by line... * function ISqr(I: word): word; assembler; This is a standard function declaration, except for the word "assembler", which tells Turbo Pascal entire function is going to be in assembly (if you try to insert some Pascal code in assembler function, it won't work). * asm "Asm" means start of assembly block, just like "begin" means start of Pascal block. Since our function was declared assembler, it has only asm block; without "assembler" keyword it would have to start with "begin": function Foo; begin asm { some assembly code } end; end; You can use asm blocks anywhere in your Pascal code, but conventions for pure asm functions are somewhat different. * mov cx, 1 MOV (Move) instruction is assembly assignment statement. This line is equivalent to CX := 1 in Pascal. * @@1: This is a local label, not unlike Pascal labels, used with dreaded GOTO statements. Unfortunately, the only means of flow control in asm is using GOTO-like jumps (conditional or unconditional), so you would better get used to it. Destination of such jumps can be a Pascal-style, previously declared label or a local label, like the one above. Local labels don't have to be previously declared, but they should start with @ (at sign). * mov bx, cx * mov ax, I Some more MOVing. Notice we can move data between two registers or register and memory (argument I is stored on stack -- in memory). We can't, however, directly move one memory variable into another: most 80x86 instructions can't have two memory operands. If you ever need to assign one memory variable to another, it should be done through a register, like this: mov ax, X mov Y, ax The same applies to most other instructions with two operands. * sub dx, dx SUB (Subtract) subtracts the right operand from left and leaves the result in left operand. SUB AX, CX is equivalent to AX := AX - CX in Pascal. As you may have noticed, we are subtracting DX from itself. This is a better way of setting register to 0 (100 - 100 = 0). We could use MOV DX, 0, but SUB instruction is one byte smaller and a few clock cycles faster. Some programmers use XOR for the same purpose: a number XORed with itself results in 0 too. We need DX to be 0 for division. * div cx DIV (Divide). 80x86 divide and multiply instructions are different from other arithmetic instructions. You only need to specify divisor; other operands are assumed to be AL, AH or AX, DX registers. This table summarizes both DIV variants: Dividend ³Divisor ³Quotient ³Remainder ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÅÄÄÄÄÄÄÄÄÄÄÄÅÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÅ 16 bit (AX) ³8 bit ³8 bit (AL) ³8 bit (AH) 32 bit (DX:AX) ³16 bit ³16 bit (AX) ³16 bit (DX) (continued in next message...) --- * D.W.'s TOOLBOX, Atlanta GA, 404-471-6636 * PostLink(tm) v1.05 DWTOOLBOX (#1035) : RelayNet(tm) =========================================================================== BBS: The Beta Connection Date: 06-03-93 (00:12) Number: 682 From: CHRIS PRIEDE Refer#: NONE To: ALL Recvd: NO Subj: BASM TUT01 (3/4) Conf: (232) T_Pascal_R --------------------------------------------------------------------------- The size of divisor selects between 16 and 32 bit divide. In our function both dividend and divisor are 16 bit words, which is why we had to zero DX, effectively extending word in AX to 32 bits. Divisor should be a register or memory variable. If you need to divide by immediate value, first move it in a register: mov bx, 320 div bx Use IDIV (Integer Divide) for signed numbers (integer, longint). IDIV works exactly like DIV, but performs signed division. * add ax, cx ADD (Addition). Pascal equivalent: AX := AX + CX. * shr ax, 1 Bitwise Shift Right. Pascal equivalent: AX := AX shr 1. As the name implies, this instruction shifts bits right: AX before shift: 0000101100110110 (decimal 2870) AX after shift: 0000010110011011 (decimal 1435) If you look at the decimal values on the right, you will notice shifting divided the number by 2. That is correct: shifting a binary number N bits left/right is equivalent to multiplying/dividing it by 2^N. CPU can shift bits much faster than divide and shift instructions are not restricted to certain register(s) like DIV -- remember this when you need to multiply/divide by a power of 2. The first operand (value to be shifted) can be either register or memory. The second operand is number of bits to shift -- immediate value or CL register. 8086 allows _only_ immediate value of 1; to shift by more than one bit use the following: mov cl, 3 (bit count) shr ax, cl You can also shift several times by one (I would use this method only to shift by 2 - 3 bits, otherwise it gets too long): shr ax, 1 shr ax, 1 shr ax, 1 286+ can shift by any immediate count. If you are compiling for 286 and better ({$G+} compiler directive), you can do this: shr ax, 3 * cmp ax, 1 CMP (Compare) compares two operands and sets CPU flags to reflect their relationship. Flag state are later used to decide if a conditional jump instruction should jump or not. This two step process is used to control program flow, like if..then statements and loops in Pascal. * jg @@1 ...and this is a conditional jump. JG (Jump if Greater) will transfer control (GOTO) to label @@1 if the last compare found left operand to be greater than right, otherwise it "falls through": execution continues at the next instruction. JG assumes operands were signed (integers). Use JA (Jump if Above) for unsigned values (words). The following is a summary of conditional jumps for arithmetic relationships: JA/JNBE Jump if Above (">", unsigned) JG/JNLE Jump if Greater (">", signed) JAE/JNB Jump if Above or Equal (">=", unsigned) JGE/JNL Jump if Greater or Equal (">=", signed) JE/JZ Jump if Equal ("=") JNE/JNZ Jump if Not Equal ("<>") JB/JNAE Jump if Below ("<", usigned) JL/JNGE Jump if Less ("<", signed) JBE/JNA Jump if Below or Equal ("<=", unsigned) JLE/JNG Jump if Less or Equal ("<=", signed) For ease of use, assemblers recognize two different mnemonics for most conditional jumps. Use the one you find less cryptic. Since conditional jump instructions simply inspect flags set by previous compare, there may be other instructions in between, provided they don't alter flags -- for example, MOV. Flags are not cleared and can be tested more than once. For example, you could do this: (continued in next message...) --- * D.W.'s TOOLBOX, Atlanta GA, 404-471-6636 * PostLink(tm) v1.05 DWTOOLBOX (#1035) : RelayNet(tm) =========================================================================== BBS: The Beta Connection Date: 06-03-93 (00:29) Number: 683 From: CHRIS PRIEDE Refer#: NONE To: ALL Recvd: NO Subj: BASM TUT01 (4/4) Conf: (232) T_Pascal_R --------------------------------------------------------------------------- mov ax, X cmp ax, Y jg @XGreater { X > Y } jl @XLess { X < Y } .... { fell through -- X = Y } * end; Like Pascal blocks, this means the end of BASM block. If you were using a standalone assembler, you would have to add RET (Return from subroutine) instruction and possibly some other (cleanup) code. BASM adds this and similar purpose code at the top of the function automatically -- it is called entry & exit code and usually amounts to 1 - 3 instructions, depending from number of arguments and local variables. Well, looks like everything is covered... Quite a bit for the first lesson too. If you have the instruction set reference, check it for more detailed descriptions. If you don't, I strongly recommend to obtain one. This is just one of many sources: The Waite Group's "Microsoft Macro Assembler Bible" or "Turbo Assembler Bible", ISBN 0-672-22659-6 (MASM flavor). $29.95, published by SAMS. A very good reference book, comes in MASM and TASM flavors, you choose. Finally, here is disassembly of compiled TP code. [BP+n] means reference to function argument, [BP-n] -- to local variable. This is provided mainly to satisfy your curiosity, but it shows we thought very much like the compiler, only coded it better: ISQR: isqr1.pas#9:begin 0000:0000 55 PUSH BP 0000:0001 89E5 MOV BP,SP 0000:0003 83EC06 SUB SP,+06 isqr1.pas#10: Root := 1; 0000:0006 C746FC0100 MOV [WORD BP-04],0001 isqr1.pas#11: repeat isqr1.pas#12: LRoot := Root; 0000:000B 8B46FC MOV AX,[BP-04] 0000:000E 8946FA MOV [BP-06],AX isqr1.pas#13: Root := ((I div Root) + Root) div 2; 0000:0011 8B4604 MOV AX,[BP+04] 0000:0014 31D2 XOR DX,DX 0000:0016 F776FC DIV [BP-04] 0000:0019 0346FC ADD AX,[BP-04] 0000:001C D1E8 SHR AX,1 0000:001E 8946FC MOV [BP-04],AX isqr1.pas#14: until ((integer(Root - LRoot) <= 1) and isqr1.pas#15: (integer(Root - LRoot) >= -1)); 0000:0021 8B46FC MOV AX,[BP-04] 0000:0024 2B46FA SUB AX,[BP-06] 0000:0027 3D0100 CMP AX,0001 0000:002A 7FDF JG isqr1.pas#12(000B) 0000:002C 8B46FC MOV AX,[BP-04] 0000:002F 2B46FA SUB AX,[BP-06] 0000:0032 3DFFFF CMP AX,0FFFF (-1) 0000:0035 7CD4 JL isqr1.pas#12(000B) isqr1.pas#16: ISqr := Root; 0000:0037 8B46FC MOV AX,[BP-04] 0000:003A 8946FE MOV [BP-02],AX isqr1.pas#17:end; 0000:003D 8B46FE MOV AX,[BP-02] 0000:0040 89EC MOV SP,BP 0000:0042 5D POP BP 0000:0043 C20200 RETN 0002 TP's code doesn't place variables in registers and does some unnecessary work (ie, subtracts Root - Lroot twice; see above). Entry and exit code is shown too. Our version is slightly faster, but I didn't pick this routine to demonstrate optimization -- integer math is about the only area where most compilers do well. A good optimizing compiler should be able to generate code as good as ours. I would be curious to see what this looks like compiled with SBP+ (Guy: consider this a strong hint :)). Most code you would normally write in assembly will show much greater improvement (string routines, etc.). What to expect: next time we will discuss how to call DOS or BIOS interrupts using BASM; what registers are available; which have special uses or should be preserved; how to access strings, arrays and records. Somewhere in near future: writing object methods in BASM. * * * --- * D.W.'s TOOLBOX, Atlanta GA, 404-471-6636 * PostLink(tm) v1.05 DWTOOLBOX (#1035) : RelayNet(tm)