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.

32 KiB

Kernel initialization. Part 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

We stopped at the following point:

for (i = 0; i < NUM_EXCEPTION_VECTORS; i++)
	set_intr_gate(i, early_idt_handler_array[i]);

Here we call set_intr_gate in the loop, which takes two parameters:

  • Number of an interrupt or vector number;
  • Address of the idt handler.

and inserts an interrupt gate to the IDT table which is represented by the &idt_descr array. First of all let's look on the early_idt_handler_array array. It is an array which is defined in the arch/x86/include/asm/segment.h header file contains addresses of the first 32 exception handlers:

#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 is 288 bytes array which contains address of exception entry points every nine bytes. Every nine bytes of this array consist of two bytes optional instruction for pushing dummy error code if an exception does not provide it, two bytes instruction for pushing vector number to the stack and five bytes of jump to the common exception handler code.

As we can see, We're filling only first 32 IDT entries in the loop, because all of the early setup runs with interrupts disabled, so there is no need to set up interrupt handlers for vectors greater than 32. The early_idt_handler_array array contains generic idt handlers and we can find its definition in the arch/x86/kernel/head_64.S assembly file. For now we will skip it, but will look it soon. Before this we will look on the implementation of the set_intr_gate macro.

The set_intr_gate macro is defined in the arch/x86/include/asm/desc.h header file and looks:

#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)

First of all it checks with that passed interrupt number is not greater than 255 with BUG_ON macro. We need to do this check because we can have only 256 interrupts. After this, it make a call of the _set_gate function which writes address of an interrupt gate to the 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);
}

At the start of _set_gate function we can see call of the pack_gate function which fills gate_desc structure with the given values:

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);
}

As I mentioned above, we fill gate descriptor in this function. We fill three parts of the address of the interrupt handler with the address which we got in the main loop (address of the interrupt handler entry point). We are using three following macros to split address on three parts:

#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)

With the first PTR_LOW macro we get the first 2 bytes of the address, with the second PTR_MIDDLE we get the second 2 bytes of the address and with the third PTR_HIGH macro we get the last 4 bytes of the address. Next we setup the segment selector for interrupt handler, it will be our kernel code segment - __KERNEL_CS. In the next step we fill Interrupt Stack Table and Descriptor Privilege Level (highest privilege level) with zeros. And we set GAT_INTERRUPT type in the end.

Now we have filled IDT entry and we can call native_write_idt_entry function which just copies filled IDT entry to the IDT:

static inline void native_write_idt_entry(gate_desc *idt, int entry, const gate_desc *gate)
{
        memcpy(&idt[entry], gate, sizeof(*gate));
}

After that main loop will finished, we will have filled idt_table array of gate_desc structures and we can load Interrupt Descriptor table with the call of the:

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

Where idt_descr is:

struct desc_ptr idt_descr = { NR_VECTORS * 16 - 1, (unsigned long) idt_table };

and load_idt just executes lidt instruction:

asm volatile("lidt %0"::"m" (*dtr));

You can note that there are calls of the _trace_* functions in the _set_gate and other functions. These functions fills IDT gates in the same manner that _set_gate but with one difference. These functions use trace_idt_table the Interrupt Descriptor Table instead of idt_table for tracepoints (we will cover this theme in the another part).

Okay, now we have filled and loaded Interrupt Descriptor Table, we know how the CPU acts during an interrupt. So now time to deal with interrupts handlers.

Early interrupts handlers

As you can read above, we filled IDT with the address of the early_idt_handler_array. We can find it in the arch/x86/kernel/head_64.S assembly file:

	.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

We can see here, interrupt handlers generation for the first 32 exceptions. We check here, if exception has an error code then we do nothing, if exception does not return error code, we push zero to the stack. We do it for that would stack was uniform. After that we push exception number on the stack and jump on the early_idt_handler_array which is generic interrupt handler for now. As we may see above, every nine bytes of the early_idt_handler_array array consists from optional push of an error code, push of vector number and jump instruction. We can see it in the output of the objdump util:

$ 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
...
...
...

As i wrote above, CPU pushes flag register, CS and RIP on the stack. So before early_idt_handler will be executed, stack will contain following data:

|--------------------|
| %rflags            |
| %cs                |
| %rip               |
| rsp --> error code |
|--------------------|

Now let's look on the early_idt_handler_common implementation. It locates in the same arch/x86/kernel/head_64.S assembly file and first of all we can see check for NMI. We don't need to handle it, so just ignore it in the early_idt_handler_common:

	cmpl $2,(%rsp)
	je .Lis_nmi

where is_nmi:

is_nmi:
	addq $16,%rsp
	INTERRUPT_RETURN

drops an error code and vector number from the stack and call INTERRUPT_RETURN which is just expands to the iretq instruction. As we checked the vector number and it is not NMI, we check early_recursion_flag to prevent recursion in the early_idt_handler_common and if it's correct we save general registers on the stack:

	pushq %rax
	pushq %rcx
	pushq %rdx
	pushq %rsi
	pushq %rdi
	pushq %r8
	pushq %r9
	pushq %r10
	pushq %r11

We need to do it to prevent wrong values of registers when we return from the interrupt handler. After this we check segment selector in the stack:

	cmpl $__KERNEL_CS,96(%rsp)
	jne 11f

which must be equal to the kernel code segment and if it is not we jump on label 11 which prints PANIC message and makes stack dump.

After the code segment was checked, we check the vector number, and if it is #PF or Page Fault, we put value from the cr2 to the rdi register and call early_make_pgtable (well see it soon):

	cmpl $14,72(%rsp)
	jnz 10f
	GET_CR2_INTO(%rdi)
	call early_make_pgtable
	andl %eax,%eax
	jz 20f

If vector number is not #PF, we restore general purpose registers from the stack:

	popq %r11
	popq %r10
	popq %r9
	popq %r8
	popq %rdi
	popq %rsi
	popq %rdx
	popq %rcx
	popq %rax

and exit from the handler with iret.

It is the end of the first interrupt handler. Note that it is very early interrupt handler, so it handles only Page Fault now. We will see handlers for the other interrupts, but now let's look on the page fault handler.

Page fault handling

In the previous paragraph we saw first early interrupt handler which checks interrupt number for page fault and calls early_make_pgtable for building new page tables if it is. We need to have #PF handler in this step because there are plans to add ability to load kernel above 4G and make access to boot_params structure above the 4G.

You can find implementation of the early_make_pgtable in the arch/x86/kernel/head64.c and takes one parameter - address from the cr2 register, which caused Page Fault. Let's look on it:

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;
	...
	...
	...
}

It starts from the definition of some variables which have *val_t types. All of these types are just:

typedef unsigned long   pgdval_t;

Also we will operate with the *_t (not val) types, for example pgd_t and etc... All of these types defined in the arch/x86/include/asm/pgtable_types.h and represent structures like this:

typedef struct { pgdval_t pgd; } pgd_t;

For example,

extern pgd_t early_level4_pgt[PTRS_PER_PGD];

Here early_level4_pgt presents early top-level page table directory which consists of an array of pgd_t types and pgd points to low-level page entries.

After we made the check that we have no invalid address, we're getting the address of the Page Global Directory entry which contains #PF address and put it's value to the pgd variable:

pgd_p = &early_level4_pgt[pgd_index(address)].pgd;
pgd = *pgd_p;

In the next step we check pgd, if it contains correct page global directory entry we put physical address of the page global directory entry and put it to the pud_p with:

pud_p = (pudval_t *)((pgd & PTE_PFN_MASK) + __START_KERNEL_map - phys_base);

where PTE_PFN_MASK is a macro:

#define PTE_PFN_MASK            ((pteval_t)PHYSICAL_PAGE_MASK)

which expands to:

(~(PAGE_SIZE-1)) & ((1 << 46) - 1)

or

0b1111111111111111111111111111111111111111111111

which is 46 bits to mask page frame.

If pgd does not contain correct address we check that next_early_pgt is not greater than EARLY_DYNAMIC_PAGE_TABLES which is 64 and present a fixed number of buffers to set up new page tables on demand. If next_early_pgt is greater than EARLY_DYNAMIC_PAGE_TABLES we reset page tables and start again. If next_early_pgt is less than EARLY_DYNAMIC_PAGE_TABLES, we create new page upper directory pointer which points to the current dynamic page table and writes it's physical address with the _KERPG_TABLE access rights to the page global directory:

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;

After this we fix up address of the page upper directory with:

pud_p += pud_index(address);
pud = *pud_p;

In the next step we do the same actions as we did before, but with the page middle directory. In the end we fix address of the page middle directory which contains maps kernel text+data virtual addresses:

pmd = (physaddr & PMD_MASK) + early_pmd_flags;
pmd_p[pmd_index(address)] = pmd;

After page fault handler finished it's work and as result our early_level4_pgt contains entries which point to the valid addresses.

Conclusion

This is the end of the second part about linux kernel insides. If you have questions or suggestions, ping me in twitter 0xAX, drop me email or just create issue. In the next part we will see all steps before kernel entry point - start_kernel function.

Please note that English is not my first language and I am really sorry for any inconvenience. If you found any mistakes please send me PR to linux-insides.