Пришло время написать первую отдельную программу для нашего ядра — оболочку. Она будет храниться отдельным .elf файлом и запускаться init процессом при старте ядра.
Это последняя статья из цикла по разработке своей операционной системы.
Система сборки (make, gcc, gas). Первоначальная загрузка (multiboot). Запуск (qemu). Библиотека C (strcpy, memcpy, strext).
Библиотека C (sprintf, strcpy, strcmp, strtok, va_list ...). Сборка библиотеки в режиме ядра и в режиме пользовательского приложения.
Системный журнал ядра. Видеопамять. Вывод на терминал (kprintf, kpanic, kassert).
Динамическая память, куча (kmalloc, kfree).
Организация памяти и обработка прерываний (GDT, IDT, PIC, syscall). Исключения.
Виртуальная память (каталог страниц и таблица страниц).
Процесс. Планировщик. Многозадачность. Системные вызовы (kill, exit, ps).
Драйверы символьных устройств. Системные вызовы (ioctl, fopen, fread, fwrite). Библиотека C (fopen, fclose, fprintf, fscanf).
Файловая система ядра (initrd), elf и его внутренности. Системные вызовы (exec). Оболочка как полноценная программа для ядра.
Пользовательский режим защиты (ring3). Сегмент состояния задачи (tss).
В предыдущей статье мы рассмотрели драйвера символьных устройств и написали драйвер терминала.
Теперь у нас есть все необходимое для создания первого консольного приложения.
Напишем само консольное приложение, которое будем компилировать в отдельный эльфик.
Нам потребуется инициализировать стандартную библиотеку и передать управление всем знакомой main функции.
Далее в цикле мы просто считываем строку и выполняем команду.
Команды парсим через strtok_r если есть аргументы.
Фактически мы просто дергаем системные вызовы.
Напомню про инициализацию стандартной библиотеки.
В прошлом уроке мы написали в библиотеке такую функцию:
Она просто открывает специальные файлы драйвера терминала для чтения и записи, что соответствует вводу с клавиатуры и выводу на экран.
После того как мы собрали наш эльфик с оболочкой, его нужно разместить на изначальной файловой системе ядра (initrd).
Initial ram disk грузится загрузчиками ядра как multiboot модуль, поэтому адрес в памяти нашего initrd мы знаем.
Осталось организовать файловую систему для initrd, что легко сделать по статье Джеймса Моллоя.
Поэтому формат будет такой:
Далее вспомним формат 32 битного эльфика.
Тут нам интересна точка входа и адрес таблицы програмных заголовков.
Секция кода и данных у нас будет первым заголовком, а секция стека вторым (по результатам изучения elf через objdump).
Этой инфы достаточно чтобы написать загрузчик elf файлов.
Мы уже умеем выделять старницы для пользовательских процессов.
Поэтому нам всего лишь нужно выделить достаточное количество страниц для заголовков и скопировать в них содержимое.
Напишем функцию которая будет создавать процесс на основе распаршенного elf файла.
Как парсить эльфик смотри в видеоуроке.
Нам нужно загрузить всего один программный заголовок с кодом и данными, поэтому не будем обобщать и заложимся на этот случай.
Самое интересное тут — создание каталога страниц и таблицы страниц.
Обрати внимание, сначала мы выделяем физические страницы (mm_phys_alloc_pages), а затем маппим их на логические через (mmu_occupy_user_page).
Тут предполагается что страницы в физической памяти выделены непрерывно.
На этом все. Теперь ты можешь реализовать собственную оболочку для своего ядра! Смотри видеоурок и вникай в детали.
Надеюсь эта серия статей оказалась тебе полезна.
Мы еще не рассмотрели с тобой кольца защиты, но из-за низкой актуальности темы и неоднозначных отзывов продолжать уже влом.
Я думаю ты и сам теперь готов к дальнейшим исследованиям, ибо все самое главное мы с тобой рассмотрели.
Поэтому затягивай ремень, и в бой! К написанию собственной операционной системы!
У меня ушло около месяца (если считать фуллтаймом по 6-8 часов в день) на реализацию всего, что мы с тобой изучили с нуля.
Поэтому за 2-3 месяца ты вполне сможешь написать полноценную ОС с настоящей файловой системой, которую мы с тобой не успели реализовать.
Знай только что qemu не умеет работать с initrd произвольного формата, и обрезает его в 4кб, поэтому тебе понадобится либо делать его как в Linux, либо юзать вместо qemu борщ.
Если ты знаешь как обойти эту проблему, отпиши в личку, буду тебе очень признателен.
На этом все! До новых уже не встреч!
Смотри видеоурок для дополнительной информации.
Исходный код в git репозиторий (тебе нужна ветка lesson9).
1. James Molloy. Roll your own toy UNIX-clone OS.
2. Зубков. Ассемблер для DOS, Windows, Unix
3. Калашников. Ассемблер — это просто!
4. Таненбаум. Операционные системы. Реализация и разработка.
5. Роберт Лав. Ядро Linux. Описание процесса разработки.
Это последняя статья из цикла по разработке своей операционной системы.
Оглавление
Система сборки (make, gcc, gas). Первоначальная загрузка (multiboot). Запуск (qemu). Библиотека C (strcpy, memcpy, strext).
Библиотека C (sprintf, strcpy, strcmp, strtok, va_list ...). Сборка библиотеки в режиме ядра и в режиме пользовательского приложения.
Системный журнал ядра. Видеопамять. Вывод на терминал (kprintf, kpanic, kassert).
Динамическая память, куча (kmalloc, kfree).
Организация памяти и обработка прерываний (GDT, IDT, PIC, syscall). Исключения.
Виртуальная память (каталог страниц и таблица страниц).
Процесс. Планировщик. Многозадачность. Системные вызовы (kill, exit, ps).
Драйверы символьных устройств. Системные вызовы (ioctl, fopen, fread, fwrite). Библиотека C (fopen, fclose, fprintf, fscanf).
Файловая система ядра (initrd), elf и его внутренности. Системные вызовы (exec). Оболочка как полноценная программа для ядра.
Пользовательский режим защиты (ring3). Сегмент состояния задачи (tss).
Оболочка как полноценная программа для ядра
В предыдущей статье мы рассмотрели драйвера символьных устройств и написали драйвер терминала.
Теперь у нас есть все необходимое для создания первого консольного приложения.
Напишем само консольное приложение, которое будем компилировать в отдельный эльфик.
/*
* Elf entry point
*/
void start()
{
u_int errno;
stdio_init();
errno = main();
stdio_deinit();
exit(errno);
}
Нам потребуется инициализировать стандартную библиотеку и передать управление всем знакомой main функции.
int main()
{
char cmd[255];
while (1) {
printf(prompt);
flush();
scanf(cmd);
if (!execute_command(cmd)) {
break;
}
}
return 0;
}
Далее в цикле мы просто считываем строку и выполняем команду.
Команды парсим через strtok_r если есть аргументы.
static bool execute_command(char* cmd)
{
if (!strcmp(cmd, cmd_ps)) {
/* show tasks list */
struct clist_definition_t *task_list;
task_list = ps();
printf(" -- process list\n");
clist_for_each(task_list, print_task_info);
} else if (!strcmp(cmd, cmd_clear)) {
/* clear screen */
clear();
flush();
} else if (!strncmp(cmd, cmd_kill, strlen(cmd_kill))) {
/* kill task */
char* save_ptr = null;
strtok_r(cmd, " ", &save_ptr);
char* str_tid = strtok_r(null, " ", &save_ptr);
u_short tid = atou(str_tid);
if (!kill(tid)) {
printf(" There is no process with pid %u\n", tid);
};
} else if (!strncmp(cmd, cmd_exit, strlen(cmd_exit))) {
/* exit */
clear();
printf(prompt);
flush();
return false;
} else if (!strncmp(cmd, cmd_exec, strlen(cmd_exec))) {
/* exec file on intrd */
char* save_ptr = null;
strtok_r(cmd, " ", &save_ptr);
char* str_file = strtok_r(null, " ", &save_ptr);
exec(str_file);
} else if (!strncmp(cmd, cmd_dev, strlen(cmd_dev))) {
/* show device list */
struct clist_definition_t *dev_list;
dev_list = devs();
printf(" -- device list\n");
clist_for_each(dev_list, print_dev_info);
} else {
printf(" There is no such command.\n Available command list:\n");
printf(" %s %s %s <pid> %s <file.elf> %s %s\n", cmd_ps, cmd_exit, cmd_kill, cmd_exec, cmd_clear, cmd_dev);
}
return true;
}
Фактически мы просто дергаем системные вызовы.
Напомню про инициализацию стандартной библиотеки.
В прошлом уроке мы написали в библиотеке такую функцию:
extern void stdio_init()
{
stdin = fopen(tty_dev_name, MOD_R);
stdout = fopen(tty_dev_name, MOD_W);
asm_syscall(SYSCALL_IOCTL, stdout, IOCTL_INIT);
asm_syscall(SYSCALL_IOCTL, stdin, IOCTL_READ_MODE_LINE);
asm_syscall(SYSCALL_IOCTL, stdin, IOCTL_READ_MODE_ECHO);
}
Она просто открывает специальные файлы драйвера терминала для чтения и записи, что соответствует вводу с клавиатуры и выводу на экран.
После того как мы собрали наш эльфик с оболочкой, его нужно разместить на изначальной файловой системе ядра (initrd).
Initial ram disk грузится загрузчиками ядра как multiboot модуль, поэтому адрес в памяти нашего initrd мы знаем.
Осталось организовать файловую систему для initrd, что легко сделать по статье Джеймса Моллоя.
Поэтому формат будет такой:
extern struct initrd_node_t {
unsigned char magic; /* magic number */
char name[8]; /* file name */
unsigned int offset; /* file base */
unsigned int length; /* file length */
};
extern struct initrd_fs_t {
int count; /* files count */
struct initrd_node_t node[INITRD_MAX_FILES]; /* files headers */
};
Далее вспомним формат 32 битного эльфика.
struct elf_header_t {
struct elf_header_ident_t e_ident;
u16 e_type;
u16 e_machine;
u32 e_version;
u32 e_entry; /* virtual address of entry point */
u32 e_phoff; /* program headers table offset */
u32 e_shoff; /* program headers sections table offset */
u32 e_flags;
u16 e_ehsize; /* file header size */
u16 e_phentsize; /* single header size */
u16 e_phnum; /* headers count */
u16 e_shentsize; /* section header size */
u16 e_shnum; /* sections headers count */
u16 e_shstrndx;
};
Тут нам интересна точка входа и адрес таблицы програмных заголовков.
Секция кода и данных у нас будет первым заголовком, а секция стека вторым (по результатам изучения elf через objdump).
struct elf_program_header_t {
u32 p_type; /* segment type */
u32 p_offset; /* segment offset from file begin */
u32 p_vaddr; /* target virtual address */
u32 p_paddr; /* target physical address */
u32 p_filesz; /* segment size in file */
u32 p_memsz; /* segment size in memory */
u32 p_flags; /* permissions and etc */
u32 p_align; /* alignment */
} attribute(packed);
Этой инфы достаточно чтобы написать загрузчик elf файлов.
Мы уже умеем выделять старницы для пользовательских процессов.
Поэтому нам всего лишь нужно выделить достаточное количество страниц для заголовков и скопировать в них содержимое.
Напишем функцию которая будет создавать процесс на основе распаршенного elf файла.
Как парсить эльфик смотри в видеоуроке.
Нам нужно загрузить всего один программный заголовок с кодом и данными, поэтому не будем обобщать и заложимся на этот случай.
/*
* Api - execute elf as a task
*/
extern void elf_exec(struct elf_header_t* header)
{
assert(header->e_ident.ei_magic == EI_MAGIC);
printf(MSG_KERNEL_ELF_LOADING, header->e_phnum);
// elf_dump(header);
size_t elf_base = (size_t)header;
size_t entry_point = header->e_entry;
struct task_mem_t task_mem;
memset(&task_mem, 0, sizeof(struct task_mem_t));
// load sections in memory
for (int i = 0; i < header->e_phnum; ++i) {
struct elf_program_header_t* p_header = (void*)(header->e_phoff + elf_base + i * header->e_phentsize);
task_mem.pages_count = (p_header->p_memsz / MM_PAGE_SIZE) + 1;
if (p_header->p_memsz == 0) {
continue;
}
// allocate pages
assert(task_mem.pages_count > 0);
assert(task_mem.pages == null);
task_mem.pages = mm_phys_alloc_pages(task_mem.pages_count);
void* section = (void*)(elf_base + p_header->p_offset);
memcpy(task_mem.pages, section, p_header->p_memsz);
// setup virtual memory
task_mem.page_table = mmu_create_user_page_table();
task_mem.page_dir = mmu_create_user_page_directory(task_mem.page_table);
for (int i = 0; i < task_mem.pages_count; ++i) {
mmu_occupy_user_page(task_mem.page_table, (void*)((size_t)task_mem.pages + i * MM_PAGE_SIZE));
}
}
// create task
u_short tid = next_tid++;
assert(task_create(tid, (void*)entry_point, &task_mem));
// run task
struct task_t* task;
task = task_get_by_id(tid);
task->status = TASK_RUNNING;
strncpy(task->name, "elf", sizeof(task->name));
printf(MSG_KERNEL_ELF_LOADED);
}
Самое интересное тут — создание каталога страниц и таблицы страниц.
Обрати внимание, сначала мы выделяем физические страницы (mm_phys_alloc_pages), а затем маппим их на логические через (mmu_occupy_user_page).
Тут предполагается что страницы в физической памяти выделены непрерывно.
На этом все. Теперь ты можешь реализовать собственную оболочку для своего ядра! Смотри видеоурок и вникай в детали.
Заключение
Надеюсь эта серия статей оказалась тебе полезна.
Мы еще не рассмотрели с тобой кольца защиты, но из-за низкой актуальности темы и неоднозначных отзывов продолжать уже влом.
Я думаю ты и сам теперь готов к дальнейшим исследованиям, ибо все самое главное мы с тобой рассмотрели.
Поэтому затягивай ремень, и в бой! К написанию собственной операционной системы!
У меня ушло около месяца (если считать фуллтаймом по 6-8 часов в день) на реализацию всего, что мы с тобой изучили с нуля.
Поэтому за 2-3 месяца ты вполне сможешь написать полноценную ОС с настоящей файловой системой, которую мы с тобой не успели реализовать.
Знай только что qemu не умеет работать с initrd произвольного формата, и обрезает его в 4кб, поэтому тебе понадобится либо делать его как в Linux, либо юзать вместо qemu борщ.
Если ты знаешь как обойти эту проблему, отпиши в личку, буду тебе очень признателен.
На этом все! До новых уже не встреч!
Ссылки
Смотри видеоурок для дополнительной информации.
Исходный код в git репозиторий (тебе нужна ветка lesson9).
Список литературы
1. James Molloy. Roll your own toy UNIX-clone OS.
2. Зубков. Ассемблер для DOS, Windows, Unix
3. Калашников. Ассемблер — это просто!
4. Таненбаум. Операционные системы. Реализация и разработка.
5. Роберт Лав. Ядро Linux. Описание процесса разработки.