Доброго времени суток %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/
Стоит ли развивать тему?

Проголосовало 517 человек. Воздержался 131 человек.

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

Комментарии (27)


  1. CONSTantius
    06.06.2015 22:57

    Спасибо, очень интересно.

    А я правильно понимаю, что в этом режиме Go работает без GC и нельзя использовать ничего, что вызвало бы выделение памяти в куче? Или GC включается в образ ядра как часть среды исполнения? Хотя тогда, конечно, интересно, как он без виртуальной памяти будет работать.


    1. t0pep0 Автор
      06.06.2015 23:02

      Да, насколько мне известно, понимаете правильно, Garbage Collector отключен, так-же как и весь рантайм, т.е. при такой разработке надо вручную заботится о памяти. Если будет продолжение, то будет описанно и управление памятью в том числе.


      1. CONSTantius
        06.06.2015 23:04

        А как будет выглядеть попытка выделения в куче в текущем состоянии? Скажем, попытаюсь я какой-то вектор создать, Go выдаст ошибку компиляции?


        1. t0pep0 Автор
          06.06.2015 23:09
          +3

          Да, ошибка компиляции, необходимо реализовавать функцию __go_new


  1. t0pep0 Автор
    06.06.2015 23:02
    +1

    del


  1. d_olex
    06.06.2015 23:11
    -10

    Продолжайте, конечно. Разработка ядра ОС с использованием более безопасных языков чем иторически устоявшиеся С и С++ — интересное и правильное дело.


    1. ingrysty
      06.06.2015 23:57
      +15

      От языка тут одно название.


      1. t0pep0 Автор
        07.06.2015 09:35
        +3

        По большому счету на данный момент Вы правы. Однако не забывайте, на данный момент код работает на той стадии, когда рантайм Go просто не применим, по причине его отсутсвия. Даже если бы разработка велась на C, то на данном этапе и от него были бы лишь только название да немного синтаксиса.


  1. QtRoS
    07.06.2015 00:07
    +2

    Неплохой таймкиллер по моему мнению, в исследовательских целях применить язык в другом амплуа всегда весело)

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

    code = .; _code = .; __code = .;

    Значения различаются только подчеркиванием, в чем смысл?


    1. t0pep0 Автор
      07.06.2015 09:43

      Честно говоря, link.ld был мной взят с одного из руководств и особо я с ним не разбирался, так что не могу ответть на Ваш вопрос



  1. hardex
    07.06.2015 09:45
    -1

    Если будет PM, виртуальная память, многозадачность и POSIX, то да. А если нет, то подобных hello world'ов полно, и Go не настолько отличается от C, чтобы повторять.


  1. AndreySu
    07.06.2015 22:45
    -1

    В начале файлы с исходниками, но кто объяснить что код в них означает?


    1. t0pep0 Автор
      07.06.2015 23:07

      Весь код на Go вроде бы понятен, даже если б не было комментариев ввиду предельной простоты синстаксиса. Makefile и link.ld — тема не для данной статьи, слишком объемно выйдет. Что касается кода на асме — это немного модифицированный пример из доков multiboot, все изменения указаны. Второй кусок кода на асме описывается под ним.


  1. monah_tuk
    09.06.2015 12:58
    +2

    //Ниже мы создаем обертку для Go над нашей функцией __unsafe_get_addr
    //extern __unsafe_get_addr
    func getAddr(addr uint32) *[totalMax]uint16
    


    Я так и не понял как связаны __unsafe_get_addr и getAddr. Это манглинг имён такой или как?


    1. t0pep0 Автор
      09.06.2015 13:05

      У нас есть функция на asm __unsafe_get_addr
      В указанном нами коде мы говорим линкеру и компилятору, что когда происходит вызов getAddr нужно вызывать __unsafe_get_addr


      1. CONSTantius
        09.06.2015 13:33

        Как-то мы неявно это говорим, если getAddr упоминается только в одном месте, и никакой видимой связи с __unsafe_get_addr нет


        1. t0pep0 Автор
          09.06.2015 13:47

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

           extern int __unsafe_get_addr(int);
          

          P.S. абстрактный пример в вакуме


          1. CONSTantius
            09.06.2015 13:51
            +1

            «Почти также», «совпадать». Вот про совпадение имён я и говорю. getAddr сворачивается Go в __unsafe_get_addr, значит.


            1. t0pep0 Автор
              09.06.2015 13:59

              В указанном нами коде мы говорим линкеру и компилятору, что когда происходит вызов getAddr нужно вызывать __unsafe_get_addr

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

              Приводит к сворачиванию функции internal_name, в функцию external_name. При этом internal_name может быть множество на единственную external_name


              1. CONSTantius
                09.06.2015 14:15

                Не понял, эта связь устанавливается комментарием?


                1. t0pep0 Автор
                  09.06.2015 14:18

                  Да. Только в данном случае это не комментарий, а дирректива


                  1. CONSTantius
                    09.06.2015 14:21
                    +2

                    Откуда это следует? Потому что он подходит под регулярное выражение?

                    Жесть какая-то, по-моему.


                    1. t0pep0 Автор
                      09.06.2015 14:29

                      Вы ужаснетесь, когда столкнетесь с cgo =)


                  1. monah_tuk
                    09.06.2015 16:13

                    Язык, практически, только появился, а костыляки уже присутствуют. Или это «расширение» gccgo?


  1. Gorthauer87
    09.06.2015 16:49

    Прикольно, так, наверное, не трудно будет и на армовском контроллере завести go.


    1. t0pep0 Автор
      09.06.2015 19:51

      Да, когда доберусь до юзерспейса планирую сделать порт на arm