Теперь попробуем перейти в этот самый режим пользователя с CPL = 3, а так же организовать переключение задач с различным уровнем привилегий.
- Создать сегмент TSS и определить функции для работы с ним, загрузить TR селектором этого сегмента;
- Модифицировать планировщик для обновления поля ESP0 в TSS при переключении задач.
- Перейти в режим пользователя.
1. Создание сегмента состояния задачи TSS
Структуру сегмента TSS и его дескриптор определим так
Листинг 101. Структуры для инициализации TSS (descriptor_tables.h)
/*-----------------------------------------------------------------
// Сегмент состояния задачи TSS
//---------------------------------------------------------------*/
struct tss_entry
{
u32int prev_tss;
u32int esp0; /* Указатель на текущий стек ядра */
u32int ss0; /* Селектор сегмента текущего стека ядра */
u32int esp1;
u32int ss1;
u32int esp2;
u32int ss2;
u32int cr3;
u32int eip;
u32int eflags;
u32int eax;
u32int ecx;
u32int edx;
u32int ebx;
u32int esp;
u32int ebp;
u32int esi;
u32int edi;
u32int es;
u32int cs;
u32int ss;
u32int ds;
u32int fs;
u32int gs;
u32int ldtr;
u16int task_flags;
u16int iomap_offset; /* Смещение от начала TSS до I/O map */
u8int iomap; /* Байт-терминатор, имитирующий карту I/O */
} __attribute__((packed));
typedef struct tss_entry tss_entry_t;
/*-----------------------------------------------------------------
// Дескриптор сегмента состояния задачи TSS
//---------------------------------------------------------------*/
struct tss_descriptor
{
u16int limit_15_0; /* Биты 15-0 лимита */
u16int base_15_0; /* Биты 15-0 базы */
u8int base_23_16; /* Биты 23-16 базы */
u8int type:4; /* Тип сегмента */
u8int sys:1; /* Системный сегмент */
u8int DPL:2; /* Уровень привилегий сегмента */
u8int present:1; /* Бит присутствия */
u8int limit_19_16:4; /* Биты 19-16 лимита */
u8int AVL:1; /* Зарезервирован */
u8int allways_zero:2; /* Всегда нулевые */
u8int gran:1; /* Бит гранулярности */
u8int base_31_24; /* Биты 31-24 базы */
}__attribute__((packed));
typedef struct tss_descriptor tss_descriptor_t;
Смысл полей данных структур мы разбирали в предыдущей, теоретической статье. Здесь же привожу комментарии, раскрывающие смысл задействованных нами полей данных структур.
Необходимо инициализировать дескриптор сегмента состояния задачи и добавить его в GDT
Листинг 102. Инициализация дескриптора сегмента TSS и добавление его в GDT (descriptor_tables.c)
gdt_entry_t gdt_entries[6]; /* Теперь в GDT будет 6 записей */
gdt_ptr_t gdt_ptr;
idt_entry_t idt_entries[256];
idt_ptr_t idt_ptr;
tss_entry_t tss; /* TSS */
extern u32int init_esp; /* Вершина стека главного потока ядра */
/*-----------------------------------------------------------------
//
//---------------------------------------------------------------*/
void write_tss(s32int num, u32int ss0, u32int esp0)
{
/* Чистим память под TSS */
memset(&tss, 0, sizeof(tss_entry_t));
/* Инициализируем селектор и указатель стека ядра */
tss.ss0 = ss0;
tss.esp0 = esp0;
/* Инициализируем селекторы кода, данных и стека текущей задачи CPL = 0 */
tss.cs = 0x08;
tss.ss = tss.ds = tss.es = tss.fs = tss.gs = 0x10;
/* Инициализируем завершающий байт, устанавливая все его биты */
/* и расчитываем смещение до него от начала TSS */
tss.iomap = 0xFF;
tss.iomap_offset = (u16int) ( (u32int) &tss.iomap - (u32int) &tss );
/* Вычисляем базу и лимит сегмента TSS (лимит должен быть не менее 67h!!!) */
u32int base = (u32int) &tss;
u32int limit = sizeof(tss)-1;
/* Создаем указатель на соответствующую запись в GDT для доступа и
инициализации отдельных полей дескриптора TSS */
tss_descriptor_t* tss_d = (tss_descriptor_t*) &gdt_entries[num];
/* Устанавливаем базу и лимит */
tss_d->base_15_0 = base & 0xFFFF;
tss_d->base_23_16 = (base >> 16) & 0xFF;
tss_d->base_31_24 = (base >> 24) & 0xFF;
tss_d->limit_15_0 = limit & 0xFFFF;
tss_d->limit_19_16 = (limit >> 16) & 0xF;
/* Заполняем другие биты */
tss_d->present = 1; /* Взводим бит присутствия сегмента */
tss_d->sys = 0; /* Это не системный сегмент */
tss_d->DPL = 0; /* Уровень привилегий сегмента - уровень ядра */
tss_d->type = 9; /* Тип сегмента - свободный 32-битный TSS */
tss_d->AVL = 0; /* Всегда ноль */
tss_d->allways_zero = 0; /* Всегда ноль */
tss_d->gran = 0; /* Бит гранулярности - ноль */
}
gdt_ptr_t gdt_ptr;
idt_entry_t idt_entries[256];
idt_ptr_t idt_ptr;
tss_entry_t tss; /* TSS */
extern u32int init_esp; /* Вершина стека главного потока ядра */
/*-----------------------------------------------------------------
//
//---------------------------------------------------------------*/
void write_tss(s32int num, u32int ss0, u32int esp0)
{
/* Чистим память под TSS */
memset(&tss, 0, sizeof(tss_entry_t));
/* Инициализируем селектор и указатель стека ядра */
tss.ss0 = ss0;
tss.esp0 = esp0;
/* Инициализируем селекторы кода, данных и стека текущей задачи CPL = 0 */
tss.cs = 0x08;
tss.ss = tss.ds = tss.es = tss.fs = tss.gs = 0x10;
/* Инициализируем завершающий байт, устанавливая все его биты */
/* и расчитываем смещение до него от начала TSS */
tss.iomap = 0xFF;
tss.iomap_offset = (u16int) ( (u32int) &tss.iomap - (u32int) &tss );
/* Вычисляем базу и лимит сегмента TSS (лимит должен быть не менее 67h!!!) */
u32int base = (u32int) &tss;
u32int limit = sizeof(tss)-1;
/* Создаем указатель на соответствующую запись в GDT для доступа и
инициализации отдельных полей дескриптора TSS */
tss_descriptor_t* tss_d = (tss_descriptor_t*) &gdt_entries[num];
/* Устанавливаем базу и лимит */
tss_d->base_15_0 = base & 0xFFFF;
tss_d->base_23_16 = (base >> 16) & 0xFF;
tss_d->base_31_24 = (base >> 24) & 0xFF;
tss_d->limit_15_0 = limit & 0xFFFF;
tss_d->limit_19_16 = (limit >> 16) & 0xF;
/* Заполняем другие биты */
tss_d->present = 1; /* Взводим бит присутствия сегмента */
tss_d->sys = 0; /* Это не системный сегмент */
tss_d->DPL = 0; /* Уровень привилегий сегмента - уровень ядра */
tss_d->type = 9; /* Тип сегмента - свободный 32-битный TSS */
tss_d->AVL = 0; /* Всегда ноль */
tss_d->allways_zero = 0; /* Всегда ноль */
tss_d->gran = 0; /* Бит гранулярности - ноль */
}
Далее, вызываем функцию write_tss(...) для добавления записи в GDT
Листинг 103. Инициализация GDT (descriptor_tables.c)
extern void tss_flush(u32int tr_selector); /* Внешняя функция загрузки TR */
/*-----------------------------------------------------------------
//
//---------------------------------------------------------------*/
void init_gdt(void)
{
/* Обратите внимание - теперь в GDT 6 записей!!! */
gdt_ptr.limit = (sizeof(gdt_entry_t)*6) - 1;
gdt_ptr.base = (u32int) &gdt_entries;
gdt_set_gate(0, 0, 0, 0, 0); /* Нулевой сегмент (должент быть!) */
gdt_set_gate(1, 0, 0xFFFFFFFF, 0x9A, 0xCF); /* Сегмент кода ядра */
gdt_set_gate(2, 0, 0xFFFFFFFF, 0x92, 0xCF); /* Сегмент данных ядра */
gdt_set_gate(3, 0, 0xFFFFFFFF, 0xFA, 0xCF); /* Сегмент прикладного кода */
gdt_set_gate(4, 0, 0xFFFFFFFF, 0xF2, 0xCF); /* Сегмент прикладных данных */
write_tss(5, 0x10, init_esp); /* Сегмент TSS */
gdt_flush( (u32int) &gdt_ptr); /* Загружаем таблицу дескрипторов */
tss_flush(0x28); /* Загружаем регистр задачи TR
сегментом TSS */
}
Функция загрузки регистра TR, традиционно уже для нас при работе с регистрами, реализована на ассемблере
Листинг 104. Загрузка селектора TSS в TR (gdt.s)
/*-----------------------------------------------------------------
* Загрузка регистра TR
*---------------------------------------------------------------*/
.global tss_flush
tss_flush:
mov 4(%esp), %eax
ltr %ax
ret
* Загрузка регистра TR
*---------------------------------------------------------------*/
.global tss_flush
tss_flush:
mov 4(%esp), %eax
ltr %ax
ret
Поскольку дескриптор TSS имеет индекс 5 в GDT, то его селектор, с учетом RPL = 0, будет равен tss_selector = 5*8 | RPL = 40, или 0x28 в шеснадцатиричном виде. Именно это значение мы загружаем в TR.
Теперь добавим ещё пару функций, одна необходимо для инициализации, вторая для отладки и контроля
Листинг 105. Вспомогательные функции для работы с TSS (descriptor_tables.c)
/*-----------------------------------------------------------------
// Запись в TSS вершины стека ядра
//---------------------------------------------------------------*/
void set_kernel_stack_in_tss(u32int stack)
{
tss.esp0 = stack;
}
// Запись в TSS вершины стека ядра
//---------------------------------------------------------------*/
void set_kernel_stack_in_tss(u32int stack)
{
tss.esp0 = stack;
}
/*-----------------------------------------------------------------
// Чтение текущего значения вершины стека ядра из TSS
//---------------------------------------------------------------*/
u32int get_tss_esp0(void)
{
return tss.esp0;
}
{
return tss.esp0;
}
Первая функция изначально предполагалась для модификации TSS при переключении задач, однако планировщик пришлось переписать на ассемблере (об этом рассказывалось в одной из предыдущих статей), и там модификация tss.esp0 происходит напрямую, так что рассматриваемая функция пригодна лишь для инициализации.
Вторая функция, изначально использовалась для отладки ассемблерного планировщика, пригодится нам при контроле того что происходит при переключении задач на разных уровнях привилегий, и поверьте, это будет интересно.
2. Модификация планировщика задач
А вот об этом мы уже говорили. Код планировщика приведем ещё раз с учетом необходимости модифицировать TSS
Листинг 106. Планировщик задач (switch_task.s)
/*-------------------------------------------------/ Переключение задач
/------------------------------------------------*/
.extern current_thread /* Внешние ссылки на текущую задачу */
.extern tss /* и сегмент состояния задачи */
.global task_switch /* Делаем функцию глобальной, доступной извне */
task_switch:
push %ebp /* Пролог функции совместимой с C по вызову */
pushf /* Проталкиваем флаги в стек */
cli /* Выключаем прерывания */
/* Сохраняем указатель стека текущей задачи */
mov current_thread, %edx /* Грузим EDX адресом структуры текущей задачи */
mov %esp, 28(%edx) /* Пишем текущий ESP в структуру задачи */
/* Берем новую задачу из очереди */
mov 4(%edx), %ecx /* Грузим ECX адресом структуры следующей задачи */
mov %ecx, current_thread /* Модифицируем указатель на текущую задачу */
/* Переключаем стек */
mov current_thread, %edx /* Грузим EDX указателем на новую задачу */
mov 28(%edx), %esp /* Загружаем в ESP указатель стека новой задачи */
/* Модифицируем вершину стека ядра в TSS */
mov 40(%edx), %eax /* Читаем вершину стека из структуры потока */
mov $tss, %edx /* Грузим EDX адресом TSS */
mov %eax, 4(%edx) /* Пишем вершину стека в поле tss.esp0 */
popf /* Возвращаем флаги из стека, неявно включая прерывания */
pop %ebp /* Эпилог функции совместимой с C по вызову */
ret
Здесь мы в отличие от предыдущей статьи можем раскомментировать код, работающий с полем tss.esp0.
3. Переключение в пользовательский режим
Само переключение выполняет вот такой код, который напишем, исходя из теоретической информации, приведенной в предыдущей статье.
Листинг 107. Переключение в пользовательский режим (usr.s)
/*---------------------------------------------------------------------
/
/ Переключение в 3-е кольцо защиты
/
/--------------------------------------------------------------------*/
.set USER_CS, 0x1B /* Селектор прикладного кода */
.set USER_SS, 0x23 /* Селектор прикладного стека */
.set USER_DS, 0x23 /* Селектор прикладных данных */
.global user_mode_switch
user_mode_switch:
mov 4(%esp), %edx /* Адрес точки входа помещаем в EDX */
/* Настраиваем пользовательские сегменты данных, загружая
в них соответствующие селекторы */
mov $USER_DS, %ax
mov %ax, %ds
mov %ax, %es
/* Подготовливаем текущий стек к загрузке EIP, CS, EFLAGS, ESP и SS */
mov 8(%esp), %eax /* Читам указатель на новый стек из параметра в EAX */
pushl $USER_SS /* Проталкиваем в стек селектор прикладного стека */
pushl %eax /* Проталкивам в стек указатель на прикладной стек */
pushf /* Проталкиваем флаги в стек */
push $USER_CS /* Проталкиваем селектор прикладного кода в стек */
push %edx /* Проталкиваем точку входа на прикладной код */
iret /* Выполняем возврат из прерывания в ring 3! */
/
/ Переключение в 3-е кольцо защиты
/
/--------------------------------------------------------------------*/
.set USER_CS, 0x1B /* Селектор прикладного кода */
.set USER_SS, 0x23 /* Селектор прикладного стека */
.set USER_DS, 0x23 /* Селектор прикладных данных */
.global user_mode_switch
user_mode_switch:
mov 4(%esp), %edx /* Адрес точки входа помещаем в EDX */
/* Настраиваем пользовательские сегменты данных, загружая
в них соответствующие селекторы */
mov $USER_DS, %ax
mov %ax, %ds
mov %ax, %es
/* Подготовливаем текущий стек к загрузке EIP, CS, EFLAGS, ESP и SS */
mov 8(%esp), %eax /* Читам указатель на новый стек из параметра в EAX */
pushl $USER_SS /* Проталкиваем в стек селектор прикладного стека */
pushl %eax /* Проталкивам в стек указатель на прикладной стек */
pushf /* Проталкиваем флаги в стек */
push $USER_CS /* Проталкиваем селектор прикладного кода в стек */
push %edx /* Проталкиваем точку входа на прикладной код */
iret /* Выполняем возврат из прерывания в ring 3! */
При этом, перед переключением необходимо создать стек для потока пользовательского режима.
Листинг 108. Инициализация пользовательского режима (scheduler.c)
/* Внешняя функция переключения */
extern void user_mode_switch(void* entry_point, /* Точка входа в поток */
u32int user_stack_top); /* Вершина стека потока */
extern void user_mode_switch(void* entry_point, /* Точка входа в поток */
u32int user_stack_top); /* Вершина стека потока */
/*---------------------------------------------------------------------
* Инициализация пользовательского режима
*-------------------------------------------------------------------*/
void init_user_mode(void* entry_point, size_t stack_size)
{
/* Выделяем память под стек */
void* user_stack = kmalloc(stack_size);
/* Переключаемся */
user_mode_switch(entry_point, (u32int) user_stack + stack_size);
}
* Инициализация пользовательского режима
*-------------------------------------------------------------------*/
void init_user_mode(void* entry_point, size_t stack_size)
{
/* Выделяем память под стек */
void* user_stack = kmalloc(stack_size);
/* Переключаемся */
user_mode_switch(entry_point, (u32int) user_stack + stack_size);
}
Для тестирования, пока что, память под стек выделяем динамически, в куче ядра.
Кроме того, нам необходимо модифицировать код инициализации планировщика и создания потока, добавив инициализацию поля stack_top в структуре потока
Листинг 109. Корректировка функций инициализации планировщика и создания потока (scheduler.c)
/*----------------------------------------------------------------
* Инициализация планировщика
*--------------------------------------------------------------*/
void init_task_manager(void)
{
.
.
* Инициализация планировщика
*--------------------------------------------------------------*/
void init_task_manager(void)
{
.
.
.
kernel_thread->stack_size = 0x4000;
kernel_thread->suspend = false;
kernel_thread->esp = esp;
kernel_thread->stack_top = init_esp;
list_add(&thread_list, &kernel_thread->list_item);
current_proc = kernel_proc;
current_thread = kernel_thread;
/* Установить флаг готовности */
multi_task = true;
asm volatile ("sti");
}
kernel_thread->stack_size = 0x4000;
kernel_thread->suspend = false;
kernel_thread->esp = esp;
kernel_thread->stack_top = init_esp;
list_add(&thread_list, &kernel_thread->list_item);
current_proc = kernel_proc;
current_thread = kernel_thread;
/* Установить флаг готовности */
multi_task = true;
asm volatile ("sti");
}
/*----------------------------------------------------------------
* Создание потока
*--------------------------------------------------------------*/
thread_t* thread_create(process_t* proc,
void* entry_point,
size_t stack_size,
bool kernel,
bool suspend)
{
void* stack = NULL;
u32int eflags;
.
.
* Создание потока
*--------------------------------------------------------------*/
thread_t* thread_create(process_t* proc,
void* entry_point,
size_t stack_size,
bool kernel,
bool suspend)
{
void* stack = NULL;
u32int eflags;
.
.
.
/* Создаем стек потока */
stack = kmalloc(stack_size);
tmp_thread->stack = stack;
tmp_thread->esp = (u32int) stack + stack_size - 12;
tmp_thread->stack_top = (u32int) stack + stack_size;
.
.
.
}
/* Создаем стек потока */
stack = kmalloc(stack_size);
tmp_thread->stack = stack;
tmp_thread->esp = (u32int) stack + stack_size - 12;
tmp_thread->stack_top = (u32int) stack + stack_size;
.
.
.
}
Для простоты просто укажу место, где необходимо отредактировать код, добавленный код выделив полужирным шрифтом. Теперь модифицируем файл main.c
Листинг 110. Переход в режим пользователя (main.c)
#include "main.h"
u32int count01 = 0;
u32int count02 = 0;
u32int count01 = 0;
u32int count02 = 0;
u32int count04 = 0; /* Новый счетчик */
vscreen_t* vs01;
vscreen_t* vs02;
vscreen_t* vs04; /* Новый виртуальный экран #4 */
u8int start_y = 21; /* Начальная вертикальная позиция вывода на экран */
thread_t* thread01;
thread_t* thread02;
u32int init_esp = 0;
.
.
.
/*----------------------------------------------------------
// Поток #4 - поток пользовательского режима
//--------------------------------------------------------*/
void task04(void)
{
char tmp_str[256];
int dig;
vs04 = (vscreen_t*) get_vscreen();
while (1)
{
vs04->cur_x = 0;
vs04->cur_y = start_y + 2;
dec2dec_str(count04, tmp_str);
/* Подчеркиваем, что этот поток работает в 3-м кольце! */
vprint_text(vs04, "I'm user thread #4: ");
vs04->cur_x = 22;
vprint_text(vs04, tmp_str);
/* Преобразуем в строку и выведем на экран поле tss.esp0
причем это необходимо сделать в каждом потоке,
чтобы проиллюстрировать модификацию данного поля при переключении
задач */
dec2hex_str(get_tss_esp0(), tmp_str);
vs04->cur_x = 31;
vprint_text(vs04, tmp_str);
count04++;
}
destroy_vscreen(vs04);
}
/*----------------------------------------------------------
// Точка входа в ядро
//--------------------------------------------------------*/
int main(multiboot_header_t* mboot, u32int initial_esp)
{
init_esp = initial_esp;
.
.
.
init_memory_manager(init_esp);
init_timer(BASE_FREQ);
asm volatile ("sti");
init_task_manager();
process_t* proc = get_current_proc();
thread01 = thread_create(proc,
&task01,
0x4000,
true,
false);
thread02 = thread_create(proc,
&task02,
0x4000,
true,
false);
/* Инициализируем режим пользователя для функции task04() со стеком в 16 Кб */
init_user_mode(&task04, 0x4000);
return 0;
}
vscreen_t* vs01;
vscreen_t* vs02;
vscreen_t* vs04; /* Новый виртуальный экран #4 */
u8int start_y = 21; /* Начальная вертикальная позиция вывода на экран */
thread_t* thread01;
thread_t* thread02;
u32int init_esp = 0;
.
.
.
/*----------------------------------------------------------
// Поток #4 - поток пользовательского режима
//--------------------------------------------------------*/
void task04(void)
{
char tmp_str[256];
int dig;
vs04 = (vscreen_t*) get_vscreen();
while (1)
{
vs04->cur_x = 0;
vs04->cur_y = start_y + 2;
dec2dec_str(count04, tmp_str);
/* Подчеркиваем, что этот поток работает в 3-м кольце! */
vprint_text(vs04, "I'm user thread #4: ");
vs04->cur_x = 22;
vprint_text(vs04, tmp_str);
/* Преобразуем в строку и выведем на экран поле tss.esp0
причем это необходимо сделать в каждом потоке,
чтобы проиллюстрировать модификацию данного поля при переключении
задач */
dec2hex_str(get_tss_esp0(), tmp_str);
vs04->cur_x = 31;
vprint_text(vs04, tmp_str);
count04++;
}
destroy_vscreen(vs04);
}
/*----------------------------------------------------------
// Точка входа в ядро
//--------------------------------------------------------*/
int main(multiboot_header_t* mboot, u32int initial_esp)
{
init_esp = initial_esp;
.
.
.
init_memory_manager(init_esp);
init_timer(BASE_FREQ);
asm volatile ("sti");
init_task_manager();
process_t* proc = get_current_proc();
thread01 = thread_create(proc,
&task01,
0x4000,
true,
false);
thread02 = thread_create(proc,
&task02,
0x4000,
true,
false);
/* Инициализируем режим пользователя для функции task04() со стеком в 16 Кб */
init_user_mode(&task04, 0x4000);
return 0;
}
Кроме того, для корректной работы с памятью в ring 3 в нашем случае надо установить для всех отображаемых страниц каталога ядра дополнительно флаг PAGE_USER. Думаю Вы вполне сможете проделать этот самостоятельно, без приведения исходного кода менеджера памяти. В любом случае в работающем примере всё это уже сделано, можно посмотреть в нем.
Что у нас вышло?
Теперь один из потоков, а именно поток #4 работает в 3-м кольце защиты. При этом одновременно с ним выполняются два потока ядра, и происходит переключение стеков ядра через структуру TSS - её содержимое выводится с крайнем правом столбце для каждого из потоков.
Заключение
Итак, мы сделали большое дело - перешли в непривилегированный режим пользователя. В следующей статье мы исследуем особенности работы системы в таком режиме.
Комментариев нет:
Отправить комментарий