Захотелось мне пописать что-то ненормальное. Выбор пал на ОС, в конце-концов каждый программист должен написать свою ОС, пусть хотя бы учебную.
Как некоторым известно, я очень люблю язык 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. Прошу простить меня за опечатки, если они есть. Обязуюсь исправить.
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)
d_olex
06.06.2015 23:11-10Продолжайте, конечно. Разработка ядра ОС с использованием более безопасных языков чем иторически устоявшиеся С и С++ — интересное и правильное дело.
ingrysty
06.06.2015 23:57+15От языка тут одно название.
t0pep0 Автор
07.06.2015 09:35+3По большому счету на данный момент Вы правы. Однако не забывайте, на данный момент код работает на той стадии, когда рантайм Go просто не применим, по причине его отсутсвия. Даже если бы разработка велась на C, то на данном этапе и от него были бы лишь только название да немного синтаксиса.
QtRoS
07.06.2015 00:07+2Неплохой таймкиллер по моему мнению, в исследовательских целях применить язык в другом амплуа всегда весело)
P.S. Кто-нибудь расскажет про файл link.ld, а конкретно строчки вида:
code = .; _code = .; __code = .;
Значения различаются только подчеркиванием, в чем смысл?t0pep0 Автор
07.06.2015 09:43Честно говоря, link.ld был мной взят с одного из руководств и особо я с ним не разбирался, так что не могу ответть на Ваш вопрос
hardex
07.06.2015 09:45-1Если будет PM, виртуальная память, многозадачность и POSIX, то да. А если нет, то подобных hello world'ов полно, и Go не настолько отличается от C, чтобы повторять.
AndreySu
07.06.2015 22:45-1В начале файлы с исходниками, но кто объяснить что код в них означает?
t0pep0 Автор
07.06.2015 23:07Весь код на Go вроде бы понятен, даже если б не было комментариев ввиду предельной простоты синстаксиса. Makefile и link.ld — тема не для данной статьи, слишком объемно выйдет. Что касается кода на асме — это немного модифицированный пример из доков multiboot, все изменения указаны. Второй кусок кода на асме описывается под ним.
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. Это манглинг имён такой или как?t0pep0 Автор
09.06.2015 13:05У нас есть функция на asm __unsafe_get_addr
В указанном нами коде мы говорим линкеру и компилятору, что когда происходит вызов getAddr нужно вызывать __unsafe_get_addrCONSTantius
09.06.2015 13:33Как-то мы неявно это говорим, если getAddr упоминается только в одном месте, и никакой видимой связи с __unsafe_get_addr нет
t0pep0 Автор
09.06.2015 13:47В C мы говорим почти так-же, только имена функций должны совпадать.
extern int __unsafe_get_addr(int);
P.S. абстрактный пример в вакумеCONSTantius
09.06.2015 13:51+1«Почти также», «совпадать». Вот про совпадение имён я и говорю. getAddr сворачивается Go в __unsafe_get_addr, значит.
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_nameCONSTantius
09.06.2015 14:15Не понял, эта связь устанавливается комментарием?
t0pep0 Автор
09.06.2015 14:18Да. Только в данном случае это не комментарий, а дирректива
CONSTantius
09.06.2015 14:21+2Откуда это следует? Потому что он подходит под регулярное выражение?
Жесть какая-то, по-моему.
monah_tuk
09.06.2015 16:13Язык, практически, только появился, а костыляки уже присутствуют. Или это «расширение» gccgo?
Gorthauer87
09.06.2015 16:49Прикольно, так, наверное, не трудно будет и на армовском контроллере завести go.
CONSTantius
Спасибо, очень интересно.
А я правильно понимаю, что в этом режиме Go работает без GC и нельзя использовать ничего, что вызвало бы выделение памяти в куче? Или GC включается в образ ядра как часть среды исполнения? Хотя тогда, конечно, интересно, как он без виртуальной памяти будет работать.
t0pep0 Автор
Да, насколько мне известно, понимаете правильно, Garbage Collector отключен, так-же как и весь рантайм, т.е. при такой разработке надо вручную заботится о памяти. Если будет продолжение, то будет описанно и управление памятью в том числе.
CONSTantius
А как будет выглядеть попытка выделения в куче в текущем состоянии? Скажем, попытаюсь я какой-то вектор создать, Go выдаст ошибку компиляции?
t0pep0 Автор
Да, ошибка компиляции, необходимо реализовавать функцию __go_new