четверг, 22 августа 2013 г.

PhantomEx: Модификация планировщика задач (и о вреде asm volatile)

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

Рэндел Хайд "Искусство программирования на языке Assembler"



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

А началось всё с того, что после реализации потоков ядра в рабочей версии PhantomEx я начал готовить почву для перехода в пользовательский режим (Ring 3). Об этом тоже будет целая статья, даже несколько статей - если учесть что в рабочей версии уже всё это реализовано ;). Пока скажу лишь о том, то при переключении задач разного уровня привелегий используется атавизм архитектуры x86 - сегмент состояния задачи TSS, о котором я уже упоминал несколько раз. В аппаратной реализации многозадачности 286/386 процессоров он предназначался для хранения контекста выполнения задачи, на каждую задачу полагался такой дескриптор. Он являет собой структуру, сегодня от него осталось одно значимое поле:  esp0 - указатель на вершину стека ядра, который модифицируется каждый раз при переключении задач, даже если оно происходит программно. Без этого невозможно переключение задач разных уровней привелегий. И вот на этой простой операции - записи значения в поле структуры мой планировщик подложил мне жирную свинью. Но обо всём по порядку

1. Проблема


Вот код который порождал ошибки

Листинг 77. Функция переключения задач (scheduler.c)
void switch_task(void)
{
    if (multi_task)
    {
        /* Проталкиваем флаги в стек, выключаем прерывания */
        asm volatile ("pushf; cli");

        /* Запоминаем ESP текущей задачи */
        asm volatile ("mov %%esp, %0":"=a"(current_thread->esp));

        /* Выбираем новую задачу из очереди с учетом флага паузы */
        do
        {
            current_thread = (thread_t*) current_thread->list_item.next;
            current_proc = (process_t*) current_proc->list_item.next;

        }    while ( (current_thread->suspend) || (current_proc->suspend) );

        /* Переключаем директорию страниц */
        /*asm volatile ("mov %0, %%cr3"::"a"(current_proc->page_dir));*/
        /* Переключаем стек */
        asm volatile ("mov %0, %%esp"::"a"(current_thread->esp));

        /* Модифицируем вершину стека ядра в TSS */

        /*set_kernel_stack_in_tss((u32int) current_thread->stack + 
                                   current_thread->stack_size);*/

        /* Enable interrupts */
        asm volatile ("popf");
    }
}



Функция-виновник закомментирована и выделена красным. Это очень простая функция, недостойная даже отдельного листинга

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

void set_kernel_stack_in_tss(u32int stack)
{
    tss.esp0 = stack;
}

обычная запись в память. Однако при выходе из переключателя задач она порождала исключение #TS - некорректный сегмент TSS.

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

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

Листинг 78. Результат компиляции функции переключения задач.

          switch_task:
002034e8:   push %ebp                 /* Стандартный пролог */
002034e9:   mov %esp,%ebp             /* функции С */

002034eb:   sub $0x18,%esp            /* Эта инструкция увеличила стековый кадр на 24 байта!!! */         
 89          if (multi_task)
002034ee:   mov 0x205070,%eax
002034f3:   test %eax,%eax
002034f5:   je 0x20355d <switch_task+117>
 92             asm volatile ("pushf; cli");
002034f7:   pushf
002034f8:   cli
 95             asm volatile ("mov %%esp, %0":"=a"(current_thread->esp));
002034f9:   mov 0x205dcc,%edx
002034ff:   mov %esp,%eax
00203501:   mov %eax,0x1c(%edx)
100                current_thread = (thread_t*) current_thread->list_item.next;
00203504:   mov 0x205dcc,%eax
00203509:   mov 0x4(%eax),%eax
0020350c:   mov %eax,0x205dcc
101                current_proc = (process_t*) current_proc->list_item.next;
00203511:   mov 0x205ddc,%eax
00203516:   mov 0x4(%eax),%eax
00203519:   mov %eax,0x205ddc
103             }   while ( (current_thread->suspend) || (current_proc->suspend) );
0020351e:   mov 0x205dcc,%eax
00203523:   mov 0x10(%eax),%eax
00203526:   test %eax,%eax
00203528:   jne 0x203504 <switch_task+28>
0020352a:   mov 0x205ddc,%eax
0020352f:   mov 0x14(%eax),%eax
00203532:   test %eax,%eax
00203534:   jne 0x203504 <switch_task+28>
108             asm volatile ("mov %0, %%esp"::"a"(current_thread->esp));
00203536:   mov 0x205dcc,%eax
0020353b:   mov 0x1c(%eax),%eax
0020353e:   mov %eax,%esp
110             set_kernel_stack_in_tss((u32int) current_thread->stack + current_thread->stack_size);
00203540:   mov 0x205dcc,%eax
00203545:   mov 0x18(%eax),%eax
00203548:   mov %eax,%edx
0020354a:   mov 0x205dcc,%eax
0020354f:   mov 0x14(%eax),%eax
00203552:   add %edx,%eax
00203554:   mov %eax,(%esp)
00203557:   call 0x20132a <set_kernel_stack_in_tss>
113             asm volatile ("popf");
0020355c:   popf
115       }
0020355d:   leave     /* Эта инструкция попыталась исправить положение, по пути модифицировав ESP
                         и испортив к чертовой матери весь стек */
0020355e:   ret


При формировании стека задачи мы не учитывали этих манипуляций со стеком, рассчитывая на стандартный пролог и эпилог функции C и последующий ret. Компилятор решил всё по другому.

2. Решение


Для проверки этого предположения было принято решения переписать переключатель на чистом ассемблере

Листинг 79. Новая версия переключателя задач (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                

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

/*-----------------------------------------------------
 *     Структура описатель потока
 *---------------------------------------------------*/

typedef    struct
{                                /* Смещения полей от начала структуры, байт */
    list_item_t  list_item;      /* + 0, а до поля next: +4 - оно нам и нужно  */   
    process_t*   process;        /* + 12 */
    bool         suspend;        /* + 16 */
    size_t       stack_size;     /* + 20 */
    void*        stack;          /* + 24 */
    u32int       esp;            /* + 28 */
    u32int       entry_point;    /* + 32 */
    u32int       id;             /* + 36 */
    u32int       stack_top;      /* + 40 */

}__attribute__((packed)) thread_t;

Листинг 80. Обработчик системного таймера (timer.c)

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

static void timer_callback(registers_t regs)
{
    if (is_multitask()) /* Проверяем готовность планировщика */
    {
        task_switch();  /* Переключаемся */
    }
}

Для вызова функции task_switch нам нужен её прототип в заголовке планировщика

Листинг 81. Прототип функции переключения задач (scheduler.h)

/* Внешняя функция на ассемблере */
extern void task_switch(void);

удаленную функциональность можно добавить и потом, а на тот момент стоял вопрос, поможет ли такое решение.

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

tmp_thread->stack_top = (u32int) stack + stack_size;

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

3. Выводы


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

Особенности конкретной архитектуры, для которой разрабатывается ОС вынужнают нас так или иначе применять ассемблер, в этом, надеюсь, Вы уже убедились. Даже в таком "низкоуровневом" языке высокого уровня как C. И если уж применять ассемблер, то необходимо применять его грамотно. На откуп ассемблерным вставкам можно отдать выполнение одной, двух, ну трех команд в подавляющем окружении высокоуровневого кода. А критические участки кода насыщенные низкоуровневыми инструкциями лучше реализовать "по честному", тем более что интеграция ассемблера в код на C, в инструментарии GNU, выполнена очень хорошо.

Вместо заключения 


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

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

Листинг в тексте статьи получен в IDE Eclipse CDT во время сессии отладки ядра.