Разработка OS на Go+asm Part 0x00

  • Tutorial
Доброго времени суток %username%.

Захотелось мне пописать что-то ненормальное. Выбор пал на ОС, в конце-концов каждый программист должен написать свою ОС, пусть хотя бы учебную.

Как некоторым известно, я очень люблю язык Go ну, и решил попробовать написать на нем. Что из этого получилось — под хабракатом.

Part 0x00
Part 0x01


Step 0x00


Писать свой загрузчик я не буду, не для того умные люди придумывали спеку multiboot.

Для начала напишем код первоначальной загрузки (файл multiboot.s)
MBOOT_PAGE_ALIGN    equ 1<<0
MBOOT_MEM_INFO      equ 1<<1
MBOOT_HEADER_MAGIC  equ 0x1BADB002
MBOOT_HEADER_FLAGS  equ MBOOT_PAGE_ALIGN | MBOOT_MEM_INFO
MBOOT_CHECKSUM      equ -(MBOOT_HEADER_MAGIC + MBOOT_HEADER_FLAGS)

[BITS 32]

[GLOBAL mboot]
[EXTERN code]
[EXTERN bss]
[EXTERN end]

mboot:
  dd    MBOOT_HEADER_MAGIC
  dd    MBOOT_HEADER_FLAGS
  dd    MBOOT_CHECKSUM
  dd    mboot
  dd    code
  dd    bss
  dd    end
  dd    start

[GLOBAL start]
extern go.kernel.Load ;Указываем на то, что у нас есть внешняя функция на Go

start:
  push  ebx
  cli
  call  go.kernel.Load ;Вызываем внешнюю функцию, которая содержит основной код ядра
  jmp   $


теперь создадим файл kernel.go следующего содержания:
package kernel

func Load(){
//Как видим наше ядро пока ничего не делает
}


Создадим файл link.ld
ENTRY(start)
SECTIONS
{

    .text 0x100000 :
    {
        code = .; _code = .; __code = .;
        *(.text)
        . = ALIGN(4096);
    }

    .data :
    {
        data = .; _data = .; __data = .;
        *(.data)
        *(.rodata)
        . = ALIGN(4096);
    }

    .bss :
    {
        bss = .; _bss = .; __bss = .;
        *(.bss)
        . = ALIGN(4096);
    }

    end = .; _end = .; __end = .;
}


и Makefile
SOURCES=multiboot.o kernel.go.o

GOFLAGS= -nostdlib -nostdinc -fno-stack-protector -fno-split-stack -static -m32 -g -I.
GO=gccgo
ASFLAGS= -felf
NASM= nasm $(ASFLAGS)
OBJCOPY=objcopy

LDFLAGS=-T link.ld -m elf_i386
 

all: $(SOURCES) link

clean: 
	rm *.o  kernel 

link:
	ld $(LDFLAGS) -o kernel $(SOURCES)


%.go.o: %.go
	$(GO)	$(GOFLAGS) -o $@ -c $<

%.o: %.s
	$(NASM) $<


Теперь, выполнив make в дирректории проекта вы получите на выходе файл kernel, который можно загрузить с помощью qemu:

qemu-system-i386 -kernel ./kernel

Ядро успешно загрузится и ничего не будет делать )

Step 0x01



Настало время поздороваться с миром.

Для начала добавим в multiboot.s следующие строки:
global __go_runtime_error
global __go_register_gc_roots
global __unsafe_get_addr

__unsafe_get_addr:
  push ebp
  mov ebp, esp
  mov eax, [ebp+8]
  mov esp, ebp
  pop ebp
  ret

__go_register_gc_roots:
__go_runtime_error:
  ret


функция __unsafe_get_addr нужна для того, что бы мы могли конвертировать uint32 в указатели внутри Go

Другие две функции — просто затычки для компилятора

Теперь создадим файл screen.go
package screen

var (
	frameBuffer      *[totalMax]uint16 //Старшие 8 бит - символ, младшие - его атрибуты
	cursorX, cursorY uint8
)

const (
	frameBufferAddr = 0xB8000
	maxX            = 80
	maxY            = 25
	totalMax        = maxX * maxY
	whiteOnBlack    = 0x07
)
//Ниже мы создаем обертку для Go над нашей функцией __unsafe_get_addr
//extern __unsafe_get_addr
func getAddr(addr uint32) *[totalMax]uint16

func Init() {
	cursorX = 0
	cursorY = 0
	frameBuffer = getAddr(frameBufferAddr) //Получаем доступ к видеобуферу
}

//Очистка экрана, просто заполняем весь видеобуфер нулями
func Clear() {
	for i := 0; i < totalMax; i++ {
		frameBuffer[i] = 0
	}
	cursorX = 0
	cursorY = 0
}

//Меняем позицию курсора
func SetCursor(x, y uint8) {
	cursorX = x
	cursorY = y
}

//Скроллим экран если он заполнен
func scroll() {
	if cursorY >= maxY { 
		for i := 0; i < 24*maxX; i++ {
			frameBuffer[i] = frameBuffer[i+80] //Смещаем все строки на одну вверх
		}
		for i := 24 * 80; i < totalMax; i++ {
			//Очищаем нижнюю строку
			frameBuffer[i] = 0x20 | (((0 << 4) | (15 & 0x0F)) << 8)
			frameBuffer[i] = 0
		}
		cursorY = 24
		cursorX = 0
	}
}

//Вывод символов
func putChar(c byte) {
	switch c {
	case 0x08: //backspace
		if cursorX > 0 {
			cursorX--
		}
	case 0x09: //tab
		cursorX = (cursorX + 8) & (8 - 1)
	case '\r': //return
		cursorX = 0
	case '\n': //new line
		cursorX = 0
		cursorY++
	default: 
		if c >= 0x20 { //Все печатные символы
			frameBuffer[cursorY*80+cursorX] = uint16(c) | (((0 << 4) | (15 & 0x0F)) << 8)
			cursorX++
		}
	}
	if cursorX >= 80 { //Если надо перемещаем курсор
		cursorX = 0
		cursorY++
	}
	scroll()
}

//Выводим строку
func PrintStr(s string) {
	for i := 0; i < len(s); i++ {
		putChar(s[i])
	}
}


Теперь надо подключить наш модуль screen к ядру — в kernel.go добавляем import «screen», там же, в функци Load() пишем:
	screen.Init()
	screen.Clear()
	screen.PrintStr("Hello Habrahar!")


Теперь надо указать компилятору как все это дело собирать нам понадобится добавить в Makefile следующие строки:
%.gox: %.go.o
		$(OBJCOPY) -j .go_export $< $@

И там же, в переменную SOURCES между multiboot.o и kernel.go.o добавить screen.go.o и screen.gox

После проведения всех манипуляций вызываем команду make и запускаем qemu с нашим ядром. Дожидаемся загрузки и радуемся

P.S. Прошу простить меня за опечатки, если они есть. Обязуюсь исправить.
P.P.S. На днях код будет выложен на github
github.com/t0pep0/Daria/tree/Part0x00

_____________________________________

Обсуждение на osdev.ru:
http://osdev.ru/viewtopic.php?f=4&t=1100
Чат в слаке (в golang-ru):
https://golang-ru.slack.com/messages/daria/

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Стоит ли развивать тему?
Поделиться публикацией
Комментарии 27
    0
    Спасибо, очень интересно.

    А я правильно понимаю, что в этом режиме Go работает без GC и нельзя использовать ничего, что вызвало бы выделение памяти в куче? Или GC включается в образ ядра как часть среды исполнения? Хотя тогда, конечно, интересно, как он без виртуальной памяти будет работать.
      0
      Да, насколько мне известно, понимаете правильно, Garbage Collector отключен, так-же как и весь рантайм, т.е. при такой разработке надо вручную заботится о памяти. Если будет продолжение, то будет описанно и управление памятью в том числе.
        0
        А как будет выглядеть попытка выделения в куче в текущем состоянии? Скажем, попытаюсь я какой-то вектор создать, Go выдаст ошибку компиляции?
          +3
          Да, ошибка компиляции, необходимо реализовавать функцию __go_new
      +1
      del
        –10
        Продолжайте, конечно. Разработка ядра ОС с использованием более безопасных языков чем иторически устоявшиеся С и С++ — интересное и правильное дело.
          +15
          От языка тут одно название.
            +3
            По большому счету на данный момент Вы правы. Однако не забывайте, на данный момент код работает на той стадии, когда рантайм Go просто не применим, по причине его отсутсвия. Даже если бы разработка велась на C, то на данном этапе и от него были бы лишь только название да немного синтаксиса.
          +2
          Неплохой таймкиллер по моему мнению, в исследовательских целях применить язык в другом амплуа всегда весело)

          P.S. Кто-нибудь расскажет про файл link.ld, а конкретно строчки вида:
          code = .; _code = .; __code = .;

          Значения различаются только подчеркиванием, в чем смысл?
          –1
          Если будет PM, виртуальная память, многозадачность и POSIX, то да. А если нет, то подобных hello world'ов полно, и Go не настолько отличается от C, чтобы повторять.
            –1
            В начале файлы с исходниками, но кто объяснить что код в них означает?
              0
              Весь код на Go вроде бы понятен, даже если б не было комментариев ввиду предельной простоты синстаксиса. Makefile и link.ld — тема не для данной статьи, слишком объемно выйдет. Что касается кода на асме — это немного модифицированный пример из доков multiboot, все изменения указаны. Второй кусок кода на асме описывается под ним.
              +2
              //Ниже мы создаем обертку для Go над нашей функцией __unsafe_get_addr
              //extern __unsafe_get_addr
              func getAddr(addr uint32) *[totalMax]uint16
              


              Я так и не понял как связаны __unsafe_get_addr и getAddr. Это манглинг имён такой или как?
                0
                У нас есть функция на asm __unsafe_get_addr
                В указанном нами коде мы говорим линкеру и компилятору, что когда происходит вызов getAddr нужно вызывать __unsafe_get_addr
                  0
                  Как-то мы неявно это говорим, если getAddr упоминается только в одном месте, и никакой видимой связи с __unsafe_get_addr нет
                    0
                    В C мы говорим почти так-же, только имена функций должны совпадать.
                     extern int __unsafe_get_addr(int);
                    

                    P.S. абстрактный пример в вакуме
                      +1
                      «Почти также», «совпадать». Вот про совпадение имён я и говорю. getAddr сворачивается Go в __unsafe_get_addr, значит.
                        0
                        В указанном нами коде мы говорим линкеру и компилятору, что когда происходит вызов getAddr нужно вызывать __unsafe_get_addr

                        Если написать в общем виде то
                        //extern <external_func_name>
                        func internal_name(...arg) ...res
                        

                        Приводит к сворачиванию функции internal_name, в функцию external_name. При этом internal_name может быть множество на единственную external_name
                          0
                          Не понял, эта связь устанавливается комментарием?
                            0
                            Да. Только в данном случае это не комментарий, а дирректива
                              +2
                              Откуда это следует? Потому что он подходит под регулярное выражение?

                              Жесть какая-то, по-моему.
                                0
                                Вы ужаснетесь, когда столкнетесь с cgo =)
                                0
                                Язык, практически, только появился, а костыляки уже присутствуют. Или это «расширение» gccgo?
                  0
                  Прикольно, так, наверное, не трудно будет и на армовском контроллере завести go.
                    0
                    Да, когда доберусь до юзерспейса планирую сделать порт на arm

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое