Chapter 1 and Chapter 2 show you how to declare and access simple variables in an assembly language program. This chapter fully explains 80x86 memory access. You will learn how to efficiently organize your variable declarations to speed up access to their data. This chapter will teach you about the 80x86 stack and how to manipulate data on the stack. Finally, this chapter will teach you about dynamic memory allocation and the heap.
This chapter discusses several important concepts, including:
This chapter will teach to you make efficient use of your computer's memory resources.
The 80x86 processors let you access memory in many different ways. Until now, you've seen only a single way to access a variable, the so-called displacement-only addressing mode. In this section you'll see some additional ways your programs can access memory using 80x86 memory addressing modes. The 80x86 memory addressing modes provide flexible access to memory, allowing you to easily access variables, arrays, records, pointers, and other complex data types. Mastery of the 80x86 addressing modes is the first step toward mastering 80x86 assembly language.
When Intel designed the original 8086 processor, it provided the processor with a flexible, though limited, set of memory addressing modes. Intel added several new addressing modes when it introduced the 80386 microprocessor. However, in 32-bit environments like Windows, Mac OS X, FreeBSD, and Linux, these earlier addressing modes are not very useful; indeed, HLA doesn't even support the use of these older, 16-bit-only addressing modes. Fortunately, anything you can do with the older addressing modes can be done with the new addressing modes. Therefore, you won't need to bother learning the old 16-bit addressing modes when writing code for today's high-performance operating systems. Do keep in mind, however, that if you intend to work under MS-DOS or some other 16-bit operating system, you will need to study up on those old addressing modes (see the 16-bit edition of this book at http://webster.cs.ucr.edu/ for details).
Most 80x86 instructions can operate on the 80x86's general-purpose register set. By specifying the name of the register as an operand to the instruction, you can access the contents of that register. Consider the 80x86 mov
(move) instruction:
mov(source
,destination
);
This instruction copies the data from the source
operand to the destination
operand. The 8-bit, 16-bit, and 32-bit registers are certainly valid operands for this instruction. The only restriction is that both operands must be the same size. Now let's look at some actual 80x86 mov
instructions:
mov( bx, ax ); // Copies the value from bx into ax mov( al, dl ); // Copies the value from al into dl mov( edx, esi ); // Copies the value from edx into esi mov( bp, sp ); // Copies the value from bp into sp mov( cl, dh ); // Copies the value from cl into dh mov( ax, ax ); // Yes, this is legal!
The registers are the best place to keep variables. Instructions using the registers are shorter and faster than those that access memory. Of course, most computations require at least one register operand, so the register addressing mode is very popular in 80x86 assembly code.
The 80x86 provides hundreds of different ways to access memory. This may seem like quite a lot at first, but fortunately most of the addressing modes are simple variants of one another, so they're very easy to learn. And learn them you should! The key to good assembly language programming is the proper use of memory addressing modes.
The addressing modes provided by the 80x86 family include displacement-only, base, displacement plus base, base plus indexed, and displacement plus base plus indexed. Variations on these five forms provide all the different addressing modes on the 80x86. See, from hundreds down to five. It's not so bad after all!
The most common addressing mode, and the one that's easiest to understand, is the displacement-only (or direct) addressing mode. The displacement-only addressing mode consists of a 32-bit constant that specifies the address of the target location. Assuming that variable j
is an int8
variable appearing at address $8088, the instruction mov( j, al );
loads the AL register with a copy of the byte at memory location $8088. Likewise, if int8
variable k
is at address $1234 in memory, then the instruction mov( dl, k );
stores the value in the DL register to memory location $1234 (see Figure 3-1).
The displacement-only addressing mode is perfect for accessing simple scalar variables. This is named the displacement-only addressing mode because a 32-bit constant (displacement) follows the mov
opcode in memory. On the 80x86 processors, this displacement is an offset from the beginning of memory (that is, address 0). The examples in this chapter often access bytes in memory. Don't forget, however, that you can also access words and double words on the 80x86 processors by specifying the address of their first byte (see Figure 3-2).
The 80x86 CPUs let you access memory indirectly through a register using the register-indirect addressing modes. The term indirect means that the operand is not the actual address, but rather the operand's value specifies the memory address to use. In the case of the register-indirect addressing modes, the value held in the register is the address of the memory location to access. For example, the instruction mov( eax, [ebx] );
tells the CPU to store EAX's value at the location whose address is in EBX (the square brackets around EBX tell HLA to use the register-indirect addressing mode).
There are eight forms of this addressing mode on the 80x86. The following instructions are examples of these eight forms:
mov( [eax], al ); mov( [ebx], al ); mov( [ecx], al ); mov( [edx], al ); mov( [edi], al ); mov( [esi], al ); mov( [ebp], al ); mov( [esp], al );
These eight addressing modes reference the memory location at the offset found in the register enclosed by brackets (EAX, EBX, ECX, EDX, EDI, ESI, EBP, or ESP, respectively).
Note that the register-indirect addressing modes require a 32-bit register. You cannot specify a 16-bit or 8-bit register when using an indirect addressing mode.[34] Technically, you could load a 32-bit register with an arbitrary numeric value and access that location indirectly using the register-indirect addressing mode:
mov( $1234_5678, ebx ); mov( [ebx], al ); // Attempts to access location $1234_5678.
Unfortunately (or fortunately, depending on how you look at it), this will probably cause the operating system to generate a protection fault because it's not always legal to access arbitrary memory locations. As it turns out, there are better ways to load the address of some object into a register; you'll see how to do this shortly.
The register-indirect addressing modes have many uses. You can use them to access data referenced by a pointer, you can use them to step through array data, and, in general, you can use them whenever you need to modify the address of a variable while your program is running.
The register-indirect addressing mode provides an example of an anonymous variable. When using a register-indirect addressing mode, you refer to the value of a variable by its numeric memory address (e.g., the value you load into a register) rather than by the name of the variable—hence the phrase anonymous variable.
HLA provides a simple operator that you can use to take the address of a static
variable and put this address into a 32-bit register. This is the &
(address-of) operator (note that this is the same symbol that C/C++ uses for the address-of operator). The following example loads the address of variable j
into EBX and then stores EAX's current value into j
using a register-indirect addressing mode:
mov( &j, ebx ); // Load address of j into ebx. mov( eax, [ebx] ); // Store eax into j.
Of course, it would have been easier to store EAX's value directly into j
rather than using two instructions to do this indirectly. However, you can easily imagine a code sequence where the program loads one of several different addresses into EBX prior to the execution of the mov( eax, [ebx]);
statement, thus storing EAX into one of several different locations depending on the execution path of the program.
The &
(address-of ) operator is not a general address-of operator like the &
operator in C/C++. You may apply this operator only to static variables.[35] You cannot apply it to generic address expressions or other types of variables. In 3.13 Obtaining the Address of a Memory Object, you will learn about the load effective address instruction that provides a general solution for obtaining the address of some variable in memory.
The indexed addressing modes use the following syntax:
mov(VarName
[ eax ], al ); mov(VarName
[ ebx ], al ); mov(VarName
[ ecx ], al ); mov(VarName
[ edx ], al ); mov(VarName
[ edi ], al ); mov(VarName
[ esi ], al ); mov(VarName
[ ebp ], al ); mov(VarName
[ esp ], al );
VarName
is the name of some variable in your program.
The indexed addressing modes compute an effective address[36] by adding the address of the variable to the value of the 32-bit register appearing inside the square brackets. Their sum is the actual memory address the instruction accesses. So if VarName
is at address $1100 in memory and EBX contains 8, then mov(
VarName
[ ebx ], al);
loads the byte at address $1108 into the AL register (see Figure 3-3).
The indexed addressing modes are really handy for accessing elements of arrays. You will see how to use these addressing modes for that purpose in Chapter 4.
There are two important syntactical variations of the indexed addressing mode. Both forms generate the same basic machine instructions, but their syntax suggests other uses for these variants.
The first variant uses the following syntax:
mov( [ ebx +constant
], al ); mov( [ ebx -constant
], al );
These examples use only the EBX register. However, you can use any of the other 32-bit general-purpose registers in place of EBX. This form computes its effective address by adding the value in EBX to the specified constant or subtracting the specified constant from EBX (see Figure 3-4 and Figure 3-5).
This particular variant of the addressing mode is useful if a 32-bit register contains the base address of a multibyte object and you wish to access a memory location some number of bytes before or after that location. One important use of this addressing mode is accessing fields of a record (or structure) when you have a pointer to the record data. This addressing mode is also invaluable for accessing automatic (local) variables in procedures (see Chapter 5 for more details).
The second variant of the indexed addressing mode is actually a combination of the previous two forms. The syntax for this version is the following:
mov(VarName
[ ebx +constant
], al ); mov(VarName
[ ebx -constant
], al );
Once again, this example uses only the EBX register. You may substitute any of the 32-bit general-purpose registers in lieu of EBX in these two examples. This particular form is useful when accessing elements of an array of records (structures) in an assembly language program (more on that in Chapter 4).
These instructions compute their effective address by adding or subtracting the constant
value from VarName
's address and then adding the value in EBX to this result. Note that HLA, not the CPU, computes the sum or difference of VarName
's address and constant
. The actual machine instructions above contain a single constant value that the instructions add to the value in EBX at runtime. Because HLA substitutes a constant for VarName
, it can reduce an instruction of the form
mov(VarName
[ ebx +constant
], al );
to an instruction of the form
mov(constant1
[ ebx +constant2
], al );
Because of the way these addressing modes work, this is semantically equivalent to
mov( [ebx + (constant1
+constant2
)], al );
HLA will add the two constants together at compile time, effectively producing the following instruction:
mov( [ebx + constant_sum
], al );
Of course, there is nothing special about subtraction. You can easily convert the addressing mode involving subtraction to addition by simply taking the two's complement of the 32-bit constant and then adding this complemented value (rather than subtracting the original value).
The scaled-indexed addressing modes are similar to the indexed addressing modes with two differences: (1) The scaled-indexed addressing modes allow you to combine two registers plus a displacement, and (2) the scaled-indexed addressing modes let you multiply the index register by a (scaling) factor of 1, 2, 4, or 8. The syntax for these addressing modes is
VarName
[IndexReg32
*scale
]VarName
[IndexReg32
*scale
+displacement
]VarName
[IndexReg32
*scale
-displacement
] [BaseReg32
+IndexReg32
*scale
] [BaseReg32
+IndexReg32
*scale
+displacement
] [BaseReg32
+IndexReg32
*scale
-displacement
]VarName
[BaseReg32
+IndexReg32
*scale
]VarName
[BaseReg32
+IndexReg32
*scale
+displacement
]VarName
[BaseReg32
+IndexReg32
*scale
-displacement
]
In these examples, BaseReg32
represents any general-purpose 32-bit register, IndexReg32
represents any general-purpose 32-bit register except ESP, and scale
must be one of the constants 1, 2, 4, or 8.
The primary difference between the scaled-indexed addressing modes and the indexed addressing modes is the inclusion of the IndexReg32
*
scale
component. These modes compute the effective address by adding in the value of this new register multiplied by the specified scaling factor (see Figure 3-6 for an example involving EBX as the base register and ESI as the index register).
In Figure 3-6, suppose that EBX contains $100, ESI contains $20, and VarName
is at base address $2000 in memory; then the following instruction
mov( VarName
[ ebx + esi*4 + 4 ], al );
will move the byte at address $2184 ($100 + $20*4 + 4) into the AL register.
The scaled-indexed addressing modes are useful for accessing elements of arrays whose elements are 2, 4, or 8 bytes each. These addressing modes are also useful for access elements of an array when you have a pointer to the beginning of the array.
Well, believe it or not, you've just learned several hundred addressing modes! That wasn't hard now, was it? If you're wondering where all these modes came from, just note that the register-indirect addressing mode isn't a single addressing mode but eight different addressing modes (involving the eight different registers). Combinations of registers, constant sizes, and other factors multiply the number of possible addressing modes on the system. In fact, you need only memorize about two dozen forms and you've got it made. In practice, you'll use less than half the available addressing modes in any given program (and many addressing modes you may never use at all). So learning all these addressing modes is actually much easier than it sounds.
[34] Actually, the 80x86 does support addressing modes involving certain 16-bit registers, as mentioned earlier. However, HLA does not support these modes and they are not useful under 32-bit operating systems.
[35] The term static here indicates a static
, readonly
, or storage
object.
[36] The effective address is the ultimate address in memory that an instruction will access, once all the address calculations are complete.