пятница, 16 августа 2013 г.

PhantomEx: Реализация страничной памяти - начало

Теперь можно непосредственно приступить к организации страничной адресации в нашем учебном ядре. Изначально я в этом вопросе опирался на руководства James Malloy, однако из-за глубоких противоречий и небрежности написания приведенного там кода от тупого следования этому мануалу пришлось отказаться и писать менеджер памяти самостоятельно. Именно поэтому цикл статей задержался почти на месяц.

Рассмотрение страничной адресации в нашем ядре начнем с создания схемы распределения ВАП, от которой будем отталкиваться при формировании информационных структур, ответственных за работу системы страничной адресации.




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 = .;   
}

Таким образом память выше 0x200000 будет теперь занята ядром, вплоть до адреса метки end, определяемого размером бинарного файла ядра. 

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


Судя по ней мы достаточно щедро выделяем ядру целых 17 Мб физической памяти. В современных условиях это копейки, однако при проектировании реальных систем, особенно во встраиваемых решениях требуется более вдумчиво подходить к этому вопросу. Однако для наших учебных целей такой вариант вполне приемлем.

Итак, 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 /* Размер страницы */  
#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 KERNEL_SIZE       0x1000000
#define KERNEL_PAGE_TABLE 0x500000       
 

/*-----------------------------------------------------
// Временная страница для доступа к физической памяти
//---------------------------------------------------*/
#define TEMP_PAGE         (KERNEL_BASE + KERNEL_SIZE - PAGE_SIZE)

/* Переключение в страничный режим */
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;    

/*---------------------------------------------------------
//        Переключение в страничный режим адресации
//-------------------------------------------------------*/

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 +
                                              table_idx*1024) |
                                              table_flags;

        /* Формируем флаги для страницы */
        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));
}

Для начала в файл common.h необходимо добавить определение типов 64-разрядного целого, так как в процессе мы будем иметь дело с величинами, разрядность которых несколько превышает 32 бита.

Листинг 45. Переопределение 64-разрядных типов данных (common.h)

/* 64-bit types */
typedef    unsigned long long    u64int;
typedef    long long             s64int;

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

Так как у нас ещё нет механизма динамического выделения памяти, мы создаем указатель на каталог страниц, непосредственно преобразовывая его адрес в указатель на массив 32-битных значений

/* Создаем каталог страниц */
u32int* kernel_dir = (u32int*) kernel_page_dir;

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

/* Создаем указатель на таблицы страниц */
u32int* page_table = (u32int*) (kernel_page_dir + 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 +
                                      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;

Обратите внимание на то, что флаги не входят в физический адрес таблицы, и её соделжимое необходимо писать по адресу без учета флагов, именно там их будет искать процессор. Как видно из приведенного кода, создание страницы заключается в присвоении её дескриптору физического адреса размещения страницы в памяти 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));

Обратите внимание на то что мы предварительно считываем значение CR0, так как в нем могут быть установлены и другие биты, в частности бит 0 включения защищенного режима, сброс которого без предварительной подготовки приведет к сбою системы и перезагрузке. Наложив маску для установки бита 31 возвращаем значение cr0 обратно в регистр CR0.

Таким образом мы оказываемся в режиме страничной адресации. Добавим вызов switch_page_mode() в функцию main() соберем ядро, установим его и запустим.


Сообщение о включении режима страничной адресации так же можно добавить в функцию main(), и надеюсь то как это делается уже не надо пояснять )

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

Заключение


То чего мы сегодня добились - лишь малая часть того что предстоит сделать в процессе создания более менее функционального менеджера памяти. Но об этом - в следующих статьях.