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.
The makefile provides the following targets:
- kernel
- iso
- run
- gdb
It sniffs the OS type and when running on non-linux hosts it uses
vagrant ssh and runs make with the above targets inside the vagrant box.
The kernel build process consists of the following steps:
1) Compile arch-specific (only x86 for now) assembly files.
2) Run go build -n to obtain the build commands for our kernel. The
makefile sets the build target to 386/linux so that our current rt0
implementation does not need to switch to long-mode.
3) The build commands are then patched to:
- use build/ as the output directory
- force the go linker to use external link mode and to place its output files
(--tmpdir) to the build folder. By forcing external link mode, the go
linker will emit a single go.o file which can be used by ld.
4) We run our own link step and use ld to link the rt0 .o files with the
go.o file and provide a custom linker script to ensure that our
multiboot record is located at the top of the kernel image so that grub
can find it.
The ISO build process sets up a minimal folder structure for building a
bootable ISO (basically the kernel image plus a grub configuration) and
runs grub-mkrescue to produce the ISO file.
Both the run and the gdb targets assume that qemu is installed. The gdb
target starts qemu, attaches the debugger and sets a breakpoint to the
rt0 entrypoint.