- Tutorial
Это последняя статья из цикла по разработке своей операционной системы.
Оглавление
Система сборки (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 listn");
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 %un", 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 listn");
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 %sn", 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. Описание процесса разработки.
- Теги: