Skip to content

Commit

Permalink
GDB and userspace updates
Browse files Browse the repository at this point in the history
* Add smoldtb and drvos to our projects

* replace you with we/us in debu chapter

* Fix typo

* Fix titles

* Add information on how to test userspace

* changes requested in review

* typo fix

* Typo fixes

* Fix typo
  • Loading branch information
dreamos82 authored Dec 1, 2023
1 parent 6a97731 commit 733599f
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 39 deletions.
4 changes: 3 additions & 1 deletion 00_Introduction/03_AboutTheAuthors.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

*I'm a hobbyist programmer, and have been working on my operating system kernel since 2021, called northport. I've experimented with a few other projects in that time, namely a micro-kernel and a window manager. Before getting into osdev my programming interests were game engines and system utilities. My first programming project that I finished was a task manager clone in C#. These days C++ is my language of choice, I like the freedom the language offers, even if its the freedom to cause a triple fault.* - Dean.

## Our Projects:
## Our Projects

* DreamOs64: A x86_64 kernel written in C, with memory management, scheduling and a VFS. Written by Ivan. [https://github.com/dreamos82/Dreamos64](https://github.com/dreamos82/Dreamos64)
* DRVOs: A tiny kernel, for riscv64, with very limited functionalities by Ivan. [https://codeberg.org/dreamos82/DRvOs](https://codeberg.org/dreamos82/DRvOs)
* Northport: 64-bit kernel supporting multiple architectures (x86_64 and riscv64) and SMP. Written in C++ by Dean. [https://github.com/DeanoBurrito/northport](https://github.com/DeanoBurrito/northport)
* Smoldtb: Tiny and portable device tree parser, written in C. by Dean. [https://github.com/DeanoBurrito/smoldtb/](https://github.com/DeanoBurrito/smoldtb/)
45 changes: 45 additions & 0 deletions 06_Userspace/02_Switching_Modes.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,51 @@ Since we have paging enabled, that means page-level protections are in effect. I

*Authors Note: For my VMM, I always set write-enabled + present flags on every page entry that is present, and also the user flag if it's a lower-half address. The exception is the last level of the paging structure (pml1, or pml2 for 2mb pages) where I apply the flags I actually need. For example, for a read-only user data page I would set the R/W + U/S + NX + Present bits in the final entry. This keeps the rest of implementation simple. - DT.*

#### Testing userspace

This also leaves us with a problem: how to test if userspace is working correctly? If the scheduler has been implemented using [part five](../05_Scheduling/01_Overview.md) of this book, just creating a thread with user level `ss` and `cs` is not enough, since the thread to run uses the code that is present in the higher half (even the function to execute), and this mean that according to our design that area is marked as supervisor only.

The best way to test it should be implementing support for an executable format (this is explained on [part nine](../09_Loading_Elf/01_Elf_Theory.md)), in this case we're going to write a simple program with just one instruction that loops infinitely. compile it (but not link it to the kernel), and load it somewhere in memory while booting the os (for example as a mulbiboot2 module). Later on we can put it together with the VFS, to load and execute programs for there.

But the problem is that this takes some time to implement, and what we probably want is just check that our kernel can enter and exit the user mode safely. A quick solution to this problem is:

* Write an infinite loop in assembly language:

```x86asm
loop:
jmp loop
```

and compile it, in using _binary_ as format specifier , for example using nasm:

```x86asm
nasm -f bin example.s -o example
```


* Get the binary code of the compiled source, for example using the following `objdump` command:

```sh
objdump -D -b binary -m i386:x86-64 ../example
```

we get the following output:

```
example: file format binary
Disassembly of section .data:
0000000000000000 <.data>:
0: eb fe jmp 0x0
```

The code is stored in the `.data` section, and as you can see in this case is very trivial, and its binary is just two bytes: `eb fe`.

* Assign those two bytes in a `char` array somewhere in our code.
* Now we can map the address of variable containing the program to a userspace memory location, and pass assign this pointer as the new `rip` value for the userspace thread.(how to do it is left as exercise).

In this way the function being executed by the thread will be a userspace executable address containing an infinite loop. If the scheduler keep switching between the idle thread and this thread, well everything should be working fine.

### Actually Getting to User Mode

First we push the 5 values on to the stack, in this order:
Expand Down
8 changes: 4 additions & 4 deletions 06_Userspace/03_Handling_Interrupts.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ This is not a complete guide on how to handle interrupts. It assumes we already

On `x86_64` there are two main structures involved in handling interrupts. The first is the IDT, which we should already be familiar with. The second is the task state segment (TSS). While the TSS is not technically mandatory for handling interrupts, once we leave ring 0 it's functionally impossible to handle interrupts without it.

### The Why
## The Why

*Why is getting back into supervisor mode on x86_64 so long-winded?* It's an easy question to ask. The answer is a combination of two things: legacy compatibility, and security. The security side is easy to understand. The idea with switching stacks on interrupts is to prevent leaking kernel data to user programs. Since the kernel may process sensitive data inside of an interrupt, that data may be left on the stack. Of course a user program can't really know when it's been interrupted and there might be valuable kernel data on the stack to scan for, but it's not impossible. There have already been several exploits that work like this. So switching stacks is an easy way to prevent a whole class of security issues.

As for the legacy part? `X86` is an old architecture, oringinally it had no concept of rings or protection of any kind. There have been many attempts to introduce new levels of security into the architecture over time, resulting in what we have now. However for all that, it does leave us with a process that is quite flexible, and provides a lot of possibilities in how interrupts can be handled.

### The How
## The How

The TSS served a different purpose on `x86` (protected mode, not `x86_64`), and was for *hardware task switching*. Since this proved to be slower than *software task switching*, this functionality was removed in long-mode. The 32 and 64 bit TSS structures are very different and not compatible. Note that the example below uses the `packed` attribute, as is always a good idea when using structures that are dealing with hardware directly. We want to ensure our compiler lays out the memory as we expect. A `C` version of the long mode TSS is given below:

Expand Down Expand Up @@ -88,7 +88,7 @@ Now that we have a TSS, lets review what happens when the cpu is in user mode, a
- The cpu now jumps to the handler function stored in the IDT entry.
- The interrupt handler runs on the new stack.

### The TSS and SMP
## The TSS and SMP

Something to be aware of if we support multiple cores is that the TSS has no way of ensuring exclusivity. Meaning if core 0 loads the `rsp0` stack and begins to use it for an interrupt, and core 1 gets an interrupt it will also happily load `rsp0` from the same TSS. This ultimately leads to much hair pulling and confusing stack corruption bugs.

Expand All @@ -100,7 +100,7 @@ There's a few ways to go about this:
- Have a single GDT shared between all cores, but each core gets a separate TSS selector. This would require some logic to decide which core uses which selector.
- Have a single GDT and a single TSS descriptor within it. This works because the task register caches the values it loads from the GDT until it is next reloaded. Since the TR is never changed by the cpu, if we never change it ourselves, we are free to change the TSS descriptor after using it to load the TR. This would require logic to determine which core can use the TSS descriptor to load its own TSS. Uses the least memory, but the most code of the three options.

### Software Interrupts
## Software Interrupts

On `x86(_64)` IDT entries have a 2-bit DPL field. The DPL (Descriptor Privilege Level) represents the highest ring that is allowed to call that interrupt from software. This is usually left to zero as default, meaning that ring 0 can use the `int` instruction to trigger an interrupt from software, but all rings higher than 0 will cause a general protection fault. This means that user mode software (ring 3) will always trigger a #GP instead of being able to call an interrupt handler.

Expand Down
2 changes: 1 addition & 1 deletion 10_Going_Beyond/01_Going_Beyond.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ There are a few options when it comes, let's quickly look at the common ones:
- *Write your own*: This route is not recommended for beginners as a standard library is heavily stressed code (more so than the kernel at times), and it's easy to introduce subtle bugs. A standard library is again an entirely separate project that rival the size of your kernel. However if you do go this route, you have an excellent reference: the C programming standard! This document (or collection of them) describes what is and isn't legal in C, as well as defines what functionality the standard library needs to be provide. Most standard libraries assume your kernel provides a POSIX-like interface to userspace, if your kernel does not then this may be your best option.
- *Glibc*: The GNU libc is arguably one of the most feature complete (as is the LLVM equivalent) C standard libraries out there. It also boasts a broad range of compatability across multiple architectures. However all this comes at the cost of complexity, and that includes the requirements of the kernel hosting it. Porting Glibc requires a nearly complete POSIX-like kernel, often with linux systems in some places. This is a better option than writing your own, but it does require a bit of work. Porting glibc or llvm-libc does provide the most compatability.
- *Mlibc*: This libc is written and maintained by the team behind the managarm kernel. It was also built with hobby operating systems in mind and designed to be quite modular. As a result it is quite easy to port and several projects have done so and had their changes merged upstream. This makes porting it to other systems even easier as you can see what other developers have done for their projects. The caveat is that mlibc is quite new and there are occasional compatbility issues with particular library calls. Most software is fine, but more esoteric code can break, especially code that takes advantage of bugs in existing standard libraries that have become semi-standard.
- *Mlibc*: This libc is written and maintained by the team behind the managarm kernel. It was also built with hobby operating systems in mind and designed to be quite modular. As a result it is quite easy to port and several projects have done so and had their changes merged upstream. This makes porting it to other systems even easier as you can see what other developers have done for their projects. The caveat is that mlibc is quite new and there are occasional compatibility issues with particular library calls. Most software is fine, but more esoteric code can break, especially code that takes advantage of bugs in existing standard libraries that have become semi-standard.
There are also other options for porting a libc that deserve a mention, like newlib and musl.
Expand Down
Loading

0 comments on commit 733599f

Please sign in to comment.