CS 451 Lab 5
Intel Machine Language


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 call, ret, and nop> instructions.

Overview

The purpose of this lab is to explore an instruction set and machine language that is different from MIPS. In particular, we will be exploring the Intel IA-32 machine language. As you work through this lab pay attention to the differences between MIPS and IA-32 including instruction length, addressing modes, and the variety of instructions included in the instruction sets.

Resources

Overview of the Intel 80x86 Architecture

A typical view of the Intel register set shows eight general-purpose, 32-bit registers (eax, ebx, ecx, edx, esi, edi, esp, ebp). Although these registers are technically general purpose (meaning that they can hold any 32-bit value), in practice only eax, ebx, ecx, and edx are used for general data. esp is typically the stack pointer; and ebp is 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 example, int x may be at location %ebp -4, and 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 esp and 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:

Generating and Understanding Assembler Code

The easiest way to write correct assembly code is to let a compiler do it for us!  Then we just have to figure out what it's doing and why.  A quick look at the man pages for the gcc compiler under Linux shows that the '-S' switch directs the compiler to generate assembly and stop (no executable is produced).  We can use this feature to learn the nature of a particular instruction set by writing simple, understandable programs in a high-level language (like C) and studying what the compiler produces.

A brief note about assembler notation in the interest of making the programs easier to read:

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

  1. Identify which instructions (i.e., lines of code) "set up" the stack and frame pointers, and which instructions restore the stack and frame pointers.
  2. For each of the first four assembly instructions in 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.

  1. Based on the assembly code, where does the calling function look for the return value?

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.

  1. Explain what each of the assembly language instructions in exampleIF-3.s does 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:

  1. The version of gcc on the Pentium 4s does not use pushl and popl to put function parameters on the stack. Explain how the assembly code does get the parameters to printf on 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).

Intel Machine Language

In comparison to MIPS, the Intel machine language is extremely complex: Instructions can be anywhere from 1 to 11 bytes long; and, each instruction supports many different addressing modes. Fortunately, most of the extremely complex instructions are very specialized and rarely used (e.g., the MMX instructions). The instructions you will examine today are much simpler.

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:

Look at Table 2-2 on page 2-6. The y-axis lists the possible values for an instruction's "memory" operand (the operand that may access memory). The brackets signify a memory access. For example [EAX] means that the operand is the data in the memory location whose address is stored in the register eax. In contrast, EAX identifies 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 EDX would have a ModR/M byte of 0x10. Notice that some R/M bits (0x04, 0x05, 0x84, 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.
  1. Using Table 2-2, identify the addressing mode that corresponds (in general) to each of the four possible values of Mod. (See Section 6.5 for more discussion on addressing modes.)

Now, let's look at some real Intel machine code:

Now, it's your turn:
  1. List the machine instruction for each of the instructions marked with an asterisk (*), and identify the meaning of each byte. For instructions with an "extended opcode" in the ModR/M byte, show how the byte is divided and the meaning of each sub-field. Some hints and sample output appear below.

    Your answers should look something like this:

  2. Notice that some of the push instructions are only one byte long. How did the designers squeeze both the opcode and the operator into one byte?
  3. When using Table 2-2, sometimes the y-axis refers to the source operand, and sometimes it refers to the destination. How can you tell whether the y-axis refers to the first or second operator? Hint: Compare instructions main+31 and main+34.
  4. MIPS expands all immediate values (i.e., "constants"), even small immediate values like 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.