You can do these basic operations with pointers and addresses:
Initialize a pointer variable by storing an address in it
Load an address into registers, directly or from a pointer
The sections in the rest of this chapter describe variations of these tasks with both pointers and addresses. The examples in these sections assume that you have previously defined the following pointer types with the TYPEDEF directive:
PBYTE TYPEDEF PTR BYTE ; Pointer to bytes
NPBYTE TYPEDEF NEAR PTR BYTE ; Near pointer to bytes
FPBYTE TYPEDEF FAR PTR BYTE ; Far pointer to bytes
Summary: Let the assembler initialize pointer variables when possible.
If the value of a pointer is known at assembly time, the assembler can initialize it automatically so that no processing time is wasted on the task at run time. The following example illustrates how to do this:
Msg BYTE "String", 0
pMsg PBYTE Msg
If a pointer variable can be conditionally defined to one of several constant addresses, initialization must be delayed until run time. The technique is different for near pointers than for far pointers, as shown below:
Msg1 BYTE "String1"
Msg2 BYTE "String2"
npMsg NPBYTE ?
fpMsg FPBYTE ?
.
.
.
mov npMsg, OFFSET Msg1 ; Load near pointer
mov WORD PTR fpMsg[0], OFFSET Msg2 ; Load far offset
mov WORD PTR fpMsg[2], SEG Msg2 ; Load far segment
If you know that the segment for a far pointer is currently in a register, you can load it directly:
mov WORD PTR fpMsg[2], ds ; Load segment of
; far pointer
Often the address to be initialized is dynamic. You know the register or registers containing the address, and you want to save them in a variable for later use. Typical situations include memory allocated by DOS (see interrupt 21h function 48h in online help) and addresses found by the SCAS or CMPS instructions (see Section 5.1.3.1). The technique for saving dynamic addresses is illustrated below:
; Dynamically allocated buffer
fpBuf FPBYTE 0 ; Initialize so offset will be zero
.
.
.
mov ah, 48h ; Allocate memory
mov bx, 10h ; Request 16 paragraphs
int 21h ; Call DOS
jc error ; Return segment in AX
mov WORD PTR fpBuf[2], ax ; Load segment
. ; (offset is already 0)
.
.
error: ; Handle error
Summary: There are several options for copying pointers.
Sometimes one pointer variable must be initialized by copying from another. Here are two ways to copy a far pointer:
fpBuf1 FPBYTE ?
fpBuf2 FPBYTE ?
.
.
.
; Copy through registers is faster, but requires a spare register
mov bx, WORD PTR fpBuf1[0]
mov WORD PTR fpBuf2[0], bx
mov bx, WORD PTR fpBuf1[2]
mov WORD PTR fpBuf2[2], bx
; Copy through stack is slower, but does not use a register
push WORD PTR fpBuf1[0]
push WORD PTR fpBuf1[2]
pop WORD PTR fpBuf2[2]
pop WORD PTR fpBuf2[0]
Summary: Pointers passed as procedure arguments are pushed onto the stack.
When a pointer is passed as an argument to a procedure, it must be pushed onto the stack. The procedure then sets up a stack frame so that it can access the arguments from the stack. This technique is discussed in detail in Section 7.3.2, “Passing Arguments on the Stack.” Pushing a pointer is illustrated below:
; Push a far pointer (segment always pushed first)
push WORD PTR fpMsg[2] ; Push segment
push WORD PTR fpMsg[0] ; Push offset
Pushing an address is somewhat different:
; Push a far address as a far pointer
mov ax, SEG fVar ; Load and push segment
push ax
mov ax, OFFSET fVar ; Load and push offset
push ax
On the 80186 and later processors, you can shorten pushing a constant to one step:
push SEG fVar ; Push segment
push OFFSET fVar ; Push offset
Loading an address into a pair of registers is one of the most common tasks in assembly-language programming. You cannot do processing work with a constant address or a pointer variable until the address is loaded into registers.
Summary: Certain register pairs have standard uses.
You often load addresses into particular segment:offset pairs. The following pairs have specific uses:
Segment:Offset Pair | Standard Use |
DS:SI | Source for string operations |
ES:DI | Destination for string operations |
DS:DX | Input for DOS functions |
ES:BX | Output from DOS functions |
In addition, you can use ES:SI, DS:DI, DS:BX, or any segment:offset pair for your own indirect memory operands. You can use SS:BP with a displacement to access procedure arguments or local variables in procedures.
For near addresses, you need only load the offset; the segment is assumed as SS for stack-based data and as DS for other data. You must load both segment and offset for far pointers.
Here is an example of loading an address to DS:BX from a near data segment:
.DATA
Msg BYTE "String"
.
.
.
mov bx, OFFSET Msg ; Load address to BX
; (DS already loaded)
If the data is in a far data segment, it is loaded like this:
.FARDATA
Msg BYTE "String"
.
.
.
mov ax, SEG Msg ; Load address to ES:BX
mov es, ax
mov bx, OFFSET Msg
The technique for loading the address of a stack variable is significantly different from the technique for loading near addresses. You may need to put the correct segment value into ES for string operations. The following example illustrates how to load the address of a local (stack) variable to ES:DI:
Task PROC
LOCAL Arg[4]:BYTE
push ss ; Since it's stack-based, segment is SS
pop es ; Copy SS to ES
lea di, Arg ; Load offset to DI
Summary: Use LEA to load the offset of an indirect memory operand.
The local variable in this case actually evaluates to SS:[BP-4]. This is an offset from the stack frame (described in Section 7.3.2, “Passing Arguments on the Stack”). Since you cannot use the OFFSET operator to get the offset of an indirect memory operand, you must use the LEA (Load Effective Address) instruction.
Summary: Use MOV and OFFSET to load the offset of a direct memory operand.
To get the address of a direct memory operand, you can use the MOV instruction with OFFSET or the LEA instruction. MASM 6.0 automatically optimizes the LEA statement by generating the smaller and faster code, as shown in this example:
lea si, Msg ; If you code this statement,
mov si, OFFSET Msg ; MASM 6.0 generates this code
The LEA instruction can be used to determine the address of indirect memory operands, as shown below.
lea si, [bx] ; Legal - LEA required for indirect
; mov si, OFFSET [bx] ; Illegal - no OFFSET on indirect
Use the LES and LDS instructions to load far pointers. Use the MOV instruction to load a near pointer. The following example shows how to load a far pointer to ES:DI and a near pointer to SI (assuming DS as the segment):
InBuf BYTE 20 DUP (1)
OutBuf BYTE 20 DUP (0)
npIn NPBYTE InBuf
fpOut FPBYTE OutBuf
.
.
.
les di, fpOut ; Load far pointer to ES:DI
mov si, npIn ; Load near pointer to SI (assume DS)
Copying from one register pair to another is complicated by the fact that you cannot copy one segment register directly to another. Two methods are shown below. Timings are for the 8088 processor:
; Copy DS:SI to ES:DI, generating smaller code
push ds ; 1 byte, 14 clocks
pop es ; 1 byte, 12 clocks
mov di, si ; 2 bytes, 2 clocks
; Copy DS:SI to ES:DI, generating faster code
mov di, ds ; 2 bytes, 2 clocks
mov es, di ; 2 bytes, 2 clocks
mov di, si ; 2 bytes, 2 clocks
Summary: Use conditional assembly to write memory-model independent code.
Often you may want to write code that is memory-model independent. If you are writing libraries that must be available for different memory models, you can use conditional assembly to handle different sizes of pointers. You can use the predefined symbols @DataSize and @Model to test the current assumptions.
Summary: Use conditional assembly to handle pointers that have no specified distance.
You can use conditional assembly to write code that works with pointer variables that have no specified distance. The predefined symbol @DataSize tests the pointer size for the current memory model:
Msg1 BYTE "String1"
pMsg PBYTE ?
.
.
.
IF @DataSize
mov WORD PTR pMsg[0], OFFSET Msg1 ; Load far offset
mov WORD PTR pMsg[2], SEG Msg1 ; Load far segment
ELSE
mov pMsg, OFFSET Msg1 ; Load near pointer
ENDIF
In the following example, a procedure receives as an argument a pointer to a word variable. The code inside the procedure uses @DataSize to determine whether the current memory model supports far or near data. It loads and processes the data accordingly:
; Procedure that receives an argument by reference
mul8 PROC arg:PTR WORD
IF @DataSize
les bx, arg ; Load far pointer to ES:BX
mov ax, es:[bx] ; Load the data pointed to
ELSE
mov bx, arg ; Load near pointer to BX (assume DS)
mov ax, [bx] ; Load the data pointed to
ENDIF
shl ax, 1 ; Multiply by 8
shl ax, 1
shl ax, 1
ret
mul8 ENDP
If you have many routines, writing the conditionals for each case can be tedious. The following conditional statements generate the proper instructions and segment overrides automatically.
; Equates for conditional handling of pointers
IF @DataSize
lesIF TEXTEQU <les>
ldsIF TEXTEQU <lds>
esIF TEXTEQU <es:>
ELSE
lesIF TEXTEQU <mov>
ldsIF TEXTEQU <mov>
esIF TEXTEQU <>
ENDIF
Once you define these conditionals, you can use them to simplify code that must handle several types of pointers. This next example rewrites the above mul8 procedure to use conditional code.
mul8 PROC arg:PTR WORD
lesIF bx, arg ; Load pointer to BX or ES:BX
mov ax, esIF [bx] ; Load the data from [BX] or ES:[BX]
shl ax, 1 ; Multiply by 8
shl ax, 1
shl ax, 1
ret
mul8 ENDP
The conditional statements from the examples above can be defined once in an include file and used whenever you need to handle pointers.