Теперь можно непосредственно приступить к организации страничной адресации в нашем учебном ядре. Изначально я в этом вопросе опирался на руководства James Malloy, однако из-за глубоких противоречий и небрежности написания приведенного там кода от тупого следования этому мануалу пришлось отказаться и писать менеджер памяти самостоятельно. Именно поэтому цикл статей задержался почти на месяц.
Рассмотрение страничной адресации в нашем ядре начнем с создания схемы распределения ВАП, от которой будем отталкиваться при формировании информационных структур, ответственных за работу системы страничной адресации.
Рассмотрим в принципе как распределяется физическое адресное пространство в компьютерах архитектуры x86.
Судя по ней мы достаточно щедро выделяем ядру целых 17 Мб физической памяти. В современных условиях это копейки, однако при проектировании реальных систем, особенно во встраиваемых решениях требуется более вдумчиво подходить к этому вопросу. Однако для наших учебных целей такой вариант вполне приемлем.
Итак, 3 Мб памяти мы резервируем под разбухание бинарника ядра (0x200000 - 0x4FFFFF); 4 Мб отдаем под каталог страниц (0х500000 - 0х8FFFFF); ещё 2 Мб резервируем под прочие нужды.
Таким образом на начальном этапе мы должны отобразить 18 Мб виртуальной памяти - от адреса 0х00000000 до 0х01200000, для обеспечения нормальной работы ядра при переключении в страничный режим, причем отображать мы их будем так, что виртуальные адреса будут равны физическим.
Начнем писать менеджер памяти с заголовочного файла, содержащего некоторые определения
Листинг 43. Заголовочный файл менеджера памяти (memory.h)
Как всегда вводим облегчающие нам жизнь макроопределения, хотя в данном случае они скорее формализуют дальнейший код. В данном случае они позволят нам производить действия по трансляции виртуальных адресов в нашем коде. Кроме того, введем макроопределения для установки флагов в служебной части дескрипторов страниц.
Переопределяем тип u32int как physaddr_t для того чтобы отличать переменные определяющие физические адреса от остальных 32-битных значений.
Пока что нам необходимо лишь включить режим страничной адресации, поэтому реализуем единственную функцию, которая будет выполнять данную операцию.
Листинг 44. Реализация менеджера памяти (memory.c)
/*---------------------------------------------------------
// Переключение в страничный режим адресации
//-------------------------------------------------------*/
void switch_page_mode(void)
{
u64int vaddr = 0;
u32int frame = 0;
physaddr_t paddr = 0;
u32int table_idx = 0;
u32int page_idx = 0;
u32int cr0;
u32int table_flags = 0;
u32int page_flags = 0;
/* Создаем каталог страниц */
u32int* kernel_dir = (u32int*) kernel_page_dir;
/* Создаем указатель на таблицы страниц */
u32int* page_table = (u32int*) (kernel_page_dir + PAGE_SIZE);
/* Чистим память каталога страниц */
memset(kernel_dir, 0, PAGE_SIZE);
/* Отображаем всю память, необходимую ядру */
Рассмотрение страничной адресации в нашем ядре начнем с создания схемы распределения ВАП, от которой будем отталкиваться при формировании информационных структур, ответственных за работу системы страничной адресации.
1. Схема распределения физической памяти
Рассмотрим в принципе как распределяется физическое адресное пространство в компьютерах архитектуры x86.
Для простоты рассмотрим пока что распределение памяти ниже адреса 0x100000, то есть первый мегабайт. Такое распределение сложилось исторически ещё со времен процессора 8086, имевшего 20-разрядную адресную шину и соответственно более этого мегабайта не адресовавшего.
Первые 640 Кб называются базовой (стандартной) памятью, и являются в принципе свободной для использования областью, за исключением адресов 0x000 - 0x3FF отведенных под вектора прерываний реального режима. именно эта область памяти, в свое время, использовалась для программ реального режима. GRUB2 по умолчанию размещает здесь стек ядра по адресу 0x7FFFF.
Диапазон адресов 0x9FC00 - 0x9FFFF используется для нужд BIOS. Память в диапазоне 0xA0000 - 0xBFFFF используется в качестве видеобуфера, что мы уже с успехом применяли для вывода информации на экран.
Адреса 0xC0000 - 0xFFFFF резервируется для ПЗУ некоторых устройств и BIOS, эту память можно использовать с известной осторожностью. Подробнее с распределением памяти в этой области можно познакомится в этой статье.
Нас интересует память выше первого мегабайта, доступная в защищенном режиме процессора начиная с 80286 (16 Мб, режим редко использовался) и 80386 с полноценной реализацией защищенного режима и 32-разрядной адресной шиной, расширившей диапазон физических адресов до 4 Гб.
Ядро нашей ОС грузится в память как раз начиная с адреса 0x100000, это значение мы указывали в скрипте компоновщика. В процессе реализации менеджера памяти возникла необходимость поднять ядро повыше, чтобы его код не затирался размещаемой по адресу 0x100000 служебной информацией, поэтому в фале link.ld необходимо внести изменения
Листинг 42. Изменяем адрес загрузки ядра в память (файл link.ld)
ENTRY (init)
SECTIONS
{
. = 0x00200000;
.text ALIGN (0x1000) :
{
*(.mboot)
*(.text)
}
.data ALIGN (0x1000) :
{
*(.data)
}
.bss :
{
*(.bss)
}
/* */
end = .; _end = .; __end = .;
}
SECTIONS
{
. = 0x00200000;
.text ALIGN (0x1000) :
{
*(.mboot)
*(.text)
}
.data ALIGN (0x1000) :
{
*(.data)
}
.bss :
{
*(.bss)
}
/* */
end = .; _end = .; __end = .;
}
Таким образом память выше 0x200000 будет теперь занята ядром, вплоть до адреса метки end, определяемого размером бинарного файла ядра.
Первоначально предполагалось использовать end для выделения памяти под структуры каталога страниц и других данных ядра, однако в настоящий момент принято решение не делать костылей в виде динамического выделения памяти в сегментном режиме. Поэтому на первом этапе распланируем распределение памяти вручную. Рассмотрим следующую схему.
Итак, 3 Мб памяти мы резервируем под разбухание бинарника ядра (0x200000 - 0x4FFFFF); 4 Мб отдаем под каталог страниц (0х500000 - 0х8FFFFF); ещё 2 Мб резервируем под прочие нужды.
Таким образом на начальном этапе мы должны отобразить 18 Мб виртуальной памяти - от адреса 0х00000000 до 0х01200000, для обеспечения нормальной работы ядра при переключении в страничный режим, причем отображать мы их будем так, что виртуальные адреса будут равны физическим.
2. Менеджер памяти
Начнем писать менеджер памяти с заголовочного файла, содержащего некоторые определения
Листинг 43. Заголовочный файл менеджера памяти (memory.h)
/*-----------------------------------------------------
*
* Менеджер памяти
*
*---------------------------------------------------*/
#ifndef MEMORY_H
#define MEMORY_H
#include "common.h"
#define PAGE_SIZE 0x1000 /* Размер страницы */
*
* Менеджер памяти
*
*---------------------------------------------------*/
#ifndef MEMORY_H
#define MEMORY_H
#include "common.h"
#define PAGE_SIZE 0x1000 /* Размер страницы */
#define PAGE_OFFSET_BITS 12 /* Размер смещения */
/* внутри страницы */
/* внутри страницы */
#define PAGE_OFFSET_MASK 0xFFF /* Маска смещения */
#define PAGE_TABLE_INDEX_BITS 10 /* Число бит индекса */
/* таблицы */
#define PAGE_TABLE_INDEX_MASK 0x3FF /* Маска индекса таблицы */
#define PHYSADDR_BITS 32 /* Разрядность физического */
/* адреса */
/*-----------------------------------------------------
// Флаги страниц/таблиц
//---------------------------------------------------*/
#define PAGE_PRESENT (1 << 0) /* Флаг присутствия */
#define PAGE_WRITEABLE (1 << 1) /* Разрешение записи */
#define PAGE_USER (1 << 2) /* Доступ из пользова- */
/* тельского режима */
#define PAGE_WRITE_THROUGH (1 << 3) /* Сквозная запись */
#define PAGE_CACHE_DISABLE (1 << 4) /* Запрет кэширования */
#define PAGE_ACCESSED (1 << 5) /* Флаг доступа */
#define PAGE_DIRTY (1 << 6) /* Флаг записи */
#define PAGE_GLOBAL (1 << 8) /* Глобальная страница */
/*-----------------------------------------------------
// Определение некоторых типов данных
//---------------------------------------------------*/
typedef u32int physaddr_t;
/*-----------------------------------------------------
// Некоторые характерные адреса
//---------------------------------------------------*/
#define LAST_ADDR 0xFFFFFFFF
#define KERNEL_BASE 0x200000
#define PAGE_TABLE_INDEX_BITS 10 /* Число бит индекса */
/* таблицы */
#define PAGE_TABLE_INDEX_MASK 0x3FF /* Маска индекса таблицы */
#define PHYSADDR_BITS 32 /* Разрядность физического */
/* адреса */
/*-----------------------------------------------------
// Флаги страниц/таблиц
//---------------------------------------------------*/
#define PAGE_PRESENT (1 << 0) /* Флаг присутствия */
#define PAGE_WRITEABLE (1 << 1) /* Разрешение записи */
#define PAGE_USER (1 << 2) /* Доступ из пользова- */
/* тельского режима */
#define PAGE_WRITE_THROUGH (1 << 3) /* Сквозная запись */
#define PAGE_CACHE_DISABLE (1 << 4) /* Запрет кэширования */
#define PAGE_ACCESSED (1 << 5) /* Флаг доступа */
#define PAGE_DIRTY (1 << 6) /* Флаг записи */
#define PAGE_GLOBAL (1 << 8) /* Глобальная страница */
/*-----------------------------------------------------
// Определение некоторых типов данных
//---------------------------------------------------*/
typedef u32int physaddr_t;
/*-----------------------------------------------------
// Некоторые характерные адреса
//---------------------------------------------------*/
#define LAST_ADDR 0xFFFFFFFF
#define KERNEL_BASE 0x200000
#define KERNEL_SIZE 0x1000000
#define KERNEL_PAGE_TABLE 0x500000
/*-----------------------------------------------------
// Временная страница для доступа к физической памяти
//---------------------------------------------------*/
/*-----------------------------------------------------
// Временная страница для доступа к физической памяти
//---------------------------------------------------*/
#define TEMP_PAGE (KERNEL_BASE + KERNEL_SIZE - PAGE_SIZE)
/* Переключение в страничный режим */
void switch_page_mode(void);
#endif
/* Переключение в страничный режим */
void switch_page_mode(void);
#endif
Как всегда вводим облегчающие нам жизнь макроопределения, хотя в данном случае они скорее формализуют дальнейший код. В данном случае они позволят нам производить действия по трансляции виртуальных адресов в нашем коде. Кроме того, введем макроопределения для установки флагов в служебной части дескрипторов страниц.
Переопределяем тип u32int как physaddr_t для того чтобы отличать переменные определяющие физические адреса от остальных 32-битных значений.
Пока что нам необходимо лишь включить режим страничной адресации, поэтому реализуем единственную функцию, которая будет выполнять данную операцию.
Листинг 44. Реализация менеджера памяти (memory.c)
/*---------------------------------------------------------
*
* Менеджер памяти
*
*-------------------------------------------------------*/
#include "memory.h"
#include "text_framebuffer.h"
/*---------------------------------------------------------
// Глобальные переменные
//-------------------------------------------------------*/
physaddr_t kernel_page_dir = KERNEL_PAGE_TABLE;
*
* Менеджер памяти
*
*-------------------------------------------------------*/
#include "memory.h"
#include "text_framebuffer.h"
/*---------------------------------------------------------
// Глобальные переменные
//-------------------------------------------------------*/
physaddr_t kernel_page_dir = KERNEL_PAGE_TABLE;
/*---------------------------------------------------------
// Переключение в страничный режим адресации
//-------------------------------------------------------*/
void switch_page_mode(void)
{
u64int vaddr = 0;
u32int frame = 0;
physaddr_t paddr = 0;
u32int table_idx = 0;
u32int page_idx = 0;
u32int cr0;
u32int table_flags = 0;
u32int page_flags = 0;
/* Создаем каталог страниц */
u32int* kernel_dir = (u32int*) kernel_page_dir;
/* Создаем указатель на таблицы страниц */
u32int* page_table = (u32int*) (kernel_page_dir + PAGE_SIZE);
/* Чистим память каталога страниц */
memset(kernel_dir, 0, PAGE_SIZE);
/* Отображаем всю память, необходимую ядру */
/* для нормальной работы */
for (vaddr = 0;
vaddr < (KERNEL_BASE + KERNEL_SIZE);
vaddr += PAGE_SIZE)
{
/* Выполняем трансляцию виртуального адреса */
frame = vaddr >> PAGE_OFFSET_BITS;
table_idx = frame >> PAGE_TABLE_INDEX_BITS;
page_idx = frame & PAGE_TABLE_INDEX_MASK;
/* Формируем необходимые флаги для таблицы */
table_flags = PAGE_PRESENT | PAGE_WRITEABLE | PAGE_USER;
/* Создаем таблицу */
*(kernel_dir + table_idx) = (u32int) (page_table +
for (vaddr = 0;
vaddr < (KERNEL_BASE + KERNEL_SIZE);
vaddr += PAGE_SIZE)
{
/* Выполняем трансляцию виртуального адреса */
frame = vaddr >> PAGE_OFFSET_BITS;
table_idx = frame >> PAGE_TABLE_INDEX_BITS;
page_idx = frame & PAGE_TABLE_INDEX_MASK;
/* Формируем необходимые флаги для таблицы */
table_flags = PAGE_PRESENT | PAGE_WRITEABLE | PAGE_USER;
/* Создаем таблицу */
*(kernel_dir + table_idx) = (u32int) (page_table +
table_idx*1024) |
table_flags;
/* Формируем флаги для страницы */
page_flags = PAGE_PRESENT | PAGE_WRITEABLE;
/* Создаем страницу */
*(page_table + table_idx*1024 + page_idx) = paddr |
/* Формируем флаги для страницы */
page_flags = PAGE_PRESENT | PAGE_WRITEABLE;
/* Создаем страницу */
*(page_table + table_idx*1024 + page_idx) = paddr |
page_flags;
paddr += PAGE_SIZE;
}
/* Загружаем регистр CR3 адресом каталога страниц */
asm volatile ("mov %0, %%cr3"::"r"(kernel_page_dir));
/* Переключаемся в режим страничной адресации */
asm volatile ("mov %%cr0, %0":"=r"(cr0));
cr0 |= 0x80000000;
asm volatile ("mov %0, %%cr0"::"r"(cr0));
}
paddr += PAGE_SIZE;
}
/* Загружаем регистр CR3 адресом каталога страниц */
asm volatile ("mov %0, %%cr3"::"r"(kernel_page_dir));
/* Переключаемся в режим страничной адресации */
asm volatile ("mov %%cr0, %0":"=r"(cr0));
cr0 |= 0x80000000;
asm volatile ("mov %0, %%cr0"::"r"(cr0));
}
Для начала в файл common.h необходимо добавить определение типов 64-разрядного целого, так как в процессе мы будем иметь дело с величинами, разрядность которых несколько превышает 32 бита.
Листинг 45. Переопределение 64-разрядных типов данных (common.h)
/* 64-bit types */
typedef unsigned long long u64int;
typedef long long s64int;
typedef unsigned long long u64int;
typedef long long s64int;
Настройку режима страничной адресации начинаем с создания каталога таблиц страниц. По сути это массив 32-разрядных дескрипторов таблиц размеров в 4096 байт, то есть ровно в одну страницу.
Так как у нас ещё нет механизма динамического выделения памяти, мы создаем указатель на каталог страниц, непосредственно преобразовывая его адрес в указатель на массив 32-битных значений
/* Создаем каталог страниц */
u32int* kernel_dir = (u32int*) kernel_page_dir;
u32int* kernel_dir = (u32int*) kernel_page_dir;
Далее создаем указатель для доступа к таблицам страниц, расположенный сразу за каталогом таблиц
/* Создаем указатель на таблицы страниц */
u32int* page_table = (u32int*) (kernel_page_dir + PAGE_SIZE);
u32int* page_table = (u32int*) (kernel_page_dir + PAGE_SIZE);
Обязательно выполняем очистку памяти каталога таблиц, нахождение там мусора чревато для нормальной работы процессора в режиме страничной адресации.
/* Чистим память каталога страниц */
memset(kernel_dir, 0, PAGE_SIZE);
memset(kernel_dir, 0, PAGE_SIZE);
Теперь необходимо перебрать все виртуальные адреса в диапазоне от 0x00000000 до KERNEL_START + KERNEL_SIZE и создать соотвествующие записи в каталоге страниц. Перебор виртуальных адресов происходит с шагом в величину страницы PAGE_SIZE.
В этом цикле из виртуального адреса получаем индекс таблицы и индекс страницы ему сооветствующих. Так как индекс таблицы расположен в страших 10 битах виртуального адреса, его легко получить сдвигом адреса на 22 бита вправо. Для получения индекса страницы необходимо выполнить сдвиг на 12 бит вправо и наложить на результат маску 0x3FF. Легко проверить что эти два действия эквивалентны делению адреса на 4096, а затем взятию остатка от деления результата на 1024.
Далее создаем таблицу, путем записи в каталог таблиц её физического адреса с установкой необходимых флагов: присутствия, записи, доступа из пользовательского режима
/* Создаем таблицу */
*(kernel_dir + table_idx) = (u32int) (page_table +
что эквиавлентно и такой записи
/* Создаем таблицу */
kernel_dir[table_idx] = (u32int) (page_table +
В этом цикле из виртуального адреса получаем индекс таблицы и индекс страницы ему сооветствующих. Так как индекс таблицы расположен в страших 10 битах виртуального адреса, его легко получить сдвигом адреса на 22 бита вправо. Для получения индекса страницы необходимо выполнить сдвиг на 12 бит вправо и наложить на результат маску 0x3FF. Легко проверить что эти два действия эквивалентны делению адреса на 4096, а затем взятию остатка от деления результата на 1024.
Далее создаем таблицу, путем записи в каталог таблиц её физического адреса с установкой необходимых флагов: присутствия, записи, доступа из пользовательского режима
/* Создаем таблицу */
*(kernel_dir + table_idx) = (u32int) (page_table +
table_idx*1024) |
table_flags;
что эквиавлентно и такой записи
/* Создаем таблицу */
kernel_dir[table_idx] = (u32int) (page_table +
table_idx*1024) |
table_flags;
Создав таблицу, создаем дескрипторы страниц внутри неё
/* Формируем флаги для страницы */
page_flags = PAGE_PRESENT | PAGE_WRITEABLE;
/* Создаем страницу */
*(page_table + table_idx*1024 + page_idx) = paddr |
Создав таблицу, создаем дескрипторы страниц внутри неё
/* Формируем флаги для страницы */
page_flags = PAGE_PRESENT | PAGE_WRITEABLE;
/* Создаем страницу */
*(page_table + table_idx*1024 + page_idx) = paddr |
page_flags;
Обратите внимание на то, что флаги не входят в физический адрес таблицы, и её соделжимое необходимо писать по адресу без учета флагов, именно там их будет искать процессор. Как видно из приведенного кода, создание страницы заключается в присвоении её дескриптору физического адреса размещения страницы в памяти paddr и установке флагов. Приведенная операция разименования указателя так же эквивалентна
Приращение виртуальных адресов на величину страницы происходит в заголовке цикла, приращение физического адреса произведем явно
paddr += PAGE_SIZE;
Теперь необходимо включить режим страничной адресации, выполнив несколько ассемблерных команд: загрузку регистра CR3 адресом каталога страниц, и установку бита 31 в регистре CR0 для включения режима страничной адресации
Обратите внимание на то что мы предварительно считываем значение CR0, так как в нем могут быть установлены и другие биты, в частности бит 0 включения защищенного режима, сброс которого без предварительной подготовки приведет к сбою системы и перезагрузке. Наложив маску для установки бита 31 возвращаем значение cr0 обратно в регистр CR0.
Таким образом мы оказываемся в режиме страничной адресации. Добавим вызов switch_page_mode() в функцию main() соберем ядро, установим его и запустим.
Обратите внимание на то, что флаги не входят в физический адрес таблицы, и её соделжимое необходимо писать по адресу без учета флагов, именно там их будет искать процессор. Как видно из приведенного кода, создание страницы заключается в присвоении её дескриптору физического адреса размещения страницы в памяти paddr и установке флагов. Приведенная операция разименования указателя так же эквивалентна
page_table[table_idx*1024 + page_idx] = paddr | page_flags;
Приращение виртуальных адресов на величину страницы происходит в заголовке цикла, приращение физического адреса произведем явно
paddr += PAGE_SIZE;
Теперь необходимо включить режим страничной адресации, выполнив несколько ассемблерных команд: загрузку регистра CR3 адресом каталога страниц, и установку бита 31 в регистре CR0 для включения режима страничной адресации
/* Загружаем регистр CR3 адресом каталога страниц */
asm volatile ("mov %0, %%cr3"::"r"(kernel_page_dir));
/* Переключаемся в режим страничной адресации */
asm volatile ("mov %%cr0, %0":"=r"(cr0));
cr0 |= 0x80000000;
asm volatile ("mov %0, %%cr0"::"r"(cr0));
asm volatile ("mov %0, %%cr3"::"r"(kernel_page_dir));
/* Переключаемся в режим страничной адресации */
asm volatile ("mov %%cr0, %0":"=r"(cr0));
cr0 |= 0x80000000;
asm volatile ("mov %0, %%cr0"::"r"(cr0));
Обратите внимание на то что мы предварительно считываем значение CR0, так как в нем могут быть установлены и другие биты, в частности бит 0 включения защищенного режима, сброс которого без предварительной подготовки приведет к сбою системы и перезагрузке. Наложив маску для установки бита 31 возвращаем значение cr0 обратно в регистр CR0.
Таким образом мы оказываемся в режиме страничной адресации. Добавим вызов switch_page_mode() в функцию main() соберем ядро, установим его и запустим.
Сообщение о включении режима страничной адресации так же можно добавить в функцию main(), и надеюсь то как это делается уже не надо пояснять )
Корректный вывод сообщения свидетельствует о правильном отображении видеопамяти, а работа системы без перезагрузки - о том что мы всё сделали правильно.
Заключение
То чего мы сегодня добились - лишь малая часть того что предстоит сделать в процессе создания более менее функционального менеджера памяти. Но об этом - в следующих статьях.
Комментариев нет:
Отправить комментарий