Решил сегодня важный вопрос - наладил автоматический возврат управления из незацикленной программы. До этого у меня были такие программы
Код:
int main(void)
{
.
.
while (1)
{
....
/* Тут я что-то делаю */
....
}
return 0;
}
Конечно такие "приложения" никому не нужны. Если не зацикливать, то управление уходит в никуда - по случайному адресу, который лежит в стеке потока. Раньше, с потоками, я решал проблему возврата из функции потока путем добавления в его конец некой функции thread_exit(....) с указателем на описатель потока в качестве параметра. Эта функция удаляла структуры потока, убирала его из очереди и передавала управление планировщику.
Если с потоками такое ещё прокатывает - в линуксе есть подобные pthread_exit() функции, то с приложениями не хорошо - пользователь не обязна применять каких то мер по выходу из программы, кроме
return. (Хотя в старых книгах по *nix кодингу встречал некий exit()...)
Решил пойти путем автоматизации - поместить адрес возврата в стек. В качестве адреса возврата указать ядреную функцию завершения задачи.
Снова взял в руки отладчик. Дизасемблерный код функции main() содержит такие пролог и эпилог
Код:
main:
9000000d: push %ebp
9000000e: mov %esp,%ebp
90000010: and $0xfffffff0,%esp
90000013: sub $0x220,%esp
................................................
Тут мы что-то делаем
................................................
900000c5: leave
900000c6: ret
Тот leave который попортил мне крови...
Так вот - в начале в стек проталкивается EBP, затем в него записывается значение ESP. Команда LEAVE делает наоборот - записывает в ESP значение сохраненное ранее в EBP. То есть в конце ESP указывает место в стеке, лежащее на 4 байта глубже той позиции, на которую указывал ESP после передачи управления на данную задачу. И если перед запуском задачи мы поместим в эту позицию адрес возврата, то при завершении функции потока уйдем контролируемо в нужное нам место ядра.
Для ядерный потоков и процессов можно сразу передать управление в функцию уничтожения потока/процесса, с последующим переходом на планировщик. Для прикладных потоков сначала надо перейти в Ring 0, ибо при обычном прыжке мы останемся в Ring 3. Для этого я использовал системный вызов такого вида
Код:
.global kernel_mode_switch
.extern destroy_proc
kernel_mode_switch:
/* Формируем в стеке кадр: EFLAGS, CS, EIP - этого достаточно так как мы идем в Ring 0 */
pushf
push $0x8
push $destroy_proc
/* И выходим из прерыывания */
iret
В системный вызов это завернуть пришлось чтобы получить необходимые значения DS и SS, так как в Ring 3 он загружаться не захотел, вываливая #GP, из-за несоблюдения мною условия DPL >= max(RPL,CPL) (действительно DPL = 0, RPL = 0, CPL = 3, то есть max(RPL,CPL) = 3, DPL < CPL). А SS я явно и не загрузил бы, я это не учел, только сейчас вспомнил.
Обертка в системный вызов дает нам выполнить данный код в Ring 0, учитывая что CS указывает на сегмент с тем же уровнем привилегий что и текущий, IRET затаскивает по местам только три значения, а не пять, как при переходе Ring0 --> Ring 3.
После этого управление переходит к функции уничтожения потока/процесса.
Вот теперь у меня все приложения завершаются без вывала системы.
На скрине: Сообщение от завершенного пользовательского процесса, работающее приложение ring 0 и два потока ядра