









Warning
If someone is reading this blog, please be aware that the writer DID NOT consider the experience of the other readers.
After all, the most important thing is about writing things down for better memorization.
1 | #define BITMASK(bits) ((1ull << (bits)) - 1) |
First we take a look at BITMASK:
This macro creates a bitmask of a specified number of bits.
For example, BITMASK(3) would create a bitmask of 3 bits: 0000...0111, which is the number 7.
The macro works by shifting the number 1 to the left by bits positions and then subtracting 1 from the result:
1 -> 0000...1000 (shift 1 to the left by 3 bits)0000...1000 -> 0000...0111 (subtract 1 from the result)NB: 1ull stands for ‘unsigned long long integer’. The suffix ‘ull’ here is used to make the number 1 64-bit
Then BITS:
This macro extracts bits from position lo to position hi from the value x.
It does it by performing several bit operations:
x to the right by lo positions, which effectively removes the bits lower than the bit on lo position.x with the bitmask created by the macro explained above to wipe out all values higher than position hi of the original x.1 | #define SEXT(x, len) \ |
SEXT stands for Sign Extension, which is the process of extending the sign bit of a binary number when moving it to a larger bit width.
In the macro, the expression uses a bit field within a struct to define a signed integer of length len bits. The bit field ensures that the value of x is treated as a signed integer of that specific length.
The struct with the bit field automatically sign-extends the value when it is assigned to the int64_t member. The result is then cast back to uint64_t to maintain the full precision of the extended value.
Note
The second parameter len of SEXT macro means the number of bits to treat x as for sign extension, instead of the target length.
The reason why sign extension is needed is that, the offset(immediate) is a signed value and it’s length is less than 64 bits(well of course since the length of the whole instruction is merely 32 bits). If we directly store it in a 64-bit variable, the higher bits are filled with zeros by default.
Problem that sign extension resolves:
If we have a negative offset(immediate) presented as two’s complement, which means that it’s higher bits should all be 1.
Now if we store it directly into a 64-bit variable, the higher bits are becoming 0s, thus making the variable a positive one.
Example:
11111111 (which is -1 in two’s complement notation when interpreted as a 8-bit signed integer).00000000 00000000 00000000 11111111 (which is 255 in decimal, a positive value).In the implementation of instruction mulh, I encountered a suttle issue deeply rooted in C’s type conversion conventions.
RISC-V manual defines mulh as follows:
MUL performs an XLEN-bit×XLEN-bit multiplication of rs1 by rs2 and places the lower XLEN bits
in the destination register. MULH, MULHU, and MULHSU perform the same multiplication but return the upper XLEN bits of the full 2×XLEN-bit product, for signed×signed, unsigned×unsigned,
and signed rs1×unsigned rs2 multiplication, respectively.
Essentially, it’s just an extended version mul, designed for caculating larger numbers with lower precision.
My first implementation looks like this:
1 | INSTPAT(...); |
During the test of mul-longlong.c, I used difftest to locate the problem is at pc = 0x800000a0, which is:
1 | 800000a0:402fc17b3 mulh a5,s8,a5 |
The difftest showed a discrepancy:
1 | [src/isa/riscv32/difftest/dut.c:23 isa_difftest_checkregs] a5 is different after executing instruction at pc = 0x800000a0, right = 0x19d29ab9, wrong = 0x7736200d, diff = 0x6ee4bab4 |
At first glance, the logic written in inst.c seemed correct:
It turns out that the problem rooted in the conversion process.
In C, when converting a smaller signed type to a larger signed type, for example, casting a 32-bit signed integer(int32_t) to a 64-bit signed integer(int64_t), C correctly handles the sign extension, preserving the value after the conversion.
The same thing happens when we convert an unsigned type to a same size signed type, C will do the sign extension automatically as well.
But, if we convert a smaller unsigned type, to a larger signed type, C does not automatically perform sign extension, which leads to the problem that I was facing with.
Consider this simple example program:
1 | #include <stdint.h> |
output:
1 | u32: 2930885290 |
From the example above we can see that: when directly convert a 32-bit unsigned integer to a 64-bit signed integer, C does not automatically perform sign extension.
That is the reason why my first implementation for instruction mulh was incorrect: it did not conduct sign conversion.
The solution is simple, first convert the value into a 32-bit signed int, then convert it into a 64-bit signed one:
1 | INSTPAT("0000001 ????? ????? 001 ????? 01100 11", mulh, R, |
Though the notes shown above are correct, it’s more likely that the course wants us to use the SEXT macro for sign extension.
The final version of mulh looks like this:
1 | INSTPAT("0000001 ????? ????? 001 ????? 01100 11", mulh, R, |
SEXT treats src1 as a 32-bit value(uint32_t), and sign-extends it to 64 bits.
When testing the implemented instructions, we would need to run tests in am-kernels/tests/cpu-tests/tests/.
The execution command is(take dummy.c as example):
1 | make ARCH=$ISA-nemu ALL=dummy run |
However, my machine(cpu) is of x86 architecture, which means that it can not compile source code to riscv32 executables directly.
We would need the following packages to perform the compilation:
1 | sudo pacman -Sy riscv64-linux-gnu-gcc |
Though still some errors may occur:
wordsize.h :1 | /usr/riscv64-linux-gnu/include/bits/wordsize.h:28:3: error: |
- **solution**: modify `/usr/riscv64-linux-gnu/include/bits/wordsize.h` :

stubs.h :1 | /usr/riscv64-linux-gnu/include/gnu/stubs.h:8:11: fatal error: gnu/stubs-ilp32.h: No such file or directory |
- **solution**: modify `/usr/riscv64-linux-gnu/include/gnu/stubs.h` :

int parameter of memsetWhen implementing our own lib utils, I encountered a little problem in memset.
The first version of my memset looks like this:
1 | void *memset(void *s, int c, size_t n) { |
The problem is that, the size of the memory which would be set by this function is of unit byte. However, here in my implemtation, the unit is int.
The correct version is:
1 | void *memset(void *s, int c, size_t n) { |
We use unsigned char to represent a byte in memory, as it’s size is exactly one byte.
As for the reason of using unsigned, it is because of that we want to eliminate the ambiguity of the various implemetation of signed char.
ftrace development. I knew nothing about the elf file before reading the man page of elf for two afternoons.Wikipedia:
In computing, the Executable and Linkable Format(ELF, formerly named Extensible Linking Format) is a common standard file format for executable files, object code, shared libraries, and core dumps.
readelf -a to extract the content stored in an ELF file.hello.c as an example:1 | readelf -a hello |
There’re a lot of information shown here. Let’s just focus on what we need: function symbols.
1 | Symbol table '.symtab' contains 25 entries: |
From the above symbol table we can see that, among all of the symbols, those with type FUNC should be the ones that we want for ftrace.
However, it’s not that simple.
NAME in the symbol table is actually an offset used to search from string table.To resolve these two issues, we will need to take a look at the structure of a common ELF file.
Basically, a common ELF file contains an ELF header, which includes information about itself, such as:
e_ident: a magic number identifying the file as an ELF filee_shoff: section header’s offsete_shnum: count of section headerse_shstrndx: index of the names’ section in the tableThose variables are the guides for us to extract useful content from the ELF file.
Other parts of ELF file, such as section header table, string table and section name string table, can be found using these offsets extracted from ELF header.
But how exactly should we read them from an ELF file? The answer is, with the help of <elf.h>.
<elf.h>man 5 elf :
The header file <elf.h> defines the format of ELF executable binary files. Amongst these files are normal executable files, relocatable object files, core files, and shared objects.
An executable file using the ELF file format cosists of an ELF header, followed by a program header table or a section header table, or both. The ELF header is always at offset zero of the file. The program header table and the section header table’s offset in the file are defined in the ELF header. The two tables describe the rest of the particularities of the file.
This header file defines a lot of types(structs) to help users read from the binary ELF files.
For example, a struct Elf32_Ehdr of ELF header:
1 | typedef struct { |
We can use it to define a variable and store the content read from the file to it:
1 |
|
BTW, there’s a macro ElfW used here:
1 | #if defined(CONFIG_RV64) |
I have to say, that, macro is kinda useful. While I disliked it when first encountered it.
We could then use the e_shoff from the ELF header to locate the position of section headers:
1 |
|
And so on.
Sometimes gcc automatically inlines functions written in the source file.
In this case, they are not shown in the symbol table as there’s no such symbol after pre-compilation.
We can use an attribute to stop gcc from inlining these functions:
1 | int is_prime(int n) __attribute__((noinline)); |
1 | FuncSym func = { |
I ran into this problem as the code shown above. This is the first time I encounter with a use-after-free error, so it took me a while to figure out what’s happening.
The tricky part of this bug is that, there’re no errors reported by compiler or the program itself, it just caused the output info from the function table to be random characters.
Solution: use strdup to duplicate a string from the string_table.
1 | FuncSym func = { |
<string.h><string.h> here.strcmp1 | int strcmp(const char *s1, const char *s2) { |
memset1 | void *memset(void *s, int c, size_t n) { |
unsigned char as a byte in memory.sprintf%d and %s.I was first very hesitate to begin with this implementation cuz I thought it was going to be difficult. But as soon as I dived into it, it’s actually not that hard as I anticipated.
My first idea was to maintain a dynamic array with malloc and realloc to store the final buffer for output. But I soon found that I would have to implement malloc and realloc as well this way…So I turned to a fiexed length buffer instead.
fmt argument character by character, put that character into buffer if it is not %, which is the start of a specifier.va_arg to pop the next arguement with proper type, convert it into a character or a string, and then take it into the buffer.fmt is done, use strcpy we’ve implemented before to copy the buffer into out.device-tree-compiler on ArchLinuxdevice-tree-compiler is a package required for conducting difftest on spike.
This pacakge is named dtc in Arch Linux official repo.
Why could we directly read memory-mapped address in a way of pointer dereferencing in AM?
1 | static inline uint8_t inb(uintptr_t addr) { return *(volatile uint8_t *)addr; } |
Cuz these memory reading codes will be compiled to assembly, which is basically riscv32 instructions containing the address to be read. When nemu proceeds through these instructions, it will parse it and invoke paddr_read function(Mr macro, which invoke vaddr_read -> paddr_read), and that’s where the callback function binds to a mapped memory area reading is invoked.
volatile ?volatile is used to prevent the compiler from optimizing away memory access, especially in this case, which is dealing with memory-mapped IO (MMIO).volatile, the compiler might optimize away repeated memory reads or writes, assuming the memory value won’t change unexpectedly. This assumption is valid for normal variables, but memory-mapped registers and hardware I/O can change asynchronously.volatile, the compiler might cache the value read from memory in a register and never actually perform the memory read again.此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。