Retro video games delivered to your door every month!
Click above to get retro games delivered to your door ever month!
X-Hacker.org- Peter Norton Programmer's Guide - Norton Guide http://www.X-Hacker.org [<<Previous Entry] [^^Up^^] [Next Entry>>] [Menu] [About The Guide]

  An interface routine's form varies with its intended use. An
  assembly-language interface is a handshaker between your programming
  language and a ROM BIOS service, so it has to be tailored to meet the
  needs of both ends. It matters which programming language is being used;
  it matters which ROM BIOS service is being invoked; and it matters whether
  any data is being passed in one direction or the other. However, the
  general outline of an assembly-language interface is basically the same,
  no matter what you are doing.

  One of the best ways to understand how an assembly-language interface is
  coded is to view it as five nested parts, which are outlined here:

    Level 1: General assembler overhead
      Level 2: Subroutine assembler overhead
        Level 3: Entry code
          Level 4: Get parameter data from caller
            Level 5: Invoke ROM BIOS service
          Level 4: Pass back results to caller
        Level 3: Exit code
      Level 2: Finish subroutine assembler overhead
    Level 1: Finish general assembler overhead

  In this outline, Levels 1 and 2 tell the assembler what's going on, but
  don't produce any working instructions. Levels 3 through 5 produce the
  actual machine-language instructions.

  We'll examine each of these levels to show you the rules and explain
  what's going on. Don't forget that the specific requirements of an
  interface routine change for different circumstances. We'll point out the
  few design elements that are universal to all routines.

  Here is a simple ROM BIOS interface routine. It's designed to be called
  from a C program, but the elements of the interface design are the same
  whether you use this routine as is or adapt it to another programming
  language.

  _TEXT               SEGMENT      byte public 'CODE'
                      ASSUME       cs:_TEXT

                      PUBLIC       _GetMemSize
  _GetMemSize         PROC         near

                      push         bp
                      mov          bp,sp

                      int          12H
                      pop          bp
                      ret

  _GetMemSize         ENDP

  _TEXT               ENDS

                      END

  In the next few pages we'll examine the construction of this routine.

  Level 1: General assembler overhead

  Here is an outline of a typical Level-1 section of an interface routine,
  with the lines numbered for reference:

  1-1  _TEXT        SEGMENT    byte public 'CODE'
  1-2               ASSUME     cs:_TEXT

  (Levels 2 through 5 appear here)

  1-3  _TEXT        ENDS
  1-4               END

  Line 1-1 is a SEGMENT directive that declares the name of a logical
  grouping of executable machine instructions and informs the assembler (and
  any person who reads the source code) that what follows consists of
  executable code. Line 1-2, the ASSUME directive, tells the assembler to
  associate the CS register with any address labels in the _TEXT segment.
  This makes sense because the CS register is used by the 8086 to address
  executable code.

  Line 1-3 ends the segment started in line 1-1, and line 1-4 marks the end
  of the source code for this routine.

  The names _TEXT and CODE conform to the conventions used by virtually all
  C language compilers for PCs and PS/2s, as do the BYTE and PUBLIC
  attributes. Alternative names and attributes are available to advanced
  programmers, but for now we'll stick with the simplest.

  Level 2: Subroutine assembler overhead

  Next, let's look at an outline of a typical Level 2, the assembler
  overhead for a subroutine (called a procedure in assembler parlance). The
  sample on the following page shows some typical Level-2 coding.

  2-1                  PUBLIC   _GetMemSize
  2-2  _GetMemSize     PROC     near

  (Levels 3 through 5 appear here)

  2-3  _GetMemSize     ENDP

  Line 2-1 instructs the assembler to make the name of the procedure,
  _GetMemSize, public information, which means that the link program can
  then connect it to other routines that refer to it by name.

  Lines 2-2 and 2-3 bracket the procedure, named _GetMemSize. PROC and ENDP
  are mandatory and surround any procedure, with PROC defining the beginning
  of the procedure and ENDP signaling the end of it. Again, the near
  attribute on the PROC statement follows the conventions established for
  linking assembly-language routines to C programs. In more advanced C
  programs and in routines linked with programs written in languages like
  FORTRAN and BASIC, you must sometimes use a different attribute, far.
  (More about this in Chapter 20.)

  Level 3: Entry and exit code

  Levels 3, 4, and 5 contain actual executable instructions. In Level 3, the
  assembly-language routine handles the housekeeping overhead required if a
  subroutine is to work cooperatively with the calling program. The key to
  this cooperation is the stack.

  When the calling program transfers control to the subroutine, it does so
  by means of a CALL instruction. (In this example, the instruction would be
  CALL _GetMemSize.) When this instruction executes, the 8086 pushes a
  return address--the address of the instruction following the CALL--onto
  the stack. Later, the assembly-language routine can return control to the
  calling program by executing a RET instruction, which pops the return
  address off the stack and transfers control to the instruction at that
  address.

  If any parameters are to be passed to the assembly-language routine, the
  calling program pushes them onto the stack before it executes the CALL
  instruction. Thus, when the routine gets control, the value on top of the
  stack is the return address, and any parameters are found on the stack
  below the return address. If you keep in mind that the stack grows from
  higher to lower addresses and that each value on the stack is 2 bytes in
  size, you end up with the situation depicted in Figure 8-3.

  To access the parameters on the stack, most compilers and
  assembly-language programmers copy the value in SP into register BP. In
  this way the values on the stack can be accessed even within a routine
  that changes SP by pushing parameters or calling a subroutine. The
  conventional way of doing this is shown by the code on the next page.

  3-1        push       bp         ; preserve the current contents of BP
  3-2        mov        bp,sp      ; copy SP to BP

  (Levels 4 and 5 appear here)

  3-3        pop        bp
  3-4        ret

  After lines 3-1 and 3-2 have executed, the stack is addressable as in
  Figure 8-4. (In a moment, we'll show how useful this is.) When it's time
  to return control to the calling program, the routine restores the
  caller's BP register value (line 3-3) and then executes a RET instruction
  (line 3-4).

                    Bottom of stack
                           .
                   |       |        |
                   |       |        |
  Higher addresses |----------------|
                   |   Parameter    |
                   |----------------|.---- SP + 4
                   |   Parameter    |
                   |----------------|.---- SP + 2
                   | Return address |
   Lower addresses +----------------+.---- SP

  Figure 8-3.  The stack at the time a subroutine is called.

   Bottom of stack
          .
  |       |        |
  |       |        |
  |----------------|
  |   Parameter    |
  |----------------|.---- BP + 6
  |   Parameter    |
  |----------------|.---- BP + 4
  | Return address |
  |----------------|.---- BP + 2
  |  Caller's BP   |
  +----------------+.---- BP

  Figure 8-4.  The stack after register BP is initialized.

  If you think about it, you'll realize that things could be more
  complicated. For example, a calling program might use either a near or a
  far CALL instruction to transfer control to a subroutine. If your program
  uses far subroutine calls by convention (instead of the near calls used by
  default in C), the PROC directive (Line 2-2) would require the far
  attribute instead of near. This would instruct the assembler to generate a
  far RET instruction instead of a near RET.

  Furthermore, with a far calling convention, the return address on the
  stack would be 4 bytes in size instead of 2 bytes, so the first parameter
  would be at address [BP + 6] instead of [BP + 4] as shown in Figure 8-4.
  In this book, however, we'll stick to the most straightforward case: near
  PROCs and 2-byte return addresses.

  Level 4: Get parameter data from caller

  Level 4 deals with the parameters by passing them from the caller to the
  ROM BIOS, and with the results by passing them from the ROM BIOS to
  the caller. (Note, however, that the sample program contains no parameters
  from the caller.) The caller's parameters are on the stack, either in the
  form of data or addresses. (See Chapter 20 for help with this.) The
  registers, mostly AX through DX, are used for ROM BIOS input and output.
  The trick here--and it can be tricky--is to use the correct stack offsets
  to find the parameters. We'll sneak up on this problem in stages.

  First, you get to the parameters on the stack by addressing relative to
  the address stored in BP in lines 3-1 and 3-2. (Refer to Figure 8-2 to
  determine how items on the stack relate to the value in BP.) When more
  than one parameter is present on the stack, you must decide which
  parameter is which. Most languages push their parameters onto the stack in
  the order they are written. This means that the last parameter is the one
  closest to the top of the stack, at [BP + 4]. However, C uses the reverse
  order, so that the parameter at [BP + 4] is the first one written in the
  calling program.

  Parameters normally take up 2 or 4 bytes on the stack, although 2 bytes is
  more common. If any of these parameters were 4 bytes in size, you would
  need to adjust the subsequent references accordingly.

  If data were placed on the stack, then you could get it immediately by
  addressing it like this: [BP + 4]. If an address were placed on the stack,
  two steps would be needed: First, you would get the address, and second,
  you would use the address to get the data. A Level-4 example showing both
  data ([BP + 4]) and address ([BP + 6]) retrieval follows on the next page.

  4-1        mov     ax,[bp+4]       ; value of parameter1
  4-2        mov     bx,[bp+6]       ; address of parameter2
  4-3        mov     dx,[bx]         ; value of parameter2

  (Level 5 appears here)

  4-4        mov    bx,[bp+6]        ; address of parameter2 (again)
  4-5        mov    [bx],dx          ; store new value at parameter2 address

  All of these MOV instructions move data from the second operand to the
  first operand. Line 4-1 grabs data right off the stack and slaps it into
  the AX register. Lines 4-2 and 4-3 get data by means of an address on the
  stack: Line 4-2 gets the address (parking it in BX), and then line 4-3
  uses that address to get to the actual data, which is moved into DX. Lines
  4-4 and 4-5 reverse this process: Line 4-4 gets the address again, and
  then line 4-5 moves the contents of DX into that memory location.

  --------------------------------------------------------------------------
  NOTE:
    A crucial bit of assembler notation is demonstrated here: BX refers to
    what's in BX, and [BX] refers to a memory location whose address is in
    BX. A reference like [BP + 6] indicates a memory location 6 bytes past
    the address stored in register BP.
  --------------------------------------------------------------------------

  While sorting out these references may not be a snap, if you think it
  through carefully, it works out right.

  Level 5: Invoke ROM BIOS service

  Level 5 is our final step: It simply invokes the ROM BIOS service.

  Once all registers contain appropriate values (usually passed from
  the calling program and copied into registers by means of the stack), the
  routine can transfer control to the ROM BIOS using an interrupt:

  5-1        int     12h

  In this example, this single INT instruction does all the work for you.
  The ROM BIOS returns the computer's memory size in register AX, where C
  expects the routine to leave it when the routine returns control to the
  calling program. In other cases, you might need to leave a result
  elsewhere, as in Lines 4-4 and 4-5, above.

  Most ROM BIOS interrupts, however, provide access to several different
  services. In such cases, you must specify a service number in register AH
  before you execute the interrupt. For example, to access the first video
  service, you would execute the commands on the following page.

  mov     ah,0            ; AH=service number 0
  int     10h             ; ROM BIOS video services interrupt

  This five-step process outlines the basic principles of nearly all aspects
  of an assembly-language interface. In the following chapters, you'll see
  how this design is used in specific examples.

Online resources provided by: http://www.X-Hacker.org --- NG 2 HTML conversion by Dave Pearson