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.

36 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 или LDT
  • IST - обеспечивает возможность переключения на новый стек для обработки прерываний.

И последнее поле Тип описывает тип записи 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/kernel/idt.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:

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 и теперь мы можем загрузить таблицу векторов прерываний вызовом:

load_idt((const struct desc_ptr *)&idt_descr);

Где idt_descr:

struct desc_ptr idt_descr __ro_after_init = {
	.size		= (IDT_ENTRIES * 2 * sizeof(unsigned long)) - 1,
	.address	= (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.

Ссылки