Skip to main content

Lab 4 and 5 : RISC V Disassembler and Emulator

The student repo for this lab will be based on assignment 2. Before you start the lab make sure you have created and cloned the repo

Acknowledgements

This lab has been modified by your CMPT 295 instructor and authors of RISC-V.

Learning Objectives:

  • Gain familiarity with RISC V instruction disassembling
  • Constructing emulator for RISC V programs
  • Disassemble and run a simple mac.input program. Add support for disassembling and running add,mul,addi, sw and lw instructions.
  • The base source code for the student repo is assignment 2.

FILES TO BE MODIFIED

  • part1.c
  • part2.c
  • utils.c

Ask ChatGPT or TA

  • Can you provide example to distinguish between C union and struct
  • Show the ASCII art of a C union example assuming starting address is 0.
  • Explain alignment rules in structs
  • Bitfields. The smallest data type in Cs is a char (8 byte). However, in many programs that wish to compactly encode the data (e.g., network programing) multiple data elements may be packed into the bits of a byte. Bitfields help achieve that. In this lab and assignment 2, bit fields help access particular fields of the opcode.
  • Unions. When multiple datatypes have the same size, unions are convenient for using a single representation and avoiding casting across data types e.g., use a 32 bit instruction but interpret as different instructions based on the type.

Core data structures

  • Instruction. This is the core data structure that is used to represent an instruction in both part 1 and part 2. It is declared as a union composed of a base struct with fields opcode and rest. The other members of the union include structs representing different instruction types.
// Lines 48---110 . types.h
typedef union {

    /* access opcode with: instruction.opcode */
    struct {
	unsigned int opcode : 7;
        unsigned int rest : 25;
    };

    /* access rtype with: instruction.rtype.(opcode|rd|funct3|rs1|rs2|funct7) */
    struct {
	unsigned int opcode : 7;
	unsigned int rd : 5;
	unsigned int funct3 : 3;
	unsigned int rs1 : 5;
	unsigned int rs2 : 5;
	unsigned int funct7 : 7;
    } rtype;

    /* access itype with: instruction.itype.(oppcode|rs|rt|imm) */
    struct {
	unsigned int opcode : 7;
	unsigned int rd : 5;
	unsigned int funct3 : 3;
	unsigned int rs1 : 5;
	unsigned int imm : 12;
    } itype;
 ....
}
  • Processor. The processor data is a struct with an array of 32 registers and a special register identifying the program counter.
typedef struct {
    Register R[32];
    Register PC;
} Processor;


Input program

Here is the assembly listing for a program that performs a simple mac operation (multiply and accumulate). RISC V lacks a mac instructions. Hence this has to be accomplished using a sequence of steps. Below we show an annotated assembly code. The assembly code has been compiled to a binary. The binary's layout in memory is shown as well. The instructions start at address 0x1000.

code/ref/mac.solution lists the reference disassembled program.

# Implement 3*5+10.
addi	x10, x10, 3 // Move 0x3 to %x10
addi	x11, x11, 5 // Move 0x5 to %x11
addi	x12, x12, 10 // Move 0x10 to %x12
mul	x13, x10, x11 // Multiple %x10 and %x11 and store in %x13
## Below 2 instruction sequence is not necessary
## We use it to illustrate how to implement memory ops
sw	x13, 0(x0) // Save the value onto address 0x0.
lw	x11, 0(x0) // Load the value from the 0x0
add	x13, x11, x12 # Add value to result. x11 now contains 3*5
addi	x11, x13, 0 # Display value on screen
addi	x10, x0, 1
ecall
addi	x10, x0, 10 # Exit the program
ecall
addi	x0, x0, 0

Binary Listing code/input/mac.input

Address Instruction
0x1000 0x00350513
0x1004 0x00558593
0x1008 0x00a60613
0x100c 0x02b506b3
0x1010 0x00d02023
0x1014 0x00002583
0x1018 0x00c586b3
0x101c 0x00068593
0x1020 0x00100513
0x1024 0x00000073
0x1028 0x00a00513
0x102c 0x00000073
0x1030 0x00000013

Part 1: Disassembler Lab 4

In this module, we are going to read the 32 bits corresponding to each instruction from mac.input, parse the bits, and fill in the fields on Instruction. We walk through the steps for the R-type instruction and ask you to implement the I and S types. Talk to the TA if you are confused..

Modify parse instruction in utils.c to support R instructions. Flowchart below outlines the steps. The figure also shows the format of the r type instructions. You can obtain the other instruction formats by referring here. RISCV Card

<iframe allowfullscreen frameborder="0" style="width:640px; height:480px" src="https://www.lucidchart.com/documents/embeddedchart/e855ba7b-4b4a-4e1e-94e9-26925414192a" id="YqWYAjD3bEvg">
  • Modify parse_instruction in utils.c
    • Step 1: Obtain opcode instruction_bits & ((1U << 7) - 1). This &s the instruction bits with a mask (bottom 6 1s = 0x0000 0000 0011 11111) to obtain the opcode. Following this shift right out the bottom seven bits to interpret the next set of fields.
    • Step 2: Obtain the destination field Rd. Obtain the 5 bits for destination instruction_bits & ((1U << 5) - 1);
    • Step 3: Obtain the funct3 3bit field indicating the type of arithmetic operation
    • Step 4: Obtain the source registers rs1 and rs2
    • Step 5: Obtain func7 (7 bit field indicating additional functionality)
Instruction parse_instruction(uint32_t instruction_bits)
{
    /* YOUR CODE HERE */
    Instruction instruction;
    // add x8, x0, x0     hex : 00000433  binary = 0000 0000 0000 0000 0000 01000 Opcode: 0110011 (0x33)
    // 1. Get the Opcode by &ing with 0b 0000 0000 00 111111, bottom 7 bits
    instruction.opcode = instruction_bits & ((1U << 7) - 1);

    // 2. Shift right to move to pointer to interpret next fields in instruction.
    instruction_bits >>= 7;

    switch (instruction.opcode)
    {
    // R-Type
    case 0x33:
        // instruction: 0000 0000 0000 0000 0000 destination : 01000
        instruction.rtype.rd = instruction_bits & ((1U << 5) - 1);
        instruction_bits >>= 5;

        // instruction: 0000 0000 0000 0000 0 func3 : 000
        instruction.rtype.funct3 = instruction_bits & ((1U << 3) - 1);
        instruction_bits >>= 3;

        // instruction: 0000 0000 0000  src1: 00000
        instruction.rtype.rs1 = instruction_bits & ((1U << 5) - 1);
        instruction_bits >>= 5;

        // instruction: 0000 000        src2: 00000
        instruction.rtype.rs2 = instruction_bits & ((1U << 5) - 1);
        instruction_bits >>= 5;

        // funct7: 0000 000  add
        instruction.rtype.funct7 = instruction_bits & ((1U << 7) - 1);
        break;
    // I-Type
    // Do this for I-type and S-type.

Ask ChatGPT or TA

  • You can copy-paste code in chatGPT to check if you are on the right track.
  • Show me the layout of an R-type instruction.

Modify part1.c to print the R type instruction. Use the provided RTYPE_FORMAT in utils.h

  • Add a printf statement. Specify the format RTYPE
  • include the following fields from type union in instruction (you have already set these in the previous step), name, instruction.rtype.rd, instruction.rtype.rs1, and instruction.rtype.rs2
void print_rtype(char *name, Instruction instruction) {
  printf(RTYPE_FORMAT, name, instruction.rtype.rd, instruction.rtype.rs1,
         instruction.rtype.rs2);
  /* YOUR CODE HERE */
}

Testing R instructions

Congratulations you are now ready to test the parsing of any R instruction

make riscv
./riscv -d ./code/input/R/R.input

Check Yourself

Parsing mac.input requires support for additionally I-type (ecall and addi), S-type (handling the SW).

  • Implement the I-type instruction. Refer to the RISC V card. Note that there are multiple potential opcodes
# Opcodes 0x1110011, 0x0010011, 1110011
instruction.itype.rd = instruction_bits & ((1U << 5) - 1);
instruction_bits >>= 5;
....
instruction.itype.rs1 = instruction_bits & ((1U << 5) - 1);
instruction_bits >>= 5;
instruction.itype.imm = instruction_bits & ((1U << 12) - 1);
  • Implement the S-type instruction. Hint. The immediate fields are split and need to be concatenated and sign extended. Refer to Week 2 slides 25---30
	int offset = 0x00000000;

	offset |= instruction.stype.imm5 & 0x0000001f; // imm[0:4]

	offset |= (instruction.stype.imm7 << 5) & 0x00000fe0; // imm[5:11]

	int immediate = sign_extend_number(offset, 12);

IF YOU ARE STUCK, YOU CAN ASK THE TA TO HELP YOU. MAKE SURE YOU COMMIT YOUR CODE and SEND A LINK TO THE TA, BEFORE ASKING THE TA.

  • Congrats! if you got here. That means you are ready to test disassembly of mac.input
$ make riscv
$ ./riscv -d ./code/input/mac.input

Part 2: Emulator Lab 5

In this module, we are going to look at the fields of the instruction parse in part 1 and implement the functionality of each instruction on the soft processor (a software data structure representing the hardware). Every RISCV instruction reads or modifies three variables Processor.R[32], Processor.R[PC], and Memory. You can think of the emulator as an interpreter that reading the program instruction-by-instruction and invoking the appropriate execute_ method in part2.c . Each execute_ modifies the processor state.

Background

  • Read the Description(C) column on page 1 of the RISC V card RISCV Card.

Source

Modify parse instruction in part2.c to support R instructions and I instructions.

Take a look at the output of the disassembler. The R-instruction types present in the mac program are add and mul. We add support in the processor model for these instructions. In part2.c

Step 1 Implement add.

The semantics of add instruction is rd = rs1 + rs2. rd and rs1 and rs2 are specified by the instruction. Under the switch case in Line 60: part2.c // Add in function execute_rtype(Instruction instruction, Processor *processor) . you will be implementing the instruction. To implement add do the following.

processor->R[instruction.rtype.rd] =
                ((sWord)processor->R[instruction.rtype.rs1]) +
                ((sWord)processor->R[instruction.rtype.rs2]);

What is the above statement doing ? It gets the registers to be modified from instruction bits passed to the function instruction.rtype.rs1,rs2,rd. It then reads and writes the processor->R registers. Since the R[32] is an unsigned variable and + is a signed operator, we cast the Rs to signed (sWord) before updating the destination register. Similarly we can implement multiply.

processor->R[instruction.rtype.rd] =
                ((sWord)processor->R[instruction.rtype.rs1]) *
                ((sWord)processor->R[instruction.rtype.rs2]);

Step 2 - Implementing addi

Addi is similar to add except the second operand is derived from the instruction as an immediate, not as a register. Further, we need to sign extend it from the 12th bit. What is sign extension slides 25-28.

processor->R[instruction.itype.rd] =
            ((sWord)processor->R[instruction.itype.rs1]) +
            sign_extend_number(instruction.itype.imm, 12);
# utils.c
int sign_extend_number(unsigned int field, unsigned int n)
{
    ...
    return (int)field << (32 - n) >> (32 - n);
}

Step 3 - Implement sw

The final step before we can run mac is to implement loads and stores. Loads and stores will now deal with processor->Memory. The execution of the load and store is split into two parts: argument setup and reading/writing from memory. Let us first implement the stores. # store writes to memory from a register. There are four parameters required by a store, the processors ram, the value to be written, the number of bytes, and the address. The value to be written is obtained by modifying the get_store_offset in utils.c.

  • Obtain store address offset
// utils.c
int get_store_offset(Instruction instruction)
{
    /* YOUR CODE HERE */
    int offset = 0x00000000;

    offset |= instruction.stype.imm5 & 0x0000001f; // imm[0:4]

    offset |= (instruction.stype.imm7 << 5) & 0x00000fe0; // imm[5:11]

    return sign_extend_number(offset, 12);
    return 0;
}
  • Pass parameters from instruction to store function
// part2.c execute_store()
case 0x2:
        // SW
        store(
            memory,
            ((sWord)processor->R[instruction.stype.rs1]) +
                ((sWord)get_store_offset(instruction)),
            LENGTH_WORD,
            processor->R[instruction.stype.rs2]);
        break;
  • Modify memory. The store value is a 32bit, while memory is an array of bytes. We have to shift-mask and obtain bytes from value before storing them in memory. value & 0x000000ff obtains byte 0, (value & 0x0000ff00) >> 8 obtains byte 1 and so on. We store this starting at memory[address] to memory[address+4]. In assignment 2 we might be testing with bytes or half words, or words. Make sure you handle those cases.
// e.g., value = 0xdeadbeef
// value & 0x000000ff = 0xef
// ((value & 0x0000ff00) >> 8) = 0xbe;
// (Byte)((value & 0x0000ff00) >> 8) = 0xad;
// (Byte)((value & 0x00ff0000) >> 16) = 0xbe
if (alignment == LENGTH_WORD)
{
    memory[address] = (Byte)(value & 0x000000ff);
    memory[address + 1] = (Byte)((value & 0x0000ff00) >> 8);
    memory[address + 2] = (Byte)((value & 0x00ff0000) >> 16);
    memory[address + 3] = (Byte)((value & 0xff000000) >> 24);
}
$ make riscv
$ ./riscv -r ./code/input/mac.input

Ask ChatGPT or TA

  • How to implement lb instructions assume memory is an array of type... we will let you figure out what the type of memory is
  • What is the program counter in RISC-V ? Which RISC-V instructions update the PC?