Разработка Unix-Подобной Ос — Виртуальное Адресное Пространство (6)

В предыдущей статье мы рассмотрели основы работы защищенного режима IA-32. Сегодня пришло время научиться работать с виртуальным адресным пространством.



Оглавление

Система сборки (производство, gcc, газ).

Начальная загрузка (мультизагрузка).

Запустить (кему).

Библиотека C (strcpy, memcpy, strext).

Библиотека C (sprintf, strcpy, strcmp, strtok, va_list.).

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

Системный журнал ядра.

Видеопамять.

Вывод на терминал (kprintf, kpanic, kassert).

Динамическая память, куча (kmalloc, kfree).

Организация памяти и обработка прерываний (GDT, IDT, PIC, системный вызов).

Исключения.

Виртуальная память (каталог страниц и таблица страниц).

Процесс.

Планировщик.

Многозадачность.

Системные вызовы (kill, выход, ps).

Файловая система ядра (initrd), elf и ее внутренности.

Системные вызовы (exec).

Драйверы символьных устройств.

Системные вызовы (ioctl, fopen, fread, fwrite).

Библиотека C (fopen, fclose, fprintf, fscanf).

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

Пользовательский режим защиты (кольцо 3).

Сегмент состояния задачи (tss).



Виртуальная память

Виртуальная память нужна для того, чтобы каждый процесс мог быть изолирован от другого, т.е.

не мог его остановить.

Если бы не было виртуальной памяти, нам пришлось бы каждый раз загружать elf-файлы по разным адресам в памяти.

Но как известно, исполняемые файлы могут содержать ссылки на определённые адреса (абсолютные).

Поэтому при компиляции elf уже известно, по какому адресу памяти он будет загружен (см.

скрипт компоновщика).

Поэтому мы не можем загрузить два файла elf без виртуальной памяти.

Но даже при включенной виртуальной памяти существуют динамические библиотеки (например, .

so), которые можно загрузить по любому адресу.

Их можно скачать на любой адрес благодаря тому, что в них есть раздел релокации.

В этом разделе собраны все места, где используется абсолютная адресация, и ядро при загрузке такого эльф-файла должно вручную исправлять эти адреса, т.е.

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

Мы рассмотрим работу со страницами размером 4 килобайта.

В этой ситуации мы сможем адресовать до 4 мегабайт оперативной памяти.

Нам этого достаточно.

Карта распределения адресов будет выглядеть так: 0-1 мб : не трогай.

1-2 МБ : код ядра и данные.

2-3 мб : куча ядра.

3-4 МБ : пользовательские страницы для загруженных эльфийских файлов.

Линейный адрес (полученный из плоской модели) при включенной адресации страниц не равен физическому адресу.

Вместо этого адрес делится на смещение (младшие биты), индекс записи таблицы страниц и индекс каталога страниц (старшие биты).

Каждый процесс будет иметь свой каталог страниц и, соответственно, таблицы страниц.

Именно это позволяет организовать виртуальное адресное пространство.

Элемент каталога страниц выглядит следующим образом:

  
  
  
  
  
  
  
  
  
   

struct page_directory_entry_t { u8 present : 1; u8 read_write : 1; u8 user_supervisor : 1; u8 write_through : 1; u8 cache_disabled : 1; u8 accessed : 1; u8 zero : 1; u8 page_size : 1; u8 ignored : 1; u8 available : 3; u32 page_table_addr : 20; } attribute(packed);

Элемент таблицы страниц выглядит следующим образом:

struct page_table_entry_t { u8 present : 1; u8 read_write : 1; u8 user_supervisor : 1; u8 write_through : 1; u8 cache_disabled : 1; u8 accessed : 1; u8 dirty : 1; u8 zero : 1; u8 global : 1; u8 available : 3; u32 page_phys_addr : 20; } attribute(packed);

Для ядра мы описываем каталог страниц и таблицу страниц как статические переменные.

Однако существует требование, чтобы они были выровнены по границе страницы.



static struct page_directory_entry_t kpage_directory attribute(aligned(4096)); static struct page_table_entry_t kpage_table[MMU_PAGE_TABLE_ENTRIES_COUNT] attribute(aligned(4096));

Мы пойдем простым путем и убедимся, что все физическое адресное пространство доступно ядру.

Уровень привилегий страниц ядра должен быть супервизором, чтобы никто не мог получить к ним доступ.

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

В этом процессе у нас будет очередь отложенных выполнения для обработки ожидающих прерываний.

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

Прежде чем рассматривать эту тему, нам еще предстоит реализовать многозадачность.

Мы создаем каталог страниц ядра и соответствующую таблицу страниц.

Когда ядро инициализируется, оно будет активным.



/* * Api - init kernel page directory * Here assumed each entry addresses 4Kb */ extern void mmu_init() { memset(&kpage_directory, 0, sizeof(struct page_directory_entry_t)); /* set kernel page directory */ kpage_directory.zero = 1; kpage_directory.accessed = 0; kpage_directory.available = 0; kpage_directory.cache_disabled = 0; kpage_directory.ignored = 0; kpage_directory.page_size = 0; /* 4KB */ kpage_directory.present = 1; /* kernel pages always in memory */ kpage_directory.read_write = 1; /* read & write */ kpage_directory.user_supervisor = 1; /* kernel mode pages */ kpage_directory.write_through = 1; kpage_directory.page_table_addr = (size_t)kpage_table >> 12; /* set kernel table */ for (int i = 0; i < MMU_PAGE_TABLE_ENTRIES_COUNT; ++i) { kpage_table[i].

zero = 0; kpage_table[i].

accessed = 0; kpage_table[i].

available = 0; kpage_table[i].

cache_disabled = 0; kpage_table[i].

dirty = 0; kpage_table[i].

global = 1; kpage_table[i].

present = 1; /* kernel pages always in memory */ kpage_table[i].

read_write = 1; /* read & write */ kpage_table[i].

user_supervisor = 1; /* kernel mode pages */ kpage_table[i].

write_through = 1; kpage_table[i].

page_phys_addr = (i * 4096) >> 12; /* assume 4Kb pages */ } }

Когда мы загружаем файлы elf, нам нужно будет создать каталог страниц для пользовательского процесса.

Вы можете сделать это с помощью следующей функции:

/* * Api - Create user page directory */ extern struct page_directory_entry_t* mmu_create_user_page_directory(struct page_table_entry_t* page_table) { struct page_directory_entry_t* upage_dir; upage_dir = malloc_a(sizeof(struct page_directory_entry_t), 4096); upage_dir->zero = 1; upage_dir->accessed = 0; upage_dir->available = 0; upage_dir->cache_disabled = 0; upage_dir->ignored = 0; upage_dir->page_size = 0; /* 4KB */ upage_dir->present = 1; upage_dir->read_write = 1; /* read & write */ upage_dir->user_supervisor = 0; /* user mode pages */ upage_dir->write_through = 1; upage_dir->page_table_addr = (size_t)page_table >> 12; /* assume 4Kb pages */ return upage_dir; }

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

/* * Api - Create user page table */ extern struct page_table_entry_t* mmu_create_user_page_table() { struct page_table_entry_t* upage_table; upage_table = malloc_a(sizeof(struct page_table_entry_t) * MMU_PAGE_TABLE_ENTRIES_COUNT, 4096); /* share kernel pages */ memcpy(upage_table, kpage_table, sizeof(struct page_table_entry_t) * MMU_KERNEL_PAGES_COUNT); /* fill user pages */ for (int i = MMU_KERNEL_PAGES_COUNT; i < MMU_PAGE_TABLE_ENTRIES_COUNT; ++i) { struct page_table_entry_t* current; current = upage_table + i; current->zero = 0; current->accessed = 0; current->available = 0; current->cache_disabled = 0; current->dirty = 0; current->global = 1; current->present = 0; /* not present as so as there is no user pages yet */ current->read_write = 1; /* read & write */ current->user_supervisor = 0; /* user mode page */ current->write_through = 1; current->page_phys_addr = 0; /* page is not present */ } return upage_table; }

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

В этом нам поможет функция:

/* * Api - Occupy user page */ extern bool mmu_occupy_user_page(struct page_table_entry_t* upage_table, void* phys_addr) { for (int i = MMU_KERNEL_PAGES_COUNT; i < MMU_PAGE_TABLE_ENTRIES_COUNT; ++i) { struct page_table_entry_t* current; current = upage_table + i; if (current->present) { /* page is buzy */ continue; } current->zero = 0; current->accessed = 0; current->available = 0; current->cache_disabled = 0; current->dirty = 0; current->global = 1; current->present = 1; current->read_write = 1; /* read & write */ current->user_supervisor = 0; /* user mode page */ current->write_through = 1; current->page_phys_addr = (size_t)phys_addr >> 12; /* assume 4Kb pages */ return true; } return false; }

Режим адресации страниц включается и выключается битом в регистре флагов процессора.



/* * Enable paging * void asm_enable_paging(void *page_directory) */ asm_enable_paging: mov 4(%esp),%eax # page_directory mov %eax,%cr3 mov %cr0,%eax or $0x80000001,%eax # set PE & PG bits mov %eax,%cr0 ret /* * Disable paging * void asm_disable_paging() */ asm_disable_paging: mov %eax,%cr3 mov %cr0,%eax xor $0x80000000,%eax # unset PG bit mov %eax,%cr0 ret

После того как мы научились формировать адресное пространство процессов, нам необходимо как-то управлять физическими страницами, какие из них заняты, а какие свободны.

Для этого существует растровый механизм: один бит на страницу.

Мы не будем описывать страницы до 3-х мегабайт, так как они принадлежат ядру и всегда заняты.

Начнем выбирать страницы пользователей с 3-го по 4-й мегабайт.

static u32 bitmap[MM_BITMAP_SIZE];

Физические страницы выделяются и освобождаются в соответствии со следующими функциями.

По сути, мы просто находим нужный бит в карте по физическому адресу страницы и наоборот. Неудобство в том, что у нас ограничен размер ячейки памяти, поэтому нам приходится использовать две координаты: номер байта и номер бита.



/* * Api - allocate pages */ extern void* mm_phys_alloc_pages(u_int count) { /* find free pages */ for (int i = 0; i < MM_DYNAMIC_PAGES_COUNT; ++i) { bool is_found = true; for (int j = 0; j < count; ++j) { is_found = is_found && !mm_get_bit(i + j); } if (is_found) { /* occupy */ for (int j = 0; j < count; ++j) { assert(!mm_get_bit(i + j)); mm_set_bit(i + j); } return (void *)mm_get_addr(i); } } return null; } /* * Api - free page */ extern bool mm_phys_free_pages(void* ptr, u_int count) { size_t address = (size_t)ptr; assert(address >= MM_AREA_START); assert(address % MM_PAGE_SIZE == 0); /* find page */ for (int i = 0; i < MM_DYNAMIC_PAGES_COUNT; ++i) { size_t addr = mm_get_addr(i); if (addr == address) { /* free pages */ for (int j = 0; j < count; ++j) { assert(mm_get_bit(i + j)); mm_clear_bit(i + j); } return true; } } return false; }

Этого вполне достаточно, чтобы внедрить в ваше ядро полную поддержку виртуальной памяти.



Ссылки

Подробности и пояснения в видеоурок .

Источник в git-репозиторий (вам нужна ветка урок6).



Библиография

1. Джеймс Моллой.

Создайте свой собственный игрушечный UNIX-клон ОС.

2. Зубков.

Ассемблер для DOS, Windows, Unix. 3. Калашников.

Язык ассемблера — это просто! 4. Таненбаум.

ОПЕРАЦИОННЫЕ СИСТЕМЫ.

Внедрение и развитие.

5. Роберт Лав.

Ядро Линукс.

Описание процесса разработки.

В опросе могут участвовать только зарегистрированные пользователи.

Войти , Пожалуйста.

Хотите создать собственную ОС для ARM и IoT? 46,15% Да 12 26,92% Нет 7 26,92% Хз 7 Проголосовали 26 пользователей.

9 пользователей воздержались.

Теги: #C++ #ядро #Системное программирование #Ассемблер #адресное пространство #виртуальная память #как создать свою ОС

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.