The page table walker provides a mechanism for accessing the individual
page table entries that correspond to a particular virtual memory
address. This implementation will serve as the basis for implementing
page mapping/unmapping and virtual to physical address translation.
This is equivalent to pmm.Frame (also a uintptr) but having different
types for physical and virtual frames serves as an additional layer of
protection for functions/methods that receive physical and/or virtual
page arguments.
To keep the implementation portable, the Frame type had to be changed
from uint64 to uintptr. Using uintptr ensures that the frame will always
match the pointer size of the platform.
When converting strings to []byte so that they can be used with the tty
io.Writer interface Go calls a runtime method called
"stringtoslicebyte". If the input length exceeds a particular size then
this method will allocate a new []byte and copy the data into it. This
obviously causes our kernel to crash.
To fix this, all early_fmt functions have been changed to iterate any
string arguments and output them to the active TTY one byte at a time.
The switch to 64-bit mode allows us to use 48-bit addressing and to
relocate the kernel to virtual address 0xffff800000000000 + 1M. The
actual kernel is loaded by the bootloader at physical address 1M.
The rt0 code has been split in two parts. The 32-bit part provides the
entrypoint that the bootloader jumps to after loading the kernel. Its
purpose is to make sure that:
- the kernel was booted by a multiboot-compliant bootloader
- the multiboot info structures are copied to a reserved memory block
where they can be accessed after enabling paging
- the CPU meets the minimum requirements for the kernel (CPUID, SSE,
support for long-mode)
Since paging is not enabled when the 32-bit code runs, it needs to
translate all memory addresses it accesses to physical memory addresses
by subtracting PAGE_OFFSET. The 32-bit rt0 code will set up a page table
that identity-maps region: 0 to 8M and region: PAGE_OFFSET to
PAGE_OFFSET+8M. This ensures that when paging gets enabled, we will still
be able to access the kernel using both physical and virtual memory
addresses. After enabling paging, the 32-bit rt0 will jump to a small
64-bit trampoline function that updates the stack pointer to use the
proper virtual address and jumps to the virtual address of the 64-bit
entry point.
The 64-bit entrypoint sets up the minimal g0 structure required by the
go function prologue for stack checks and sets up the FS register to
point to it. The principle is the same as with 32-bit code (a segment
register has the address of a pointer to the active g) with the
difference that in 64-bit mode, the FS register is used instead of GS
and that in order to set its value we need to write to a MSR.
The go compiler uses SSE instructions to optimize some of the generated code. We need to explicitly enable SSE support by manipulating the appropriate CR flags; otherwise the kernel will triple-fault
The kfmt/early package provides a minimal printf implementation that
does not use memory allocations (everything is allocated on the stack).
This implementation can be used to emit debug messages before the memory
manager is initialized.
If kernel.Kmain defines a nested function then a symbol like `github.com/achilleasa/gopher-os/kernel.Kmain.func1` will be generated. To make sure we always pick the `github.com/achilleasa/gopher-os/kernel.Kmain` symbol address we just need to add a `$` to the grep argument.
The go compiler exposes a fully qualified symbol for kernel.Kmain that
also includes the full package name (github.com/.../kernel.Kmain). Since nasm
cannot work with external symbols that include slashes we use objcopy to
create an alias symbol "kernel.Kmain" that points to the symbol
generated by the go compiler.
To use the "--add-symbol" argument we need to use objcopy 2.6+. The
makefile was modified to include an additional pre-compile check for the
installed objcopy version.
We still keep the required main func in stub.go to prevent the compiler
from optimizing the code out. We also force the compiler not to inline
the call to kernel.Kmain so we can find the symbol in the generated .o
file.
Since the actual size of each memory entry is not known in advance
(bootloaders may append additional information to it) but needs to be
queried off the memory map tag header we cannot reserve space for it as
no memory allocation is yet available.
Instead, a visitor pattern was implemented to allow the memory
manager initialization block to easily mark the appropriate pages as reserved
The terminal uses console.Vga as its output device. A proper terminal
implementation would be using a console.Console interface as its output.
However, at this point we cannot use Go interfaces as the fn pointers in
the itables have not been yet initialized. The Go runtime bits that set
up the itables need access to a memory allocator, a facility which is
not yet provided by the kernel.