PMP registers and User Mode

I found the pmp.c file in the Freedom metal library along with it’s header file (pmp.h). I also found the pmp.c example code on configuring them. My project is focusing on using Rust programming with RISC-V, so I was wondering if it would be sound to just take the code from the metal library and convert it into Rust programming and make a crate out of it. Or would it be easier to configure the pmp with assembly? Also how is user/machine mode utilized. The privilege spec manual talks about vertical and horizontal traps in association with privilege modes, can anyone elaborate on this any? Do I explicitly define when I want to be in user mode versus machine mode, or is it implicit according to what I am doing?



I haven’t tried this myself, but have been looking around the metal sources and the privileged ISA as well. Specifically checkout https://github.com/sifive/freedom-metal/blob/8db2c3c1880e7d63fba7c1edb5e9fce5fe4a1afd/src/privilege.c#L18 for how freedom-metal transitions to U-mode.

AFAICT, the xRET instructions are the ones you use to change to a desired privilege level, and would require some asm to issue that instruction, but aside from that the rest is just register programming: program reg mepc to where you want the code to jump to, sp to a block of memory you want to use for the U-mode stack, and then mPP to the value for U-mode (00). Then when mRET is called it will “restore” the privilege mode to whats in mPP, and “return” to mepc.

Interrupts by default seem like they always are run in M-mode, but it sounds like there are ways to trap into other privilege levels, but on the FE310 I think this is moot since there is only M/U mode, and no U-mode interrupt extension implemented (aka, always M-mode when trapping).

Regarding PMP, I think that is entirely register programming, so whatever you’d be more comfortable with?

1 Like

Appreciate the reply. I was reading the privilege manual last night and found the pmp register’s addresses and format. That shouldnt be too bad. User/machine modes seem to be a bit harder to deal with. One thing at a time…once I get pmp configured I will come back to your comments on modes. Thanks.

There is a PMP baremetal example in case this interests you: https://github.com/sifive/example-pmp-baremetal

1 Like

Wow thats a lot of code for something I thought was simple :sweat: I appeciate the link though, I will go through it line by line. I’m sure I will find something useful.

Since my project is focusing on security, Once I get a PMP region setup and a small application running as U-mode in that region, what operations would require me to use machine mode? For example, to start off, I thought I would setup UART communications that would except some commands and display results. One command might be “pmp config” which would read the PMP configuration registers and print the results on the console. I would also like to have maybe an LED setup which indicates which privilege mode I’m currently in. I’m new to embedded systems programming, so I apologize if my questions are trivial. Also I’m new to interrupts, so links for me to understand when and where to use them would be great. thanks,
Daniel

I’m slowly getting the gist of how to program RISC-V with Rust. Looking at user mode example in C, a stack is created for usermode (768 bytes).
Then a stack pointer is defined as the address of the created stack plus the size of the stack. This value will be set as sp in the register file. My question is what do you set as the return address in the reg file? Looking at Freedom Metal’s privilege.c it sets both ra and sp but only sp was defined in the user mode example.

I don’t know rust, but the general recommendation is to set ra to 0. User mode code should end with a system call or trap that returns to a higher privilege level so you don’t need any particular ra setting for that. But the stack unwinding code recognizes an ra value of 0 as end of stack, so stack unwinding works better if _start sets ra to 0 so we can terminate the unwind when we reach _start. Stack unwinding is used for exception handling and debugging. The SiFive user mode example is setting ra to 0, as uninit array values will be 0, it just isn’t doing this explicitly.

1 Like

Can you tell me the importance of the memory being aligned to 16 bytes? Would 4 or 8 suffice, or is there better performance or requirement for 16 bytes? Reminder, I’m using a HiFive1 Rev B Thanks
Daniel

16 is the max data alignment, for 128-bit long double and 128-bit quad word int. If you use smaller alignment, then the linker might add extra alignment to align data, and then that could accidentally break things like the _gp calculation. But this stuff tends to be black magic and guesses, so I don’t know if the 16 byte alignment is actually required, it is just seems safer not to touch it.

That makes sense. Appreciate the intel. Whenever I follow an example or tutorial I don’t like blindly copying things if I don’t know why it is done a certain way. :slight_smile: Plus I’m trying to learn as much as possible along the way.

The Embedded ABI is proposing to drop the stack alignment down to 4 bytes (at least for 32 bit CPUs). I don’t know how anything bad could happen on HiFive1 if you did that.

So @bruce , as is, it should handle 4 byte alignment? I will end up testing it both ways probably.
And while I have you guys on the other line, is the only way to set the stack pointer for user mode entry is with assembly?

Four byte alignment for stack breaks double support, because double requires 8 byte alignment, which is why we don’t support rv32ed. Even if you use soft float double still requires 8 byte alignment and things break. Also 64-bit long long requires 8 byte alignment. But it is just the badly designed ilp32e ABI that has 4 byte stack alignment. The proposed EABI sets stack to 8 bytes for rv32 so that double support works, and to 16 bytes for rv64, well it says to support multiple load/stores per cycle on optimized cores, shrug.

Assembly code is the safe way to set sp. If you try to do it in C code then you have to fight what the compiler is doing with the stack frame and things can get complicated.

1 Like

Playing around with memory in Rust, I can create a 8 byte alignment by simply creating an array with u64, unsigned 64 bits. I can create a 16 byte alignment, but I have to wrap the array in a structure that uses some kind of representation attribute #[repr(C,align(16))]. In case you are curious, I’ve setup a little code for testing the alignments in the rust playground.

“Even if you use soft float double still requires 8 byte alignment and things break.”

I’m really confused how that can be the case.

Sure, if you simply use soft float ABI on a CPU that actually has a hardware FPU then that’s obviously true.

But on a 32 bit CPU with only the integer ISA? What can break if the doubles are not 8-byte aligned?

The only thing I can see is if something has a direct pointer to the first half of the double and then does “p | 0x4” to get to the second half instead of “p + 0x4” (assuming byte pointers), or correspondingly tries to go from the 2nd half to the first half using “p & ~0x4” instead of subtracting. If you have a pointer to the first half then 0(ptr) and 4(ptr) work fine regardless (which is more likely).

If something like that is happening then it just seems like a bug that can be fixed at zero performance cost.

The ABI says that the stack is 4 byte aligned and doubles are 8 byte aligned. Also, long long has 8 byte alignment. Whether code is soft-float or not is irrelevant.

If you change alignment of doubles and long long to 4, then that is an ABI change, and ilp32 and ilp32d are no longer compatible. Structures have different layout, arguments are passed differently, etc. And note that running soft-float code on rv32id is allowed, but breaks if doubles have 4 byte alignment when soft-float, as you can end up doing a fld on a 4-byte aligned pointer which traps. Changing the stack to 8 byte alignment is the simpler and safer solution.

Of course if you don’t care if anything is compatible with anything else, then yes you can change double and long long alignment to 4. But you lose data portability across ABIs, and the ability to run soft-float code on hard-float targets. Those will be major problems for some people.

Rust doesn’t use the platform ABI on any machine, unless it calls out to external C code. I took it that @dkhayes117 is interested in what the hardware will support.

I don’t plan on porting my project to any other platform for now, but I still find the information very interesting and good to know. From what I can tell, if I create the memory stack as 8 byte alignment I shouldn’t have any issues with the program. This is just a proof-of-concept project.

Now for another noob question about this. If I create an array like the sifive example using an unsigned char type and 768 elements for 768 bytes, is that fundamentally any different than using rust to create an array using u64 type and 96 elements. This is still 768 bytes so it seems that it wouldn’t matter which data type you use to define the memory space. The u64 will give me 8 byte alignment by default. I know it isn’t as intuitive as using an 8 bit type to directly correlate the total byte size to the array size, but it is simpler, code wise, in rust.

It seems that it might still be 16 byte alignment is the best choice since the array pointer will be set to the stack pointer.