Приветствую!

Не так давно(примерно полгода назад) я сильно-таки начал изучать D, и даже делал свои мини-ОС. Поэтому, я решил включить часть своих знаний в этой области, написав статью.

Требования

Для корректной работы, вам понадобится следующее:

  • компилятор GDC;

  • компилятор NASM;

  • кросс-компилятор GCC(для корректной линковки и прочих возможных вещей);

  • Unix-подобная среда(для создания .ISO-образа);

  • QEMU(для тестирования).

Устройство

Думаю, не стоит лишний раз объяснять, как идёт загрузка ядра. Поэтому лучше расскажу, какое у нас будет устройство ядра.

Входная точка у нас будет на ассемблере. Это само собой.
Однако, она будет вызывать не D-код, а С!
А С-код уже будет вызывать наш D-код.
Этот способ я использую в своих проектах, и он довольно сильно помогает в случаях, когда нужно портировать библиотеку или часть кода.

Этап 1. Написание входной точки

Это самый лёгкий этап, т.к. мы используем готовый загрузчик в виде GRUB.

Всё, что нам нужно - Написать входную точку для нашего ядра.

Что ж, давайте её напишем:

; Ниже описываются нужные для GRUB заголовки

MBALIGN  equ  1 << 0     
MEMINFO  equ  1 << 1    
FLAGS    equ  MBALIGN | MEMINFO 
MAGIC    equ  0x1BADB002     
CHECKSUM equ -(MAGIC + FLAGS)  

section .multiboot
align 4
	dd MAGIC
	dd FLAGS
	dd CHECKSUM

section .bss
align 16
stack_bottom:
resb 16384 ; 16 KiB
stack_top:
 
section .text
global start:function (start.end - start)
start:

	mov esp, stack_top
 
	extern entry ; Импортируем нашу функцию entry()
	call entry ; ...и вызываем её.
 
	cli
.hang:	hlt
	jmp .hang
.end:

Создадим файл boot.asm, и скопируем туда наш код.

Вот и всё, загрузчик готов!

Можете скомпилировать boot.asm через команду:

nasm -felf32 -o boot.o boot.asm

Этап 2. Написание минимального ядра

Тут сделаю маленькое отступление.

Все функции, которые мы будем писать, обязаны иметь флаг extern(C) перед их объявлением, т.к. стандартная библиотека D нам не доступна.

Также, перед объявлением глобальных переменных, вам тоже нужно вставлять флаг shared из-за отсутствия TLS в нашем ядре.

Отступление закончено. Возвращаемся к коду.

Теперь, мы должны написать "вторую входную точку" на С. Что ж, это весьма легко - давайте так и сделаем!

void entry() {main();} // Вызываем нашу функцию main()
// Также, обратите внимание, как двухсторонне легко вызываются функции из C в D и наоборот    

Сохраняем этот код в st2.c, и компилируем следующей командой:

i686-elf-gcc -c st2.c -o st2.o -std=gnu99 -ffreestanding -O2 -Wall -Wextra
# Надеюсь, вы имеете кросс-компилятор GCC?

Что ж, если st2.c вызывает функцию main, давайте её и напишем!

module kernel;
 
extern(C) void main() {
	for (;;) { // Бесконечный цикл, чтобы наша ОС работала бесконечно
 
	}
}

Сохраним этот код в файл kernel.d, и скомпилируем его с помощью команды:

gdc -fno-druntime -m32 -c kernel.d -o kernel.o -g

Этап 3. Линковка файлов и тестирование ОС

Перед линковкой давайте создадим файл linker.ld, включив туда следующее:

ENTRY(start)

SECTIONS
{
	. = 1M;
	.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)
	}
}

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

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

Вот и всё!
Чтобы протестировать нашу ОС, давайте пропишем следующую команду:

qemu-system-i386 -kernel myos.bin

Если вы увидели нечто вроде такого, то я вас поздравляю!

Этап 4. Написание минимального вывода в консоль

Сейчас, наше ядро просто создаёт бесконечный цикл. Это никуда не годится.
Давайте сделаем минимальные функции для работы с консолью в нашем файле st2.c:

#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>

size_t strlen(char* str) // Давайте создадим функцию для вычисления длины строки.
{
	size_t len = 0;
	while (str[len])
		len++;
	return len;
}

// Теперь, для удобства, сделаем функцию
// для объединения символа и цвета
static inline uint16_t vga_entry(unsigned char uc, uint8_t color) 
{
	return (uint16_t) uc | (uint16_t) color << 8;
}

// Координаты "курсора"
size_t t_x = 0;
size_t t_y = 0;

// Указатель на видеопамять, в которую мы будем писать символы
uint16_t* vidmem = (uint16_t*) 0xB8000;

// Давайте создадим функцию для очистки экрана!
void clear() {
 		for (size_t y = 0; y < 25; y++) {
			for (size_t x = 0; x < 80; x++) {
				const size_t index = y * 80 + x;
				vidmem[index] = vga_entry(' ', 0x0F);
			}
		} 
}

void putc_at(char c, size_t x, size_t y) // Создаём функцию для вывода символа по заданным координатам       
{
	const size_t index = y * 80 + x;
	vidmem[index] = vga_entry(c,0x0F);
}

// Делаем функцию для вывода символа без указания координат
void putc(char c) {
  	if(c == '\n') {
      	// Если наш символ - '\n', то переводим "курсор" на следующую строку 
     		t_x = 0;
      	t_y += 1;
    }
  	else {
     		putc_at(c,t_x,t_y);
      	if(++t_x == 80) {
         		t_x = 0;
          	t_y += 1;
          	// Если наш "курсор" имеет X равный 80, то переводим "курсор" на следующую строку  
        }
    }
}

void puts(char* s) { // Что ж, давайте сделаем вывод строки!
 		const length = strlen(s);
  	for(int i=0; i<length; i++) {putc(s[i]);}
}

void entry() {
  main();
}

А также чуть изменим само наше ядро:

module kernel;

// Импортируем функции из С(да, вот так просто!)
extern(C) void puts(char* s);
extern(C) void clear();
 
extern(C) void main() {
  clear(); // Очищаем экран
  puts(cast(char*)"Hello, world!"); // Выводим строку
	for (;;) { // Бесконечный цикл, чтобы наша ОС работала бесконечно
 
	}
}

Линкуем проект, и, если вы всё правильно сделали, нас встречает надпись "Hello, world"!

Вот и всё. Теперь вы можете писать ядро на C и D - по своему усмотрению.