1
0
mirror of https://github.com/0xAX/linux-insides.git synced 2024-11-19 22:48:13 +00:00
linux-insides/Initialization/linux-initialization-2.md

504 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Инициализация ядра. Часть 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/include/asm/desc.h](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/include/asm/desc.h):
```C
#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`:
```C
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` заданными значениями:
```C
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);
}
```
Как я уже упоминал выше, мы заполняем шлюз дескриптора в этой функции. Мы заполняем три части адреса обработчика прерываний адресом, который мы получили в основном цикле (адрес точки входа обработчика прерывания). Мы используем три следующих макроса для разделения адреса на три части:
```C
#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`:
```C
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` и теперь мы можем загрузить `таблицу векторов прерываний` вызовом:
```C
load_idt((const struct desc_ptr *)&idt_descr);
```
Где `idt_descr`:
```C
struct desc_ptr idt_descr = { NR_VECTORS * 16 - 1, (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
.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](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/kernel/head_64.S#L343) и первое что мы можем видеть это проверка [NMI](http://en.wikipedia.org/wiki/Non-maskable_interrupt). Нам не нужно обрабатывать их, поэтому просто игнорируем их в коде:
```assembly
cmpl $2,(%rsp)
je .Lis_nmi
```
где `is_nmi`:
```assembly
is_nmi:
addq $16,%rsp
INTERRUPT_RETURN
```
удаляет код ошибки и номер вектора из стека и вызывает макрос `INTERRUPT_RETURN`, который раскрывается до инструкции `iretq`. После проверки номера вектора (и это не `NMI`), мы проверяем `early_recursion_flag`, чтобы предотвратить рекурсию в `early_idt_handler_common`, и если он корректен, сохраняем регистры общего назначения в стек:
```assembly
pushq %rax
pushq %rcx
pushq %rdx
pushq %rsi
pushq %rdi
pushq %r8
pushq %r9
pushq %r10
pushq %r11
```
Мы должны сделать это, чтобы предотвратить появление неверных значений регистров при возврате из обработчика прерываний. После этого мы проверяем селектор сегмента в стеке:
```assembly
cmpl $__KERNEL_CS,96(%rsp)
jne 11f
```
который должен быть равен сегменту кода ядра, и если нет, мы переходим к метке `11`, которая печатает сообщение `PANIC` и выводит дамп стека.
После проверки сегмента кода мы проверяем номер вектора, и если это `#PF` или [ошибка страницы (Page Fault)](https://en.wikipedia.org/wiki/Page_fault), мы помещаем значение `cr2` в регистр `rdi` и вызываем `early_make_pgtable` (мы скоро это увидим):
```assembly
cmpl $14,72(%rsp)
jnz 10f
GET_CR2_INTO(%rdi)
call early_make_pgtable
andl %eax,%eax
jz 20f
```
Если номер вектора не равен `#PF`, мы восстанавливаем регистры общего назначения из стека:
```assembly
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](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/kernel/head64.c) и он принимает только один параметр - адрес из регистра `cr2`, который вызывал ошибку страницы. Давайте посмотрим на неё более подробно:
```C
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`. Все эти типы всего-навсего:
```C
typedef unsigned long pgdval_t;
```
Также мы будем работать с типами `*_t`, например `pgd_t` и т.д. Все эти типы определены в [arch/x86/include/asm/pgtable_types.h](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/include/asm/pgtable_types.h) и представляют собой структуры:
```C
typedef struct { pgdval_t pgd; } pgd_t;
```
Для примера,
```C
extern pgd_t early_level4_pgt[PTRS_PER_PGD];
```
Здесь `early_level4_pgt` представляет начальный каталог таблиц страниц верхнего уровня, который состоит из массива типа `pgd_t` и `pgd` указывает на записи страниц нижнего уровня.
После того как мы проверили, что у нас корректный адрес, мы получаем адрес записи глобального каталога страниц, который содержит адрес `#PF`, и присваиваем его значение переменной `pgd`:
```C
pgd_p = &early_level4_pgt[pgd_index(address)].pgd;
pgd = *pgd_p;
```
На следующем шаге мы проверяем `pgd`, если он содержит верную запись в глобальном каталоге страниц, мы помещаем физический адрес записи в `pud_p`:
```C
pud_p = (pudval_t *)((pgd & PTE_PFN_MASK) + __START_KERNEL_map - phys_base);
```
где `PTE_PFN_MASK` является макросом:
```C
#define PTE_PFN_MASK ((pteval_t)PHYSICAL_PAGE_MASK)
```
который раскрывается до:
```C
(~(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` в глобальный каталог страниц:
```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;
*pgd_p = (pgdval_t)pud_p - __START_KERNEL_map + phys_base + _KERNPG_TABLE;
```
После этого мы исправляем адрес верхнего каталога страниц:
```C
pud_p += pud_index(address);
pud = *pud_p;
```
На следующем шаге мы делаем те же действия что и ранее, но с промежуточным каталогом страниц. В конце мы исправляем адрес промежуточного каталога страниц, который содержит отображения текста ядра+виртуальные адреса данных:
```C
pmd = (physaddr & PMD_MASK) + early_pmd_flags;
pmd_p[pmd_index(address)] = pmd;
```
После того как обработчик ошибки страницы завершён, `early_level4_pgt` содержит записи, которые указывают на корректные адреса.
Заключение
--------------------------------------------------------------------------------
Это конец второй части инициализации ядра 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)