Исключительные ситуации
Демонстрация calltrace с помощью macros.inc
Программные исключения
Обработка событий в ЭВМ
Что такое «событие»?
- Происходит в процессе работы (какой-либо программы)
Происходит по какой-то причине, явно в программе не заданной
дополнительно: программная имитация события
- Вызывает изменение потока вычислений
- Вариант реализации: меняется значение счётчика команд
Скорее всего, обрабатывается ядром ОС
- Асинхронное / активированное синхронно
- Неожиданное / предполагаемое
- Требующее возврата к прежнему вычислительному процессу / финальное Терминология плавающая. В архитектуре Intel принято все события называть «прерываниями» В MIPS:
прерывания — это события, вызванные внешними причинами (аппаратные)
исключения — события, вызванные внутренними причинами (программные)
Тип события |
Место в программе известно |
Предусмотрено |
Источник |
Термин |
Нештатная ситуация вычислительного процесса (деление на 0, переполнение, защита памяти, нет такой инструкции и т. п.) |
да |
нет |
Внутренний |
Исключение (exception) |
Имитация нештатной ситуации («система, разбирайся»). В MIPS — исключение TRAP_EXCEPTION |
да |
да |
Внутренний |
Ловушка (trap) |
Обращение к функции операционной системы |
да |
да |
Внутренний |
Системный вызов (system call) |
Запрос внешнего устройства |
нет |
нет |
Внешний |
Прерывание (interrupt) |
Сбой внешнего устройства |
иногда |
нет |
Внешний |
Прерывание (interrupt) |
Строго говоря, системный вызов — не событие, т. к. всегда явно вызывается программой, но также требует обработки ядром. Общая идея обработки событий.
- Аппаратура процессора обнаруживает событие и осуществляет передачу управления.
- на фиксированный адрес (как в MIPS, адрес обработчика 0x80000180)
с помощью «вектора прерываний» (он же таблица переходов) — аппаратно поддерживаемая таблица адресов. Предположим, что она находится по адресу 0x80000100
Адрес |
Содержимое |
Пояснение |
0x80000100 |
0x80000180 |
адрес обработчика исключения № 0 |
0x80000104 |
0x800007ac |
адрес обработчика исключения № 1 |
0x80000108 |
0x800015b0 |
адрес обработчика исключения № 2 |
… |
… |
… |
0x80000120 |
0x80000e54 |
адрес обработчика исключения № 8 |
|
|
|
Таблица переходов в MIPS отсутствует:
- Аппаратная поддержка: двойная косвенность.
- При получении исключения вычисляется адрес ячейки, из неё берётся адрес обработчика (+операция доступа к памяти).
- Программная обработка события
Аппаратная поддержка: запрет и/или дисциплина обработки повторных событий (событие внутри события); возможно, специальный режим работы процессора
- Аппаратная поддержка: тип события (выставляется следящей аппаратурой)
- Возврат к нормальному порядку исполнения инструкций
- Аппаратная поддержка: адрес возврата запоминается аппаратурой
- Возможны сложности с конвейером (см. лекцию про конвейер)
Обработка исключний в MIPS
Следит за аппаратурой, устанавливает и хранит дополнительные данные о событиях специальный управляющий сопроцессор MIPS (coprocessor0).
MARS имитирует основные элементы механизма MIPS32 исключений Регистры сопроцессора 0 MARS:
Название |
Номер |
Назначение |
BadVAddr |
8 |
Адрес, при обращении к которому произошло исключение (при записи/чтении по защищённому адресу) |
Status |
12 |
Состояние: маска прерываний, биты разрешений, ... |
Cause |
13 |
Тип («причина») исключения и биты отложенных прерываний |
EPC |
14 |
Адрес инструкции, которая вызвала исключение (или во время выполнения которой произошло прерывание) |
Инструкции для работы с регистрами управляющего сопроцессора:
- mfc0 Rdest, C0src — загрузить содержимое управляющего регистра C0src в регистр общего назначения Rdest
- mtc0 Rsrc, C0dest — загрузить содержимое регистра общего назначения C0src в управляющий регистр Rdest
- eret — вернуться из обработчика исключительной ситуации Когда происходит исключение, сопроцессор совершает следующие действия:
- Устанавливает бит 1 управляющего регистра $12 (EXception Level, EXL).
- Устанавливает биты 2-6 управляющего регистра $13(причина) согласно типу исключения
- Устанавливает управляющий регистр $14 (EPC). В регистре сохраняется адрес инструкции, вызвавшей исключение.
Если исключение было вызвано обращением к неправильному адресу памяти, управляющий регистр $8 (BadVaddr) устанавливается на этот неверный адрес.
Поток выполнения переключается с текущей инструкции MIPS на адрес 0x8000180. Этот адрес в сегменте текста ядра (.ktext) является стандартным для расположением обработчика исключений MIPS32.
Обработчик исключений может вернуть управление программе, используя команду eret. Это поместит значение из "EPC" (регистр $14) в счетчик команд("PC"). Кроме того, eret очищает бит EXL (см. далее) Замечание: Увеличение регистра $14 на 4 перед возвращением позволит пропустить инструкцию, вызвавшую исключение.
Лень и надувательство (см. спойлер):
Регистр Status:
bits |
31-16 |
15-8 |
7-5 |
4 |
3,2 |
1 |
0 |
target |
unused |
Int. mask |
unused |
K/U |
unused |
Exception level |
Int enable |
- Interrupt mask - Mаска прерываний. Соответствующий прерыванию бит равен 1, если данное прерывание разрешено. Если бит равен 0, соответствующее прерывание не обрабатывается (запрещено)
- K/U - Kernel Mode / User Mode. User mode не эмулируется Mars; всегда 1.
- Exception level - устанавливается автоматически при исключении; предотвращает повторный вход.
- Interrupt enable - глобальное разрешение прерываний (0 - отключить). В других версиях MIPS биты 0-5 регистра Status используются по-иному:
- бит 1 отображает состояние Kernel(=0)/User(=1) mode (то есть работает ли в данный момент процессор в режиме Kernel mode или в режиме User mode)
- бит 0 отображает резрешение (=1) прерываний
- когда происходит обработка исключений, биты 3-2 записываются в биты 5-4, биты 1-0 — в биты 3-2, а в биты 1-0 записывается текущее состояние процессора
- когда происходит выход из обработчика исключений, восстанавливаются предыдущие значения: 3-2 → 1-0, 5-4 → 3-2
Таким образом, в регистре Status формируется трёхуровневый стек для хранения состояний. Если обработка прерывания в прерывании запрещена, достаточно двух уровней, но есть, например, программные ловушки (trap).
Регистр Cause:
31 |
30-16 |
15-8 |
7 |
6-2 |
1-0 |
Br |
unused |
Pending interrupts |
unused |
Exception code |
unused |
- Br = 1, если исключение произошло на инструкции в слоте задержки перехода (это деталь конвейера, не обращайте внимания пока:)
Pending interrupts - ожидающие прерывания. Прерывания асинхронны и вызываются внешними причинами, так что одновременно их вполне может произойти несколько.
- Exception code - код исключения.
Обработчик исключений
Типы исключений (не обязательно реализованы):
- ADDRESS_EXCEPTION_LOAD (4)
- ADDRESS_EXCEPTION_STORE (5)
SYSCALL_EXCEPTION (8) (в MARS — исключение внутри системного вызова),
BREAKPOINT_EXCEPTION (9) (в MARS — DIVIDE_BY_ZERO_EXCEPTION),
- RESERVED_INSTRUCTION_EXCEPTION (10),
- ARITHMETIC_OVERFLOW_EXCEPTION (12),
- TRAP_EXCEPTION ( 13),
- DIVIDE_BY_ZERO_EXCEPTION (15),
- FLOATING_POINT_OVERFLOW (16),
- FLOATING_POINT_UNDERFLOW (17).
При написании обработчика можно без сохранения использовать только регистры $k0 и $k1, т. к. прерывания могут возникнуть когда угодно, а после возвращения ничего не должно меняться.
⇒ В «больших» системах предусматривается отдельный стек ядра (пользовательский стек может быть испорчен).
Пример тривиального обработчика (пройти под отладчиком Mars):
1 .text
2 nop
3 lw $t0, ($zero) # Попытка чтения по адресу 0
4 li $v0 10
5 syscall
6
7 .ktext 0x80000180
8 mfc0 $k0 $14 # В регистре EPC — адрес инструкции, где произошло прерывание
9 addi $k0 $k0,4 # Добавим к этому адресу 4
10 mtc0 $k0 $14 # Запишем обратно в EPС
11 eret # Продолжим работу программы
Пример обработчика. Заметим, что коду обработчика разрешается менять только два регистра — $k0 и $k1, все остальные регистры следует сохранять, если они используется в коде.
1 .text
2 lui $t0 0x7fff
3 addi $t0 $t0 0xffff
4 addi $t0 $t0 0xffff # Целочисленное переполнение
5 sw $t0 0x400 # Обращение к недоступному адресу памяти
6 divu $t0 $t0 $zero # Деление на 0
7 teq $zero $zero # Программная имитация (ловушка)
8 li $v0 10
9 syscall
10 .kdata
11 msg: .asciiz "Exception "
12 .ktext 0x80000180
13 move $k0 $v0 # Сохраняем $v0
14 move $k1 $a0 # Сохраняем $a0
15 la $a0 msg # Выводим сообщение
16 li $v0 4
17 syscall
18 mfc0 $a0 $13 # Регистр Cause
19 srl $a0 $a0 2 # Выделяем в нём поле «причина»
20 andi $a0 $a0 0x1f
21 li $v0 1 # Выводим как целое
22 syscall
23 li $a0 10
24 li $v0 11 # Выводим перевод строки
25 syscall
26
27 move $v0 $k0 # Восстанавливаем $v0
28 move $a0 $k1 # Восстанавливаем $a0
29
30 li $k0 0
31 mtc0 $k0 $13 # Затираем регистр Cause
32 mfc0 $k0 $14 # В регистре EPC — адрес инструкции, где произошло прерывание
33 addi $k0 $k0,4 # Добавим к этому адресу 4
34 mtc0 $k0 $14 # Запишем обратно в EPС
35 eret # Продолжим работу программы
Вопрос: какой регистр мы всё-таки испортили в этом обработчике?
Замечание: в Mars исключение №9 — это деление на 0, а точки останова реализованы «аппаратно», т. е. внутри самого MARS
Пример полностью программного обработчика прерываний и исключений в проекте SPIM
Имитация исключений
С одной стороны, на все случаи жизни исключения предусмотреть нельзя.
С другой стороны, это очень эффективный механизм обращения к ОС почти (или совсем) без дополнительных действий, в отличие от syscall. Системный обработчик по адресу 0x80000180 вызывается немедленно одной инструкцией, атомарно с операцией сравнения.
teq $t1,$t2 |
Trap if equal |
Trap if $t1 is equal to $t2 |
teqi $t1,-100 |
Trap if equal to immediate |
Trap if $t1 is equal to sign-extended 16 bit immediate |
tge $t1,$t2 |
Trap if greater or equal |
Trap if $t1 is greater than or equal to $t2 |
tgei $t1,-100 |
Trap if greater than or equal to immediate |
Trap if $t1 greater than or equal to sign-extended 16 bit immediate |
tgeiu $t1,-100 |
Trap if greater or equal to immediate unsigned |
Trap if $t1 greater than or equal to sign-extended 16 bit immediate, unsigned comparison |
tgeu $t1,$t2 |
Trap if greater or equal unsigned |
Trap if $t1 is greater than or equal to $t2 using unsigned comparision |
tlt $t1,$t2 |
Trap if less than |
Trap if $t1 less than $t2 |
tlti $t1,-100 |
Trap if less than immediate |
Trap if $t1 less than sign-extended 16-bit immediate |
tltiu $t1,-100 |
Trap if less than immediate unsigned |
Trap if $t1 less than sign-extended 16-bit immediate, unsigned comparison |
tltu $t1,$t2 |
Trap if less than unsigned |
Trap if $t1 less than $t2, unsigned comparison |
tne $t1,$t2 |
Trap if not equal |
Trap if $t1 is not equal to $t2 |
tnei $t1,-100 |
Trap if not equal to immediate |
Trap if $t1 is not equal to sign-extended 16 bit immediate |
Таким образом гарантируется, что за время, прошедшее между сравнением и обращением к системе, никакие данные не изменятся (потому что этого времени нет ).
Назначение инструкций типа trap:
- Отладочный останов программы (т. н. assertion). Допустим, в регистре не должно получаться 0, а если ошибочно получится, то лучше пускай система обработает эту ошибку, чем программа будет дальше считать. Например, можно проверить, что изменение счётчика в цикле вообще происходит:
- Обработка исключительной ситуации, если аппаратного обнаружения её нет. Обычно используется команда R-типа, в которой можно в неиспользуемом аппаратно поле хранить причину исключения. Ср. архитектура RISCore:
TEQ - Ловушка по равенству
31
25
20
15
5
SPECIAL 000000
rs
rt
code
TEQ 110100
Содержимое поля code игнорируется системой и может быть использовано для кодированиия информации для системного ПО. Для получения этой информации программа должна загрузить слово команды из памяти (адрес хранится в регистре EPC сопроцессора 0). В ассемблере Mars эта возможность отсутствует, обидно.
Многофайловая сборка
Свойства макроассемблера MARS
Замечания авторов MARS относительно их макроассемблера:
Макроопределение должно идти в тексте программы до соответствующей макрокоманды (иначе пришлось бы анализировать текст дважды)
Макроопределение локально в пределах одного файла. Это с очевидностью вытекает из самого процесса макроподстановки перед трансляцией. Если нужно, чтобы один и тот же макрос был виден из нескольких файлов, используйте .include
Вложенные макросы не поддерживаются, т. е. внутри макроопределения не может встречаться директива .macro
- Внутри макроопределения, как и в тексте программы, могут встречаться только ранее определённые макрокоманды, искать их определения далее по тексту никто не будет
Все метки меняются в процессе макроподстановки, превращаясь в метка_M№
(Замечание от меня: нет, на все! если передать метку в качестве параметра, а потом написать что-то вроде %label: , _M№ к ней не припишется. Не знаю, как и зачем это можно использовать…)
- Несколько макроса с одинаковым именем, но разным количеством параметров, считаются различными, и их можно использовать все
- Повторное определение макроса с тем же именем и тем же количеством параметров игнорируется, макрокоманда раскрывается в первое определение
Параметром макроса (в силу ограниченной реализации) может быть только атомарная лексема языка ассемблера. Например, параметром не может быть "4($t0)", потому что это две лексемы, а не одна
- Макросредства ассемблера не входят ни в какой стандарт и остаются на усмотрение авторов ассемблера
В больших многофайловых проектах принято все макросы складывать в отдельный файл и включать их в код программы с помощью директивы .include файл_с_макросами . Подпрограммы при этом складываются в другой файл (возможно. не один), т. н. «библиотеку», и подключаются посредством многофайловой сборки. На предыдущем примере:
Файл с программой prog.asm:
Файл с подпрограммами lib.asm:
На забываем метки всех подпрограмм, которые понадобятся в других файлах, объявлять как .globl
Файл с макросами macro.inc (имя файла не заканчивается на .asm в знак того, что его не нужно транслировать отдельно):
1 .macro input %msg %reg 2 .data 3 msg: .ascii %msg 4 .asciiz ": " 5 .text 6 la $a0 msg 7 jal _input 8 move %reg $v0 9 .end_macro 10 11 .macro print %msg %reg 12 .data 13 msg: .ascii %msg 14 .asciiz ": " 15 .text 16 la $a0 msg 17 move $a1 %reg 18 jal _print 19 .end_macro 20 21 .macro exit 22 li $v0 10 23 syscall 24 .end_macro
Д/З
EJudge: NoErrors 'Без ошибок'
Написать полную программу, которая пытается вводить целые числа и обрабатывает ошибки ввода до тех пор, пока не введёт 10 штук целых. Затем она их выводит, каждое с новой строки.
zzz 20 fwewefqwe .654 71 -124 0.1 82 6. 334423 -94 VII 7535 6 . - 17 8968
20 71 -124 82 334423 -94 7535 6 17 8968
EJudge: ExCalc 'Простой калькулятор'
Написать полную программу — простой целочисленный калькулятор с обработкой ошибок ввода, деления и переполнения.
- Работа калькулятора:
- Ввод первого числа
- Ввод знака арифметической операции
- Ввод второго числа
- Вывод результата (применение операции к первому и второму числу). Результат выводится, даже если была ошибка.
- Результат становится первым числом для следующей операции
- Переход на п. 2
- Поддерживаемые действия: "+", "-", "*", "/"
- Дополнительно (обязательно с использованием обработчика исключений) отслеживаются:
- Ввод нечислового значения вместо числа (исключение 8). В этом случае:
- выводится сообщение "Invalid input"
- число вводится заново (и так до тех пор, пока не будет введено число)
Деление на 0 (в Mars — исключение 9; обратите внимание на то, как разворачивается псевдоинструкция div $t0 $t1 %t2, и только она). В этом случае
- выводится сообщение "Division by zero"
- результат операции должен быть равен 0
- Знаковое переполнение (исключение 12). В этом случае
- выводится сообщение "Error"
- результат операции равен второму слагаемому
- Ввод нечислового значения вместо числа (исключение 8). В этом случае:
- Ввод заканчивается, если введённый знак действия не поддерживается (пустая строка, точка, пробел и т п.)
22 * lll 3 / 0 + 43545 + 2147483647 .
Invalid input 66 Division by zero 0 43545 Error 2147483647
- Работа калькулятора:
TODO
- ???