[Back to FAQ SWAG index] [Back to Main SWAG index] [Original]

    ===========================================================================
     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)

[Back to FAQ SWAG index] [Back to Main SWAG index] [Original]