You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

599 lines
40 KiB

Инициализация ядра. Часть 2.
================================================================================
Начальная обработка прерываний и исключений
--------------------------------------------------------------------------------
В предыдущей [части](linux-initialization-1.md) мы остановились перед настройкой начальных обработчиков прерываний. На данный момент мы находимся в распакованном ядре Linux, у нас есть базовая структура [страничной организации памяти](https://en.wikipedia.org/wiki/Page_table) для начальной загрузки, и наша текущая цель - завершить начальную подготовку до того, как основной код ядра начнёт свою работу.
Мы уже начали эту подготовку в предыдущей [первой](linux-initialization-1.md) части этой [главы](README.md). Мы продолжим в этой части и узнаем больше об обработке прерываний и исключений.
Как вы можете помнить, мы остановились перед этим циклом:
```C
for (i = 0; i < NUM_EXCEPTION_VECTORS; i++)
set_intr_gate(i, early_idt_handler_array[i]);
```
из файла [arch/x86/kernel/head64.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/kernel/head64.c). Но прежде чем начать разбирать этот код, нам нужно знать о прерываниях и обработчиках.
Некоторая теория
--------------------------------------------------------------------------------
Прерывание - это событие, вызванное программным или аппаратным обеспечением в CPU. Например, пользователь нажал клавишу на клавиатуре. Во время прерывания, CPU останавливает текущую задачу и передаёт управление специальной процедуре - [обработчику прерываний](https://en.wikipedia.org/wiki/Interrupt_handler). Обработчик прерываний обрабатывает прерывания и передаёт управление обратно к ранее остановленной задаче. Мы можем разделить прерывания на три типа:
* Программные прерывания - когда программное обеспечение сигнализирует CPU, что ему нужно обратиться к ядру. Эти прерывания обычно используются для системных вызовов;
* Аппаратные прерывания - когда происходит аппаратное событие, например нажатие кнопки на клавиатуре;
* Исключения - прерывания, генерируемые процессором, когда CPU обнаруживает ошибку, например деление на ноль или доступ к странице памяти, которая не находится в ОЗУ.
Каждому прерыванию и исключению присваивается уникальный номер - `номер вектора`. `Номер вектора` может быть любым числом от `0` до `255`. Существует обычная практика использовать первые `32` векторных номеров для исключений, а номера от `32` до `255` для пользовательских прерываний. Мы можем видеть это в коде выше - `NUM_EXCEPTION_VECTORS`, определённый как:
```C
#define NUM_EXCEPTION_VECTORS 32
```
CPU использует номер вектора как индекс в `таблице векторов прерываний` (мы рассмотрим её позже). Для перехвата прерываний CPU использует [APIC](http://en.wikipedia.org/wiki/Advanced_Programmable_Interrupt_Controller). В следующей таблице показаны исключения `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 или LDT
* `IST` - обеспечивает возможность переключения на новый стек для обработки прерываний.
И последнее поле `Тип` описывает тип записи `IDT`. Существует три различных типа обработчиков для прерываний:
* Дескриптор задачи
* Дескриптор прерывания
* Дескриптор ловушки
Дескрипторы прерываний и ловушек содержат дальний указатель на точку входа обработчика прерываний. Различие между этими типами заключается в том, как CPU обрабатывает флаг `IF`. Если обработчик прерываний был вызван через шлюз прерывания, CPU очищает флаг `IF` чтобы предотвратить другие прерывания, пока выполняется текущий обработчик прерываний. После выполнения текущего обработчика прерываний CPU снова устанавливает флаг `IF` с помощью инструкции `iret`.
Остальные биты в шлюзе прерывания зарезервированы и должны быть равны 0. Теперь давайте посмотрим, как CPU обрабатывает прерывания:
* CPU сохраняет регистр флагов, `CS`, и указатель на инструкцию в стеке.
* Если прерывание вызывает код ошибки (например, `#PF`), CPU сохраняет ошибку в стеке после указателя на инструкцию;
* После выполнения обработчика прерываний для возврата из него используется инструкция `iret`.
Теперь вернёмся к коду.
Заполнение и загрузка IDT
--------------------------------------------------------------------------------
Мы остановились на следующем моменте:
```C
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](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/include/asm/segment.h) и содержит адреса первых `32` обработчиков исключений:
```C
#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](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/kernel/head_64.S). Пока что мы пропустим его, но вскоре вернёмся к нему. Перед этим мы рассмотрим реализацию функции `set_intr_gate`.
Функция `set_intr_gate` определена в файле [arch/x86/kernel/idt.c](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/idt.c):
```C
static void set_intr_gate(unsigned int n, const void *addr)
{
struct idt_data data;
BUG_ON(n > 0xFF);
memset(&data, 0, sizeof(data));
data.vector = n;
data.addr = addr;
data.segment = __KERNEL_CS;
data.bits.type = GATE_INTERRUPT;
data.bits.p = 1;
idt_setup_from_table(idt_table, &data, 1, false);
}
```
Прежде всего она проверяет, что переданный номер прерывания не больше чем `255` с помощью макроса `BUG_ON`. Нам нужно сделать эту проверку, поскольку максимально возможное количество прерываний - `256`. Далее мы устаналиваем данные IDT заданными значениями. И уже после этого мы вызываем функцию `idt_setup_from_table`:
```C
static void
idt_setup_from_table(gate_desc *idt, const struct idt_data *t, int size, bool sys)
{
gate_desc desc;
for (; size > 0; t++, size--) {
desc.offset_low = (u16) t->addr;
desc.segment = (u16) t->segment
desc.bits = t->bits;
desc.offset_middle = (u16) (t->addr >> 16);
desc.offset_high = (u32) (t->addr >> 32);
desc.reserved = 0;
memcpy(&idt[t->vector], &desc, sizeof(desc));
if (sys)
set_bit(t->vector, system_vectors);
}
}
```
которая заполняет три части адреса обработчика прерываний адресом, который мы получили в основном цикле (адрес точки входа обработчика прерывания). Далее мы просто копируем дескриптор шлюза в запись IDT.
После завершения основного цикла у нас в распоряжении будет заполненный массив `idt_table` структур `gate_desc` и теперь мы можем загрузить `таблицу векторов прерываний` вызовом:
```C
load_idt((const struct desc_ptr *)&idt_descr);
```
Где `idt_descr`:
```C
struct desc_ptr idt_descr __ro_after_init = {
.size = (IDT_ENTRIES * 2 * sizeof(unsigned long)) - 1,
.address = (unsigned long) idt_table,
};
```
и `load_idt` просто выполняет инструкцию `lidt`:
```C
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](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/kernel/head_64.S):
```assembly
ENTRY(early_idt_handler_array)
i = 0
.rept NUM_EXCEPTION_VECTORS
.if ((EXCEPTION_ERRCODE_MASK >> i) & 1) == 0
UNWIND_HINT_IRET_REGS
pushq $0 # Dummy error code, to make stack frame uniform
.else
UNWIND_HINT_IRET_REGS offset=8
.endif
pushq $i # 72(%rsp) Vector number
jmp early_idt_handler_common
UNWIND_HINT_IRET_REGS
i = i + 1
.fill early_idt_handler_array + i*EARLY_IDT_HANDLER_SIZE - ., 1, 0xcc
.endr
UNWIND_HINT_IRET_REGS offset=16
END(early_idt_handler_array)
```
Функции, которые, как правило, не вызываются напрямую другими функциями, такие как `syscall` и обработчики прерываний, часто имеют необычные вещи вроде функции не C-типа с указателем стека. Такой код необходимо аннотировать с помощью макроса `UNWIND_HINT_IRET_REGS`, чтобы `objtool` смог его понять. Здесь мы видим создание обработчиков прерываний для первых `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](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/head_64.S) и прежде всего инкрементирует `early_recursion_flag`, чтобы предотвратить рекурсию в `early_idt_handler_common`:
```assembly
incl early_recursion_flag(%rip)
```
Далее мы сохраняем регистры общего назначения в стек:
```assembly
pushq %rsi
movq 8(%rsp), %rsi
movq %rdi, 8(%rsp)
pushq %rdx
pushq %rcx
pushq %rax
pushq %r8
pushq %r9
pushq %r10
pushq %r11
pushq %rbx
pushq %rbp
pushq %r12
pushq %r13
pushq %r14
pushq %r15
UNWIND_HINT_REGS
```
Мы должны сделать это, чтобы предотвратить появление неверных значений регистров при возврате из обработчика прерываний. После этого мы проверяем номер вектора, и если он `#PF` или [ошибка страницы (Page Fault)](https://en.wikipedia.org/wiki/Page_fault), мы помещаем значение регистра `cr2` в регистр `rdi` и вызываем `early_make_pgtable` (мы скоро это увидим):
```assembly
cmpq $14,%rsi
jnz 10f
GET_CR2_INTO(%rdi)
call early_make_pgtable
andl %eax,%eax
jz 20f
```
Если номер вектора не равен `#PF`, мы вызываем функцию `early_fixup_exception` с передачей указателя ядра (смотрите [соглашение о вызовах x86-64](https://en.wikipedia.org/wiki/X86_calling_conventions#x86-64_calling_conventions)):
```assembly
10:
movq %rsp,%rdi
call early_fixup_exception
```
Мы увидим реализацию функции `early_fixup_exception` позже.
```assembly
20:
decl early_recursion_flag(%rip)
jmp restore_regs_and_return_to_kernel
```
После того как мы уменьшим значение параметра `initial_recursion_flag`, мы восстанавливаем регистры из стека, которые мы сохранили ранее, и возвращаемся из обработчика с помощью `iretq`.
Это конец первого обработчика прерываний. Обратите внимание, что это очень ранний обработчик прерываний, поэтому он обрабатывает только ошибку страницы. Мы увидим обработчики и для других прерываний, но пока давайте посмотрим на обработчик ошибки страницы.
Обработка ошибки страницы
--------------------------------------------------------------------------------
В предыдущем разделе мы увидели первый начальный обработчик прерываний, который проверяет, что номер прерывания относится к ошибке страницы и вызывает `early_make_pgtable` для создания новых таблиц страниц. На данном этапе нам необходим обработчик `#PF`, поскольку планируется добавить способность загружать ядро выше `4G` и сделать структуру `boot_params` доступной над 4G.
Вы можете найти реализацию `early_make_pgtable` в [arch/x86/kernel/head64.c](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/head64.c) и он принимает только один параметр - адрес из регистра `cr2`, который вызывал ошибку страницы. Давайте посмотрим на неё более подробно:
```C
int __init early_make_pgtable(unsigned long address)
{
unsigned long physaddr = address - __PAGE_OFFSET;
pmdval_t pmd;
pmd = (physaddr & PMD_MASK) + early_pmd_flags;
return __early_make_pgtable(address, pmd);
}
```
Затем мы вызываем функцию `__early_make_pgtable`, которая определена в том же файле, что и функция `early_make_pgtable`, как показано ниже:
```C
int __init __early_make_pgtable(unsigned long address, pmdval_t pmd)
{
unsigned long physaddr = address - __PAGE_OFFSET;
pgdval_t pgd, *pgd_p;
p4dval_t p4d, *p4d_p;
pudval_t pud, *pud_p;
pmdval_t *pmd_p;
...
...
...
}
```
Она начинается с определения некоторых переменных, которые имеют типы `*val_t`. Все эти типы всего-навсего:
```C
typedef unsigned long pgdval_t;
```
Также мы будем работать с типами `*_t`, например `pgd_t` и т.д. Все эти типы определены в [arch/x86/include/asm/pgtable_types.h](https://github.com/torvalds/linux/blob/master/arch/x86/include/asm/pgtable_types.h) и представляют собой структуры:
```C
typedef struct { pgdval_t pgd; } pgd_t;
```
Для примера,
```C
extern pgd_t early_top_pgt[PTRS_PER_PGD];
```
Здесь `early_top_pgt` представляет начальный каталог таблиц страниц верхнего уровня, который состоит из массива типа `pgd_t` и `pgd` указывает на записи страниц нижнего уровня.
После того как мы проверили, что у нас корректный адрес, мы получаем адрес записи глобального каталога страниц, который содержит адрес `#PF`, и присваиваем его значение переменной `pgd`:
```C
pgd_p = &early_top_pgt[pgd_index(address)].pgd;
pgd = *pgd_p;
```
Затем мы проверяем, включена ли пятиуровневая страничная организация памяти:
```C
if (!pgtable_l5_enabled())
p4d_p = pgd_p;
```
В большинстве случаев пятиуровневая страничная организация не включена, поэтому `p4d_p` скорее всего равен `pgd_p`.
После этого мы исправляем адрес `p4d` с помощью:
```C
p4d_p += p4d_index(address);
p4d = *p4d_p;
```
На следующем шаге мы проверяем `p4d`, если он содержит верную запись p4d, мы помещаем физический адрес записи p4d в `pud_p`:
```C
pud_p = (pudval_t *)((p4d & PTE_PFN_MASK) + __START_KERNEL_map - phys_base);
```
где `PTE_PFN_MASK` является макросом:
```C
#define PTE_PFN_MASK ((pteval_t)PHYSICAL_PAGE_MASK)
```
который раскрывается до:
```C
(signed long)(~(PAGE_SIZE-1)) & ((1 << 52) - 1)
```
Здесь используется [расширение знака](https://en.wikipedia.org/wiki/Sign_extension). Развёрнутая форма:
```
0b1111111111111111111111111111111111111111111111111111
```
т.е макрос состоит из 52 бит для маскирования фрейма страниц.
Если `p4d` не содержит верный адрес, мы проверяем что `next_early_pgt` не больше чем `EARLY_DYNAMIC_PAGE_TABLES`, который равен `64` и представляет фиксированное количество буферов для настройки новых таблиц страниц по требованию. Если `next_early_pgt` больше, чем `EARLY_DYNAMIC_PAGE_TABLES` мы сбрасываем таблицы страниц и начинаем всё заново. Если `next_early_pgt` меньше, чем `EARLY_DYNAMIC_PAGE_TABLES`, мы создаём новый указатель верхнего каталога страниц, который указывает на текущую динамическую таблицу страниц и записываем его физический адрес с правами доступа `_KERNPG_TABLE` в p4d:
```C
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;
*p4d_p = (p4dval_t)pud_p - __START_KERNEL_map + phys_base + _KERNPG_TABLE;
```
Как уже говорили ранее, мы исправляем адрес верхнего каталога страниц:
```C
pud_p += pud_index(address);
pud = *pud_p;
```
На следующем шаге мы делаем те же действия что и ранее, но с промежуточным каталогом страниц. В конце мы исправляем адрес промежуточного каталога страниц, который содержит отображения текста ядра+виртуальные адреса данных:
```C
pmd_p[pmd_index(address)] = pmd;
```
После того как обработчик ошибки страницы завершён, `early_top_pgt` содержит записи, которые указывают на корректные адреса.
Другие обработчики исключений
--------------------------------------------------------------------------------
На раннем этапе прерывания, исключения, кроме ошибки страницы, обрабатываются функцией `early_fixup_exception`, которая определена в [arch/x86/mm/extable.c](https://github.com/torvalds/linux/blob/master/arch/x86/mm/extable.c) и принимает два аргумента - указатель на стек ядра, состоящий из сохраненных регистров, и номер прерывания:
```C
void __init early_fixup_exception(struct pt_regs *regs, int trapnr)
{
...
...
...
}
```
Прежде всего нам нужно проверить некоторые условия:
```C
if (trapnr == X86_TRAP_NMI)
return;
if (early_recursion_flag > 2)
goto halt_loop;
if (!xen_pv_domain() && regs->cs != __KERNEL_CS)
goto fail;
```
Здесь мы просто игнорируем [NMI](https://en.wikipedia.org/wiki/Non-maskable_interrupt). Также мы убеждаемся, что мы не в рекурсивной ситуации.
Далее:
```C
if (fixup_exception(regs, trapnr))
return;
```
Функция `fixup_exception` определена в том же файле, что и `early_fixup_exception`:
```C
int fixup_exception(struct pt_regs *regs, int trapnr)
{
const struct exception_table_entry *e;
ex_handler_t handler;
e = search_exception_tables(regs->ip);
if (!e)
return 0;
handler = ex_fixup_handler(e);
return handler(e, regs, trapnr);
}
```
`ex_handler_t` - тип указателя функции, определённый следующий образом:
```C
typedef bool (*ex_handler_t)(const struct exception_table_entry *,
struct pt_regs *, int)
```
Функция `search_exception_tables` ищет данный адрес в таблице исключений (т.е содержимое ELF-секции `__ex_table`). После этого мы получаем фактический адрес с помощью функции `ex_fixup_handler`. Наконец, мы вызываем фактический обработчик. Для получения дополнительной информации о таблице исключений смотрите [Documentation/x86/exception-tables.txt](https://github.com/torvalds/linux/blob/master/Documentation/x86/exception-tables.txt).
Вернёмся к функции `early_fixup_exception`. Следующий шаг:
```C
if (fixup_bug(regs, trapnr))
return;
```
Функция `fixup_bug` определена в [arch/x86/kernel/traps.c](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/traps.c). Давайте посмотрим на реализацию этой функции.
```C
int fixup_bug(struct pt_regs *regs, int trapnr)
{
if (trapnr != X86_TRAP_UD)
return 0;
switch (report_bug(regs->ip, regs)) {
case BUG_TRAP_TYPE_NONE:
case BUG_TRAP_TYPE_BUG:
break;
case BUG_TRAP_TYPE_WARN:
regs->ip += LEN_UD2;
return 1;
}
return 0;
}
```
Всё что делает эта функция - возвращает `1`, если генерируется исключение `#UD` (или [неверный опкод (Invalid Opcode)](https://wiki.osdev.org/Exceptions#Invalid_Opcode)) и функция `report_bug` вернула `BUG_TRAP_TYPE_WARN`, иначе возвращает `0`.
Заключение
--------------------------------------------------------------------------------
Это конец второй части инициализации ядра Linux. В следующей части мы увидим все шаги перед точкой входа в ядро - функции `start_kernel`.
**От переводчика: пожалуйста, имейте в виду, что английский - не мой родной язык, и я очень извиняюсь за возможные неудобства. Если вы найдёте какие-либо ошибки или неточности в переводе, пожалуйста, пришлите pull request в [linux-insides-ru](https://github.com/proninyaroslav/linux-insides-ru).**
Ссылки
--------------------------------------------------------------------------------
* [GNU assembly .rept](https://sourceware.org/binutils/docs-2.23/as/Rept.html)
* [APIC](http://en.wikipedia.org/wiki/Advanced_Programmable_Interrupt_Controller)
* [NMI](http://en.wikipedia.org/wiki/Non-maskable_interrupt)
* [Таблица страниц](https://en.wikipedia.org/wiki/Page_table)
* [Обработчик прерываний](https://en.wikipedia.org/wiki/Interrupt_handler)
* [Ошибка страницы](https://en.wikipedia.org/wiki/Page_fault)
* [Предыдущая часть](linux-initialization-1.md)