Go Bare Bones

From OSDev.wiki
(Redirected from Go Barebones)
Jump to navigation Jump to search
WAIT! Have you read Getting Started, Beginner Mistakes, and some of the related OS theory?
Difficulty level

Medium

In this tutorial you'll learn how to get started using the Go language to write your own OS. It will be an example of how to create a very minimal system to get text on the screen. It's in no way an example of how you should organize or structure your project.

Preface

It is highly recommended to first read Bare Bones before following this tutorial because it is based on it. Also please keep in mind that the Go language itself was not designed for operating systems development. As such you'll be fighting the language every step of the way.

This tutorial assumes you are using a Unix-like operating system such as Linux.

Building a Cross-Compiler

Main articles: GCC Cross-Compiler and Why do I need a Cross Compiler?

The first thing you should do is set up a GCC Cross Compiler that supports Go. To do this read and follow GCC Cross-Compiler to the letter with one exception. When configuring the build for GCC we need to enable Go to get the i686-elf-gccgo compiler.

So instead of using:

../gcc-x.y.z/configure --target=$TARGET --prefix="$PREFIX" --disable-nls --enable-languages=c,c++ --without-headers

We use:

../gcc-x.y.z/configure --target=$TARGET --prefix="$PREFIX" --disable-nls --enable-languages=c,c++,go --without-headers

Overview

By now, you should have set up your cross-compiler for i686-elf that supports Go (as described above). This tutorial provides a minimal solution for creating an operating system for x86. It doesn't serve as a recommend skeleton for project structure, but rather as an example of a minimal kernel using another package to put text on the screen. In this simple case, we just need four input files:

  • boot.s - kernel entry point that sets up the processor environment
  • kernel.go - your actual kernel routines
  • terminal.go - a package for printing text to the screen
  • linker.ld - for linking the above files

Bootstrap Assembly

We will now create the file called boot.s and discuss its contents a little.

# Declare constants used for creating a multiboot header.
.set ALIGN,    1<<0             # align loaded modules on page boundaries
.set MEMINFO,  1<<1             # provide memory map
.set FLAGS,    ALIGN | MEMINFO  # this is the Multiboot 'flag' field
.set MAGIC,    0x1BADB002       # 'magic number' lets bootloader find the header
.set CHECKSUM, -(MAGIC + FLAGS) # checksum of above, to prove we are multiboot

# Declare a header as in the Multiboot Standard.
.section .multiboot
.align 4
.long MAGIC
.long FLAGS
.long CHECKSUM

# Currently the stack pointer register (esp) points at anything and using it may
# cause massive harm. Instead, we'll provide our own stack.
.section .bootstrap_stack, "aw", @nobits
stack_bottom:
.skip 16384 # 16 KiB
stack_top:

# The linker script specifies _start as the entry point to the kernel and the
# bootloader will jump to this position once the kernel has been loaded. It
# doesn't make sense to return from this function as the bootloader is gone.
.section .text
.global _start
.type _start, @function
_start:
	# To set up a stack, we simply set the esp register to point to the top of
	# our stack (as it grows downwards).
	movl $stack_top, %esp

	# We are now ready to actually execute Go code. Functions that start with a
	# capital letter are exported as "go.<package>.<function>".
	call go.kernel.Main

	# In case the function returns, we'll want to put the computer into an
	# infinite loop.
	cli
	hlt
.Lhang:
	jmp .Lhang

# Set the size of the _start symbol to the current location '.' minus its start.
# This is useful when debugging or when you implement call tracing.
.size _start, . - _start

# The Go runtime is a big problem when wanting to write a kernel in Go.
# Although not impossible, it's certainly not trivial. At the very least
# you'd need to implement parts of libc that it uses. Porting the runtime
# goes way beyond the scope of this barebone. 
#
# Beware that there are almost no language features you can use at this point.
# The linker will fail with missing symbols when you try to use them.
#
# You have to implement those missing symbols yourself to get things working.
# Sometimes you can get away with declaring them as an empty function. 
# But this won't always work.
#
# After you get this bare bone working, the first priority should be to write
# your own memory allocator. A simple sbrk implementation should suffice for
# symbols like __go_new.
#
# For now we just implement the symbols below as empty functions to get this
# barebone up and running.
#

.global __go_register_gc_roots
.type __go_register_gc_roots, @function
__go_register_gc_roots:
	ret
.size __go_register_gc_roots, . - __go_register_gc_roots

.global __go_runtime_error
.type __go_runtime_error, @function
__go_runtime_error:
	ret
.size __go_runtime_error, . - __go_runtime_error

You can then assemble boot.s using:

i686-elf-as boot.s -o boot.o

Writing the Terminal package in Go

Please read Printing to Screen to understand what this code does.

Now we'll create the file terminal.go. It's the package "terminal" our kernel will depend on for printing text to the screen.

package terminal

import "unsafe"

/*
 * Map the text mode video memory into a multi-dimensional array that can be safely
 * used from Go.
 */

func get_vidMem(addr uint32) *[25][80][2]byte {
	buff := (*[25][80][2]byte)(unsafe.Pointer(uintptr(addr)))
	return buff	
}

/*
 * This isn't a tutorial on the language itself so the rest should make sense to you.
 */

const (
	Black        = 0
	Blue         = 1
	Green        = 2
	Cyan         = 3
	Red          = 4
	Magenta      = 5
	Brown        = 6
	LightGrey    = 7
	DarkGrey     = 8
	LightBlue    = 9
	LightGreen   = 10
	LightCyan    = 11
	LightRed     = 12
	LightMagenta = 13
	LightBrown   = 14
	White        = 15
)

var Column, Row int
var Color byte
var vidMem *[25][80][2]byte

func Init() {
	vidMem = get_vidMem(0xB8000)
	Color = MakeColor(LightGrey, Black)
	Column = 0
	Row = 0
}

func MakeColor(fg, bg byte) byte {
	return fg | bg<<4
}

func ScrollUp() {
	for x := 1; x < 25; x++ {
		vidMem[x-1] = vidMem[x]
	}
	Row = 24
}

func Clear() {
	for r := 0; r < 25; r++ {
		for c := 0; c < 80; c++ {
			vidMem[r][c][0] = 32
			vidMem[r][c][1] = Color
		}
	}
}

func Poke(c rune) {
	if c == '\n' {
		Column = 0
		Row++
		if Row > 24 {
			ScrollUp()
		}
	} else {
		vidMem[Row][Column][0] = byte(c)
		vidMem[Row][Column][1] = Color
		Column++
		if Column > 79 {
			Column = 0
			Row++
			if Row > 24 {
				ScrollUp()
			}
		}
	}
}

func Print(s string) {
	for c := 0; c < len(s); c++ {
		Poke(rune(s[c]))
	}
}

Compile using:

i686-elf-gccgo -static -Werror -nostdlib -nostartfiles -nodefaultlibs -c terminal.go -o terminal.go.o

How imported packages are found

When you import a package with gccgo it will look for the import data in the following files and will use the first one it finds.

  • .gox
  • .so
  • .a
  • .o

A .gox file contains just the import data. If you wanted to extract it from our terminal.go.o object file we use:

i686-elf-objcopy -j .go_export terminal.go.o terminal.gox

Writing a kernel in Go

Now we create the file kernel.go that contains the Main() function called from our bootstrap assembly we've already created and compiled above. It will import the terminal package we also created & compiled. Then we use that package to print text to the screen.

package kernel

import "terminal"

func Main() {
	terminal.Init()                      // Initialize terminal
	terminal.Color = terminal.MakeColor( // Set color to..
		terminal.White, // White text...
		terminal.Blue)  // on a blue background
	terminal.Clear() // Clear screen

	// Center the text a little
	terminal.Row = 11
	terminal.Column = 30

	// Print our Hello, World!
	terminal.Print("Hello, Kernel World!\n")
}

Compile using:

i686-elf-gccgo -static -Werror -nostdlib -nostartfiles -nodefaultlibs -c kernel.go -o kernel.go.o

Linking the Kernel

Now we create the file linker.ld which will be used as instructions to link the object files we have into the final kernel program, usable by the bootloader.

/* The bootloader will look at this image and start execution at the symbol
   designated as the entry point. */
ENTRY(_start)

/* Tell where the various sections of the object files will be put in the final
   kernel image. */
SECTIONS
{
	/* Begin putting sections at 1 MiB, a conventional place for kernels to be
	   loaded at by the bootloader. */
	. = 1M;

	/* First put the multiboot header, as it is required to be put very early
	   early in the image or the bootloader won't recognize the file format.
	   Next we'll put the .text section. */
	.text BLOCK(4K) : ALIGN(4K)
	{
		*(.multiboot)
		*(.text)
	}

	/* Read-only data. */
	.rodata BLOCK(4K) : ALIGN(4K)
	{
		*(.rodata)
	}

	/* Read-write data (initialized) */
	.data BLOCK(4K) : ALIGN(4K)
	{
		*(.data)
	}

	/* Read-write data (uninitialized) and stack */
	.bss BLOCK(4K) : ALIGN(4K)
	{
		*(COMMON)
		*(.bss)
		*(.bootstrap_stack)
	}

	/* The compiler may produce other sections, by default it will put them in
	   a segment with the same name. Simply add stuff here as needed. */
}

You can link your kernel using:

i686-elf-gcc -T linker.ld -o myos.bin -ffreestanding -O2 -nostdlib boot.o terminal.go.o kernel.go.o -lgcc

The file myos.bin is now your kernel. Note that we are linking against libgcc, which implements various routines that your cross-compiler depends on. Leaving it out will give you problems in the future.

Testing your operating system (QEMU)

Virtual Machines are very useful for developing operating systems, as they allow you to quickly test your code and have access to the source code during the execution. Otherwise, you would be in for an endless cycle of reboots that would only annoy you. They start very quickly, especially combined with small operating systems such as ours.

QEMU supports booting multiboot kernels directly without using a bootable medium:

qemu-system-i386 -kernel myos.bin

External Links