среда, 28 августа 2013 г.

PhantomEx: Системные вызовы

Что нельзя, то нельзя. Но если очень хочется - то можно...!

Народная мудрость


Необходимость в реализации внутри системы доступа к операциям ввода/вывода для пользовательских приложений, и одновременная невозможность выполнения привилегированных команд из пользовательского режима поднимает вопрос - а как же производится, например файловый ввод/вывод в пользовательских программах больших ОС? Для этого применяются так называемые системные вызовы.

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

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



1.Программное прерывание, как способ выхода в Ring 0


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

Выберем для системных вызовов номер прерывания из свободных номеров в пределах от 0x20 до 0xFF. Скажем, пусть это будет 0x50. Для этого прерывания в модуле interrupts.s создадим специальный обработчик

Листинг 111. Обработчик прерывания INT 50h (interrupts.s)

/*------------------------------------------------------------------------------
//
//  Обработчик прерывания INT 50h (INT 80) для организации системного вызова
//
//----------------------------------------------------------------------------*/

.global isr80

isr80:

      push  $0      /* Помещаем в стек фиктивный код ошибки */
      push  $80     /* и номер прерывания (для обработчика на C) */
     
      /* Проталкиваем в стек все РОН  */
      pusha                 
     
      /* Сохраняем текущий селектор сегмента данных */        
      push  %ds
     
      /* Настраиваемся на сегмент данных ядра */
      mov   $0x10, %ax
      mov   %ax, %ds     

      /* Вызываем обработчик на C */
      call  isr_handler
     
      /* Восстанавливаем селектор сегмента данных */
      pop   %ds
          
      /* Выполняем действия, аналогичные команде popa */
      /* за исключением восстановления регистра EAX */
      /* для сохранения значения, возвращенного системным вызовом */

      pop   %edi
      pop   %esi
      pop   %ebp
      add   $4, %esp
      pop   %ebx
      pop   %edx
      pop   %ecx     
     
      add   $12, %esp   /* Очищаем стек */
     
      iret

Создаем прототип функции isr80() в модуле работы с  таблицами дескриптором, и регистрируем в IDT обработчик прерывания, создав для него дескриптор шлюза ловушки

Листинг 112. Прототип обработчика прерывания INT 50h (descriptor_tables.h)

extern void isr80(void);

Листинг 113. Регистрируем шлюз ловушки в IDT (descriptor_tables.c)

/*--------------------------------------------------------------------
//   
//------------------------------------------------------------------*/

void init_idt(void)
{
  /* Вычисляем лимит и базу IDT */
  idt_ptr.limit = sizeof(idt_entry_t)*256 - 1;
  idt_ptr.base = (u32int) &idt_entries;
 
  memset(&idt_entries, 0, sizeof(idt_entry_t)*256); 
 
  /* Перепрограммируем контролер прерываний */
  outb(0x20, 0x11);
  outb(0xA0, 0x11);
  outb(0x21, 0x20);
  outb(0xA1, 0x28);
  outb(0x21, 0x04);
  outb(0xA1, 0x02);
  outb(0x21, 0x01);
  outb(0xA1, 0x01);
  outb(0x21, 0x0);
  outb(0xA1, 0x0);
 
  /* Устанавливаем обработчики на прерывания */
  idt_set_gate(0, (u32int)isr0, 0x08, 0x8E);
  .
  .
  .
  idt_set_gate(7, (u32int)isr7, 0x08, 0x8E);
 
  idt_set_gate(8, (u32int)isr8, 0x08, 0x8E);
  .
  .
  .
  idt_set_gate(15, (u32int)isr15, 0x08, 0x8E);
 
  idt_set_gate(16, (u32int)isr16, 0x08, 0x8E);
  .
  .
  .
  idt_set_gate(31, (u32int)isr31, 0x08, 0x8E);     
 
  idt_set_gate(32, (u32int)irq0, 0x08, 0x8E); 
  .
  .
  .
  idt_set_gate(47, (u32int)irq15, 0x08, 0x8E);

  /* Шлюз ловушки для прерывания INT 50h */
  idt_set_gate(0x50, (u32int)isr80, 0x08, 0xEF);

  idt_flush((u32int) &idt_ptr);
}

добавленный код выделен полужирным шрифтом. Для того чтобы прерывание вызывалось из 3-го кольца защиты, дескриптор должен иметь DPL = 3, тип шлюза ловушки имеет значение 0xF, таким образом мы получаем значение байта доступа дескриптора 0xEF - шлюз ловушки с DPL = 3 (подробнее о формате дескриптора IDT написано вот тут). Селектор сегмента кода, в котором выполняется обработчик этого прерывания - 0x8 - это селектор кода ядра, то есть данный шлюз обеспечит нам переход из CPL = 3 в CPL = 0, при вызове обработчика прерывания INT 50h.


2. Организация системного вызова


За основу возьмем механизм реализации системных вызовов в ядре Linux. Там для обращения к ядру используется прерывание INT 80h, передача параметров происходит через регистры общего назначения: EAX - номер функции в таблице системных вызовов, EBX - 1-й параметр, ECX - 2-й параметр, EDX - 3-й параметр и так далее. Возвращаемое из системного вызова значение передается через регистр EAX.

Воспользуемся аналогичной с точностью до номера прерывания схемой - номер функции и параметры будем передавать в РОН, возвращаемое значение будем забирать из EAX.

Всё хозяйство, связанное с системными вызовами соберем в отдельном модуле. Для примера реализуем два системных вызова, выполняющих синхронизируемый ядром ОС доступ к портам ввода-вывода.

Листинг 114. Модуль организации системных вызовов (syscalls.h)

/*------------------------------------------------------------------
 *
 *         Реализация системных вызовов
 *
 *----------------------------------------------------------------*/

#ifndef     SYSCALLS_H
#define     SYSCALLS_H

#include    "common.h"
#include    "isr.h"
#include    "text_framebuffer.h"
#include    "io_disp.h"               /* Диспетчер ввода-вывода */

#define     NUM_CALLS    2            /* Число системных функции в таблице */


/*------------------------------------------------------------------
 *        Номер прерывания системного вызова
 *----------------------------------------------------------------*/

#define     SYSCALL      0x50

/*------------------------------------------------------------------
 *        Номера функций ядра
 *----------------------------------------------------------------*/

#define     PORT_INPUT_BYTE     0x00  /* Прочесть байт из порта */
#define     PORT_OUTPUT_BYTE    0x01  /* Записать байт в порт */


/*------------------------------------------------------------------
 *        Функции управления
 *----------------------------------------------------------------*/

void init_syscalls(void);                            /* Инициализация */

extern u32int syscall_entry_call(void* entry_point); /* Передача управления
                                                        системной функции */


void syscall_handler(registers_t regs);              /* Обработчик прерывания */

#endif

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

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

То есть мы должны реализовать некий алгоритм безопасного доступа к портам ввода-вывода, а не просто дать интерфейс к имеющимся у нас уже функциям inb(...) и outb(...). Собственно для этого и служит диспетчер ввода-вывода

Листинг 115. Диспетчер ввода-вывода (io_disp.h)

/*------------------------------------------------------------------
 *
 *         Диспетчер ввода-вывода
 *
 *----------------------------------------------------------------*/

#ifndef     IO_DISP_H
#define     IO_DISP_H

#include    "common.h"
#include    "sync.h"


#define     PORTS_NUM    0x10000 /* Число портов ввода-вывода */

/* Инициализация диспетчера */
void init_io_dispatcher(void);

/* Синхронизированное чтение из порта */
u8int in_byte(u16int port);

/* Синхронизированная запись в порт */
void out_byte(u16int port, u8int value);

#endif

Листинг 116. Диспетчер ввода-вывода (io_disp.c)

/*------------------------------------------------------------------
 *
 *         Диспетчер ввода-вывода
 *
 *----------------------------------------------------------------*/


#include    "io_disp.h"

/*------------------------------------------------------------------
 *        Глобальные переменные
 *----------------------------------------------------------------*/

mutex_t*    port_mutex;  /* Мьютексы для синхронизации портов */

/*------------------------------------------------------------------
 *        Инициализация
 *----------------------------------------------------------------*/

void init_io_dispatcher(void)
{
    int i = 0;

    /* Выделяем память под массив мьютексов портов и освобождаем их все */
    port_mutex = (mutex_t*) kmalloc(sizeof(mutex_t)*PORTS_NUM);

    for (i = 0; i < PORTS_NUM; i++)
    {
        mutex_release(&port_mutex[i]);
    }
}

/*------------------------------------------------------------------
 *          Синхронизированное чтение байта из порта
 *----------------------------------------------------------------*/

u8int in_byte(u16int port)
{
    u8int value = 0;
    /* Захватываем мьютекс данного порта */
    mutex_get(&port_mutex[port], true);
    /* Читаем этот порт */
    value = inb(port);
    /* Отдаем мьютекс */
    mutex_release(&port_mutex[port]);
    /* Возвращаем прочитанное значение */
    return value;
}

/*------------------------------------------------------------------
 *          Синхронизированная запись в порт
 *----------------------------------------------------------------*/

void out_byte(u16int port, u8int value)
{
    /* Захватываем мьютекс порта */
    mutex_get(&port_mutex[port], true);
    /* Выводим значение в порт */
    outb(port, value);
    /* Освобождаем мьютекс */
    mutex_release(&port_mutex[port]);
}

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

Функции реализуемые в диспетчере пойдут в таблицу указателей системных функций

Листинг 117. Реализация системных вызовов (syscall.c)

/*------------------------------------------------------------------
 *
 *         System calls interface
 *
 *----------------------------------------------------------------*/


#include    "syscalls.h"

/*------------------------------------------------------------------
 *  Таблица системных функций
 *----------------------------------------------------------------*/

void* calls_table[NUM_CALLS] =
{
        /* Синхронизированный доступ к портам ввода вывода */
        &in_byte,
        &out_byte
};

/*------------------------------------------------------------------
 *   Обработчик системного вызова
 *----------------------------------------------------------------*/

void syscall_handler(registers_t regs)
{
    /* Если номер функции больше размера таблицы - выходим */
    if (regs.eax >= NUM_CALLS)
        return;

    /* Берем указатель на системную функцию из таблицы по её номеру */
    void* syscall = calls_table[regs.eax];
    /* Вызываем таблиную функцию */
    syscall_entry_call(syscall);
}

/*------------------------------------------------------------------
 *   Инициализация
 *----------------------------------------------------------------*/


void
init_syscalls(void)
{
   /* Регистрируем обработчик прерывания */
   register_interrupt_handler(0x50, &syscall_handler);
}

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

Листинг 117. Передача управления системной функции (sys_calls.s)

/*------------------------------------------------------------------
/
/       Передача управления системной функции
/
/-----------------------------------------------------------------*/

.global     syscall_entry_call

syscall_entry_call:

        push    %edx            /* Проталкиваем в стек параметры  */
        push    %ecx            /* в обратном порядке (конвенция C) */
        push    %ebx
       
        mov    16(%esp), %edx   /* Сохраняем точку входа в EDX */
       
        call    *%edx           /* Вызываем системную функцию  */
       
        add     $12, %esp       /* Освобождаем стек после вызова */
       
        ret

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

Остается только оформить сам системный вызов. Из кода на ассемблере он выглядел бы так - на примере чтения байта из буфера клавиатуры

mov    $0, %eax       /* Номер функции записываем в EAX */
mov    $0x60, %ebx    /* Номер порта - в EBX */

int    $0x50          /* Выполняем программное прерывание INT 50h */


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

Листинг 118. Макроопределения для системных вызовов (syscalls.h)

/*------------------------------------------------------------------
 *        Макроопределения системных вызовов
 *----------------------------------------------------------------*/

/*---- Один параметр ---------------------------------------------*/

#define   DECL_SYSCALL1(func, p1) int syscall_##func(p1)

#define   DEFN_SYSCALL1(func, num, P1)\
\
int syscall_##func(P1 p1)\
{   int ret = 0;\
    asm volatile ("int $0x50":"=a"(ret):"a"(num),"b"(p1));\
    return ret;\
}


/*---- Два параметра ---------------------------------------------*/
#define   DECL_SYSCALL2(func, p1, p2) int syscall_##func(p1, p2)

#define   DEFN_SYSCALL2(func, num, P1, P2)\
\
int syscall_##func(P1 p1, P2 p2)\
{   int ret = 0;\
    asm volatile ("int $0x50":"=a"(ret):"a"(num),"b"(p1),"c"(p2));\
    return ret;\
}


/*----  Три параметра --------------------------------------------*/
#define   DECL_SYSCALL3(func, p1, p2, p3) int syscall_##func(p1, p2, p3)

#define   DEFN_SYSCALL3(func, num, P1, P2, P3)\
\
int syscall_##func(P1 p1, P2 p2, P3 p3)\
{   int ret = 0;\
    asm volatile ("int $0x50":"=a"(ret):"a"(num),"b"(p1),"c"(p2), "d"(p3));\
    return ret;\
}

Эти макросы - обертки над ассемблерным кодом, позволяющие декларировать функции C, осуществляющие системные вызовы. При декларации к имени системной функции добавляется префикс syscall_. Теперь достаточно вызвать данные макросы в заголовочном файле для описания прототипов

/*------------------------------------------------------------------
 *        Описание прототипов системных вызовов
 *----------------------------------------------------------------*/

DECL_SYSCALL1(in_byte, u16int);
DECL_SYSCALL2(out_byte, u16int, u8int);

и в файле syscall.c для описания реализации

/*------------------------------------------------------------------
 *         Описание реализации системных вызовов
 *----------------------------------------------------------------*/

DEFN_SYSCALL1(in_byte, PORT_INPUT_BYTE, u16int)
DEFN_SYSCALL2(out_byte, PORT_OUTPUT_BYTE, u16int, u8int)

и у нас всё готово. Теперь, в очередной раз модифицируем файл main.c

Листинг 119. Тестирование системных вызовов (main.c)


/*------------------------------------------------------------------
//    Поток #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);

        vprint_text(vs04, "I'm  user  thread #4: ");

        /* Читаем скан-код последней нажатой клавиши */
        u8int keyb = (u8int) syscall_in_byte(0x60);

        vs04->cur_x = 22;

        vprint_text(vs04, tmp_str);

        dec2hex_str(keyb, tmp_str);

        vs04->cur_x = 31;

        vprint_text(vs04, "Last key scan code: ");

        vs04->cur_x = 51;

        vprint_text(vs04, tmp_str);

        count04++;
    }

    destroy_vscreen(vs04);   
}

/*------------------------------------------------------------------
//    Точка входа в ядро
//----------------------------------------------------------------*/

int main(multiboot_header_t* mboot, u32int initial_esp)
{
  u32int vir_addr = 0;
  u32int phys_addr = 0;
  int i = 0; 
 
  init_esp = initial_esp;
  .
  .
  .
  /* Инициализируем дистпетчер памяти */ 
  init_memory_manager(init_esp);   
 
  init_timer(BASE_FREQ);
  asm volatile ("sti");
 
  /* Инициализируем планировщик */ 
  init_task_manager();
  /* Инициализируем системные вызовы */
  init_syscalls();

  process_t* proc = get_current_proc();
 
  thread01 = thread_create(proc,
               &task01,
               0x4000,
               true,
               false);


  thread02 = thread_create(proc,
               &task02,
               0x4000,
               true,
               false);
 
  /* Инициализируем диспетчер ввода-вывода */
  init_io_dispatcher();
  
  /* Переходим в 3-е кольцо */
  init_user_mode(&task04, 0x4000);
     
  return 0;
}

В код пользовательского потока добавлен вызов syscall_in_byte(...), читающий буфер клавиатуры на порту 0x60. Результат будет таким


Как видно, всё работает, на экране выведен скан-код последней нажатой клавиши из буфера клавиатуры. При запуске последним мы жали Enter дабы выбрать наше ядро в меню GRUB2. Этот код соответствует отпущенной клавише Enter. Если теперь жать другие клавиши, это значение будет меняться.



Заключение


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