38 KiB
Инициализация ядра. Часть 2.
Начальная обработка прерываний и исключений
В предыдущей части мы остановились перед настройкой начальных обработчиков прерываний. На данный момент мы находимся в распакованном ядре Linux, у нас есть базовая структура страничной организации памяти для начальной загрузки, и наша текущая цель - завершить начальную подготовку до того, как основной код ядра начнёт свою работу.
Мы уже начали эту подготовку в предыдущей первой части этой главы. Мы продолжим в этой части и узнаем больше об обработке прерываний и исключений.
Как вы можете помнить, мы остановились перед этим циклом:
for (i = 0; i < NUM_EXCEPTION_VECTORS; i++)
set_intr_gate(i, early_idt_handler_array[i]);
из файла arch/x86/kernel/head64.c. Но прежде чем начать разбирать этот код, нам нужно знать о прерываниях и обработчиках.
Некоторая теория
Прерывание - это событие, вызванное программным или аппаратным обеспечением в CPU. Например, пользователь нажал клавишу на клавиатуре. Во время прерывания, CPU останавливает текущую задачу и передаёт управление специальной процедуре - обработчику прерываний. Обработчик прерываний обрабатывает прерывания и передаёт управление обратно к ранее остановленной задаче. Мы можем разделить прерывания на три типа:
- Программные прерывания - когда программное обеспечение сигнализирует CPU, что ему нужно обратиться к ядру. Эти прерывания обычно используются для системных вызовов;
- Аппаратные прерывания - когда происходит аппаратное событие, например нажатие кнопки на клавиатуре;
- Исключения - прерывания, генерируемые процессором, когда CPU обнаруживает ошибку, например деление на ноль или доступ к странице памяти, которая не находится в ОЗУ.
Каждому прерыванию и исключению присваивается уникальный номер - номер вектора
. Номер вектора
может быть любым числом от 0
до 255
. Существует обычная практика использовать первые 32
векторных номеров для исключений, а номера от 32
до 255
для пользовательских прерываний. Мы можем видеть это в коде выше - NUM_EXCEPTION_VECTORS
, определённый как:
#define NUM_EXCEPTION_VECTORS 32
CPU использует номер вектора как индекс в таблице векторов прерываний
(мы рассмотрим её позже). Для перехвата прерываний CPU использует APIC. В следующей таблице показаны исключения 0-31
:
-------------------------------------------------------------------------------------------------------
|Вектор|Мнемоника|Описание |Тип |Код ошибки|Источник |
-------------------------------------------------------------------------------------------------------
|0 | #DE |Деление на ноль |Ошибка |Нет |DIV и IDIV |
|------------------------------------------------------------------------------------------------------
|1 | #DB |Зарезервировано |О/Л |Нет | |
|------------------------------------------------------------------------------------------------------
|2 | --- |Немаск. прерывания |Прерыв.|Нет |Внешние NMI |
|------------------------------------------------------------------------------------------------------
|3 | #BP |Исключение отладки |Ловушка|Нет |INT 3 |
|------------------------------------------------------------------------------------------------------
|4 | #OF |Переполнение |Ловушка|Нет |Инструкция INTO |
|------------------------------------------------------------------------------------------------------
|5 | #BR |Выход за границы |Ошибка |Нет |Инструкция BOUND |
|------------------------------------------------------------------------------------------------------
|6 | #UD |Неверный опкод |Ошибка |Нет |Инструкция UD2 |
|------------------------------------------------------------------------------------------------------
|7 | #NM |Устройство недоступно |Ошибка |Нет |Плавающая точка или [F]WAIT |
|------------------------------------------------------------------------------------------------------
|8 | #DF |Двойная ошибка |Авария |Да |Инструкция, которую могут генерировать NMI |
|------------------------------------------------------------------------------------------------------
|9 | --- |Зарезервировано |Ошибка |Нет | |
|------------------------------------------------------------------------------------------------------
|10 | #TS |Неверный TSS |Ошибка |Да |Смена задачи или доступ к TSS |
|------------------------------------------------------------------------------------------------------
|11 | #NP |Сегмент отсутствует |Ошибка |Нет |Доступ к регистру сегмента |
|------------------------------------------------------------------------------------------------------
|12 | #SS |Ошибка сегмента стека |Ошибка |Да |Операции со стеком |
|------------------------------------------------------------------------------------------------------
|13 | #GP |Общее нарушение защиты|Ошибка |Да |Ссылка на память |
|------------------------------------------------------------------------------------------------------
|14 | #PF |Ошибка страницы |Ошибка |Да |Ссылка на память |
|------------------------------------------------------------------------------------------------------
|15 | --- |Зарезервировано | |Нет | |
|------------------------------------------------------------------------------------------------------
|16 | #MF |Ошибка x87 FPU |Ошибка |Нет |Плавающая точка или [F]WAIT |
|------------------------------------------------------------------------------------------------------
|17 | #AC |Проверка выравнивания |Ошибка |Да |Ссылка на данные |
|------------------------------------------------------------------------------------------------------
|18 | #MC |Проверка машины |Авария |Нет | |
|------------------------------------------------------------------------------------------------------
|19 | #XM |Исключение SIMD |Ошибка |Нет |Инструкции SSE[2,3] |
|------------------------------------------------------------------------------------------------------
|20 | #VE |Искл. виртуализации |Ошибка |Нет |Гипервизор |
|------------------------------------------------------------------------------------------------------
|21-31 | --- |Зарезервировано |Прерыв.|Нет |Внешние прерывания |
-------------------------------------------------------------------------------------------------------
Исключения делятся на три типа:
- Ошибки (Faults) - исключения, по окончании обработки которых прерванная команда повторяется;
- Ловушки (Traps) - исключения, при обработке которых CPU сохраняет состояние, следующее за командой, вызвавшей исключение;
- Аварии (Aborts) - исключения, при обработке которых CPU не сохраняет состояния и не имеет возможности вернуться к месту исключения
Для реагирования на прерывание CPU использует специальную структуру - таблицу векторов прерываний (Interrupt Descriptor Table, IDT). IDT является массивом 8-байтных дескрипторов, наподобие глобальной таблицы дескрипторов, но записи в IDT называются шлюзами
(gates). CPU умножает номер вектора на 8 для того чтобы найти индекс записи IDT. Но в 64-битном режиме IDT представляет собой массив 16-байтных дескрипторов и CPU умножает номер вектора на 16. Из предыдущей части мы помним, что CPU использует специальный регистр GDTR
для поиска глобальной таблицы дескрипторов, поэтому CPU использует специальный регистр IDTR
для таблицы векторов прерываний и инструкцию lidt
для загрузки базового адреса таблицы в этот регистр.
Запись IDT в 64-битном режиме имеет следующую структуру:
127 96
--------------------------------------------------------------------------------
| |
| Зарезервировано |
| |
--------------------------------------------------------------------------------
95 64
--------------------------------------------------------------------------------
| |
| Смещение 63..32 |
| |
--------------------------------------------------------------------------------
63 48 47 46 44 42 39 34 32
--------------------------------------------------------------------------------
| | | D | | | | | | |
| Смещение 31..16 | P | P | 0 |Тип |0 0 0 | 0 | 0 | IST |
| | | L | | | | | | |
--------------------------------------------------------------------------------
31 16 15 0
--------------------------------------------------------------------------------
| | |
| Селектор сегмента | Смещение 15..0 |
| | |
--------------------------------------------------------------------------------
где:
Смещение
- смещение к точки входа обработчика прерывания;DPL
- уровень привилегий сегмента (Descriptor Privilege Level);P
- флаг присутствия сегмента;Селектор сегмента
- селектор сегмента кода в GDT или LDTIST
- обеспечивает возможность переключения на новый стек для обработки прерываний.
И последнее поле Тип
описывает тип записи IDT
. Существует три различных типа обработчиков для прерываний:
- Дескриптор задачи
- Дескриптор прерывания
- Дескриптор ловушки
Дескрипторы прерываний и ловушек содержат дальний указатель на точку входа обработчика прерываний. Различие между этими типами заключается в том, как CPU обрабатывает флаг IF
. Если обработчик прерываний был вызван через шлюз прерывания, CPU очищает флаг IF
чтобы предотвратить другие прерывания, пока выполняется текущий обработчик прерываний. После выполнения текущего обработчика прерываний CPU снова устанавливает флаг IF
с помощью инструкции iret
.
Остальные биты в шлюзе прерывания зарезервированы и должны быть равны 0. Теперь давайте посмотрим, как CPU обрабатывает прерывания:
- CPU сохраняет регистр флагов,
CS
, и указатель на инструкцию в стеке. - Если прерывание вызывает код ошибки (например,
#PF
), CPU сохраняет ошибку в стеке после указателя на инструкцию; - После выполнения обработчика прерываний для возврата из него используется инструкция
iret
.
Теперь вернёмся к коду.
Заполнение и загрузка IDT
Мы остановились на следующем моменте:
for (i = 0; i < NUM_EXCEPTION_VECTORS; i++)
set_intr_gate(i, early_idt_handler_array[i]);
Здесь мы вызываем set_intr_gate
в цикле, который принимает два параметра:
- Номер прерывания или
номер вектора
; - Адрес обработчика idt.
и вставляет шлюз прерывания в таблицу IDT
, которая представлена массивом &idt_descr
. Прежде всего, давайте посмотрим на массив early_idt_handler_array
. Это массив, который определён в заголовочном файле arch/x86/include/asm/segment.h и содержит адреса первых 32
обработчиков исключений:
#define EARLY_IDT_HANDLER_SIZE 9
#define NUM_EXCEPTION_VECTORS 32
extern const char early_idt_handler_array[NUM_EXCEPTION_VECTORS][EARLY_IDT_HANDLER_SIZE];
The early_idt_handler_array
- это 288
байтный массив, который содержит адреса точек входа обработчиков исключений каждые девять байт. Каждый девять байт этого массива состоят из двух байт необязательной инструкции для помещения фиктивного кода ошибки, если исключение не предоставляет его, двубайтовая инструкция для помещения номера вектора в стек и пять байт jump
на общий код обработчика исключений.
Как можно видеть, в цикле мы заполняем только первые 32 элемента IDT
, поскольку все начальные настройки запускаются с отключёнными прерываниями, поэтому нет необходимости настраивать обработчики прерываний для векторов, превышающих 32
. В массиве early_idt_handler_array
содержатся общий обработчики idt и мы можем найти его определение в ассемблерном файле arch/x86/kernel/head_64.S. Пока что мы пропустим его, но вскоре вернёмся к нему. Перед этим мы рассмотрим реализацию макроса set_intr_gate
.
Макрос set_intr_gate
определён в заголовочном файле arch/x86/include/asm/desc.h:
#define set_intr_gate(n, addr) \
do { \
BUG_ON((unsigned)n > 0xFF); \
_set_gate(n, GATE_INTERRUPT, (void *)addr, 0, 0, \
__KERNEL_CS); \
_trace_set_gate(n, GATE_INTERRUPT, (void *)trace_##addr,\
0, 0, __KERNEL_CS); \
} while (0)
Прежде всего он проверяет, что переданный номер прерывания не больше чем 255
с помощью макроса BUG_ON
. Нам нужно сделать эту проверку, поскольку максимально возможное количество прерываний - 256
. После этого он вызывает функцию _set_gate
, которая записывает адрес шлюза прерывания в IDT
:
static inline void _set_gate(int gate, unsigned type, void *addr,
unsigned dpl, unsigned ist, unsigned seg)
{
gate_desc s;
pack_gate(&s, type, (unsigned long)addr, dpl, ist, seg);
write_idt_entry(idt_table, gate, &s);
write_trace_idt_entry(gate, &s);
}
В начале _set_gate
мы можем видеть вызов функции pack_gate
, которая заполняет структуру gate_desc
заданными значениями:
static inline void pack_gate(gate_desc *gate, unsigned type, unsigned long func,
unsigned dpl, unsigned ist, unsigned seg)
{
gate->offset_low = PTR_LOW(func);
gate->segment = __KERNEL_CS;
gate->ist = ist;
gate->p = 1;
gate->dpl = dpl;
gate->zero0 = 0;
gate->zero1 = 0;
gate->type = type;
gate->offset_middle = PTR_MIDDLE(func);
gate->offset_high = PTR_HIGH(func);
}
Как я уже упоминал выше, мы заполняем шлюз дескриптора в этой функции. Мы заполняем три части адреса обработчика прерываний адресом, который мы получили в основном цикле (адрес точки входа обработчика прерывания). Мы используем три следующих макроса для разделения адреса на три части:
#define PTR_LOW(x) ((unsigned long long)(x) & 0xFFFF)
#define PTR_MIDDLE(x) (((unsigned long long)(x) >> 16) & 0xFFFF)
#define PTR_HIGH(x) ((unsigned long long)(x) >> 32)
С помощью первого макроса PTR_LOW
мы получаем первые 2
байта адреса, с помощью второго PTR_MIDDLE
мы получаем вторые 2
байта адреса, а с третьим макросом PTR_HIGH
мы получаем последние 4
байта адреса. Затем мы настраиваем селектор сегмента для обработчика прерываний, это будет наш сегмент кода ядра - __KERNEL_CS
. На следующем шаге мы заполняем таблицу стека прерываний (IST)
и уровень привилегий дескриптора (DPL)
(самый высокий уровень привилегий) нулями. И в конце мы устанавливаем тип GAT_INTERRUPT
.
Теперь мы заполнили записи IDT
и можем вызвать функцию native_write_idt_entry
, которая скопирует записи в IDT
:
static inline void native_write_idt_entry(gate_desc *idt, int entry, const gate_desc *gate)
{
memcpy(&idt[entry], gate, sizeof(*gate));
}
После завершения основного цикла у нас в распоряжении будет заполненный массив idt_table
структур gate_desc
и теперь мы можем загрузить таблицу векторов прерываний
вызовом:
load_idt((const struct desc_ptr *)&idt_descr);
Где idt_descr
:
struct desc_ptr idt_descr = { NR_VECTORS * 16 - 1, (unsigned long) idt_table };
и load_idt
просто выполняет инструкцию lidt
:
asm volatile("lidt %0"::"m" (*dtr));
Мы можем заметить, что вызовы функций _trace_*
есть в _set_gate
и в остальных функциях. Эти функции заполняют шлюзы IDT
таким же образом, что и _set_gate
, но с одним отличием. Эти функции используют trace_idt_table
таблицы векторов прерываний
вместо idt_table
для контрольных точек (мы рассмотрим эту тему в другой части).
Итак, мы заполнили и загрузили таблицу векторов прерываний
и мы знаем как ведёт себя CPU во время прерывания. Теперь самое время перейти к обработчикам прерываний.
Начальные обработчики прерываний
Как говорилось ранее, мы заполнили IDT
адресом early_idt_handler_array
. Мы можем найти его в arch/x86/kernel/head_64.S:
.globl early_idt_handler_array
early_idt_handlers:
i = 0
.rept NUM_EXCEPTION_VECTORS
.if (EXCEPTION_ERRCODE_MASK >> i) & 1
pushq $0
.endif
pushq $i
jmp early_idt_handler_common
i = i + 1
.fill early_idt_handler_array + i*EARLY_IDT_HANDLER_SIZE - ., 1, 0xcc
.endr
Здесь мы видим создание обработчиков прерываний для первых 32
исключений. Мы проверяем, содержит ли исключение код ошибки, и ничего не делаем, если исключение не возвращает код ошибки, тогда мы помещаем в стек ноль. Мы делаем это для того чтобы стек был однородным. После этого мы помещаем номер исключения в стек и переходим на early_idt_handler_array
, который является общим обработчиком прерываний на данный момент. Каждый девятый байт массива early_idt_handler_array
состоит из необязательного кода ошибок, номера вектора
и инструкции перехода. Мы можем видеть это в выводе утилиты objdump
:
$ objdump -D vmlinux
...
...
...
ffffffff81fe5000 <early_idt_handler_array>:
ffffffff81fe5000: 6a 00 pushq $0x0
ffffffff81fe5002: 6a 00 pushq $0x0
ffffffff81fe5004: e9 17 01 00 00 jmpq ffffffff81fe5120 <early_idt_handler_common>
ffffffff81fe5009: 6a 00 pushq $0x0
ffffffff81fe500b: 6a 01 pushq $0x1
ffffffff81fe500d: e9 0e 01 00 00 jmpq ffffffff81fe5120 <early_idt_handler_common>
ffffffff81fe5012: 6a 00 pushq $0x0
ffffffff81fe5014: 6a 02 pushq $0x2
...
...
...
Как я писал ранее, CPU помещает регистр флагов, CS
и RIP
в стек. Поэтому, прежде чем early_idt_handler
будет выполнен, стек будет содержать следующие данные:
|--------------------|
| %rflags |
| %cs |
| %rip |
| rsp --> код ошибки |
|--------------------|
Давайте посмотрим на реализацию early_idt_handler_common
. Он находится в том же ассемблерном файле arch/x86/kernel/head_64.S и первое что мы можем видеть это проверка NMI. Нам не нужно обрабатывать их, поэтому просто игнорируем их в коде:
cmpl $2,(%rsp)
je .Lis_nmi
где is_nmi
:
is_nmi:
addq $16,%rsp
INTERRUPT_RETURN
удаляет код ошибки и номер вектора из стека и вызывает макрос INTERRUPT_RETURN
, который раскрывается до инструкции iretq
. После проверки номера вектора (и это не NMI
), мы проверяем early_recursion_flag
, чтобы предотвратить рекурсию в early_idt_handler_common
, и если он корректен, сохраняем регистры общего назначения в стек:
pushq %rax
pushq %rcx
pushq %rdx
pushq %rsi
pushq %rdi
pushq %r8
pushq %r9
pushq %r10
pushq %r11
Мы должны сделать это, чтобы предотвратить появление неверных значений регистров при возврате из обработчика прерываний. После этого мы проверяем селектор сегмента в стеке:
cmpl $__KERNEL_CS,96(%rsp)
jne 11f
который должен быть равен сегменту кода ядра, и если нет, мы переходим к метке 11
, которая печатает сообщение PANIC
и выводит дамп стека.
После проверки сегмента кода мы проверяем номер вектора, и если это #PF
или ошибка страницы (Page Fault), мы помещаем значение cr2
в регистр rdi
и вызываем early_make_pgtable
(мы скоро это увидим):
cmpl $14,72(%rsp)
jnz 10f
GET_CR2_INTO(%rdi)
call early_make_pgtable
andl %eax,%eax
jz 20f
Если номер вектора не равен #PF
, мы восстанавливаем регистры общего назначения из стека:
popq %r11
popq %r10
popq %r9
popq %r8
popq %rdi
popq %rsi
popq %rdx
popq %rcx
popq %rax
и выходим из обработчика с помощью iret
.
Это конец первого обработчика прерываний. Обратите внимание, что это очень ранний обработчик прерываний, поэтому он обрабатывает только ошибку страницы. Мы увидим обработчики и для других прерываний, но пока давайте посмотрим на обработчик ошибки страницы.
Обработка ошибки страницы
В предыдущем разделе мы увидели первый начальный обработчик прерываний, который проверяет, что номер прерывания относится к ошибке страницы и вызывает early_make_pgtable
для создания новых таблиц страниц. На данном этапе нам необходим обработчик #PF
, поскольку планируется добавить способность загружать ядро выше 4G
и сделать структуру boot_params
доступной над 4G.
Вы можете найти реализацию early_make_pgtable
в arch/x86/kernel/head64.c и он принимает только один параметр - адрес из регистра cr2
, который вызывал ошибку страницы. Давайте посмотрим на неё более подробно:
int __init early_make_pgtable(unsigned long address)
{
unsigned long physaddr = address - __PAGE_OFFSET;
unsigned long i;
pgdval_t pgd, *pgd_p;
pudval_t pud, *pud_p;
pmdval_t pmd, *pmd_p;
...
...
...
}
Она начинается с определения некоторых переменных, которые имеют типы *val_t
. Все эти типы всего-навсего:
typedef unsigned long pgdval_t;
Также мы будем работать с типами *_t
, например pgd_t
и т.д. Все эти типы определены в arch/x86/include/asm/pgtable_types.h и представляют собой структуры:
typedef struct { pgdval_t pgd; } pgd_t;
Для примера,
extern pgd_t early_level4_pgt[PTRS_PER_PGD];
Здесь early_level4_pgt
представляет начальный каталог таблиц страниц верхнего уровня, который состоит из массива типа pgd_t
и pgd
указывает на записи страниц нижнего уровня.
После того как мы проверили, что у нас корректный адрес, мы получаем адрес записи глобального каталога страниц, который содержит адрес #PF
, и присваиваем его значение переменной pgd
:
pgd_p = &early_level4_pgt[pgd_index(address)].pgd;
pgd = *pgd_p;
На следующем шаге мы проверяем pgd
, если он содержит верную запись в глобальном каталоге страниц, мы помещаем физический адрес записи в pud_p
:
pud_p = (pudval_t *)((pgd & PTE_PFN_MASK) + __START_KERNEL_map - phys_base);
где PTE_PFN_MASK
является макросом:
#define PTE_PFN_MASK ((pteval_t)PHYSICAL_PAGE_MASK)
который раскрывается до:
(~(PAGE_SIZE-1)) & ((1 << 46) - 1)
или
0b1111111111111111111111111111111111111111111111
состоящий из 46 бит для маскирования страницы.
Если pgd
не содержит верный адрес, мы проверяем что next_early_pgt
не больше чем EARLY_DYNAMIC_PAGE_TABLES
, который равен 64
и представляет фиксированное количество буферов для настройки новых таблиц страниц по требованию. Если next_early_pgt
больше, чем EARLY_DYNAMIC_PAGE_TABLES
мы сбрасываем таблицы страниц и начинаем всё заново. Если next_early_pgt
меньше, чем EARLY_DYNAMIC_PAGE_TABLES
, мы создаём новый указатель верхнего каталога страниц, который указывает на текущую динамическую таблицу страниц и записываем его физический адрес с правами доступа _KERPG_TABLE
в глобальный каталог страниц:
if (next_early_pgt >= EARLY_DYNAMIC_PAGE_TABLES) {
reset_early_page_tables();
goto again;
}
pud_p = (pudval_t *)early_dynamic_pgts[next_early_pgt++];
for (i = 0; i < PTRS_PER_PUD; i++)
pud_p[i] = 0;
*pgd_p = (pgdval_t)pud_p - __START_KERNEL_map + phys_base + _KERNPG_TABLE;
После этого мы исправляем адрес верхнего каталога страниц:
pud_p += pud_index(address);
pud = *pud_p;
На следующем шаге мы делаем те же действия что и ранее, но с промежуточным каталогом страниц. В конце мы исправляем адрес промежуточного каталога страниц, который содержит отображения текста ядра+виртуальные адреса данных:
pmd = (physaddr & PMD_MASK) + early_pmd_flags;
pmd_p[pmd_index(address)] = pmd;
После того как обработчик ошибки страницы завершён, early_level4_pgt
содержит записи, которые указывают на корректные адреса.
Заключение
Это конец второй части инициализации ядра Linux. В следующей части мы увидим все шаги перед точкой входа в ядро - функции start_kernel
.
От переводчика: пожалуйста, имейте в виду, что английский - не мой родной язык, и я очень извиняюсь за возможные неудобства. Если вы найдёте какие-либо ошибки или неточности в переводе, пожалуйста, пришлите pull request в linux-insides-ru.