辅导programs编程、讲解c++程序设计、c/c++编程辅导 辅导留学生Prolog|辅导留学生Prolog
- 首页 >> Matlab编程 Goals
Enhance understanding the functionality implemented by a CPU
Understand the inner workings of RISC-V instruction set design
Prepare you to write RISC V assembly programs
Understand how emulators work.
Background
In this project, you will create an emulator that is able to execute a subset of the RISC-V ISA. You’ll provide the machinery to decode and execute a couple dozen RISC-V instructions.
What is a disassembler and emulator ?
A disassembler translates an executable binary to a human-readable assembly program. An emulator is a software model representing the processor. Emulators are useful when you wish to run a program targeting a different CPU than the underlying hardware. For instance, we are going to be emulating RISC-V programs on an x86 processor of your laptop or CSIL machine. Perhaps the most popular emulator framework is Qemu. The virtual box software that you are using is a type of emulator.
Check Yourself
Bitfields and Unions
The RISC-V ISA card provides some information necessary for completing this project.
Source
Make sure you read through the entire spec before starting the project.
The files you will need to modify and submit are:
TODO
part1.c: The main file which you will modify for part 1.
utils.c: The helper file which will hold various helper functions for part 1.
part2.c: The main file which you will modify for part 2. You will not be submitting this file for part 1.
You will NOT be modifying any files other than the ones listed above. This means that you will not be modifying header files. If you add helper functions, please place the function prototypes in the corresponding C files. If you do not follow this step, your code will likely not compile and you will get a zero on the project.
You should definitely consult through the following, thoroughly:
types.h: C header file for the data types you will be dealing with. It is critical that you read and understand the definitions in types.h before starting the project. If they look mysterious, consult the bitfields and unions ppt with this repo.
Makefile: File which records all dependencies.
code/input: Various files to run tests.
code/ref: Reference outputs (_.solution - part 1 . _.trace - part 2)
utils.h: File that contains the format for instructions to print for part 1.
You should not need to look at these files, but here they are anyway:
riscv.h: C header file for the functions you are implementing.
riscv.c: C source file for the program loader and main function.
$ bash ./install-cunit.sh # Install unit testing framework. This will create a folder called CUnit/
$ make riscv
RISC-V Emulator
The files provided in the start kit comprise a framework for a RISC-V emulator.
In part 1, you will add code to part1.c and utils.c to print out the human-readable disassembly corresponding to the instruction’s machine code. Look for /* Your Code */
In part 2, you’ll complete the program by adding code to part2.c to execute each instruction (including performing memory accesses). Your simulator must be able to handle the machine code versions of the RISC-V machine instructions below. We’ve already given you a framework for what cases of instruction types you should be handling.
Check yourself: why does sizeof(Instruction)==4?
The instruction set that your emulator must handle is listed below. All of the information here is copied from the RISC-V green sheet for your convenience; you may still use the green card as a reference.
Just like the regular RISC-V architecture, the RISC-V system you’re implementing is little-endian. This means that when given a value comprised of multiple bytes, the least-significant byte is stored at the lowest address.
The Framework Code
The framework code we’ve provided operates by doing the following.
1.It reads the program’s machine code into the simulated memory (starting at address 0x01000). The program to “execute” is passed as a command line parameter. Each program is given 1 MiB of memory and is byte-addressable.
2.It initializes all 32 RISC-V registers to 0 and sets the program counter (PC) to 0x01000. The only exceptions to the initial initializations are the stack pointer (set to 0xEFFFF) and the global pointer (set to 0x03000). In the context of our emulator, the global pointer will refer to the static portion of our memory. The registers and Program Counter are managed by the Processor struct defined in types.h.
3.It sets flags that govern how the program interacts with the user. Depending on the options specified on the command line, the simulator will either show a dissassembly dump (-d) of the program on the command line, or it will execute the program. More information on the command line options is below.
It then enters the main simulation loop, which simply executes a single instruction repeatedly until the simulation is complete. Executing an instruction performs the following tasks:
1.It fetches an instruction from memory, using the PC as the address.
2.It examines the opcode/funct3 to determine what instruction was fetched.
3.It executes the instruction and updates the PC.
The framework supports a handful of command-line options:
-i runs the simulator in interactive mode, in which the simulator executes an instruction each time the Enter key is pressed. The disassembly of each executed instruction is printed.
-t runs the simulator in tracing mode, in which each instruction executed is printed.
-r instructs the simulator to print the contents of all 32 registers after each instruction is executed. (Part 2) This option is most useful when combined with the -i flag.
-d instructs the simulator to disassemble the entire program, then quit before executing. (Part 1)
Part 1 Disassembler
In part 1, you will be writing a disassembler that translates RISC-V machine code into human-readable assembly code. You will also be laying the building blocks for the actual emulator that you will implement in part 2. You will be implementing the following functions. The files in which they are located are in parentheses.
1.parse_instruction(instruction_bits) (utils.c): Uses the given instruction (encoded as a 32-bit integer) and returns the corresponding instruction. You will have to determine the proper format of the given instruction and use the correspdonding member of the instruction struct. You will find the green sheet particularly helpful here.
2.sign_extend_number(field, n) (utils.c): This function interprets the number in field as an n-bit number and sign-extends it to a 32-bit integer.
3.get_*_offset(instruction) (utils.c): For the corresponding instruction type (either branch, jump, or store), this function unpacks the immediate value and returns the number of bytes to offset by. In this case of branches and jumps, these functions should return the number of bytes to add to the PC to get to the desired label. In the case of stores, the corresponding function will return the offset on the destination address.
4.print_*(instruction) (part1.c): Prints the instruction to standard output. You should use the constants defined in the file utils.h to properly format your instructions. Failure to do so will cause issues with the autograder. You should also refer to registers by their numbers and not their names.
Part 2 Emulator
For part 2 of this project, you will be implementing the actual emulator that can execute RISC-V machine code. In order to accomplish this task, you will need to complete the functions below.
execute\_\*(): These functions handle the majority of the execution of the reduced RISC-V instruction set we are implementing in this project. To reduce the amount of busy work, the ladders of switch statements are already implemented for you. To complete the implementation, you will only need to fill in the switch cases. You should be updating the appropriate register values and interacting with memory when applicable.
store(): This function takes an address, a size, and a value and stores the first -size- bytes of the given value at the given address.
load(): This function accepts an address and a size and returns the next -size- bytes starting at the given address. You will need to implement load() first in order to fetch instructions properly; without a functioning load() you will get errors due to invalid instructions.
Note that a correct implementation of this part will depend on the functions in utils.c. Thus, you should ensure that these functions (which you wrote in part 1) are working correctly. By the time you’re finished, your code should handle all of the instructions in the table above.
Part 2 Debugging
$ cgdb riscv
# Inside cgdb shell
$ run -r code/input/[*].input
$ run -r -i code/input[*].input # interactive emulator mode.
Testing
There are two types of tests for this project: unit tests, instruction tests, and end-to-end tests. The unit tests can be found in the file test_utils.c. This suite tests the sign_extend_number and parse_instruction functions. You can run these tests using the command below.
Unit tests
$ touch test-utils.c; make test-utils
If you see any build errors; check if you have run the CUnit install at the beginning.
$ bash ./install-cunit.sh # Install unit testing framework. This will create a folder called CUnit/
Instruction tests
Part 1
$ ./riscv -d ./code/input/R/R.input > ./code/out/R/R.solution
$ diff ./code/out/R/R.solution ./code/ref/R/R.solution
# You can replace R with any of the other instruction types R, I, S, SB, U, J
Part 2
$ ./riscv -r ./code/input/R/R.input > ./code/out/R/R.trace
$ python3 part2_tester.py R/R
# If there is any error, check if code/out/R/R.trace exists ?
# The python script will report errors in particular traces points if any.
# You can replace R with any of the other instruction types R, I, S, SB, U, J
Part 2 tests need to check the actual values in the registers. Program tests are the recommended method for debugging Part 2.
If your isassembly does not match the output, you will get the difference between the reference output and your output. Make sure you at least pass this test before modifying part1.c
Program tests
# The possible test files are called simple, multiply, random.
$ ./riscv -r ./code/input/simple.input > ./code/out/simple.trace
$ python3 part2_tester.py simple
# Replace simple with multiply or random
An example error output
$ ./riscv -r ./code/input/random.input > ./code/out/random.trace
$ python3 part2_tester.py random
Starting random test
ERROR: instruction 10, register 5. Expected: 0x00000000, Actual: 0xffffffff
random test has failed.
You are provided with three test files that evaluate your emulator’s correctness. The output files are those with the .trace extension. The solutions are located in the folder code/ref, while your project’s code will write it’s output to the folder code/out. These trace files dump the contents of the registers after the execution of each instruction.
The testing suite is run through the python script part2_tester.py. This file will read in your output and compare it against the reference trace by looking at changes in the registers between instructions. If a test fails, you should see an output that lists which instruction and register was incorrect. You can then cross-reference this information with the tests’ .solution file (the disassembled version from part 1) to find the erroneous error.
If you would like to add new tests, you should follow the same steps as those outlined when creating tests for part 1. The only difference is that you need to create a .trace reference file that contains the register values after every instruction
End-to-End tests
Congratulations! You are ready for end-to-end tests
$ bash ./scripts/localci.sh
# if you see a file called SUCCESS in your repo and *.log.sucess then you passed. You can also check your *_Grade.json to see your tentative grade.
# If you see FAILED, then inspect *.log.failed. Check the failed section in LOG.md to see what tests failed.
# LOG.md is a markdown file. You can view it using any markdown viewer.
# Copy paste LOG.md into this viewer e.g., https://markdownlivepreview.com/
Grading
Congrats. You are now ready to receive your tentative grade.
$ cd $REPO # $REPO refers to the top of your cloned repo.
$ bash ./scripts/localci.sh # run from the repo folder
# Check if SUCCESS OR FAILED was dumped by scripts
# There will be a _Grade.json (this is your tentative grade). The file is in json format.
# The mark component in each module indicates your score for that portion.
# LOG.md contains the log of your runs. This might give you more information.
This grade is only tentative. Based on additional test cases in our evaluation, you could score less points.
Test P1 Points P2 Points
R 12 (rpart1) 12 (rpart2)
I 12 (ipart1) 12 (ipart2)
S 12 (spart1) 12 (spart2)
SB 12 (sbpart1) 12 (sbpart2)
U 12 (upart1) 12 (upart2)
J 12 (ujpart1) 12 (ujpart2)
Simple 30 30
Multiply 30 30
Random 30 30
Programs 90 (allpart1) 90 (allpart2)
Enhance understanding the functionality implemented by a CPU
Understand the inner workings of RISC-V instruction set design
Prepare you to write RISC V assembly programs
Understand how emulators work.
Background
In this project, you will create an emulator that is able to execute a subset of the RISC-V ISA. You’ll provide the machinery to decode and execute a couple dozen RISC-V instructions.
What is a disassembler and emulator ?
A disassembler translates an executable binary to a human-readable assembly program. An emulator is a software model representing the processor. Emulators are useful when you wish to run a program targeting a different CPU than the underlying hardware. For instance, we are going to be emulating RISC-V programs on an x86 processor of your laptop or CSIL machine. Perhaps the most popular emulator framework is Qemu. The virtual box software that you are using is a type of emulator.
Check Yourself
Bitfields and Unions
The RISC-V ISA card provides some information necessary for completing this project.
Source
Make sure you read through the entire spec before starting the project.
The files you will need to modify and submit are:
TODO
part1.c: The main file which you will modify for part 1.
utils.c: The helper file which will hold various helper functions for part 1.
part2.c: The main file which you will modify for part 2. You will not be submitting this file for part 1.
You will NOT be modifying any files other than the ones listed above. This means that you will not be modifying header files. If you add helper functions, please place the function prototypes in the corresponding C files. If you do not follow this step, your code will likely not compile and you will get a zero on the project.
You should definitely consult through the following, thoroughly:
types.h: C header file for the data types you will be dealing with. It is critical that you read and understand the definitions in types.h before starting the project. If they look mysterious, consult the bitfields and unions ppt with this repo.
Makefile: File which records all dependencies.
code/input: Various files to run tests.
code/ref: Reference outputs (_.solution - part 1 . _.trace - part 2)
utils.h: File that contains the format for instructions to print for part 1.
You should not need to look at these files, but here they are anyway:
riscv.h: C header file for the functions you are implementing.
riscv.c: C source file for the program loader and main function.
$ bash ./install-cunit.sh # Install unit testing framework. This will create a folder called CUnit/
$ make riscv
RISC-V Emulator
The files provided in the start kit comprise a framework for a RISC-V emulator.
In part 1, you will add code to part1.c and utils.c to print out the human-readable disassembly corresponding to the instruction’s machine code. Look for /* Your Code */
In part 2, you’ll complete the program by adding code to part2.c to execute each instruction (including performing memory accesses). Your simulator must be able to handle the machine code versions of the RISC-V machine instructions below. We’ve already given you a framework for what cases of instruction types you should be handling.
Check yourself: why does sizeof(Instruction)==4?
The instruction set that your emulator must handle is listed below. All of the information here is copied from the RISC-V green sheet for your convenience; you may still use the green card as a reference.
Just like the regular RISC-V architecture, the RISC-V system you’re implementing is little-endian. This means that when given a value comprised of multiple bytes, the least-significant byte is stored at the lowest address.
The Framework Code
The framework code we’ve provided operates by doing the following.
1.It reads the program’s machine code into the simulated memory (starting at address 0x01000). The program to “execute” is passed as a command line parameter. Each program is given 1 MiB of memory and is byte-addressable.
2.It initializes all 32 RISC-V registers to 0 and sets the program counter (PC) to 0x01000. The only exceptions to the initial initializations are the stack pointer (set to 0xEFFFF) and the global pointer (set to 0x03000). In the context of our emulator, the global pointer will refer to the static portion of our memory. The registers and Program Counter are managed by the Processor struct defined in types.h.
3.It sets flags that govern how the program interacts with the user. Depending on the options specified on the command line, the simulator will either show a dissassembly dump (-d) of the program on the command line, or it will execute the program. More information on the command line options is below.
It then enters the main simulation loop, which simply executes a single instruction repeatedly until the simulation is complete. Executing an instruction performs the following tasks:
1.It fetches an instruction from memory, using the PC as the address.
2.It examines the opcode/funct3 to determine what instruction was fetched.
3.It executes the instruction and updates the PC.
The framework supports a handful of command-line options:
-i runs the simulator in interactive mode, in which the simulator executes an instruction each time the Enter key is pressed. The disassembly of each executed instruction is printed.
-t runs the simulator in tracing mode, in which each instruction executed is printed.
-r instructs the simulator to print the contents of all 32 registers after each instruction is executed. (Part 2) This option is most useful when combined with the -i flag.
-d instructs the simulator to disassemble the entire program, then quit before executing. (Part 1)
Part 1 Disassembler
In part 1, you will be writing a disassembler that translates RISC-V machine code into human-readable assembly code. You will also be laying the building blocks for the actual emulator that you will implement in part 2. You will be implementing the following functions. The files in which they are located are in parentheses.
1.parse_instruction(instruction_bits) (utils.c): Uses the given instruction (encoded as a 32-bit integer) and returns the corresponding instruction. You will have to determine the proper format of the given instruction and use the correspdonding member of the instruction struct. You will find the green sheet particularly helpful here.
2.sign_extend_number(field, n) (utils.c): This function interprets the number in field as an n-bit number and sign-extends it to a 32-bit integer.
3.get_*_offset(instruction) (utils.c): For the corresponding instruction type (either branch, jump, or store), this function unpacks the immediate value and returns the number of bytes to offset by. In this case of branches and jumps, these functions should return the number of bytes to add to the PC to get to the desired label. In the case of stores, the corresponding function will return the offset on the destination address.
4.print_*(instruction) (part1.c): Prints the instruction to standard output. You should use the constants defined in the file utils.h to properly format your instructions. Failure to do so will cause issues with the autograder. You should also refer to registers by their numbers and not their names.
Part 2 Emulator
For part 2 of this project, you will be implementing the actual emulator that can execute RISC-V machine code. In order to accomplish this task, you will need to complete the functions below.
execute\_\*(): These functions handle the majority of the execution of the reduced RISC-V instruction set we are implementing in this project. To reduce the amount of busy work, the ladders of switch statements are already implemented for you. To complete the implementation, you will only need to fill in the switch cases. You should be updating the appropriate register values and interacting with memory when applicable.
store(): This function takes an address, a size, and a value and stores the first -size- bytes of the given value at the given address.
load(): This function accepts an address and a size and returns the next -size- bytes starting at the given address. You will need to implement load() first in order to fetch instructions properly; without a functioning load() you will get errors due to invalid instructions.
Note that a correct implementation of this part will depend on the functions in utils.c. Thus, you should ensure that these functions (which you wrote in part 1) are working correctly. By the time you’re finished, your code should handle all of the instructions in the table above.
Part 2 Debugging
$ cgdb riscv
# Inside cgdb shell
$ run -r code/input/[*].input
$ run -r -i code/input[*].input # interactive emulator mode.
Testing
There are two types of tests for this project: unit tests, instruction tests, and end-to-end tests. The unit tests can be found in the file test_utils.c. This suite tests the sign_extend_number and parse_instruction functions. You can run these tests using the command below.
Unit tests
$ touch test-utils.c; make test-utils
If you see any build errors; check if you have run the CUnit install at the beginning.
$ bash ./install-cunit.sh # Install unit testing framework. This will create a folder called CUnit/
Instruction tests
Part 1
$ ./riscv -d ./code/input/R/R.input > ./code/out/R/R.solution
$ diff ./code/out/R/R.solution ./code/ref/R/R.solution
# You can replace R with any of the other instruction types R, I, S, SB, U, J
Part 2
$ ./riscv -r ./code/input/R/R.input > ./code/out/R/R.trace
$ python3 part2_tester.py R/R
# If there is any error, check if code/out/R/R.trace exists ?
# The python script will report errors in particular traces points if any.
# You can replace R with any of the other instruction types R, I, S, SB, U, J
Part 2 tests need to check the actual values in the registers. Program tests are the recommended method for debugging Part 2.
If your isassembly does not match the output, you will get the difference between the reference output and your output. Make sure you at least pass this test before modifying part1.c
Program tests
# The possible test files are called simple, multiply, random.
$ ./riscv -r ./code/input/simple.input > ./code/out/simple.trace
$ python3 part2_tester.py simple
# Replace simple with multiply or random
An example error output
$ ./riscv -r ./code/input/random.input > ./code/out/random.trace
$ python3 part2_tester.py random
Starting random test
ERROR: instruction 10, register 5. Expected: 0x00000000, Actual: 0xffffffff
random test has failed.
You are provided with three test files that evaluate your emulator’s correctness. The output files are those with the .trace extension. The solutions are located in the folder code/ref, while your project’s code will write it’s output to the folder code/out. These trace files dump the contents of the registers after the execution of each instruction.
The testing suite is run through the python script part2_tester.py. This file will read in your output and compare it against the reference trace by looking at changes in the registers between instructions. If a test fails, you should see an output that lists which instruction and register was incorrect. You can then cross-reference this information with the tests’ .solution file (the disassembled version from part 1) to find the erroneous error.
If you would like to add new tests, you should follow the same steps as those outlined when creating tests for part 1. The only difference is that you need to create a .trace reference file that contains the register values after every instruction
End-to-End tests
Congratulations! You are ready for end-to-end tests
$ bash ./scripts/localci.sh
# if you see a file called SUCCESS in your repo and *.log.sucess then you passed. You can also check your *_Grade.json to see your tentative grade.
# If you see FAILED, then inspect *.log.failed. Check the failed section in LOG.md to see what tests failed.
# LOG.md is a markdown file. You can view it using any markdown viewer.
# Copy paste LOG.md into this viewer e.g., https://markdownlivepreview.com/
Grading
Congrats. You are now ready to receive your tentative grade.
$ cd $REPO # $REPO refers to the top of your cloned repo.
$ bash ./scripts/localci.sh # run from the repo folder
# Check if SUCCESS OR FAILED was dumped by scripts
# There will be a _Grade.json (this is your tentative grade). The file is in json format.
# The mark component in each module indicates your score for that portion.
# LOG.md contains the log of your runs. This might give you more information.
This grade is only tentative. Based on additional test cases in our evaluation, you could score less points.
Test P1 Points P2 Points
R 12 (rpart1) 12 (rpart2)
I 12 (ipart1) 12 (ipart2)
S 12 (spart1) 12 (spart2)
SB 12 (sbpart1) 12 (sbpart2)
U 12 (upart1) 12 (upart2)
J 12 (ujpart1) 12 (ujpart2)
Simple 30 30
Multiply 30 30
Random 30 30
Programs 90 (allpart1) 90 (allpart2)