"Ассемблер всегда будет актуален. С его помощью можно добиться минимального размера программ и наилучшей чистоты исходного кода, вот почему в языках высокого уровня предусмотрено использование процедур, написанных на ассемблере"
Рэндел Хайд "Искусство программирования на языке Assembler"
Следующая статья в этом блоге должна была быть об исключениях процессора в целом, и исключении страничной адресации #PF в частности. Однако обстоятельства сложились так, что придется ещё раз вернутся к реализации планировщика задач, а точнее к функции переключения потоков. А к теме обработки процессорных исключений я обязательно вернусь.
А началось всё с того, что после реализации потоков ядра в рабочей версии PhantomEx я начал готовить почву для перехода в пользовательский режим (Ring 3). Об этом тоже будет целая статья, даже несколько статей - если учесть что в рабочей версии уже всё это реализовано ;). Пока скажу лишь о том, то при переключении задач разного уровня привелегий используется атавизм архитектуры x86 - сегмент состояния задачи TSS, о котором я уже упоминал несколько раз. В аппаратной реализации многозадачности 286/386 процессоров он предназначался для хранения контекста выполнения задачи, на каждую задачу полагался такой дескриптор. Он являет собой структуру, сегодня от него осталось одно значимое поле: esp0 - указатель на вершину стека ядра, который модифицируется каждый раз при переключении задач, даже если оно происходит программно. Без этого невозможно переключение задач разных уровней привелегий. И вот на этой простой операции - записи значения в поле структуры мой планировщик подложил мне жирную свинью. Но обо всём по порядку
1. Проблема
Вот код который порождал ошибки
Листинг 77. Функция переключения задач (scheduler.c)
{
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;
}
//
//---------------------------------------------*/
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 /* функции С */
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
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
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
/ Переключение задач
/------------------------------------------------*/
.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;
* Структура описатель потока
*---------------------------------------------------*/
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(); /* Переключаемся */
}
}
//
//------------------------------------------------------*/
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 для просмотра скомпилированного бинарного файла ядра, в том числе и для быстрого его дизасемблирования. Так вот этот инструмент необходимо применять осторожно.
Вместо заключения
Помнится я рекомендовал шестнадцатеричный редактор ht для просмотра скомпилированного бинарного файла ядра, в том числе и для быстрого его дизасемблирования. Так вот этот инструмент необходимо применять осторожно.
Причина в неверном декодировании им некоторых инструкций, что наглядно показано на приведенном скриншоте. Там где должен быть пролог функции C, красуется определение константы и чтение из порта ввода/вывода, которых там и рядом быть не может.
Листинг в тексте статьи получен в IDE Eclipse CDT во время сессии отладки ядра.
Комментариев нет:
Отправить комментарий