Homework 5: CPU Simulator

Important Stuff

Questions about the project? Send them to 354-help@cs.wisc.edu .

Deadline: Thu 4/20 Monday April 24th Wed April 26th

Updates

Useful to read: Appendix B of the Intel mega-manual. Also: This table.

Example file with the opcodes you need to deal with. Here is the .S. Compiling that with

gcc -c mov.S -m32
gives you this object file to play with. Note: this is not a sensible program; rather, it just contains a bunch of opcodes you should be able to handle. Use
objdump -d
to look at the .o and then make your own code examples for testing. We will also have tests available sometime this week.

Objectives

There are three objectives to this assignment:

  • To understand binary formats better
  • To learn about simulation
  • To understand truly how a CPU works, one instruction at a time

Notes

You are allowed to have a partner.

Useful reading collected here:

Overview

In this project, you will be implementing a program called xsim , a simple, limited x86 simulator. The simulator takes in a binary object file (full of x86 instructions in binary format) and then simulates the execution of a CPU. Simulators are hugely useful tools in systems research and analysis.

Xsim: Overview

This program's task is simple: to simulate the execution of a single program in memory, one instruction at a time. The input to the program is a real x86 object file (a .o) (we'll be using a .o as it is simpler to deal with than a full-fledged executable). The simulator, xsim, will first load the instructions from the program into a simulated memory. Then, it will start the main loop that simulates the CPU: fetch, decode, execute. It will continue to do this until it reaches a special instruction that we will use to indicate the program should be finished.

To simulate a CPU, you'll have to keep track of these things: register state (i.e., what's in each of the CPU registers, such as eip, eax, etc.), memory state (what's in each byte of memory), and a few other things (e.g., a few condition codes). Each instruction just updates some subset of machine state.

One of the first challenges you will have is decoding each instruction: x86 is not the easiest instruction set to decode, but it isn't too bad. We'll show some more examples below to get you going.

A First Example

Let's look at an example. Here, we start with assembly code:

movl $300, %eax
nop

Assume the above instructions are found in file mov.S . We can then produce an object file (mov.o) by running the compiler as follows:

gcc -c mov.S -m32

At this point, we could use the standard disassembler, objdump , to perform a disassembly for us, as follows:

prompt> objdump -d mov.o

mov.o: file format elf32-i386

Disassembly of section .text:

00000000 <.text>:
0: b8 2c 01 00 00 mov $0x12c,%eax
5: 90 nop
prompt>

The important part to focus on is found in the last two lines before the second prompt, which shows us a number of interesting things. First, it shows what addresses these instructions are located at within the object file (address 0 for the first, and 5 for the second). The reason the instructions are loaded at address zero has to do with how the compilation process works: one or more object files are created as if they will be loaded at memory address zero, and then, when the final executable is linked together, these addresses are changed to reflect the final address locations. However, for this project, we'll just pretend that the program gets loaded at address zero for simplicity.

Second, you can see the binary code that is found within the object file. Here it is just six bytes, printed out in hex format, one byte at a time: b8 2c 01 00 00 90. These bits lie in memory when the program runs and are what is interpreted by the CPU to perform its work.

Third, we see the disassembly of each instruction: mov $0x12c,%eax for the first and nop for the second. This shows us what the program objdump has determined these instructions are. The first is the familiar mov instruction, which will put the value 300 into register eax. The second is what is called a no op instruction, short for no operation . This instruction literally does nothing when run on a real CPU other than advance the instruction pointer. However, we will use it in a special way here: to indicate the simulation is over. Thus, whenever you encounter a nop , your program should terminate (more details below).

How To Simulate x86: Memory

The first thing to be done is to load the program binary into memory. To get you started, we provide a simple program that knows how to find the relevant bytes within an object file. That code is available here. Run it on something like mov.o to see that it works as you expect, and make sure you understand what the code is doing.

For this assignment, we will assume that each program has a 1 MB address space (1024 * 1024 bytes) . This is quite small compared to real 32-bit or larger address spaces, but is sufficient for our purposes here. Thus, you should allocate some kind of data structure (a large array?) to hold 1 MB of information and will serve as your memory.

Once you have created this structure, you should put the code you read from the object file into this memory. The rest of memory should be initialized to zero. Now, with the program loaded, you are almost ready to begin the hard part: simulating each x86 instruction!

How To Simulate x86: Registers and Condition Codes

Before we get to the hard part, we first must also create some simple structures to model the contents of each x86 32-bit register: eax, ebx, ecx, edx, esi, edi, esp, ebp, and of course eip. In general, these should also be initialized to zero. The exception is the stack pointer, which should be set to the value of the size of memory to begin with (1 MB). We will also see, below, how to initialize these register values to something other than zero.

You'll also have to simulate a few condition codes in order for compare and subsequent jump instructions to work. Specifically, you'll have to model the contents of the zero flag, sign flag, and overflow flag. More on this below.

How To Simulate x86: Instructions

The hard part is the simulation. How should a simulation work? Basically, what the simulator does is model the behavior of the CPU, which is to fetch/decode/execute instructions until finished. Let's look at our example from above to make more sense of this:

movl $300, %eax
nop

As you recall, the objdump output for this short program is:

0: b8 2c 01 00 00 mov $0x12c,%eax
5: 90 nop

Because eip is initialized to zero, the first thing your simulator will do is to fetch and then decode the instruction that starts at address zero. With x86, the first byte of the instruction often tells you what you need to know; in this case, the first byte is 0xb8 , or hex value b8 . This operation code , or opcode for short, tells us that the instruction is a move instruction, and that the instruction moves a 4-byte immediate value (which are the next four bytes in memory) into register eax (yes, this is a very specific instruction). Looking at the immediate value, you then also see it is 2's complement integer, placed into memory in little-endian form (see for details ). If you just treat those four bytes as an integer, and print it out, you will get the value you are looking for: 300.

Once you have decoded the instruction, you can actually simulate its execution. In this case, that would mean incrementing the eip by 5 (to 5), and putting the value 300 into your structure that tracks register eax. That's it! And then, you repeat.

So, how can you know how to interpret these instructions more generally? Fortunately, a good overview of x86 instruction encoding can be found here , and a list of instructions by opcode and name can be found online.

Be wary: The above resources often represent instruction operands in destination,source order and not the source,destination order we've been seeing all year.

A second question is: which instructions does the simulator have to be able to understand? Fortunately, not all of them(!), as the x86 instruction set is huge. For this project, we only require you understand the following instructions: only basic math ops (add, sub, imul, idiv), some forms of mov, cmp, the jump family (j/jg/jl/jne/je/jge/jle), and call/ret. Of course, even those represent a large amount of work, so you can restrict yourself in the following ways:

  • You only need to handle math ops on 32-bit registers as source and destination, or immediate-value as source and register as destination (no need to interpret memory addresses for math ops).
  • You only need to handle comparisons with 32-bit registers with other 32-bit registers or perhaps an immediate field, but also not with memory.
  • In general, only worry about the 32-bit register versions of these instructions.
  • Condition codes only should be set by the cmpl instruction; no other instruction should set these values (this is simpler than x86, in which math instructions also set condition codes).

We'll provide more detail on which instructions you need to interpret soon, via test cases, hopefully soon.

Command Line Inputs and Output Format

Your simulator is generally run as follows:

prompt> xsim objectfile
There are some optional flags, shown below.

Your simulator will generally produce just a single output: the state of the machine at the end of the execution (when the nop instruction is encountered). The format of this output will be very specific. First, you will print out the register values as follows:

eip: value_in_hex
eax: value_in_hex
ebx: value_in_hex
ecx: value_in_hex
edx: value_in_hex
esi: value_in_hex
edi: value_in_hex
esp: value_in_hex
ebp: value_in_hex
Then, you will print out the condition codes, either a 1 or 0 for each of zero, sign, and overflow.
condition_codes: Z:value S:value O:value
Then, for each NON-ZERO byte in memory, print it out with the address followed by the value as follows:
address_1: value_in_hex
address_2: value_in_hex
...

Your simulator will also take some flags. Specifically, they are:

-i initial value of instruction pointer
-s initial value of stack pointer
-B initial value of base pointer
-a initial value of eax
-b initial value of ebx
-c initial value of ecx
-d initial value of edx
-S initial value of esi
-D initial value of edi
You may wish to use getopt to facilitate parsing of flags; look it up online to learn more.

Finally, your simulator should take one more flag, the verbose flag, which prints output after each step of the simulation. Thus, you might run the simulator like this:

prompt> xsim -v -i 5 mov.o
This would set eip to 5 and start executing from that point, dumping the full output after each instruction is run.

Grading

Your implementation will be graded via a number of tests that we will provide shortly. Grading will be on functionality.

Contest

We will also be holding a contest not for points but for an exclusive 354 T-shirt. The fastest emulator (as run on a very large binary object) will be deemed the winner. More details on this soon as well.

Handing in your Code

Hand in your source code and a README file. The usual place: ~cs354-3/handin/NAME/hw5/, where NAME is your login name.

You should copy all of your source files (*.c and *.h) and a Makefile which builds xsim to your hw5/ handin directory. Do not submit any .o files.

If you have a partner, put the code in ONE of your directories, and a file called PARTNER.txt in BOTH handin directories (one per partner). In that file should be the login of both partners.

.

.