Pull to refresh

Разработка Unix подобной OS — Оболочка. Заключение (9)

Reading time6 min
Views3.1K
Пришло время написать первую отдельную программу для нашего ядра — оболочку. Она будет храниться отдельным .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).

Оболочка как полноценная программа для ядра


В предыдущей статье мы рассмотрели драйвера символьных устройств и написали драйвер терминала.

Теперь у нас есть все необходимое для создания первого консольного приложения.

Напишем само консольное приложение, которое будем компилировать в отдельный эльфик.

/*
 * 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. Описание процесса разработки.
Tags:
Hubs:
Total votes 11: ↑11 and ↓0+11
Comments0

Articles