# Пример организации ОС

# Основная информация

В этом уроке мы продолжим с первых шагов и создадим минимальную операционную систему-шаблон, подходящую для дальнейшей модификации или в качестве вдохновения для вашей начальной версии операционной системы. Туториал "Первые шаги" даёт вам только абсолютно минимальный код, чтобы продемонстрировать, как правильно скомпилировать ядро, однако это не подходит в качестве примера операционной системы. Кроме того, в этом туториале реализованы необходимые функции ABI, необходимые для выполнения функций ABI и компилятора, чтобы предотвратить возможные неожиданные ошибки.

Этот туториал также служит в качестве начального гайда по тому, как создать свой собственный libc (Стандартная библиотека C). В документации GCC явно указано, что libgcc требует, чтобы автономная среда предоставляла функции memcmp, memcpy, memmove и memset. Мы выполним это требование, создав специальную библиотеку C-ядра (libk), которая содержит части пользовательского пространства libc, которые являются автономными (не требуют каких-либо функций ядра), в отличие от существующих функций libc, которые должны выполнять системные вызовы.

# Предисловие

Этот туториал является примером того, как вы можете структурировать свою операционную систему таким образом, чтобы вам не приходилось постоянно рефакторить её в будущем.

Мы назовём нашу ОС myos. Вы можете далее использовать это название или придумать своё.

# Настройка кросс-компилятора

Вы должны использовать таргет i686-elf в своем кросс-компиляторе.

Также вы должны сконфигурить cross-binutils с параметром --with-sysroot, иначе линковка завершится ошибкой this linker was not configured to use sysroots. Если вы забыли собрать cross-binutils с этим параметром, вам придется пересобрать его(пересобирать GCC необязательно).

# Зависимости разработчика после создания ОС

Вам понадобятся эти инструменты:

  • i686-набор инструментов elf.
  • GRUB, для команды grub-mkrescue, вместе с соответствующими runtime-файлами.
  • Xorriso, для создания .iso, используемый grub-mkrescue.
  • GNU make 4.0 или более поздней версии.
  • QEMU, для тестирования операционной системы.

Вам потребуется система основанная на GNU/Linux.

# Для пользователей Debian

Установите набор инструментов i686-elf, а затем установите пакеты xorriso grub-pc-bin.

# Sysroot

Обычно при компиляции программ для локальной операционной системы компилятор находит заголовки и библиотеки, в системных каталогах, таких как:

  • /usr/include
  • /usr/lib

Эти файлы конечно могут не использоваться в вашей ОС, скорее всего вы будете использовать собственные пути, например:

  • /home/depowered/myos/sysroot/usr/include
  • /home/depowered/myos/sysroot/usr/lib

Каталог /home/depowered/myos/sysroot будет работать как sysroot-каталог для вашей ОС.

Что такое sysroot?

Sysroot - это каталог, который считается корневым каталогом для поиска заголовков и библиотек. Так, например, если ваша сборка toolchain хочет найти /usr/include/foo.h но вы выполняете кросс-компиляцию, а соответствующий foo.h находится в /my/other/place/usr/include/foo.h, вы будете использовать /my/other/place как ваш sysroot.

# Заголовки

./headers.sh просто устанавливает заголовки для вашего libc и ядра (системные заголовки) в sysroot/usr/include, но на самом деле не скомпилирует вашу операционную систему. Это полезно, так как позволяет предоставить компилятору копию ваших заголовков до того, как вы фактически скомпилируете свою систему.

# Makefile

Makefile — это файл, содержащий набор инструкций, используемых утилитой make в инструментарии автоматизации сборки. Чаще всего makefile содержит указания для утилиты make о том, как компилировать и компоновать программу.

Пример Makefile:

# Дефолтные CFLAGS:
CFLAGS?=-O2 -g

# Дополнительные параметры CFLAGS:
CFLAGS:=$(CFLAGS) -Wall -Wextra

# Архитектурные директории

Проекты в этом примере (libc и ядро) хранят все зависимые от архитектуры исходные файлы в каталоге arch/ со своим собственным вложенным Makefile.

# Структура ядра

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

Ядро устанавливает свои общедоступные Header-файлы в sysroot/usr/include/kernel. Это полезно, если вы решите создать ядро с модулями, где модули могут просто получать общедоступные заголовки из основного ядра.

GNU GRUB используется в качестве загрузчика, а ядро использует мультизагрузку, как в туториале "Первые шаги".

Ядро реализует правильный способ вызова глобальных конструкторов (полезно для кода C++ и кода C с использованием __attribute__((constructor)). Начальная загрузка вызывает _init, который вызывает все глобальные конструкторы. Они вызываются самими первыми при загрузке без какого-либо конкретного порядка. Вы должны использовать их только для инициализации глобальных переменных, которые не могут быть инициализированы во время выполнения.

Специальный макрос __is_kernel позволяет исходному коду определить, является ли он частью ядра.

Модуль ожидает подключения к ядру: Модуль ожидает подключения к ядру

# Структура libc и libk

libc и libk на самом деле являются двумя версиями одной и той же библиотеки, которая хранится в каталоге libc/. Стандартная библиотека разделена на две версии: автономную и размещенную. Разница в том, что автономная библиотека (libk) не содержит кода, который работает только в пользовательском пространстве, например системных вызовов. libk также собирается с различными параметрами компилятор.

Каждая стандартная функция помещается в файл с тем же именем, что и функция в каталоге с именем заголовка. Например, strlen из string.h находится в libc/string/strlen.c и stat из sys/stat.h будет находиться в libc/sys/stat/stat.c.

Стандартные заголовки используют BSD-подобную схему, где sys/cdefs.h объявляет кучу полезных макросов препроцессора, предназначенных для внутреннего использования стандартной библиотекой. Все прототипы функций завернуты в extern "C" { ... } и таким образом, что код C++ может правильно связываться с libc. Обратите также внимание, как компилятор предоставляет внутреннее ключевое слово __restrict (даже в режиме C89), что полезно для добавления ключевого слова restrict в прототипы функций даже при компиляции кода в режиме C99 или C++.

Специальный макрос __is_libc позволяет исходному коду определить, является ли он частью libc, а __is_libk позволяет исходному коду определить, является ли он частью двоичного файла libk.

Этот пример поставляется с небольшим количеством стандартных функций, которые служат для примера и выполнения требований ABI. Обратите внимание, что включенная функция printf очень минимальна и намеренно не обрабатывает большинство распространенных функций.

# Исходный код

Исходный код можно посмотреть и загрузить на странице: https://gitlab.com/sortie/meaty-skeleton.git (opens new window)

# Основные файлы ядра

kernel/include/kernel/tty.h
#ifndef _KERNEL_TTY_H
#define _KERNEL_TTY_H

#include <stddef.h>

void terminal_initialize(void);
void terminal_putchar(char c);
void terminal_write(const char* data, size_t size);
void terminal_writestring(const char* data);

#endif

kernel/Makefile

DEFAULT_HOST!=../default-host.sh
HOST?=DEFAULT_HOST
HOSTARCH!=../target-triplet-to-arch.sh $(HOST)

CFLAGS?=-O2 -g
CPPFLAGS?=
LDFLAGS?=
LIBS?=

DESTDIR?=
PREFIX?=/usr/local
EXEC_PREFIX?=$(PREFIX)
BOOTDIR?=$(EXEC_PREFIX)/boot
INCLUDEDIR?=$(PREFIX)/include

CFLAGS:=$(CFLAGS) -ffreestanding -Wall -Wextra
CPPFLAGS:=$(CPPFLAGS) -D__is_kernel -Iinclude
LDFLAGS:=$(LDFLAGS)
LIBS:=$(LIBS) -nostdlib -lk -lgcc

ARCHDIR=arch/$(HOSTARCH)

include $(ARCHDIR)/make.config

CFLAGS:=$(CFLAGS) $(KERNEL_ARCH_CFLAGS)
CPPFLAGS:=$(CPPFLAGS) $(KERNEL_ARCH_CPPFLAGS)
LDFLAGS:=$(LDFLAGS) $(KERNEL_ARCH_LDFLAGS)
LIBS:=$(LIBS) $(KERNEL_ARCH_LIBS)

KERNEL_OBJS=\
$(KERNEL_ARCH_OBJS) \
kernel/kernel.o \

OBJS=\
$(ARCHDIR)/crti.o \
$(ARCHDIR)/crtbegin.o \
$(KERNEL_OBJS) \
$(ARCHDIR)/crtend.o \
$(ARCHDIR)/crtn.o \

LINK_LIST=\
$(LDFLAGS) \
$(ARCHDIR)/crti.o \
$(ARCHDIR)/crtbegin.o \
$(KERNEL_OBJS) \
$(LIBS) \
$(ARCHDIR)/crtend.o \
$(ARCHDIR)/crtn.o \

.PHONY: all clean install install-headers install-kernel
.SUFFIXES: .o .c .S

all: myos.kernel

myos.kernel: $(OBJS) $(ARCHDIR)/linker.ld
    $(CC) -T $(ARCHDIR)/linker.ld -o $@ $(CFLAGS) $(LINK_LIST)
    grub-file --is-x86-multiboot myos.kernel

$(ARCHDIR)/crtbegin.o $(ARCHDIR)/crtend.o:
    OBJ=`$(CC) $(CFLAGS) $(LDFLAGS) -print-file-name=$(@F)` && cp "$$OBJ" $@

.c.o:
    $(CC) -MD -c $< -o $@ -std=gnu11 $(CFLAGS) $(CPPFLAGS)

.S.o:
    $(CC) -MD -c $< -o $@ $(CFLAGS) $(CPPFLAGS)

clean:
    rm -f myos.kernel
    rm -f $(OBJS) *.o */*.o */*/*.o
    rm -f $(OBJS:.o=.d) *.d */*.d */*/*.d

install: install-headers install-kernel

install-headers:
    mkdir -p $(DESTDIR)$(INCLUDEDIR)
    cp -R --preserve=timestamps include/. $(DESTDIR)$(INCLUDEDIR)/.

install-kernel: myos.kernel
    mkdir -p $(DESTDIR)$(BOOTDIR)
    cp myos.kernel $(DESTDIR)$(BOOTDIR)

-include $(OBJS:.o=.d)
kernel/kernel/kernel.c
#include <stdio.h>

#include <kernel/tty.h>

void kernel_main(void) {
    terminal_initialize();
    printf("Hello, kernel World!\n");
}
kernel/arch/i386/tty.c
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>

#include <kernel/tty.h>

#include "vga.h"

static const size_t VGA_WIDTH = 80;
static const size_t VGA_HEIGHT = 25;
static uint16_t* const VGA_MEMORY = (uint16_t*) 0xB8000;

static size_t terminal_row;
static size_t terminal_column;
static uint8_t terminal_color;
static uint16_t* terminal_buffer;

void terminal_initialize(void) {
    terminal_row = 0;
    terminal_column = 0;
    terminal_color = vga_entry_color(VGA_COLOR_LIGHT_GREY, VGA_COLOR_BLACK);
    terminal_buffer = VGA_MEMORY;
    for (size_t y = 0; y < VGA_HEIGHT; y++) {
        for (size_t x = 0; x < VGA_WIDTH; x++) {
            const size_t index = y * VGA_WIDTH + x;
            terminal_buffer[index] = vga_entry(' ', terminal_color);
        }
    }
}

void terminal_setcolor(uint8_t color) {
    terminal_color = color;
}

void terminal_putentryat(unsigned char c, uint8_t color, size_t x, size_t y) {
    const size_t index = y * VGA_WIDTH + x;
    terminal_buffer[index] = vga_entry(c, color);
}

void terminal_scroll(int line) {
    int loop;
    char c;

    for(loop = line * (VGA_WIDTH * 2) + 0xB8000; loop < VGA_WIDTH * 2; loop++) {
        c = *loop;
        *(loop - (VGA_WIDTH * 2)) = c;
    }
}

void terminal_delete_last_line() {
    int x, *ptr;

    for(x = 0; x < VGA_WIDTH * 2; x++) {
        ptr = 0xB8000 + (VGA_WIDTH * 2) * (VGA_HEIGHT - 1) + x;
        *ptr = 0;
    }
}

void terminal_putchar(char c) {
    int line;
    unsigned char uc = c;

    terminal_putentryat(uc, terminal_color, terminal_column, terminal_row);
    if (++terminal_column == VGA_WIDTH) {
        terminal_column = 0;
        if (++terminal_row == VGA_HEIGHT)
        {
            for(line = 1; line <= VGA_HEIGHT - 1; line++)
            {
                terminal_scroll(line);
            }
            terminal_delete_last_line();
            terminal_row = VGA_HEIGHT - 1;
        }
    }
}

void terminal_write(const char* data, size_t size) {
    for (size_t i = 0; i < size; i++)
        terminal_putchar(data[i]);
}

void terminal_writestring(const char* data) {
    terminal_write(data, strlen(data));
}
kernel/arch/i386/crtn.S
.section .init
    /* gcc красиво разместит здесь содержимое раздела crtend.o .init. */
    popl %ebp
    ret

.section .fini
    /* gcc красиво разместит здесь содержимое раздела crtend.o .fini. */
    popl %ebp
    ret
kernel/arch/i386/vga.h
#ifndef ARCH_I386_VGA_H
#define ARCH_I386_VGA_H

#include <stdint.h>

enum vga_color {
    VGA_COLOR_BLACK = 0,
    VGA_COLOR_BLUE = 1,
    VGA_COLOR_GREEN = 2,
    VGA_COLOR_CYAN = 3,
    VGA_COLOR_RED = 4,
    VGA_COLOR_MAGENTA = 5,
    VGA_COLOR_BROWN = 6,
    VGA_COLOR_LIGHT_GREY = 7,
    VGA_COLOR_DARK_GREY = 8,
    VGA_COLOR_LIGHT_BLUE = 9,
    VGA_COLOR_LIGHT_GREEN = 10,
    VGA_COLOR_LIGHT_CYAN = 11,
    VGA_COLOR_LIGHT_RED = 12,
    VGA_COLOR_LIGHT_MAGENTA = 13,
    VGA_COLOR_LIGHT_BROWN = 14,
    VGA_COLOR_WHITE = 15,
};

static inline uint8_t vga_entry_color(enum vga_color fg, enum vga_color bg) {
    return fg | bg << 4;
}

static inline uint16_t vga_entry(unsigned char uc, uint8_t color) {
    return (uint16_t) uc | (uint16_t) color << 8;
}

#endif
kernel/arch/i386/make.config
KERNEL_ARCH_CFLAGS=
KERNEL_ARCH_CPPFLAGS=
KERNEL_ARCH_LDFLAGS=
KERNEL_ARCH_LIBS=

KERNEL_ARCH_OBJS=\
$(ARCHDIR)/boot.o \
$(ARCHDIR)/tty.o \
kernel/arch/i386/crti.S
.section .init
.global _init
.type _init, @function
_init:
    push %ebp
    movl %esp, %ebp
    /* gcc красиво разместит здесь содержимое раздела crtbegin.o .init. */

.section .fini
.global _fini
.type _fini, @function
_fini:
    push %ebp
    movl %esp, %ebp
    /* gcc красиво разместит здесь содержимое раздела crtbegin.o .fini. */
kernel/arch/i386/linker.ld
/* Загрузчик посмотрит на этот образ и начнет выполнение с символа, обозначенного в точке входа. */
ENTRY(_start)

/* Укажите, где различные секции объектных файлов будут помещены в окончательный образ ядра. */
SECTIONS
{
    /* Начнём размещать секции в 1 Мбайт, обычное место для загрузки ядро загрузчиком. */
    . = 1M;

    /* Сначала поместим заголовок multiboot, так как он должен быть помещен очень рано в образ, иначе загрузчик не распознает формат файла. Далее мы поместим раздел .text. */
    .text BLOCK(4K) : ALIGN(4K)
    {
        *(.multiboot)
        *(.text)
    }

    /* Данные только для чтения */
    .rodata BLOCK(4K) : ALIGN(4K)
    {
        *(.rodata)
    }

    /* Данные для чтения и записи (инициализированы) */
    .data BLOCK(4K) : ALIGN(4K)
    {
        *(.data)
    }

    /* Чтение-запись данных (неинициализированных) и стек */
    .bss BLOCK(4K) : ALIGN(4K)
    {
        *(COMMON)
        *(.bss)
    }

    /* Компилятор может создать другие разделы и помещать их в нужное место в этом файле, если вы хотите включить их в окончательное ядро. */
}
kernel/arch/i386/boot.S
# Обявление констант для заголовка multiboot.
.set ALIGN,    1<<0             # выравнивание загруженных модулей по границам страниц
.set MEMINFO,  1<<1             # предоставление карты памяти
.set FLAGS,    ALIGN | MEMINFO  # это поле мультизагрузочного флага
.set MAGIC,    0x1BADB002       # это магическое число поможет загрузчику найти заголовок
.set CHECKSUM, -(MAGIC + FLAGS) # контрольная сумма выше, чтобы доказать, что мы можем включить multiboot

# Объявление заголовков Multiboot Standard.
.section .multiboot
.align 4
.long MAGIC
.long FLAGS
.long CHECKSUM

# Резервирование стека для первоначального потока
.section .bss
.align 16
stack_bottom:
.skip 16384 # 16 KiB
stack_top:

# Входная точка в ядро
.section .text
.global _start
.type _start, @function
_start:
    movl $stack_top, %esp

    # Вызов глобальных конструкторов
    call _init

    # Передача управления ядру
    call kernel_main

    # Зависнуть если kernel_main вернёт результат
    cli
1:	hlt
    jmp 1b
.size _start, . - _start
kernel/.gitignore
*.d
*.kernel
*.o

# libc и libk

libc/include/string.h
ifndef _STRING_H
#define _STRING_H 1

#include <sys/cdefs.h>

#include <stddef.h>

#ifdef __cplusplus
extern "C" {
#endif

int memcmp(const void*, const void*, size_t);
void* memcpy(void* __restrict, const void* __restrict, size_t);
void* memmove(void*, const void*, size_t);
void* memset(void*, int, size_t);
size_t strlen(const char*);

#ifdef __cplusplus
}
#endif

#endif
libc/include/stdio.h
#ifndef _STDIO_H
#define _STDIO_H 1

#include <sys/cdefs.h>

#define EOF (-1)

#ifdef __cplusplus
extern "C" {
#endif

int printf(const char* __restrict, ...);
int putchar(int);
int puts(const char*);

#ifdef __cplusplus
}
#endif

#endif
libc/include/sys/cdefs.h
#ifndef _SYS_CDEFS_H
#define _SYS_CDEFS_H 1

#define __myos_libc 1

#endif
libc/include/stdlib.h
#ifndef _STDLIB_H
#define _STDLIB_H 1

#include <sys/cdefs.h>

#ifdef __cplusplus
extern "C" {
#endif

__attribute__((__noreturn__))
void abort(void);

#ifdef __cplusplus
}
#endif

#endif
libc/include/stdlib.h
#ifndef _STDLIB_H
#define _STDLIB_H 1

#include <sys/cdefs.h>

#ifdef __cplusplus
extern "C" {
#endif

__attribute__((__noreturn__))
void abort(void);

#ifdef __cplusplus
}
#endif

#endif
libc/Makefile
DEFAULT_HOST!=../default-host.sh
HOST?=DEFAULT_HOST
HOSTARCH!=../target-triplet-to-arch.sh $(HOST)

CFLAGS?=-O2 -g
CPPFLAGS?=
LDFLAGS?=
LIBS?=

DESTDIR?=
PREFIX?=/usr/local
EXEC_PREFIX?=$(PREFIX)
INCLUDEDIR?=$(PREFIX)/include
LIBDIR?=$(EXEC_PREFIX)/lib

CFLAGS:=$(CFLAGS) -ffreestanding -Wall -Wextra
CPPFLAGS:=$(CPPFLAGS) -D__is_libc -Iinclude
LIBK_CFLAGS:=$(CFLAGS)
LIBK_CPPFLAGS:=$(CPPFLAGS) -D__is_libk

ARCHDIR=arch/$(HOSTARCH)

include $(ARCHDIR)/make.config

CFLAGS:=$(CFLAGS) $(ARCH_CFLAGS)
CPPFLAGS:=$(CPPFLAGS) $(ARCH_CPPFLAGS)
LIBK_CFLAGS:=$(LIBK_CFLAGS) $(KERNEL_ARCH_CFLAGS)
LIBK_CPPFLAGS:=$(LIBK_CPPFLAGS) $(KERNEL_ARCH_CPPFLAGS)

FREEOBJS=\
$(ARCH_FREEOBJS) \
stdio/printf.o \
stdio/putchar.o \
stdio/puts.o \
stdlib/abort.o \
string/memcmp.o \
string/memcpy.o \
string/memmove.o \
string/memset.o \
string/strlen.o \

HOSTEDOBJS=\
$(ARCH_HOSTEDOBJS) \

OBJS=\
$(FREEOBJS) \
$(HOSTEDOBJS) \

LIBK_OBJS=$(FREEOBJS:.o=.libk.o)

#BINARIES=libc.a libk.a
BINARIES=libk.a

.PHONY: all clean install install-headers install-libs
.SUFFIXES: .o .libk.o .c .S

all: $(BINARIES)

libc.a: $(OBJS)
    $(AR) rcs $@ $(OBJS)

libk.a: $(LIBK_OBJS)
    $(AR) rcs $@ $(LIBK_OBJS)

.c.o:
    $(CC) -MD -c $< -o $@ -std=gnu11 $(CFLAGS) $(CPPFLAGS)

.S.o:
    $(CC) -MD -c $< -o $@ $(CFLAGS) $(CPPFLAGS)

.c.libk.o:
    $(CC) -MD -c $< -o $@ -std=gnu11 $(LIBK_CFLAGS) $(LIBK_CPPFLAGS)

.S.libk.o:
    $(CC) -MD -c $< -o $@ $(LIBK_CFLAGS) $(LIBK_CPPFLAGS)

clean:
    rm -f $(BINARIES) *.a
    rm -f $(OBJS) $(LIBK_OBJS) *.o */*.o */*/*.o
    rm -f $(OBJS:.o=.d) $(LIBK_OBJS:.o=.d) *.d */*.d */*/*.d

install: install-headers install-libs

install-headers:
    mkdir -p $(DESTDIR)$(INCLUDEDIR)
    cp -R --preserve=timestamps include/. $(DESTDIR)$(INCLUDEDIR)/.

install-libs: $(BINARIES)
    mkdir -p $(DESTDIR)$(LIBDIR)
    cp $(BINARIES) $(DESTDIR)$(LIBDIR)

-include $(OBJS:.o=.d)
-include $(LIBK_OBJS:.o=.d)
libc/stdlib/abort.c
#include <stdio.h>
#include <stdlib.h>

__attribute__((__noreturn__))
void abort(void) {
#if defined(__is_libk)
    // TODO: добавить нормальный кернел паник
    printf("kernel: panic: abort()\n");
#else
    // TODO: добавить нормальное завершение процесса как через SIGABRT.
    printf("abort()\n");
#endif
    while (1) { }
    __builtin_unreachable();
}
libc/string/memmove.c
#include <string.h>

void* memmove(void* dstptr, const void* srcptr, size_t size) {
    unsigned char* dst = (unsigned char*) dstptr;
    const unsigned char* src = (const unsigned char*) srcptr;
    if (dst < src) {
        for (size_t i = 0; i < size; i++)
            dst[i] = src[i];
    } else {
        for (size_t i = size; i != 0; i--)
            dst[i-1] = src[i-1];
    }
    return dstptr;
}
libc/string/strlen.c
#include <string.h>

size_t strlen(const char* str) {
    size_t len = 0;
    while (str[len])
        len++;
    return len;
}
libc/string/memcmp.c
#include <string.h>

int memcmp(const void* aptr, const void* bptr, size_t size) {
    const unsigned char* a = (const unsigned char*) aptr;
    const unsigned char* b = (const unsigned char*) bptr;
    for (size_t i = 0; i < size; i++) {
        if (a[i] < b[i])
            return -1;
        else if (b[i] < a[i])
            return 1;
    }
    return 0;
}
libc/string/memset.c
#include <string.h>

void* memset(void* bufptr, int value, size_t size) {
    unsigned char* buf = (unsigned char*) bufptr;
    for (size_t i = 0; i < size; i++)
        buf[i] = (unsigned char) value;
    return bufptr;
}
libc/string/memcpy.c
#include <string.h>

void* memcpy(void* restrict dstptr, const void* restrict srcptr, size_t size) {
    unsigned char* dst = (unsigned char*) dstptr;
    const unsigned char* src = (const unsigned char*) srcptr;
    for (size_t i = 0; i < size; i++)
        dst[i] = src[i];
    return dstptr;
}
libc/stdio/puts.c
#include <stdio.h>

int puts(const char* string) {
    return printf("%s\n", string);
}
libc/stdio/putchar.c
#include <stdio.h>

#if defined(__is_libk)
#include <kernel/tty.h>
#endif

int putchar(int ic) {
#if defined(__is_libk)
    char c = (char) ic;
    terminal_write(&c, sizeof(c));
#else
    // TODO: реализовать stdio и системный вызов write.
#endif
    return ic;
}
libc/stdio/printf.c
#include <limits.h>
#include <stdbool.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>

static bool print(const char* data, size_t length) {
    const unsigned char* bytes = (const unsigned char*) data;
    for (size_t i = 0; i < length; i++)
        if (putchar(bytes[i]) == EOF)
            return false;
    return true;
}

int printf(const char* restrict format, ...) {
    va_list parameters;
    va_start(parameters, format);

    int written = 0;

    while (*format != '\0') {
        size_t maxrem = INT_MAX - written;

        if (format[0] != '%' || format[1] == '%') {
            if (format[0] == '%')
                format++;
            size_t amount = 1;
            while (format[amount] && format[amount] != '%')
                amount++;
            if (maxrem < amount) {
                // TODO: установить значение errno в EOVERFLOW.
                return -1;
            }
            if (!print(format, amount))
                return -1;
            format += amount;
            written += amount;
            continue;
        }

        const char* format_begun_at = format++;

        if (*format == 'c') {
            format++;
            char c = (char) va_arg(parameters, int /* char promotes to int */);
            if (!maxrem) {
                // TODO: установить значение errno в EOVERFLOW.
                return -1;
            }
            if (!print(&c, sizeof(c)))
                return -1;
            written++;
        } else if (*format == 's') {
            format++;
            const char* str = va_arg(parameters, const char*);
            size_t len = strlen(str);
            if (maxrem < len) {
                // TODO: установить значение errno в EOVERFLOW.
                return -1;
            }
            if (!print(str, len))
                return -1;
            written += len;
        } else {
            format = format_begun_at;
            size_t len = strlen(format);
            if (maxrem < len) {
                // TODO: установить значение errno в EOVERFLOW.
                return -1;
            }
            if (!print(format, len))
                return -1;
            written += len;
            format += len;
        }
    }

    va_end(parameters);
    return written;
}
libc/arch/i386/make.config
ARCH_CFLAGS=
ARCH_CPPFLAGS=
KERNEL_ARCH_CFLAGS=
KERNEL_ARCH_CPPFLAGS=

ARCH_FREEOBJS=\

ARCH_HOSTEDOBJS=\
libc/.gitignore
*.a
*.d
*.o

# Дополнительные файлы

Все эти файлы находятся в корневой директории

build.sh
#!/bin/sh
set -e
. ./headers.sh

for PROJECT in $PROJECTS; do
  (cd $PROJECT && DESTDIR="$SYSROOT" $MAKE install)
done
clean.sh
#!/bin/sh
set -e
. ./config.sh

for PROJECT in $PROJECTS; do
  (cd $PROJECT && $MAKE clean)
done

rm -rf sysroot
rm -rf isodir
rm -rf myos.iso
config.sh
SYSTEM_HEADER_PROJECTS="libc kernel"
PROJECTS="libc kernel"

export MAKE=${MAKE:-make}
export HOST=${HOST:-$(./default-host.sh)}

export AR=${HOST}-ar
export AS=${HOST}-as
export CC=${HOST}-gcc

export PREFIX=/usr
export EXEC_PREFIX=$PREFIX
export BOOTDIR=/boot
export LIBDIR=$EXEC_PREFIX/lib
export INCLUDEDIR=$PREFIX/include

export CFLAGS='-O2 -g'
export CPPFLAGS=''

# Настройка кросс-компилятора для использования sysroot
export SYSROOT="$(pwd)/sysroot"
export CC="$CC --sysroot=$SYSROOT"

if echo "$HOST" | grep -Eq -- '-elf($|-)'; then
  export CC="$CC -isystem=$INCLUDEDIR"
fi
default-host.sh
#!/bin/sh
echo i686-elf
headers.sh
#!/bin/sh
set -e
. ./config.sh

mkdir -p "$SYSROOT"

for PROJECT in $SYSTEM_HEADER_PROJECTS; do
  (cd $PROJECT && DESTDIR="$SYSROOT" $MAKE install-headers)
done
iso.sh
#!/bin/sh
set -e
. ./build.sh

mkdir -p isodir
mkdir -p isodir/boot
mkdir -p isodir/boot/grub

cp sysroot/boot/myos.kernel isodir/boot/myos.kernel
cat > isodir/boot/grub/grub.cfg << EOF
menuentry "myos" {
    multiboot /boot/myos.kernel
}
EOF
grub-mkrescue -o myos.iso isodir
qemu.sh
#!/bin/sh
set -e
. ./iso.sh

qemu-system-$(./target-triplet-to-arch.sh $HOST) -cdrom myos.iso
target-triplet-to-arch.sh
#!/bin/sh
if echo "$1" | grep -Eq 'i[[:digit:]]86-'; then
  echo i386
else
  echo "$1" | grep -Eo '^[[:alnum:]_]*'
fi
.gitignore
*.iso
isodir
sysroot

Не забудьте дать скриптам права для их выполнения:

chmod +x *.sh

# Порядок выполнения скриптов

  • ./clean.sh - очищаем сборку;
  • ./headers.sh - устанавливаем системные заголовки;
  • ./iso.sh - собираем ISO-образ;
  • ./qemu.sh - запускаем ОС через эмулятор QEMU.