понедельник, 26 августа 2013 г.

PhantomEx: Переход в пользовательский режим - практика

Теперь попробуем перейти в этот самый режим пользователя с CPL = 3, а так же организовать переключение задач с различным уровнем привилегий.

Для этого нужно сделать следующие:
  1. Создать сегмент TSS и определить функции для работы с ним, загрузить TR селектором этого сегмента;
  2. Модифицировать планировщик для обновления поля ESP0 в TSS при переключении задач.
  3. Перейти в режим пользователя.
Реализуем этот план по порядку




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;             /* Бит гранулярности - ноль */
}

Далее, вызываем функцию 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


Поскольку дескриптор 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
//---------------------------------------------------------------*/
u32int get_tss_esp0(void)
{
    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! */

При этом, перед переключением необходимо создать стек для потока пользовательского режима.

Листинг 108. Инициализация пользовательского режима (scheduler.c)

/* Внешняя функция переключения */
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);
}

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

Кроме того, нам необходимо модифицировать код инициализации планировщика и создания потока, добавив инициализацию поля stack_top в структуре потока

Листинг 109. Корректировка функций инициализации планировщика и создания потока (scheduler.c)

/*----------------------------------------------------------------
 *        Инициализация планировщика
 *--------------------------------------------------------------*/

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");
}

/*----------------------------------------------------------------
 *        Создание потока
 *--------------------------------------------------------------*/

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

Для простоты просто укажу место, где необходимо отредактировать код, добавленный код выделив полужирным шрифтом. Теперь модифицируем  файл main.c

Листинг 110. Переход в режим пользователя (main.c)

#include    "main.h"

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

Кроме того, для корректной работы с памятью в ring 3 в нашем случае надо установить для всех отображаемых страниц каталога ядра дополнительно флаг PAGE_USER. Думаю Вы вполне сможете проделать этот самостоятельно, без приведения исходного кода менеджера памяти. В любом случае в работающем примере всё это уже сделано, можно посмотреть в нем.

Что у нас вышло?

Теперь один из потоков, а именно поток #4 работает в 3-м кольце защиты. При этом одновременно с ним выполняются два потока ядра, и происходит переключение стеков ядра через структуру TSS - её содержимое выводится с крайнем правом столбце для каждого из потоков.

Заключение


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