Hello, if you have any need, please feel free to consult us, this is my wechat: wx91due
ECE391: Computer Systems Engineering Spring 2025
1 Introduction
In this machine problem, you collaborate to develop the core of an operating system roughly based on Unix Version 6, with modern concepts peppered in where appropriate. You’ll implement interrupt logic, user threading (a la MP2), kernel and application paging, initialize some devices and a filesystem with ` the VIRTIO interface, and create a system call interface to support various system calls. The operating system will support running several tasks (“threads”) spawned by a number of user programs; programs will interface with the kernel via system calls.
Don’t worry, these aren’t all “boring” programs—you’ll get to run some cool games, too.
The goal for the assignment is to provide you with hands-on experience in developing the software used to interface between devices and applications, i.e., operating systems. You should notice that the work here builds on concepts from the other machine problems. Many of the abstractions used here (e.g., the VIRTIO interface) have been simplified to reduce the effort necessary to complete the project, but we hope that you will leave the class with the skills necessary to extend the implementation that you develop here along whatever direction you choose, by incrementally improving various aspects of your system.
2 Using the Group Repository
It is required to run the following commands on each computer that you clone the repository to, in order to make sure the line endings are set to LF (Unix style):
As you work on MP3 with your teammates, you may find it useful to create additional branches to avoid conflicts while editing source files. Remember to git pull each time you sit down to work on the project and git commit and git push when you are done. Doing so will ensure that all members are working on the most current version of the sources. It is highly likely that you will benefit from proper usage of the git stash command, to correctly retain desired (local) changes when doing a pull.
When using Git, keep in mind that it is, in general, bad practice to commit broken sources. You should make sure that your sources compile correctly before committing them to the repository. Make sure not to commit compiled ‘*.o’ files, besides what we have given you. You can modify your .gitignore in any way you want.
Finally, merge your changes into the main branch by each checkpoint deadline, as this is the only branch we will use for grading.
3 The Pieces
For simplicity, we will stick to text-mode graphics (for the most part), but your OS will, by the end, run the games from previous MPs as well as some new ones. We’ve included a few helpful pieces that will allow you to debug easier, such as kprintf. We highly encourage use of gdb to debug. Print statements can only take you so far. See Appendix H for details.
3.1 Getting started
This MP is difficult, which is why we do not expect you to work alone. While you cannot share code or discuss details with other groups or anyone outside your group, you should work together with your team to get unstuck and build a common understanding.
3.2 Work Plan
This project is somewhat daunting, and will require efforts from all your team members. You should partition the work accordingly to allow independent progress by all team members.
Setting up a clean testing interface will also help substantially with partitioning the work, since group mem bers can finish and test components before your groupmates finish the other parts (yet). The abstractions suggested should allow for some spots where a “working part” can be substituted with a functionally equiv alent placeholder, so to speak—more on that later.
While splitting up the work allows for you to make more progress, it is still crucial that you spend time working together to integrate all parts. You should also be maintaining active communication between group members to make sure you all have an understanding of how all your code works. Even if you did not work on a specific section, we expect you to be familiar with how the code works and be able to explain what it and how it fits into your kernel.
Throughout the first part of this semester and in most/all of your previous classes, MPs were more structured.
You were given a set of functions to write, and you only modified those functions. One of the goals of this class is to make you into a more confident and thoughtful programmer by having you practice software design. We have deliberately left the implementation of certain sections open-ended. It is up to you and your group to find a way to meet the requirements - as with the real world, there is no “perfect” solution. The only requirement that we impose is that you follow the function interfaces that we have already specified - feel free create your own helper functions or creative implementations.
4 Testing
As your operating system components are dependent on one another, you may find it useful to unit test each component individually to isolate any design or coding bugs.
You should create a main tests.c file and add it to your Makefile. This file should create a kernel image (e.g., test.elf) that you can load into QEMU and run tests with. As you add more components to your operating system, we encourage you to add corresponding tests that verify the functionality of each component at the interface level.
Keep in mind that passing all of your unit tests does not guarantee bug free code. However, the test suite provides a convenient means to run your tests frequently without having to re-debug older components as you add new functionality.
5 What to Hand in
5.1 Checkpoint 1: Filesystem and Drivers and Program Loading, (Oh My!)
Rather than do this in a “hacky” way, we want to set up some key infrastructure now which will pay dividends as the project continues on.
Note: Please ensure that all the functions that we specify do not induce a kernel panic on an invalid input.
You should return an error code (which one is up to you) if possible, otherwise do nothing.
For the checkpoint, you must have the following accomplished:
You must have your code in the shared group repository, and each group member should be able to demon strate that they can read and change the source code in the repository.
An operating system in general must communicate with external devices. One such device is obviously the real drive/disk (virtual, in this case) which contains programs and other files you want your operating system to have access to.
In order to set up this device (and any others down the line), we will need to set up the necessary framework for the VIRTIO block device. Your group must finish the implementation based on the VIRTIO documentation linked on the course website.
More information about the functions that you have to write can be found on the Doxygen site linked on the course website.
See Appendix B for more information on the io struct.
Broadly speaking, your filesystem driver should provide a comfortable interface to open, read and scan through files. In later checkpoints, you will add additional functionality.
Your ktfs.c file will need to interact with its backing device with some intermediate cache in cache.c to actually interact with the “physical” (well, virtual) device, so be sure that you understand what’s going on in files related to both the cache and the backing device. Additionally, as this interacts with virtual devices, you should be sure that you use the io struct in the proper way.
More information about the functions that you have to write can be found on the Doxygen site linked on the course website.
In order to use the filesystem, we have provided a mkfs ktfs function (see Appendix A) that generates a filesystem image for you. This filesystem image is mounted by QEMU as a drive (using the Makefile we provide) and is accessible through VIRTIO.
For every “device” that uses io, you will need to implement “iocntl” IOCTL GETEND and IOCTL GETBLKSZ.
Keep in mind that IOCTL GETBLKSZ should not return the filesystem block size, but the “block size” of the file IO object - in this case, 1.
(Hint: implement the memio and its related functions to test your KTFS filesystem driver as well as the cache without the vioblk.)
See Appendix A for additional details.
You may wonder why we need a memio device if we already have a vioblk device? The memio device is a helper backing device that allows you to test your filesystem and ELF loader without using vioblk. Within your linker script kernel.ld, there is a section from kimg blob start to kimg blob start that you can use to place your whole filesystem image or an ELF file to load (see Appendix A about the blob).
More information about the functions that you have to write can be found on the Doxygen site linked on the course website.
For better unit testing and debugging, you must implement the memio device “driver” in io.c. The following is a list of functions you need to implement:
5.1.5 Locked and . . .
Note: In order to prevent deadlocks, an exiting thread must release all held locks.
More information about the functions that you have to write can be found on the Doxygen site linked on the course website.
You should implement the following functions in thread.c:
5.1.6 . . . (ELF) Loaded
We want to be able to run many user-level programs, but for now you can focus on hello and trek. While we’ve given you the pre-compiled binary of trek, you’ll have to compile hello using the usr/Makefile.
Because of this, you also have access to hello.c. All the binaries are in a format called ELF (Executable and Linkable Format), which has a specific layout — it is the standard for Unix and Unix-like systems, historically, which means it is still very relevant. See the Tools, References, and Links page on the course website for the Linux manual page on ELF. Your loader will only need to deal with the program headers, not sections, so focus on that documentation.
Notice that since elf load should support any compliant I/O interface, that we can in general load an ELF from “any source” as long as ioread and ioseek are implemented in the given io (see Appendix B). (Hint: memio)
“Software gets slower faster than hardware gets faster.” -Wirth’s Law
This semester, you will implement a caching system to cache blocks from a backing interface. As you’ve learned in this course, communicating with devices is extremely slow relative to the CPU’s clock cycle.
Previously, you’ve implemented asynchronous communication (condition variables) so that while one thread is waiting on a device response, another thread can run.
While this significantly reduces the problem of “wasting” CPU cycles, it does not eliminate the problem of latency - the original thread still must wait a long time for the device’s response. A cache is a commonly used way to reduce this latency.
Once you complete this checkpoint, your backing interface will be the VIRTIO block device, but we will refer to it generically as the backing interface in this document. Rather than reading/writing a block directly from/to the backing interface, we first check whether the block exists in the cache. If the block exists in the cache, we access the block via the cache rather than sending a new request to the backing interface. If the block does not exist in the cache, we read it from the backing device into the cache. Note that you may or may not need to evict a block currently in the cache in order to bring in this new block.
Your cache may have any level of associativity and may be write-back, write-through, or some other concoction. Please note that we are intentionally leaving the specific details of the cache vague; this is intendedto be a design exercise.
Some additional scenarios/specifications for the cache are as follows (these would be good test cases for you to write):
More information about the functions that you have to write can be found on the Doxygen site linked on the course website.
You will implement the following 4 functions in cache.c:
As a reminder, you can also create any other helper functions that you need for your cache implementation.
During MP2, you worked with the PLIC, UART, and some other devices. You will be re-using that code for this checkpoint. You should add the following files into sys from MP2. They must be fully functional and (besides thread.c) should have the same functionality as a completed MP2. You can collaborate with your MP3 groupmates to choose whose MP2 code to use.
Finally, once you have all of your code completed, you will need to finish sys/main.c to run your program.
We’ve left some comments on how to run trek, you may also find it helpful to refer to your MP2 CP3 mainfunction implementation. You can also run hello or another user program that you create.
See Appendix H for more information about debugging and common issues.
For handin, your work must be completed and pushed to the main branch of your team’s GitHub remote repository by the deadline.
5.2 Checkpoint 2: Virtual Memory and Process Abstraction
Linked on the course website are some extra files which are needed for the Checkpoint 2 implementation.
Add them to your repo before starting Checkpoint 2 development.
Files to add to the sys directory:
In operating systems, paging is a memory management technique that allows non-contiguous allocation and efficient use of storage by dividing a process’s address space into fixed-size units called pages. Page tables exist to maintain mappings between physical and virtual memory. When a process accesses a virtual address, the page table is queried to find the corresponding physical address. A page fault occurs when a process tries to access a virtual address that is not mapped to a physical address. If you handle a page fault that occurs within the User-owned virtual memory address space (USER START VMA to USER END VMA) you should allocate a new page and map that address as U-mode accessible. This is known as “lazy allocation” or “demand paging”. For page faults that occur in other virtual memory regions, you should panic.
The operating system needs to support both single-page and multi-page allocations. When a process requiresmemory, the system selects the best-fit chunk from the free page list (the smallest chunk that fits the requestednumber of pages) to ensure optimal memory utilization. Pages can also be freed individually or in groups to ensure efficient reuse. When memory is no longer needed, in order to prevent security risks and memory leakage, the system must reset. This ensures clearing of non-global pages and freeing the associated memory.
Flags are used in memory management to control access permissions for maintaining security and track pageusage. They are critical in allocating memory, mapping and unmapping of pages, validating memory access, as well as handling page faults. Access faults occur when a process tries to access a virtual memory address that they do not have permissions to access.
Note: This checkpoint will only contain a single memory space. You will not need to create new memory spaces for this checkpoint besides the ”main” memory space.
We’ve given you a file, memory.c, where you will implement all the functions declared in memory.h. Keep in mind, many of these functions have overlap and it may be useful to look at the provided helper functionsas well as write some of your own. You must write the following functions for this checkpoint:
In order to set up virtual to physical page mappings in your kernel, you must have a way to keep track of what physical pages are available to be mapped. To do this, we have created the “free chunk list”. A page chunk is a contiguous region of physical memory addresses. Each page chunk contains a pointer to the next page chunk in the list as well as a size (in pages). Initially, all of the memory from the end of the heap (heap end, which is page-aligned) until the end of RAM (defined in conf.h) are free physical pages.
You must modify memory init to place all of these pages (in a single chunk) on the free chunk list.
Allocating and freeing physical pages must also interact with the free chunk list. To allocate physical page(s), you should go through the free chunk list and find a chunk that is greater than or equal the number of contiguous physical pages that you need to allocate. If there is no chunk large enough, you can panic. If there is a chunk, you should break off an appropriately-sized piece and provide a pointer to the start of the physical address range that was allocated. To free physical page(s), you can simply place the chunk back on the free chunk list - no need to coalesce chunks together.
Some of these functions may build off of others. Some functions will also be called by functions used to implement processes (§5.2.3). Your code must meet the functionality requirements outlined in the rubric.
Consult Appendix D for more information about virtual memory.
The process abstraction is one of the key abstractions of an operating system. A process can be definedinformally as just a ”running user program”. A user often wants to run multiple processes at once whichrequires common resources like processing power, devices, and memory to be managed.
An instance of a process structure contains everything that a process owns and uses internally. Each userprocess is actually just a wrapper around a kernel thread. What this means is whenever a user process is created, a process struct will have to be initialized to contain the information below:
In this checkpoint, all user processes will share the same memory space, dubbed the ”main” memory space.
The execution lifecycle of a process will be as follows
Our kernel space process API is made up of the functions below. These functions will reside in process.c and should be written by you unless if stated otherwise:
Initializes processes globally by initializing a process structure for the main user process (init). The init process should always be assigned process ID (PID) 0.
Executes a program referred to by the I/O interface passed in as an argument. We only require a maximum of 16 concurrent processes.
Executing a loaded program with process exec has 4 main requirements:
Context switching was relatively trivial when both contexts were at the same privilege level (i.e. machine-mode to machine-mode switching or supervisor-mode to supervisor-mode switching), but now we need to switch from a more privileged mode (supervisor-mode) to less privileged mode (user-mode).
Doing so requires using clever tricks with supervisor-mode CSRs and supervisor-mode instructions. Here are some tips to consider while implementing a context switch from supervisor-mode to user-mode
It’s a useful exercise to try to figure out how such an approach could work with the CSRs and supervisor-mode instructions on your own. However, implementation on its own is a sufficient chal lenge and we don’t require you to figure this out. You can read Appendix C to find out how you can carry out a context switch between user-mode to supervisor mode.
Cleans up after a finished process by reclaiming the resources of the process. Anything that was associated with the process at initial execution should be released. This covers:
You will also have to modify your current threading library to accommodate for processes. To do thisyou will have to both declare the specified functions in your thread.h file as well as define them in your thread.c file.
You also will have to add a struct process * proc to your thread struct
More information about the functions that you have to write can be found on the Doxygen site linked on the course website.
You will need to implement a series of system calls (syscalls) for this checkpoint. The user program uses these to request actions from the kernel.
The function handle syscall is used for system call linkage, through it calling the syscall function.
Looking into the syscall.S, we see that system call exceptions are generating using the ecall instruction.
The exception should be handled by smode trap entry from umode in trap.s. This assembly code should call the handle umode exception function in excp.c. User-mode exception handling is used for implementing both system calls and page fault handling. You will need to write both of these functions. smode trap entry from umode will be the entry point to the kernel when a trap is generated in U-mode.
Similar to it’s S-mode equivalent, you must save registers to the trap frame, go to either the exception handler or interrupt handler, restore registers, and return. However, there are subtle differences that you will need to reason about and understand to make this function work. We highly recommend that you thoroughly read and understand smode trap entry from smode before attempting this function. handle umode exception will be similar to handle smode exception, but keep in mind that we “grace fully” handle 2 exception types: page faults and environment calls. If one of these occurs (and is handled without an error) you should return from this function through smode trap entry from umode, otherwise, you should exit the current process.
In Checkpoint 1, you already implemented a read-only KTFS driver. In this checkpoint, you will extend your filesystem driver to allow for writing to files as well as file creation and deletion. Keep in mind that all previous read behavior should be maintained, and after this checkpoint, your filesystem should implement CRUD functionality with respect to files.
Writes to the disk should persist within the filesystem image even after QEMU shutdown. That is, if you runa program that creates a file and writes to it (assuming it is written to the vioblk device through the cache), then shut down QEMU, you should be able to open and read from that file the next time QEMU is launched.
This also means that you can look at your filesystem image file after QEMU exits to help debug writes. Be careful that the writes are truly persisting to disk - if you have a write-back cache, they may only be writtento the cache before QEMU exits.
Note: Writing to files is a bit trickier than reading from them, but there is a lot of overlap code. Try your best to follow DRY (Don’t Repeat Yourself).
For this checkpoint, you should implement the following functions in ktfs.c:
You must also implement IOCTL SETEND in ktfs cntl. IOCTL SETEND should extend the length of thefile and add additional data blocks if needed.
It is very important that you write thorough tests for the filesystem. There are many possible edge and corner cases and we highly encourage you to create targeted tests to ensure that you handle all of them properly.
Due to the new virtual memory, process abstraction, and system calls, we are now able to run programs in
User-mode instead of Supervisor-mode. This means that some of the code you wrote in Checkpoint 1 will no longer work. Here is a (non-exhaustive) list of modifications you’ll have to make to your existing code:
2. main.c should be modified to initialize memory and the process manager. You should also remove heap init from main.c, as memory init will do that for you.
6. scnum.h and usr/scnum.h must be modified to add the create and delete syscall numbers. Add #define SYSCALL FSCREATE 12 and #define SYSCALL FSDELETE 13 to both of these files.
You will also need to modify usr/syscall.S to add U-mode support for these two syscalls. To do this, create 2 new functions fscreate and fsdelete. They will be very similar to other syscall functions in the file.
For handin, your work must be completed and pushed to the main branch of your team’s GitHub remote repository by the deadline.
We have given you a new binary, trek cp2, which uses some but not all of this functionality. While being able to run this program is a good sign, it does not necessarily mean that all functions are correct and you are handling edgecases properly. You should write your own testcases to verify functionality.
6 Grading
Teamwork is an important part of this class and will be used in the grading of this MP. We expect that you will work to operate effectively as a team, leveraging each member’s strengths and making sure that everyone understands how the system operates to the extent that they can explain it. In the final demo, for example, we will ask each of the team members questions and expect that they will be able to answer at a reasonable level without referring to another team member. Failure to operate as a team will significantly reduce your overall grade for this MP.
At the end of the MP there will be a teammate evaluation which will factor into your final MP grade.
You are also expected to adhere to the MP3 contract.
7 Appendix A: The File System
7.1 File System Overview
First, we will provide definitions of important terms.
The filesystem image refers to a ”blob” of data that contains everything needed to mount the filesystem and all the data associated with it.
The backing device for a filesystem refers to where the filesystem image is stored. In an actual computer, it’s typically stored on the hard drive. However, for this MP, the backing device will be the VIRTIO block device, which emulates a real hard drive. To test your filesystem without the VIRTIO block device, you can use a buffer in memory (see memio).
The figure below shows the structure and contents of the filesystem image.
These fields only take up the first 14B of the 512B, so the remaining bytes are unused. The fields in the superblock are set when creating the filesystem image (see §Building the File System) and should not be modified by your OS.
The blocks following the superblock are the bitmap blocks. The bitmap blocks contain ”bits”, as the name suggests. Each bit indicates whether a block is used: 1 means used, 0 means free. For example, the 0th bit of the 0th bitmap block corresponds to the 0th block of the filesystem. If a block is free, then it can be used later when writing to a file or creating a new file. When the filesystem image is created, the superblock, bitmap blocks, and inode blocks are all marked as in-use. During filesytem image creation, the ”disk size” is set and fixed, which means the number of bitmap blocks is also fixed.
The inode blocks contain the index nodes (inodes) of the files in the filesystem. Each file is described by an inode that specifies the file’s size in bytes and the numbers of the data block (e.g. data blocks 1,5,10 ...) that make up the file. During filesystem image creation, the number of inode blocks is set and cannot change.
Each inode only provide direct access to 3 data blocks through an array within the inode. However, inodes additionally store 1 indirect data block number and 2 doubly-indirect data block numbers. An indirect data block number points to a data block that contains an array of data block numbers, instead of actual data. For example, if you want to access the 4th data block of a file, you will have to get the indirect data block number first (e.g.inode.indirect = 11), get data block 11 from backing device, and read the first uint32 t (first 4B) of data block 11 (e.g.14). Then, go to data block 14 and there will be actual data. The idea is the same for doubly-indirect blocks, except there’s one more layer of indirection: the doubly-indirect data block numberpoints to a data block that stores an array of indirect data block numbers, which further points to indirect data blocks that store an array of ”real” data block numbers.
Each indirect/doubly-indirect data block contains 512B (just like any other data block), but only the part of the indirect/doubly-indirect data block that points to data blocks necessary to contain the specified size need be valid, so be careful not to read and make use of block numbers that lie beyond those necessary to contain the file data. The data blocks that make up a file are not necessarily contiguous or in any specific order.
You must use the data block numbers in the order specified in the inode and the indirect/doubly-indirect data blocks to access the correct data in the correct order.
There’s one special inode called the root directory inode. Instead of containing the actual data of a file in its specified data blocks, these data blocks contain the directory entries. KTFS does not support nested directories, so the root directory is the only directory that you are required to support. Each directory dentry (or dentries for short) contains an inode number and a string that contains the file name. The size of each dentry is 16B: 2B for the inode number and 14B for the file name. Note that because we need 1B for thenull terminator, the maximum length of a file name is 13B (13 characters). The ”file size” specified in the root directory inode is equal to (the number of dentries) × (the size of each dentry). Therefore, you can determine the number of files in the filesystem by checking the size field of the root directory inode. When you create or delete a file, you must add/remove the dentry for that file and update the size appropriately. For this file system, there is a 1:1:1 correspondence between files, dentries, and inodes. No two dentries should contain the same inode number or file name.
Note: Dentries must be contiguous to be able to use the file size properly. This is a strict requirement that filesystem images will follow and your filesystem driver must maintain. Keep in mind that “contiguous” here does not actually mean contiguous in memory, just that if you were reading the root directory inode like a file, the first size bytes would be the dentries.
In most cases, the data blocks can be seen as normal chunks of data in a file. That means when you open a file, you should be reading data from and writing data to these actual data blocks. However, because of the root directory inode and indirect data blocks, data blocks can contain different things. So, a data block can be classified into the following types depending on what data it contains:
Note: Since the root directory inode is treated similar to a “regular” inode, it can grow in size like other files with indirect and doubly-indirect data blocks. This means that the limiting factor on the number of files in a filesystem is typically the number of inodes.
7.2 File Abstractions
In order to make your filesystem driver, you should create a struct ktfs file. This will be the internal representation of a file that your filesystem driver uses to keep track of the state of each file. This structure should contain at least the following fields (add more if you find it useful):
You need to have some way to keep track of the ktfs file structs that correspond to currently opened files (array, linked list, etc.), so that future I/O operations can be performed on them.
7.3 Building the File System
We have also provided you with a 3rd binary executable, unmkfs ktfs. This program “unwraps” the files contained with a KTFS filesystem image to allow you to parse them on your own. You may find this useful when implementing the write/writeat functions in your filesystem driver, since these functions will modify your actual filesystem image and persist between QEMU shutdowns. mkfs ktfs usage is as follows:
This would create a ktfs.raw file in the sys directory with a total size of 8MiB and 16 inodes (i.e., 1 inode block). hello and trek would be the only 2 files in the filesystem image, with the rest of the inodes free.
Note: Make sure you are running your mkfs ktfs binary and not the Linux mkfs function.
The files in your filesystem image are also given as arguments to mkfs ktfs. You can (and should) provide multiple files in order to create a full filesystem image. All the metadata related to these files will be prepared for you, including the superblock and other structures that you’ve read about in Appendix A already. All of these blocks will be put in order according to the spec above and stored in your filesystem image (i.e., ktfs.raw). This means that the filesystem image is already compliant with the KTFS spec.
To load a user program into the filesystem image, you need to compile it to an executable binary. This can be done by using the Makefile in usr. If you want to change a file in the filesystem or add/remove files (without using your filesystem driver), you must remake the filesystem image each time.
7.4 The Blob
8 Appendix B: I/O Devices
8.1 Overview
QEMU is simulating devices and their interconnects. Your filesystem image, for example, (see Appendix A) will be managed by the QEMU block device.
8.2 I/O Operations
9 Appendix C: The Process Abstraction
9.1 History
9.2 Overview
A process being scheduled by the kernel to run is just the process’s thread being put on the ready list and a process running is just the thread associated with the process running (as the current thread).
On top of owning a thread, each process also owns a virtual memory space. This virtual memory space is represented by a 3-level page table. You can read more about the paging and virtual memory in Appendix D.
In addition, a process should also keep track of files and devices that it’s interacting with. Note that we have a common interface for files and devices (io)
9.3 Context Switching between User-Mode and Supervisor-Mode
Reading the description for the ecall instruction in the RISCV Privileged ISA manual we can see that it will trigger an exception which should be delegated to the supervisor-mode trap handler. This exception being triggered from user-mode will have the following effects:
Reading the description for the sret instruction in the RISCV Privileged ISA manual we can see that an sret in supervisor-mode will return to a lower privileged mode (user-mode) with the following effects:
Now that we know the effects of ecall and sret, we can reason about how to use these instructions during context switches. There are two specific cases to consider:
10 Appendix D: RISC-V Paging
10.1 Illinix Mappings
10.2 Sv39
Virtual memory address translation, also called paging, is a hardware-level tool that supports process virtu alization. When paging is enabled and the machine is in umode, the hardware will translate all user-program memory accesses from virtual to physical addresses.
Multiple programs may be staged to execute at the same address. Memory virtualization allows each process to execute with the idea that they have the entire memory space at their disposal, and ensures that the operating system can manage many programs executing in parallel.
To use the QEMU console (useful in debugging) you can add the following line in src/kern/Makefile where you set your QEMUOPTS