The overview sections of this lab were originally written by Prof. Wolffe.
Note for Winter 2008: Skip problems 4 and 5. For problem 7, skip the
ebp). Although these registers are technically general purpose (meaning that they can hold any 32-bit value), in practice only
edxare used for general data.
espis typically the stack pointer; and
ebpis typically the frame pointer.
Local variables are typically stored in the memory around an
address stored in register
ebp, also called the frame
pointer. (Think of a "frame" around some segment of memory.) For
int x may be at location
int y at location
%ebp -8. (Remember,
ints are 4 bytes long.)
Like many machines, Intel processors use a stack. The register
esp holds the address of the next available address on
the stack. The stack grows down, meaning that the value of
esp decreases as data are added to the stack. Functions
store thir data on the stack.
Functions are required to leave the stack and frame pointers as
they found them. Therefore, the first few assembly language
instructions of any function store the current value of
ebp (typically on the stack). In
addition, the frame for local variables is typically on the stack.
Therefore, functions with local data also increase the stack pointer
enough to hold all the local variables. The last thing a function
does is restore the stack and frame pointers to their original values.
Finally, a few additional notes:
gccfollows a different convention from MIPS: The destination of an operation is typically the last operand, not the first. However,
bp. Similarly, the names (
dl) designate the most significant and least significant bytes (in earlier microprocessors Intel used 8-bit registers). See Figure 6.36 in Harris and Harris, or Figure 2.40 in the Patterson and Hennessy text.
A brief note about assembler notation in the interest of making the programs easier to read:
movl %esp, %ebpmeans to take the value of
%espand put it in
%ebp. In contrast,
movl (%esp), %ebpmeans to take the value at the top of the stack and put it in
As an example, begin with the program exampleIF-1.c, the minimal C program.
Run gcc -S exampleIF-1.c to produce assembly code for
the native machine. (I compiled this code on a Pentium 4. If
you use a different machine, your code may look slightly different. If your assembly code has instructions that end in "q" (e.g.,
movq, then you are on a 64-bit machine. I recommend switching to a 32-bit machine.)
Look at the resulting code and notice how the first few instructions
set up the stack and frame pointers, and the next few restore
them. There is a discussion of this process in the Stallings text (see
Section 10.5) along with a diagram of stack frame activation (see
Figure 10.9). This represents the minimal function entry code as
our main() is not declaring any local variables or
calling any other functions (i.e. the two lines are simply doing the
equivalent of an enter instruction).
main(i.e., beginning with
leal), identify the type of each operand ("immediate", "implicit", "register direct", etc.).
Compile exampleIF-2.c and examine the resulting assembly code.
Now, repeat this process with exampleIF-3.c, a program that declares a single integer value. Once again, only a single additional assembly instruction is added, making it easier to observe the effect of the change in the source code. Notice that variable names do not appear in the assembly code. Instead, each local variable is assigned to a memory location referenced as an offset from the frame pointer. You may want to use this trick later to declare local variables for your use.
exampleIF-3.sdoes and why. (A couple of the "whys" aren't obvious, so don't hesitate to ask for help.)
Finally, exampleIF-4.c, shows how to call the function printf. Notice that for function calls:
printfon and off the stack.
Now, you have the basic info you need to write and/or modify simple IA-32 assembly programs. If you need to figure out how to write something more complicated (other functions, loops, floating point, etc.), simply write a simple C program, look at the resulting assembly, and refer to the Intel Developer Manual: Vol. 2 if necessary. For any part of this lab, feel free to write a C program, compile it to assembly, then modify the assembly.
To run your assembly program, simply use gcc to finish compiling and linking it (e.g., gcc my_assembly.s -o my_executable).
Page 347 of Harris and Harris, as well as page 32 (aka 2-1) in the Developer's Manual, show the basic format of an Intel instruction. Some instructions have a prefix of up to four bytes. None of our instructions will have a prefix. The next 1 to 3 bytes contain the op code. The instructions we will examine all have one byte opcodes. The byte after the opcode describes the operands and the addressing modes of the operands. As shown in Figure 2-1, this byte is divided into three fields:
ebx, etc.) that uses the addressing mode specified by bits 6 and 7.
[EAX]means that the operand is the data in the memory location whose address is stored in the register
eax. In contrast,
EAXidentifies a simple, register-direct access to
eax. The x-axis lists the registers that can serve as the second operand. Thus, according to this table, an instruction whose first operand is
[EAX], and whose second operand is
EDXwould have a ModR/M byte of
0x10. Notice that some R/M bits (
0x85, etc.) indicate that the operands are listed in a second addressing bit called the
SIB. Table 2-3 lists the meaning of values in the SIB.
Now, let's look at some real Intel machine code:
exampleML-1.cdown to assembly code.
exampleML-1.swith the debug flag (i.e.,
gcc -g exampleML-1.s -o ex1).
gdb ex1). (gdb is the GNU debugger.)
disassemble main. You should see output that looks something the sample below. (If it looks drastically different, you may be using a 64-bit machine. I recommend completing this lab using a 32-bit machine.) The first column lists the address of each instruction in
main. The second column lists the address of the instruction relative to the beginning of main. The third and fourth columns contain the assembly instruction.
0x08048384 <main+0>: lea 0x4(%esp),%ecx 0x08048388 <main+4>: and $0xfffffff0,%esp *0x0804838b <main+7>: pushl 0xfffffffc(%ecx) *0x0804838e <main+10>: push %ebp 0x0804838f <main+11>: mov %esp,%ebp 0x08048391 <main+13>: push %ecx *0x08048392 <main+14>: sub $0x24,%esp *0x08048395 <main+17>: movl $0x64,0xfffffff4(%ebp) 0x0804839c <main+24>: movl $0xc8,0xfffffff8(%ebp) *0x080483a3 <main+31>: mov 0xfffffff8(%ebp),%eax *0x080483a6 <main+34>: mov %eax,0x8(%esp) 0x080483aa <main+38>: mov 0xfffffff4(%ebp),%eax 0x080483ad <main+41>: mov %eax,0x4(%esp) 0x080483b1 <main+45>: movl $0x8048484,(%esp) *0x080483b8 <main+52>: call 0x80482b8 0x080483bd <main+57>: mov $0xc7,%eax *0x080483c2 <main+62>: add $0x24,%esp 0x080483c5 <main+65>: pop %ecx 0x080483c6 <main+66>: pop %ebp 0x080483c7 <main+67>: lea 0xfffffffc(%ecx),%esp *0x080483ca <main+70>: ret *0x080483cb <main+71>: nop
x main+11to look at the machine code for the fifth instruction (
mov %esp, %ebp). Remember, Intel processors are "little-endian", so you must read the bytes right to left. In this case, the first byte of the instruction is
0x89, the second is
0xe5. The remaining two bytes are part of the next instruction.
movinstruction on page 472 (aka 3-431) of the Intel Developer's guide, you will see that
0x89is the op code for the version of the
movinstruction that moves data from either a register or memory into another register.
0xe5corresponds to the ModR/M byte that has a source of
espand a destination of
x main+0to look at the machine code for the first instruction (
lea). Remember, Intel processors are "little-endian", so you must read the bytes right to left. In this case, the first byte of the instruction is
0x8d, the second is
0x4c, the third is
0x24, and the fourth is
leainstruction on page 414 (aka 3-373) of the Intel Developer's Guide, you will see that the
0x4cindicates that the non-memory operand is
ecx, and first operand is described by the SIB byte and the 8 bits following the SIB byte.
0x24corresponds to a parameter of
[%esp](i.e., the data at the top of the stack. When you put this together with the Mod/RM byte, the first parameter is the data at the top of the stack plus the immediate value of 4.
x main+4to look at the machine code for the second instruction (
and). If you look up the
andinstruction, you will see that the
0x83opcode for the version that ands an 8-bit immediate value onto a register. That opcode is followed by
/4. This means that the Reg/Opcode bits in the ModR/M word are
100. To decipher this byte, you must then look at the individual components of the byte. The value of the ModR/M byte for this instruction is
11100100. This makes the Mod bits
11and the R/M bits
100. Looking in Table 2-2, we see that an ModR/M combination of
espas the destination register.
pushthat follows) require you to break the ModR/M byte into the three component fields. The need to break up the
andinstruction's ModR/M filed is indicated by the "/4" after the opcode in the Developer's Manual. For these instructions, you will have to to examine the ModR/M byte by hand to identify the "memory" operand. See page 42 (aka 3-1) for details.
push %ebp, see Table 3-1 on page 43 (aka 3-2) for the meaning of the
+rwin the opcode.
Your answers should look something like this:
|assembly instruction||mov %esp,%ebp|
|Machine instruction (hex)||0x89e5|
|Instruction field||op code||Mod R/M|
|Field meaning||mov||source: esp, destination ebp|
|assembly instruction||lea 0x4(%esp),%ecx|
|Machine instruction (hex)||0x8d4c2404|
|Instruction field||op code||Mod R/M||SIB||offset|
|Field meaning||lea||source from SIB + 8 bit offset, destination ecx||source is ||offset to source value|
|assembly instruction||and $0xfffffff0,%esp|
|Machine instruction (hex)||0x83e4f0|
|Instruction field||op code||Mod||Extra Opcode||R/M||immediate|
|Field meaning||and||part of destination||extra opcode||destination %esp||immediate|
pushinstructions are only one byte long. How did the designers squeeze both the opcode and the operator into one byte?
1, to 16 bits. In contrast, Intel places only the minimum number of bytes necessary into an instruction. How does the CPU determine the length of an instruction's immediate value? Be specific.