The purpose of this assignment is to understand virtual-machine code (and by extension machine code) by writing a software implementation of a simple virtual machine.
The UM has these components:
To describe the locations on the machine, we use the following notation:
The UM is initialized by providing it with a program, which is a sequence of 32-bit words. Initially
At each time step, an instruction is retrieved from the word in the 0 segment whose address is the program counter. The program counter is advanced to the next word, if any, and the instruction is then executed.
The Universal Machine recognizes 14 instructions. The instruction is coded by the four most significant bits of the instruction word. These bits are called the opcode.
Number | Operator | Action |
0 | Conditional Move | if r[C] !=0 then r[A] :=r[B] |
1 | Segmented Load | r[A] :=word(m[r[B]], r[C]) |
2 | Segmented Store | word(m[r[A]], r[B]) :=r[C] |
3 | Addition | r[A] :=(r[B] + r[C]) mod2^32 |
4 | Multiplication | r[A] :=(r[B] ×r[C]) mod2^32 |
5 | Division | r[A] :=|_r[B] / r[C]_| |
6 | Bitwise NAND | r[A] :=~(r[B] & r[C]) |
7 | Halt | Computation stops. |
8 | Map Segment | A new segment is created with a number of words equal to the value in r[C]. Each word in the new segment is initialized to 0. A bit pattern that is not all zeroes and that does not identify any currently mapped segment is placed in r[B]. The new segment is mapped as m[r[B]]. |
9 | Unmap Segment | The segment m[r[C]] is unmapped. Future Map Segment instructions may reuse the identifier r[C]. |
10 | Output | The value in r[C] is displayed on the I/O device immediately. Only values from 0 to 255 are allowed. |
11 | Input | The universal machine waits for input on the I/O device. When input arrives, r[C] is loaded with the input, which must be a value from 0 to 255. If the end of input has been signaled, then r[C] is loaded with a full 32-bit word in which every bit is 1. |
12 | Load Program | Segment m[r[B]] is duplicated, and the duplicate replaces m[0], which is abandoned. The program counter is set to point to word(m[0], r[C]). If r[B] = 0, the load-program operation is expected to be extremely quick. |
13 | Load Value | See semantics for ``other instruction'' in Section [->]. |
Semantics of UM instructions [*]
Most instructions operate on three registers. The registers are identified by number; we'll call the numbers A, B, and C. Each number coded as a three-bit unsigned integer embedded in the instruction word. The register C is coded by the three least significant bits, the register B by the three next more significant than those, and the register A by the three next more significant than those:
A C | | vvv vvv .--------------------------------. |VUTSRQPONMLKJIHGFEDCBA9876543210| `--------------------------------' ^^^^ ^^^ | | opcode BSemantics are given in Figure [<-].
One special intruction, with opcode 13, does not describe registers in the same way as the others Instead, the three bits immediately less significant than opcode describe a single register A. The remaining 25 bits indicate a value, which is loaded into r[A].
A | vvv .--------------------------------. |VUTSRQPONMLKJIHGFEDCBA9876543210| `--------------------------------' ^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^ | | | value | opcode == 13
[*] The behavior of the Universal Machine is not fully defined; under circumstances detailed below (and only these circumstances), the machine may fail.
If a UM program demands resources that your implementation is not able to provide, and if the demand does not constitute failure as defined in Section [<-], your only recourse is to halt execution with a checked run-time error.
This problem presents two challenges:
On a 32-bit machine, you could simply use a 32-bit pointer as a segment identifier and have malloc do your heavy lifting. On the 64-bit machine, you will need an abstraction that maps 32-bit segment identifiers to actual sequences of words in memory. (Any representation of segments I can think of requires at least 64 bits to store.) Hanson's CII library is there if you need it.
Plan to reuse 32-bit identifiers that have been unmapped. One way is to store them in one of Hanson's sequences (Seq_T). A wonderful C99 trick is that you can cast an uintptr_t to a void *, so statements like
Seq_addlo(ids, (void *)(uintptr_t)id); return (Umsegment_Id)(uintptr_t)Seq_remlo(ids);might be useful. (I have written typedef uint32_t Umsegment_Id.)
Your choice of abstractions can easily affect performance of your UM by a factor of 1000. We will provide a benchmark that a well-optimized UM should be able to complete in about 1 second; a UM designed without regard to performance might take 20 minutes on the same benchmark. To get decent performance, focus on two decisions:
When we test your UM, we will give it only 1000 MB of memory, and we will limit its CPU time as well. Because you can easily overlook how much time and space your UM needs, we provide the commands mem-limited and cpu-limited, which ensure that your UM runs within specified limits. For example, to run with 500 MB of memory for at most 10 seconds, you can run
mem-limited 500m cpu-limited 10 ./um midmark.umIf your UM exceeds its limits, these programs will halt it. For more information about forced halts or other failures, run
catch-signal mem-limited 500m cpu-limited 10 ./um midmark.umThese commands are not documented, but they are available when you run use comp40.
Following this advice will help you avoid common mistakes:
The documentation of your design should include
For this assignment in particular, we have high expectations for your test plan.
In this assignment we are raising the bar for your design work:
Your design submission should include unit tests for the Universal Machine segment abstraction, but not for the Universal Machine instruction set. Your final submission should also include unit tests for the instruction set, represented as described below. In your README file, include documentation of a sequence of unit tests that in toto cover all of the Universal Machine instructions. Each unit test may rely on correct functioning of instructions from previous unit tests. For example:
Every unit test must include a compiled UM binary whose name ends in .um. [You may not use the names \texttt{midmark.um} or \texttt{selfcheck.um}.] You will write code to create these binaries. No unit test may cause any of the failures listed in Section [<-].
Unit tests may also include additional files. If your UM binary is called hello.um, your unit test may include these additional files:
good-um hello.um < hello.0or if you do not provide a hello.0,
good-um hello.um < /dev/nullWe will expect this command to produce output identical to hello.1, or if you do not provide a hello.1, we will expect it to produce no output.
We expect you to write a complete and correct implementation of the Universal Machine. Moreover, we expect it to be efficient enough to execute a UM benchmark of 50 million instructions in less than 100 CPU seconds on the lab machines; that's half a million instructions per second.
The UM is a virtual machine. One of the purposes of virtualization is to insulate the real (``host'') hardware from bad behavior by client (``guest'') software. For example, in the Amazon Elastic Compute Cloud, no matter how badly the client binaries behave, Amazon makes sure that when a virtual server halts, all machine resources are recovered. (Any other strategy would leave Amazon with machine resources that aren't earning any revenue.) Similarly, no matter how badly a UM client behaves, your implementation must ensure that, when the UM finishes running, all available machine resources are recovered.
For testing, you will find it useful to implement the UM as a library. However, we will be evaluating a command-line version which is a command-line program that expects exactly one argument: the name of a file containing a UM program. When a UM program is stored in a file, words are stored using big-endian byte order.
The UM ``I/O device'' should be implemented using standard input and standard output.
Using the script submit40-um-design, please submit
Using the script submit40-um, please submit
My implementation is about 200 lines of C code; almost half is devoted to conversions between 64-bit pointers and 32-bit Universal machine identifiers. Reading arguments and loading the initial program takes about 35 lines, so the Universal Machine itself is well under 100 lines of code.
We provide the following useful items:
You may find it useful to call Um_disassemble from DDD.
umdump cat.um umdump midmark.um | lessIt is the closest counterpart I have to objdump.