Приветствую!
Не так давно(примерно полгода назад) я сильно-таки начал изучать 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 - по своему усмотрению.