owned this note
owned this note
Published
Linked with GitHub
# Computer Architecture Final Project
> Contributed by: < `HotMercury` (p76111741) >, < `freshLiver` (P76114016) >, < `tinhanho` (P76121364) >
## Summary
- <`HotMercury` (p76111741)>
- <`freshLiver` (P76114016)>
- Fix Issue #258 (Immediate Bit Range Checking)
- Add testing unit for checking bit range
- <`tinhanho` (P76121364)>
- Add cache replacement policy: FIFO
## Setup
### Build from Source
1. Clone the official repo and use the provided [dockerfile](https://github.com/mortbopet/Ripes/tree/master/docker) to build the environment
- It may take long time to build the environment
2. Clone your ripes repo
- **MUST** clone with `--recurse-submodules` option, or you will fail to build it!!!
- Otherwise, conduct ```git submodule update --init --recursive``` after cloning because there are external sources called VSRTL, ELFIO and libelfin.
3. Enter the environment with `docker run --rm --name=ripes -it --entrypoint=/bin/bash ripes:latest`
- Add new user in the docker with `useradd -m user` (==assume the host $UID is also 1000==)
- Commit the (in another terminal, make sure the docker guest is idle) with `docker commit ripes cafinal:user`
- Stop the current guest with `docker stop ripes`
4. Enter the environment with `docker run --rm --name=ripes -u 1000 -it -v $YOUR_RIPES_DIR:/ripes cafinal:user`:
- This command maps your ripes directory into guest environment
- This command runs your environment in user permission (instead of using root permission, for keeping the owner and group)
- Don't forget to **REPLACE *$YOUR_RIPES_DIR*** in the command
5. Goto the mapped dir inside the guest environment
6. Build your ripes with the following command
```bash!
$ cmake -S . -B ./build -Wno-dev -DRIPES_BUILD_TESTS=ON -DVSRTL_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH="/6.5.0/gcc_64/lib/cmake"
$ make -C build
```
7. Your Ripes should be successfully built
8. Test your ripes
- Goto the test directory inside the build path `cd /ripes/build/test`
- Testing `./tst_assembler && ./tst_expreval && ./tst_riscv`
9. The tests should be all passed:
```bash!
$ ./tst_assembler && ./tst_expreval && ./tst_riscv
[...]
Totals: 10 passed, 0 failed, 0 skipped, 0 blacklisted, 11495ms
********* Finished testing of tst_RISCV *********
```
### Run it
1. Run the compiled binary with the following command
```bash
docker run --rm -it -v $YOUR_RIPES_DIR:/ripes -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix --entrypoint=/ripes/build/Ripes cafinal:user
```
### Build in local
1. git clone ripes
- Suffix ```--recurse-submodules``` are recommended.
2. Make sure that all tools below are installed.
- Notice the version that using qt6 instead of qt5.
```
sudo apt install qt6-base-dev
sudo apt-get install qt6-tools-dev
sudo apt-get install qt6-tools-dev-tools
sudo apt-get install libqt6charts6-dev
```
3. Make sure that submodule(VSRTL ELFIO and libelfin) are all included.
- If they are not, we shall update them. ```git submodule update --init --recursive```
5. Mind that if 6.5.0 directory exists or not. If not, conduct ```sudo aqt install-qt linux desktop 6.5.0 gcc_64 -m qtcharts```
6. Under ripes directory
- Note that Cmake should create build directory and configuration in the directory.
```
cmake -B build \
-Wno-dev \
-DRIPES_BUILD_TESTS=ON \
-DVSRTL_BUILD_TESTS=ON \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH=$(pwd)/6.5.0/gcc_64/
```
7. Under build directory conduct ```sudo make install```
8. Execute ```./Ripes```
- Note that use ```sudo ./Ripes``` if we want to test system call which likes file open and so on.
<!--5. Go to build/test and make ```cd build/test && make```-->
## Issue
### [Issue #196](https://github.com/mortbopet/Ripes/issues/196)
We found that when using the `open` system call, the mode permission settings need to be specified in octal form. According to the [Linux man page](https://man7.org/linux/man-pages/man2/open.2.html), when we set `00200`, it means the user has write permission. However, when attempting a write operation, we observed a write error. The issue appears to be related to Ripes interpreting the set number as decimal instead of the octal mode.
Read write test in Ripes
```c!
.data
file_address:
.asciz "/tmp/hello_world.txt"
greeting:
.asciz "hello world"
open_error:
.asciz "open error"
read_error:
.asciz "read error"
write_error:
.asciz "write error"
successs:
.asciz "success!"
.text
main:
# open syscall 1024
# a0 Pointer to null terminated string for the path
# a1 flags
# flags -> O_RDONLY 00, O_WRONLY 01, O_RDWR 02
# S_IWUSR 00200 user has write permission
li s0, -1
la a0, file_address
li a1, 0x2
# here is the problem
ori a1, a1, 00200
li a7, 1024
ecall
beq a0, s0, open_fail
# write syscall 64
# a0 the file discriptor
# a1 address of the buffer
# a2 number of bytes to write
addi s1, a0, 0
la a1, greeting
li a2, 11
li a7, 64
ecall
beq a0, s0, write_fail
close:
# close syscall 57
# a0 the file descriptor to close
mv a0, s1
li a7, 57
ecall
j end
open_fail:
la a0, open_error
li a7, 4
ecall
j end
write_fail:
la a0, write_error
li a7, 4
ecall
j close
end:
nop
```
When using the `open(fd, flags)` syscall implemented by Ripes, it cannot write data to the file if we set the `a1` (`flags`) to 1 (`O_WRONLY`) or 2 (`O_RDWR`). Instead, we need to set it to 3.
:::info
However, the `QFile::write` doesn't require the flag to be 3, we can write a simple program to test:
```cpp
// test.cpp
#include <QFile>
using namespace std;
int main (int argc, const char *argv[1]) {
const char* msg = argv[2];
QFile file(argv[1]);
file.open(QIODevice::WriteOnly);
file.write(msg, qstrlen(msg));
file.close();
return 0;
}
```
Then, create a `CMakeLists.txt` file with the following lines:
```cmake
cmake_minimum_required(VERSION 3.16)
project(test)
find_package(Qt6 COMPONENTS Core REQUIRED)
qt_add_executable(test test.cpp)
```
And build and test the simple program with:
```bash
$ cmake -S . -B build -DCMAKE_PREFIX_PATH="/6.5.0/gcc_64/lib/cmake"
$ cmake --build build
$ ./build/test yoyo aaaa
$ cat yoyo
```
The created file `yoyo` should contain the string `aaaa`.
:::
If we check the `write` syscall implementation, we could find that it explicitly requires `O_WRONLY | O_RDWR`:
```c
// src/syscall/systemio.h
static int writeToFile(int fd, const QString &myBuffer, int lengthRequested) {
SystemIO::get(); // Ensure that SystemIO is constructed
if (fd == STDOUT || fd == STDERR) {
emit get().doPrint(myBuffer);
return myBuffer.size();
}
if (!FileIOData::fdInUse(
fd, O_WRONLY | O_RDWR)) // Check the existence of the "write" fd
{
s_fileErrorString =
"File descriptor " + QString::number(fd) + " is not open for writing";
return -1;
}
// retrieve FileOutputStream from storage
auto &outputStream = FileIOData::getStreamInUse(fd);
outputStream << myBuffer;
outputStream.flush();
return lengthRequested;
} // end writeToFile
```
:::warning
Why the implementation require the flags to be `O_WRONLY | O_RDWR` ???
:::
### [Issue #258](https://github.com/mortbopet/Ripes/issues/258) ([RP #339](https://github.com/mortbopet/Ripes/pull/339))
:::spoiler Details
#### Description
As the issue desciption said, in the latest Ripes, most of the I-type instructions will check whether the given immediate part can fit in the instruction limitation.
For example, the `addi` instruction will ensure the given immediate is less than 13 bits, as the following image shows:

However, the `lui` instruction's immediate part should only accept an immediate value that could fit in 20 bits, but current version doesn't handle this limitation correctly, as shown in the above image.
#### Tracing
To find the problem, I use `std::cout` to dump the `width` in the `checkFitsInWidth` function, which is for checking the immediate range:
```cpp
static Result<> checkFitsInWidth(Reg_T_S value, const Location &sourceLine,
ImmConvInfo &convInfo,
QString token = QString()) {
std::cout << __PRETTY_FUNCTION__ << std::endl
<< "check token '" << token.toStdString() << "' (expected width=" << width
<< ") at #" << sourceLine.sourceLine() << std::endl;
...
```
Then, rebuild the Ripes and test it with the following codes:
```asm
main:
addi x1, x1, 0x123455
lui x1, 0x12345678
```
And found that the `width` of `lui` instruction is misconfigured as 32, instead of 20:
```cpp!
static Ripes::Result<> Ripes::ImmBase<tokenIndex, width, repr, ImmParts, symbolType, transformer>::checkFitsInWidth(Ripes::ImmBase<tokenIndex, width, repr, ImmParts, symbolType, transformer>::Reg_T_S, const Ripes::Location&, Ripes::ImmConvInfo&, QString) [with unsigned int tokenIndex = 2; unsigned int width = 12; Ripes::Repr repr = Ripes::Repr::Signed; ImmParts = Ripes::ImmPartBase<0, Ripes::BitRange<20, 31, 32> >; Ripes::SymbolType symbolType = Ripes::SymbolType::None; Ripes::Reg_T (* transformer)(Ripes::Reg_T) = Ripes::defaultTransformer; Ripes::ImmBase<tokenIndex, width, repr, ImmParts, symbolType, transformer>::Reg_T_S = long int]
check token '0x123455' (expected width=12) at #1
static Ripes::Result<> Ripes::ImmBase<tokenIndex, width, repr, ImmParts, symbolType, transformer>::checkFitsInWidth(Ripes::ImmBase<tokenIndex, width, repr, ImmParts, symbolType, transformer>::Reg_T_S, const Ripes::Location&, Ripes::ImmConvInfo&, QString) [with unsigned int tokenIndex = 1; unsigned int width = 32; Ripes::Repr repr = Ripes::Repr::Hex; ImmParts = Ripes::ImmPartBase<0, Ripes::BitRange<12, 31, 32> >; Ripes::SymbolType symbolType = Ripes::SymbolType::None; Ripes::Reg_T (* transformer)(Ripes::Reg_T) = Ripes::defaultTransformer; Ripes::ImmBase<tokenIndex, width, repr, ImmParts, symbolType, transformer>::Reg_T_S = long int]
check token '0x12345678' (expected width=32) at #2
```
And this is origin from the the U-type instruction implementation, we can see that the `width` is configured as 32:
```cpp
/// A RISC-V immediate field with an input width of 32 bits.
/// Used in U-Type instructions.
///
/// It is defined as:
/// - Imm[31:12] = Inst[31:12]
/// - Imm[11:0] = 0
constexpr static unsigned VALID_INDEX = 1;
template <unsigned index, SymbolType symbolType>
struct ImmU
: public ImmSym<index, 32, Repr::Hex, ImmPart<0, 12, 31>, symbolType> {
static_assert(index == VALID_INDEX, "Invalid token index");
};
/// A U-Type RISC-V instruction
template <typename InstrImpl, RVISA::OpcodeID opcodeID,
SymbolType symbolType = SymbolType::None>
class Instr : public RV_Instruction<InstrImpl> {
template <unsigned index>
using Imm = ImmU<index, symbolType>;
public:
struct Opcode : public OpcodeSet<OpPartOpcode<opcodeID>> {};
struct Fields : public FieldSet<RegRd, Imm> {};
};
struct Auipc
: public Instr<Auipc, RVISA::OpcodeID::AUIPC, SymbolType::Absolute> {
constexpr static std::string_view NAME = "auipc";
};
struct Lui : public Instr<Lui, RVISA::OpcodeID::LUI> {
constexpr static std::string_view NAME = "lui";
};
} // namespace TypeU
```
#### Solve
By changing the width to 20, the bit range checking now works as expected, on the `lui` and `auipc` now:

#### Testing
Currently I only use the provided testing utility for checking my changes didn't break it:
```bash
/ripes/build/test$ ./tst_assembler && ./tst_expreval && ./tst_riscv
...
Totals: 10 passed, 0 failed, 0 skipped, 0 blacklisted, 9676ms
********* Finished testing of tst_RISCV *********
```
However, since I'm not that familiar with C++, I'm not sure whether I understood the bit range checking process correctly. If there is any problem in my changes, please me know.
:::
#### Add testing unit (Commit [#85e406b](https://github.com/mortbopet/Ripes/pull/339/commits/85e406b9d59e4f27b4ae5140f6d87600478fe998))
### Minor Issues
#### Typo (PR [#336](https://github.com/mortbopet/Ripes/pull/336), Commit [#07e2ef9](https://github.com/mortbopet/Ripes/commit/07e2ef9a50d2ce99130a2c18b6c445d78599e899))
In file `Ripes/src/syscall/ripes_syscall.h`:
```c!
/**
* @brief The SyscallManager class
*
* It is expected that the syscallManager can be called outside of the main GUI
* thread. As such, all syscalls who require GUI interaction must handle this
* explicitely.
*/
class SyscallManager {
...
}
```
The word `explicitely` should be `explicitly`.
## Interactions
:::info
Hi @matsievskiysv
According to the code in `systemio.h` at line 80,
```
static constexpr int O_RDONLY = 0x00000000;
static constexpr int O_WRONLY = 0x00000001;
static constexpr int O_RDWR = 0x00000002;
```
which defines the value of the flags used by the `open` syscall.
However, it cannot write data into the file correctly, if we only set `O_WRONLY` when calling `open` syscall.
Looking at the conditional statements at lines 408-409, it input `fd` and `3` as parameter
```
if (!FileIOData::fdInUse(fd, O_WRONLY | O_RDWR))
```
and then checks for the existence of the "write" flag using fdInUse at line193
```
else if ((fileFlags[fd] & flag) == static_cast<unsigned>(flag))
```
it requires the flag to be `0x3` to be true. Therefore, setting it only to `O_WRONLY` may result in a write failure.
Hi @mortbopet
Ideally, writing to a file should be allowed as long as it satisfies either `O_WRONLY` or `O_RDWR`. However, here it is specified that both conditions must be met for a write operation to proceed. I would like to inquire whether the author has any specific reason for this additional requirement.
:::
## Add Replacement Mechanism
### Motivation
<!--
The issue which have already existed is not relative with our course. Therefore, we decide to open a new issue by ourselves. After checking the ripes, we find that
### FIFO
Add one more condition in [cachesim.cpp](https://github.com/tinhanho/Ripes/blob/master/src/cachesim/cachesim.cpp). FIFO policy is relatively simple in comparison with LRU. What we do is just adding a counter and it shall circularly evict cache slots because of the first in first out rule.
```c
else if (m_replPolicy == ReplPolicy::FIFO){
ew.first = CacheSim::counter;
ew.second = &cacheLine[ew.first];
CacheSim::counter += 1;
CacheSim::counter %= getWays();
}
```
In [cachesim.h](https://github.com/tinhanho/Ripes/blob/master/src/cachesim/cachesim.h),
add int counter in CacheSim class and append one more rule.
```c
enum ReplPolicy { Random, LRU, FIFO};
...
class CacheSim : public CacheInterface {
Q_OBJECT
public:
static constexpr unsigned s_invalidIndex = static_cast<unsigned>(-1);
int counter = 0;
...
const static std::map<ReplPolicy, QString> s_cacheReplPolicyStrings{
{ReplPolicy::Random, "Random"}, {ReplPolicy::LRU, "LRU"}, {ReplPolicy::FIFO, "FIFO"}};
```
After building it, we test it with the assembly code below,
```c
lw a1 0(x0)
lw a1 512(x0)
lw a1 0(x0)
lw a1 1024(x0)
```
LRU

FIFO

It works as our expectation.
-->
[Ripes #334](https://github.com/mortbopet/Ripes/issues/334)
:::info
Hi @mortbopet,
I find that contributing to the cache replacement policy is a good way to work with. The replacement policy which possesses from now on are Random and LRU. Intuitively, FIFO can be added and it has been done through few lines of code. What I am doing now is trying to add policy like least frequently used. However, I cannot find out where the code is to expand the slot just like LRU doing. For example, when 2 ways cache used, you click Repl. policy LRU and the LRU slots show up.

I guess that it might be the UI problem but I do not know how to modify it. Could you help me?
:::
### Pull Request
[Link](https://github.com/mortbopet/Ripes/pull/335)
:::info
About Issue [#334](https://github.com/mortbopet/Ripes/issues/334).
I think that we could add a FIFO mechanism to the cache policy.
A new replacement policy, FIFO (First In, First Out), has been added to the existing enumeration. Additionally, a new counter has been introduced in the cachesim.h file. This counter plays a crucial role in cyclically determining which cache entry should be evicted.
```c
// line 19 in cachesim.h
enum ReplPolicy { Random, LRU, FIFO};
```
The code below handles the eviction process based on the FIFO replacement policy.
```c
//line 124 in cachesim.cpp
else if (m_replPolicy == ReplPolicy::FIFO){
ew.first = CacheSim::counter;
ew.second = &cacheLine[ew.first];
CacheSim::counter += 1;
CacheSim::counter %= getWays();
}
```
:::
### Author Reply
:::success
Thank you for looking into adding a new cache replacement policy!
A few comments:
As per the coding style currently used in Ripes, member variables should be prefixed with m_ and not with the class name.
I think the counter should be named better, e.g. fifoIndexCounter.
(optional) do you think there is an accompanying visualization to this? i.e., for LRU, we also show the LRU bits. Could one imagine a similar column which indicates what way in the cache line is currently up for eviction as per. FIFO?
:::
### Work on Author's suggestion
:::info
Thanks for the reply. I had already renamed the counter and tried my best to follow the coding style. If there are any problem still, please let me know.
As for third comments, I rework all the framework to implement visualizing the FIFO bit. Now, there is no need about fifoIndexCounter. Instead, boolean fifoflag is presented. This flag is set under two circumstances,
1. When an invalid entry is selected, we set the fifoflag.
2. When all the entries are full and cache miss occurs, we need to choose a entry to evicted and we set the fifoflag.
When this flag is set, we shall add 1 to fifo bits if entry is valid. In this way, we'll find that when fifo bits equal to the way of the cache, that entry should be evicted.
```c
//Line 167 in cachesim.cpp
if (it != cacheLine.end()) {
ew.first = it->first;
ew.second = &it->second;
m_fifoflag = true;
}
if (ew.second == nullptr) {
for (auto &way : cacheLine) {
if (static_cast<long>(way.second.fifo) == getWays()){
ew.first = way.first;
ew.second = &way.second;
m_fifoflag = true;
break;
}
}
}
```
Furthermore, the undo part needs to be taken into consideration as well. If the policy is FIFO, we set the fifoflag again and we need to restore the oldway.
```c
//Line 442 in cachesim.cpp
if (!trace.transaction.isHit && getReplacementPolicy() == ReplPolicy::FIFO) {
m_fifoflag = true;
way = oldWay;
}
```
```c
//Line 82 in cachesim.cpp
if (getReplacementPolicy() == ReplPolicy::FIFO) {
for(auto &set : line){
if(set.second.valid && m_fifoflag) set.second.fifo--;
}
m_fifoflag = false;
line[wayIdx].fifo = oldWay.fifo;
}
```
Demo video here,
https://github.com/mortbopet/Ripes/assets/67796326/b67aab30-17ee-4b3c-8fa7-ba56f905a282
Demo video demostrates the assembly code below,
```c
lw a1 0(x0)
lw a1 512(x0)
lw a1 0(x0)
lw a1 512(x0)
lw a1 512(x0)
lw a1 1024(x0)
lw a1 1024(x0)
lw a1 1024(x0)
lw a1 1024(x0)
lw a1 0(x0)
lw a1 0(x0)
lw a1 0(x0)
lw a1 1536(x0)
lw a1 1536(x0)
lw a1 1536(x0)
addi a0 x0 1024
addi a0 a0 1024
lw a1 0(a0)
```
Full code is in the branch FIFO of my fork,
https://github.com/tinhanho/Ripes/commit/6772d1abec6a6bff5e0e40374fd5922c67e3a427
Let me know if there are any problems of my think and implement.
:::
<!--
### LFU
LFU means least frequently used. We take the advantage of structure of LRU. Therefore, we have a slot in cache as well. The usage of the slot is to record the frequency of the entry. If the entry is hit again, We add one to the values inside the slot. When all the entry is full and we need to evict one entry, we select the less frequency entry to be evicted. Through the LFU slot, we can easily to find out which one is least freqently used.
The UI problem is solved and we find that we need to modify the code in [cachesim.cpp](https://github.com/tinhanho/Ripes/blob/master/src/cachesim/cachegraphic.cpp) and [cahcesim.h](https://github.com/tinhanho/Ripes/blob/master/src/cachesim/cachegraphic.h).
As FIFO, we append a new rule LFU and trace the code carefully to realize how LRU works. It helps us a lot to create the new rule. The main work is to deal with initial value of LFU and find a way to add one to the values inside the slot.
We test our code with assembly code below,
```c
lw a1 0(x0)
lw a1 512(x0)
lw a1 0(x0)
lw a1 512(x0)
lw a1 512(x0)
lw a1 1024(x0)
lw a1 1024(x0)
lw a1 1024(x0)
lw a1 1024(x0)
lw a1 1536(x0)
lw a1 1536(x0)
lw a1 1536(x0)
addi a0 x0 1024
addi a0 a0 1024
lw a1 0(a0)
```

It works as our expectation.
#### Problem:
- How to deal with the situation if frequency bit is too high.
- The variable and data structure which are newly added need more optimization.
[pull request](https://github.com/mortbopet/Ripes/pull/335/commits/90f083d191398f54d43da8832dbe7fa2ee34b4e6)
-->
---
## Trace Code
### Instruction Definition
The base of the instruction is the class `InstructionBase` defined in the `src/isa/instruction.h`.
```cpp
/** @brief A no-template, abstract class that defines an instruction. */
class InstructionBase {
public:
InstructionBase(unsigned byteSize) : m_byteSize(byteSize) {}
virtual ~InstructionBase() = default;
/// Assembles a line of tokens into an encoded program.
virtual AssembleRes assemble(const TokenizedSrcLine &tokens) = 0;
/// Disassembles an encoded program into a tokenized assembly program.
virtual Result<LineTokens>
disassemble(const Instr_T instruction, const Reg_T address,
const ReverseSymbolMap &symbolMap) const = 0;
...
/**
* @brief size
* @return size of assembled instruction, in bytes.
*/
unsigned size() const { return m_byteSize; }
...
protected:
...
unsigned m_byteSize;
};
```
And this class is inheritted by the `Instruction` structure:
```cpp
template <typename InstrImpl>
struct Instruction : public InstructionBase {
Instruction()
: InstructionBase(InstrByteSize<InstrImpl>::byteSize),
m_name(InstrImpl::NAME.data()) {}
AssembleRes assemble(const TokenizedSrcLine &tokens) override {
...
}
Result<LineTokens>
disassemble(const Instr_T instruction, const Reg_T address,
const ReverseSymbolMap &symbolMap) const override {
...
}
const QString &name() const override { return m_name; }
unsigned numOpParts() const override { return InstrImpl::Opcode::numParts(); }
private:
const QString m_name;
};
```
This structure implement the `assemble` and `disassemble` functions used for assembling/disassembling the instructions at runtime. But
Since the real instruction implementations are defined in the `src/isa/rv_[icm]_ext.h` files, take I-type for example:
#### I-Type Definitions
The I-type instructions are defined under the namespace `TypeI` in `src/isa/rv_i_ext.h`:
```cpp
namespace TypeI {
enum class Funct3 : unsigned {
ADDI = 0b000,
SLTI = 0b010,
SLTIU = 0b011,
XORI = 0b100,
ORI = 0b110,
ANDI = 0b111,
};
...
/// An I-Type RISC-V instruction
template <typename InstrImpl, OpcodeID opcodeID, Funct3 funct3>
struct Instr : public RV_Instruction<InstrImpl> {
struct Opcode
: public OpcodeSet<OpPartOpcode<opcodeID>,
OpPartFunct3<static_cast<unsigned>(funct3)>> {};
struct Fields : public FieldSet<RegRd, RegRs1, ImmCommon12> {};
};
...
template <typename InstrImpl, Funct3 funct3>
using Instr32 = Instr<InstrImpl, OpcodeID::OPIMM, funct3>;
...
struct Addi : public Instr32<Addi, Funct3::ADDI> {
constexpr static std::string_view NAME = "addi";
};
...
struct Jalr : public RV_Instruction<Jalr> {
struct Opcode : public OpcodeSet<OpPartOpcode<RVISA::OpcodeID::JALR>,
OpPartFunct3<static_cast<unsigned>(0b000)>> {
};
struct Fields : public FieldSet<RegRd, RegRs1, ImmCommon12> {};
constexpr static std::string_view NAME = "jalr";
};
}
```
:::info
**`RV_Instruction` is a simple wrapper of `Instruction`**
```cpp
// src/isa/rvisainfo_common.h
namespace RVISA {
...
template <typename InstrImpl>
struct RV_Instruction : public Instruction<InstrImpl> {
constexpr static unsigned instrBits() { return INSTR_BITS; } // 32
};
...
}
```
:::
These instructions have the common format `imm[11:0] | rs1 | funct3 | rd | opcode (0b0010011)` where the fields `imm` (`RegRd`), `rs1` (`RegRs1`), `rd` (`ImmCommon12`) are available when parsing the assembly codes. So, in the definition of `Addi`, the most important thing is to define the `funct3` (`Funct3::ADDI`).
:::warning
**The `JALR` instruction:**
Note that an exception is the `JALR` instruction, its opcode is `0b1100111` and thus inherit the `RV_Instruction` directly.
:::
#### U-Type Definitions
```cpp
namespace TypeU {
constexpr static unsigned VALID_INDEX = 1;
template <unsigned index, SymbolType symbolType>
struct ImmU : public ImmSym<index, 32, Repr::Hex, ImmPart<0, 12, 31>, symbolType> {
static_assert(index == VALID_INDEX, "Invalid token index");
};
/// A U-Type RISC-V instruction
template <typename InstrImpl, RVISA::OpcodeID opcodeID, SymbolType symbolType = SymbolType::None>
class Instr : public RV_Instruction<InstrImpl> {
template <unsigned index>
using Imm = ImmU<index, symbolType>;
public:
struct Opcode : public OpcodeSet<OpPartOpcode<opcodeID>> {};
struct Fields : public FieldSet<RegRd, Imm> {};
};
struct Auipc
: public Instr<Auipc, RVISA::OpcodeID::AUIPC, SymbolType::Absolute> {
constexpr static std::string_view NAME = "auipc";
};
struct Lui : public Instr<Lui, RVISA::OpcodeID::LUI> {
constexpr static std::string_view NAME = "lui";
};
} // namespace TypeU
```
### Instruction Initialization
During runtime, the instructions will be initialized by using the `enableInstructions` function, defined in the `src/isa/instruction.h`, to add the instructions into the `InstrVec m_instructions` of the `RV_ISAInfoBase` class:
```cpp
// src/isa/rvisainfo_common.h
class RV_ISAInfoBase : public ISAInfoBase {
...
const InstrVec &instructions() const override { return m_instructions; }
...
void initialize(const std::set<Option> &options = {}) {
RVISA::ExtI::enableExt(this, m_instructions, m_pseudoInstructions, options);
...
}
...
}
// src/isa/rv_i_ext.cpp
namespace ExtI {
...
void enableExt(const ISAInfoBase *isa, InstrVec &instructions,
PseudoInstrVec &pseudoInstructions,
const std::set<Option> &options) {
...
enableInstructions<Addi, Andi, Slti, Sltiu, Xori, Ori, Lb, Lh, Lw, Lbu, Lhu,
Ecall, Auipc, Lui, Jal, Jalr, Sb, Sw, Sh, Add, Sub, Sll,
Slt, Sltu, Xor, Srl, Sra, Or, And, Beq, Bne, Blt, Bge,
Bltu, Bgeu>(instructions);
...
}
```
Then, later will use the function `setInstructions` to initialize the map `InstrMap m_instructionMap` of all the previously defined instructions:
```cpp
class Assembler : public AssemblerBase {
...
void setInstructions() {
if (m_instructionMap.size() != 0) {
throw std::runtime_error("Instructions already set");
}
for (const auto &iter : m_isa->instructions()) {
const auto instr_name = iter.get()->name();
if (m_instructionMap.count(instr_name) != 0) {
throw std::runtime_error("Error: instruction with opcode '" +
instr_name.toStdString() +
"' has already been registerred.");
}
m_instructionMap[instr_name] = iter;
}
}
...
}
```
### Instruction Assembling
Then, the function `Assembler::assemble`, defined in `src/assembler/assembler.h`, will be the entry point for assembling the instructions, during runtime:
```cpp
#define runPass(resName, resType, passFunction, ...) \
auto passFunction##_res = passFunction(__VA_ARGS__); \
if (auto *errors = std::get_if<Errors>(&passFunction##_res)) { \
result.errors.insert(result.errors.end(), errors->begin(), errors->end()); \
assert(result.errors.size() != 0); \
return result; \
} \
auto resName = std::get<resType>(passFunction##_res);
...
class Assembler : public AssemblerBase {
...
assemble(const QStringList &programLines, const SymbolMap *symbols = nullptr,
const QString &sourceHash = QString()) const override {
AssembleResult result;
... // tokenize and expand pseudo instructions
/** Assemble. During assembly, we generate:
* - linkageMap: Recording offsets of instructions which require linkage
* with symbols
*/
LinkRequests needsLinkage;
runPass(program, Program, pass2, expandedLines, needsLinkage);
// Symbol linkage
runPass(unused, NoPassResult, pass3, program, needsLinkage);
Q_UNUSED(unused);
result.program = program;
result.program.sourceHash = sourceHash;
result.program.entryPoint = m_sectionBasePointers.at(".text");
return result;
}
...
}
```
Then, in this function, the function `pass2` is used for translating the instruction:
```cpp
#define runOperation(resName, operationFunction, ...) \
auto operationFunction##_res = operationFunction(__VA_ARGS__); \
if (operationFunction##_res.isError()) { \
errors.push_back(operationFunction##_res.error()); \
continue; \
} \
auto resName = operationFunction##_res.value();
...
class Assembler : public AssemblerBase {
std::variant<Errors, Program> pass2(const SourceProgram &tokenizedLines,
LinkRequests &needsLinkage) const {
...// Initialize program with initialized segments:
for (const auto &line : tokenizedLines) {
...// adjust the symbol addr based on the section base address
...// handle directive
addr_offset = currentSection->data.size();
if (!wasDirective) {
/// Maintain a pointer to the instruction that was assembled.
std::shared_ptr<InstructionBase> assembledWith;
runOperation(machineCode, assembleInstruction, line, assembledWith);
assert(assembledWith && "Expected the assembler instruction to be set");
program.sourceMapping[addr_offset].insert(line.sourceLine());
if (!machineCode.linksWithSymbol.symbol.isEmpty()) {
LinkRequest req(line.sourceLine());
req.offset = addr_offset;
req.fieldRequest = machineCode.linksWithSymbol;
req.section = m_currentSection;
req.instrAlignment = m_isa->instrByteAlignment();
needsLinkage.push_back(req);
}
...// handle misalignment
currentSection->data.append(
QByteArray(reinterpret_cast<char *>(&machineCode.instruction),
assembledWith->size()));
}
// This was a directive; append any assembled bytes to the segment.
currentSection->data.append(directiveBytes);
}
if (errors.size() != 0) {
return {errors};
}
...
return {program};
}
}
```
And the function `assembleInstruction` will be used for assembling and checking an instruction, if any error is found, the error message will be pushed to the error list and highlighted on the editor:
```cpp
virtual AssembleRes
assembleInstruction(const TokenizedSrcLine &line,
std::shared_ptr<InstructionBase> &assembledWith) const {
if (line.tokens.empty()) {
return {
Error(line, "Empty source lines should be impossible at this point")};
}
const auto &opcode = line.tokens.at(0);
auto instrIt = m_instructionMap.find(opcode);
if (instrIt == m_instructionMap.end()) {
return {Error(line, "Unknown opcode '" + opcode + "'")};
}
assembledWith = instrIt->second;
return assembledWith->assemble(line);
}
```
It will first check whether the opcode (instruction name) is legal. If true, then it will retrieve the instruction implementation from `Assembler::m_instructionMap`, and call the `assemble` function implemented by that instruction.
As explained above, because the instruction implementations are inheritted from the `Instruction` structure, if the instruction didn't override the `assemble` function, the default `assemble` implementation should be defined by the `Instruction` structure:
```cpp
template <typename InstrImpl>
struct Instruction : public InstructionBase {
...
AssembleRes assemble(const TokenizedSrcLine &tokens) override {
Instr_T instruction = 0;
FieldLinkRequest linksWithSymbol;
InstrImpl::Opcode::apply(instruction, linksWithSymbol);
if (auto fieldRes = InstrImpl::Fields::apply(tokens, instruction, linksWithSymbol); fieldRes.isError()) {
return std::get<Error>(fieldRes);
}
InstrRes res;
res.linksWithSymbol = linksWithSymbol;
res.instruction = instruction;
return res;
}
...
}
```
When the implementation being called, if the `apply` function didn't be overridden by the instruction implementation, it will first use that provided by `struct OpPartBase` defined in `src/isa/instruction.h`, to combine the fields into a complete instruction:
```cpp
struct OpPartBase {
...
const BitRangeBase range;
...
constexpr void apply(Instr_T &instruction) const {
instruction |= range.apply(value);
}
...
}
...
struct BitRangeBase {
...
constexpr unsigned width() const { return stop - start + 1; }
constexpr Instr_T getMask() const { return vsrtl::generateBitmask(width()); }
constexpr Instr_T apply(Instr_T value) const { return (value & getMask()) << start; }
...
}
```
### SysCall Handling
```c
// src/syscall/ripes_syscall.cpp
bool SyscallManager::execute(SyscallID id) {
if (m_syscalls.count(id) == 0) {
postToGUIThread([=] {
if (auto reg = ProcessorHandler::currentISA()->syscallReg();
reg.has_value()) {
...
}
});
return false;
} else {
const auto &syscall = m_syscalls.at(id);
...
syscall->execute();
...
return true;
}
}
```
When `syscall->execute()` is executed, the corresponding syscall implementation will be called. For the `open` syscall:
```c
// src/syscall/file.h
template <typename BaseSyscall>
class OpenSyscall : public BaseSyscall {
static_assert(std::is_base_of<Syscall, BaseSyscall>::value);
public:
OpenSyscall()
: BaseSyscall("Open", "Opens a file from a path",
{{0, "Pointer to null terminated string for the path"},
{1, "flags"}},
{{0, "the file decriptor or -1 if an error occurred"}}) {}
void execute() {
const AInt arg0 = BaseSyscall::getArg(BaseSyscall::REG_FILE, 0);
const AInt arg1 = BaseSyscall::getArg(BaseSyscall::REG_FILE, 1);
QByteArray string;
char byte;
unsigned int address = arg0;
do {
byte = static_cast<char>(
ProcessorHandler::getMemory().readMemConst(address++, 1) & 0xFF);
string.append(byte);
} while (byte != '\0');
int ret = SystemIO::openFile(QString::fromUtf8(string), arg1);
BaseSyscall::setRet(BaseSyscall::REG_FILE, 0, ret);
}
};
```
And then call the `SystemIO::openFile(...)`:
```c
// src/syscall/systemio.h
static int openFile(QString filename, int flags) {
SystemIO::get(); // Ensure that SystemIO is constructed
// Internally, a "file descriptor" is an index into a table
// of the filename, flag, and the File???putStream associated with
// that file descriptor.
int retValue = -1;
int fdToUse;
// Check internal plausibility of opening this file
fdToUse = FileIOData::nowOpening(filename, flags);
retValue = fdToUse; // return value is the fd
if (fdToUse < 0) {
return -1;
} // fileErrorString would have been set
try {
FileIOData::openFilestream(fdToUse, filename);
} catch (int) {
s_fileErrorString = "File " + filename + " could not be opened.";
retValue = -1;
}
return retValue; // return the "file descriptor"
}
```
This function first allocate a `fd` for the specified file by using `FileIOData::nowOpening()`, which saves the parameter `flags` into an map called `fileFlags[i]`:
```c
static int nowOpening(const QString &filename, int flag) {
...
fileNames[i] = filename; // our table has its own copy of filename
fileFlags[i] = flag;
...
return i;
}
```
Then, it will try to open the file with `FileIOData::openFilestream()`:
```c
// src/syscall/systemio.h
static constexpr int O_RDONLY = 0x00000000;
static constexpr int O_WRONLY = 0x00000001;
static constexpr int O_RDWR = 0x00000002;
static constexpr int O_APPEND = 0x00000008;
static constexpr int O_CREAT = 0x00000200; // 512
static constexpr int O_TRUNC = 0x00000400; // 1024
static constexpr int O_EXCL = 0x00000800; // 2048
...
static void openFilestream(int fd, const QString &filename) {
files.emplace(fd, filename);
const auto flags = fileFlags[fd];
const auto qtOpenFlags = // Translate from stdlib file flags to Qt flags
(flags & O_RDONLY ? QIODevice::ReadOnly : QIODevice::NotOpen) |
(flags & O_WRONLY ? QIODevice::WriteOnly : QIODevice::NotOpen) |
(flags & O_RDWR ? QIODevice::ReadWrite : QIODevice::NotOpen) |
(flags & O_TRUNC ? QIODevice::Truncate : QIODevice::Append) |
(flags & O_EXCL ? QIODevice::NewOnly : QIODevice::NotOpen);
// Try to open file with the given flags
files[fd].open(qtOpenFlags);
...
}
```
In this function, we can find that the `open` syscall relies on the [`QFile::open()`](https://doc.qt.io/qt-6/qfile.html#open) provided by Qt. The given `flags` will be converted to the [Qt defined flags](https://doc.qt.io/qt-6/qiodevicebase.html#OpenModeFlag-enum):
| **Constant** | **Value** |
|:--------------------------- |:--------------------- |
| QIODeviceBase::NotOpen | 0x0000 |
| QIODeviceBase::ReadOnly | 0x0001 |
| QIODeviceBase::WriteOnly | 0x0002 |
| QIODeviceBase::ReadWrite | ReadOnly \| WriteOnly |
| QIODeviceBase::Append | 0x0004 |
| QIODeviceBase::Truncate | 0x0008 |
| QIODeviceBase::Text | 0x0010 |
| QIODeviceBase::Unbuffered | 0x0020 |
| QIODeviceBase::NewOnly | 0x0040 |
| QIODeviceBase::ExistingOnly | 0x0080 |
:::warning
Note that the Ripes defined flags listed above are **NOT** identical to [the (**standard? linux defined?**) flags](https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/fcntl.h#L22):
```c!
#define O_ACCMODE 00000003
#define O_RDONLY 00000000
#define O_WRONLY 00000001
#define O_RDWR 00000002
#ifndef O_CREAT
#define O_CREAT 00000100 /* not fcntl */
#endif
#ifndef O_EXCL
#define O_EXCL 00000200 /* not fcntl */
#endif
#ifndef O_NOCTTY
#define O_NOCTTY 00000400 /* not fcntl */
#endif
#ifndef O_TRUNC
#define O_TRUNC 00001000 /* not fcntl */
#endif
#ifndef O_APPEND
#define O_APPEND 00002000
#endif
```
:::
### Cache Handling
In `src/cachesim/cachesim.h`, there is a enumeration `ReplPolicy` which is used for listing the available cache replacement policies:
```c
enum ReplPolicy { Random, LRU };
```
#### Select Victim
And in the `src/cachesim/cachesim.h`, the function `CacheSim::access` is the main function for accessing the cache. And in this function, if cache miss is happened, the function `CacheSim::evictAndUpdate` will be used for selecting the victim:
```c
void CacheSim::access(AInt address, MemoryAccess::Type type) {
...
if (!transaction.isHit) {
if (type == MemoryAccess::Read
|| (type == MemoryAccess::Write && getWriteAllocPolicy() == WriteAllocPolicy::WriteAllocate)) {
oldWay = evictAndUpdate(transaction);
}
} else {
oldWay = m_cacheLines[transaction.index.line][transaction.index.way];
}
...
}
```
The function `CacheSim::evictAndUpdate` will first determine which way the victim should be selected from, by using [the function `CacheSim::locateEvictionWay`](#Select-Victim-Way). And after the victim way is determined, it will update the cache line flags:
```c
CacheSim::CacheWay CacheSim::evictAndUpdate(CacheTransaction &transaction) {
const auto [wayIdx, wayPtr] = locateEvictionWay(transaction);
... // ignored, explained later
*wayPtr = CacheWay();
wayPtr->valid = true;
wayPtr->dirty = false;
wayPtr->tag = getTag(transaction.address);
...
return eviction;
}
```
However, we can find that the victim cache line is not updated directly, it's substituted with a new cache line instead. The reasons are:
- The victim cache line may be dirty
- The old value is be recorded for rolling back
Therefore, the function will also record the updates into `transaction` and return the evicted cache line.
```c
CacheSim::CacheWay CacheSim::evictAndUpdate(CacheTransaction &transaction) {
...
CacheWay eviction;
if (!wayPtr->valid) {
transaction.transToValid = true;
} else {
eviction = *wayPtr;
if (eviction.dirty) {
transaction.isWriteback = true;
}
}
...
transaction.tagChanged = true;
transaction.index.way = wayIdx;
return eviction;
}
```
:::info
#### Select Victim Way
As mentioned before, the function `CacheSim::locateEvictionWay` is used for selecting the victim way.
In this function, it first check the replacement policy. It randomly select a way if `ReplPolicy::Random` is in used:
```c
std::pair<unsigned, CacheSim::CacheWay *>
CacheSim::locateEvictionWay(const CacheTransaction &transaction) {
...
std::pair<unsigned, CacheSim::CacheWay *> ew;
...
if (m_replPolicy == ReplPolicy::Random) {
ew.first = std::rand() % getWays();
ew.second = &cacheLine[ew.first];
}
...
return ew;
}
```
Otherwise, the LRU will be performed to find the first invalid cache line. If all the cache lines are valid, it will select the LRU line:
```c
std::pair<unsigned, CacheSim::CacheWay *>
CacheSim::locateEvictionWay(const CacheTransaction &transaction) {
...
else if (m_replPolicy == ReplPolicy::LRU) {
...
auto it = std::find_if(cacheLine.begin(), cacheLine.end(), [=](const auto &way) { return !way.second.valid; });
if (it != cacheLine.end()) {
ew.first = it->first;
ew.second = &it->second;
}
if (ew.second == nullptr) {
for (auto &way : cacheLine) {
if (static_cast<long>(way.second.lru) == getWays() - 1) {
ew.first = way.first;
ew.second = &way.second;
break;
}
}
}
}
...
return ew;
}
```
:::
## Reference
- [The Ripes Official Repository](https://github.com/mortbopet/Ripes)
- [temp reference](https://hackmd.io/@Rwbh0z6QRXqUP7ovs7txiQ/HySbbdSCw)
- [draft](https://hackmd.io/@M1Il4baLQwe1hoqHMQez_g/S1AMESO86)