Инициализация ядра. Часть 1.
Первые шаги в коде ядра
Предыдущая статья была последней частью главы процесса загрузки ядра Linux и теперь мы начинаем погружение в процесс инициализации. После того как образ ядра Linux распакован и помещён в нужное место, ядро начинает свою работу. Все предыдущие части описывают работу кода настройки ядра, который выполняет подготовку до того, как будут выполнены первые байты кода ядра Linux. Теперь мы находимся в ядре, и все части этой главы будут посвящены процессу инициализации ядра, прежде чем оно запустит процесс с pid 1. Есть ещё много вещей, который необходимо сделать, прежде чем ядро запустит первый init процесс. Мы начнём с точки входа в ядро, которая находится в arch/x86/kernel/head_64.S и будем двигаться дальше и дальше. Мы увидим первые приготовления, такие как инициализацию начальных таблиц страниц, переход на новый дескриптор в пространстве ядра и многое другое, прежде чем увидим запуск функции start_kernel в init/main.c.
В последней части предыдущей главы мы остановились на инструкции jmp из ассемблерного файла arch/x86/boot/compressed/head_64.S:
jmp *%rax
В данный момент регистр rax содержит адрес точки входа в ядро Linux, который был получен в результате вызова функции decompress_kernel из файла arch/x86/boot/compressed/misc.c. Итак, наша последняя инструкция в коде настройки ядра - это переход на точку входа. Мы уже знаем, где определена точка входа ядра Linux, поэтому мы можем начать изучать, что делает ядро Linux после запуска.
Первые шаги в ядре
Хорошо, мы получили адрес распакованного образа ядра с помощью функции decompress_kernel в регистр rax. Как мы уже знаем, начальная точка распакованного образа ядра находится в файле arch/x86/kernel/head_64.S, а также в его начале можно увидеть следующие определения:
.text
__HEAD
.code64
.globl startup_64
startup_64:
...
...
...
Мы можем видеть определение подпрограммы startup_64 в секции __HEAD, которая является просто макросом, раскрывающимся до определения исполняемой секции .head.text:
#define __HEAD .section ".head.text","ax"
Определение данной секции расположено в скрипте компоновщика arch/x86/kernel/vmlinux.lds.S:
.text : AT(ADDR(.text) - LOAD_OFFSET) {
_text = .;
...
...
...
} :text = 0x9090
Помимо определения секции .text из скрипта компоновщика, мы можем понять виртуальные и физические адреса по умолчанию. Обратите внимание, что адрес _text - это счётчик местоположения, определённый как:
. = __START_KERNEL;
для x86_64. Определение макроса __START_KERNEL находится в заголовочном файле arch/x86/include/asm/page_types.h и представлен суммой базового виртуального адреса отображения ядра и физического начала:
#define __START_KERNEL (__START_KERNEL_map + __PHYSICAL_START)
#define __PHYSICAL_START ALIGN(CONFIG_PHYSICAL_START, CONFIG_PHYSICAL_ALIGN)
Или другими словами:
- Базовый физический адрес ядра Linux -
0x1000000; - Базовый виртуальный адрес ядра Linux -
0xffffffff81000000.
После того как мы очистили конфигурацию CPU, мы вызываем функцию __startup_64, которая определена в [arch/x86/kernel/head64.c](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/head64.c:
leaq _text(%rip), %rdi
pushq %rsi
call __startup_64
popq %rsi
unsigned log __head __startup_64(unsigned long physaddr,
struct boot_params *bp)
{
unsigned long load_delta, *p;
unsigned long pgtable_flags;
pgdval_t *pgd;
p4dval_t *p4d;
pudval_t *pud;
pmdval_t *pmd, pmd_entry;
pteval_t *mask_ptr;
bool la57;
int i;
unsigned int *next_pgt_ptr;
...
...
...
}
Поскольку kASLR включен, адрес start_64 может отличаться от адреса, скомпилированного для запуска, поэтому нам нужно вычислить дельту с помощью следующего кода:
load_delta = physaddr - (unsigned long)(_text - __START_KERNEL_map);
В результате load_delta содержит дельту между адресом, скомпилированным для запуска, и текущим адресом.
После того как мы получили дельту, мы проверяем правильность выравнивания адреса _text по 2 мегабайтам. Мы сделаем это с помощью следующего кода:
if (load_delta & ~PMD_PAGE_MASK)
for (;;);
Если адрес _text не выровнен по 2 мегабайтам, мы входим в бесконечный цикл. PMD_PAGE_MASK указывает маску для промежуточного каталога страниц (см. страничную организацию памяти) и определён как:
#define PMD_PAGE_MASK (~(PMD_PAGE_SIZE-1))
где макрос PMD_PAGE_SIZE определён как:
#define PMD_PAGE_SIZE (_AC(1, UL) << PMD_SHIFT)
#define PMD_SHIFT 21
Размер PMD_PAGE_SIZE можно легко вычислить - он составляет 2 мегабайта.
Если поддержка SME#Enhanced_security_and_virtualization_support) включена, мы активируем её и включаем маску шифрования SME в load_delta:
sme_enable(bp);
load_delta += sme_get_me_mask();
Хорошо, мы сделали некоторые начальные проверки, и теперь можем двигаться дальше.
Исправление базовых адресов таблиц страниц
На следующем этапе мы исправляем физические адреса в таблице страниц:
pgd = fixup_pointer(&early_top_pgt, physaddr);
pud = fixup_pointer(&level3_kernel_pgt, physaddr);
pmd = fixup_pointer(level2_fixmap_pgt, physaddr);
Давайте рассмотрим определение функции fixup_pointer, которая возвращает физический адрес переданного аргумента:
static void __head *fixup_pointer(void *ptr, unsigned long physaddr)
{
return ptr - (void *)_text + (void *)physaddr;
}
Затем мы сосредоточимся на early_top_pgt и других табличных символах, которые мы видели выше. Давайте попробуем понять, что означают эти символы. Прежде всего посмотрим на их определение:
NEXT_PAGE(early_top_pgt)
.fill 512,8,0
.fill PTI_USER_PGD_FILL,8,0
NEXT_PAGE(level3_kernel_pgt)
.fill L3_START_KERNEL,8,0
.quad level2_kernel_pgt - __START_KERNEL_map + _KERNPG_TABLE_NOENC
.quad level2_fixmap_pgt - __START_KERNEL_map + _PAGE_TABLE_NOENC
NEXT_PAGE(level2_kernel_pgt)
PMDS(0, __PAGE_KERNEL_LARGE_EXEC,
KERNEL_IMAGE_SIZE/PMD_SIZE)
NEXT_PAGE(level2_fixmap_pgt)
.fill 506,8,0
.quad level1_fixmap_pgt - __START_KERNEL_map + _PAGE_TABLE_NOENC
.fill 5,8,0
NEXT_PAGE(level1_fixmap_pgt)
.fill 512,8,0
Выглядит сложно, но на самом деле это не так. Прежде всего, давайте посмотрим на early_top_pgt. Он начинается с 4096 нулевых байтов (или 8192 байт если включён CONFIG_PAGE_TABLE_ISOLATION), это означает, что мы не используем первые 511 записей. После этого мы видим одну запись level3_kernel_pgt. В начале его определения мы видим, что он заполнен 4080 байтами нулей (L3_START_KERNEL равен 510). Впоследствии он хранит две записи, которые отображают пространство ядра. Обратите внимание, что мы вычитаем __START_KERNEL_map из level2_kernel_pgt и level2_fixmap_pgt. Как известно, __START_KERNEL_map является базовым виртуальным адресом текстового сегмента ядра, поэтому, если мы вычтем __START_KERNEL_map, мы получим физические адреса level2_kernel_pgt и level2_fixmap_pgt.
#define _KERNPG_TABLE_NOENC (_PAGE_PRESENT | _PAGE_RW | _PAGE_ACCESSED | \
_PAGE_DIRTY)
#define _PAGE_TABLE_NOENC (_PAGE_PRESENT | _PAGE_RW | _PAGE_USER | \
_PAGE_ACCESSED | _PAGE_DIRTY)
level2_kernel_pgt - это запись в таблице страниц, содержащая указатель на промежуточный каталог страниц, которая отображает пространство ядра. Она вызывает макрос PDMS, который создает 512 мегабайт из __START_KERNEL_map для .text ядра (после того как эти 512 мегабайт будут областью памяти модуля).
level2_fixmap_pgt - это виртуальные адреса, которые могут ссылаться на любые физические адреса даже в пространстве ядра. Они представлены 4048 байтами нулей, значением level1_fixmap_pgt, 8 мегабайтами, зарезервированными для отображения vsyscalls и 2 мегабайта пустого пространства.
Вы можете больше узнать об этом в статье страничная организация памяти.
Теперь, после того как мы увидели определения этих символов, вернёмся к коду. Мы инициализируем последнюю запись pgd с помощью level3_kernel_pgt:
pgd[pgd_index(__START_KERNEL_map)] = level3_kernel_pgt - __START_KERNEL_map + _PAGE_TABLE_NOENC;
Все адреса p*d могут быть неверными, если startup_64 не равен адресу по умолчанию - 0x1000000. Вы должны помнить, что load_delta содержит дельта между адресом метки startup_64, который был получен во время компоновки ядра и фактическим адресом. Таким образом, мы добавляем дельту к некоторым записям p*d:
pgd[pgd_index(__START_KERNEL_map)] += load_delta;
pud[510] += load_delta;
pud[511] += load_delta;
pmd[506] += load_delta;
После этого у нас будет:
early_top_pgt[511] -> level3_kernel_pgt[0]
level3_kernel_pgt[510] -> level2_kernel_pgt[0]
level3_kernel_pgt[511] -> level2_fixmap_pgt[0]
level2_kernel_pgt[0] -> 512 Мб, отображённые на ядро
level2_fixmap_pgt[506] -> level1_fixmap_pgt
Обратите внимание, что мы не исправили базовый адрес early_top_pgt и некоторых других каталогов таблицы страниц, потому что мы увидим это во время построения/заполнения структур этих таблиц страниц. После исправления базовых адресов таблиц страниц, мы можем приступить к их построению.
Настройка отображения 1:1 (identity mapping)
Теперь мы можем увидеть настройку отображения 1:1 начальных таблиц страниц. В страничной организации с отображением 1:1, виртуальные адреса идентичны физическими адресами. Давайте рассмотрим это подробнее. Прежде всего, мы заменим pud и pmd указателем на первую и вторую запись early_dynamic_pgts:
next_pgt_ptr = fixup_pointer(&next_early_pgt, physaddr);
pud = fixup_pointer(early_dynamic_pgts[(*next_pgt_ptr)++], physaddr);
pmd = fixup_pointer(early_dynamic_pgts[(*next_pgt_ptr)++], physaddr);
Давайте посмотрим на определение early_dynamic_pgts:
NEXT_PAGE(early_dynamic_pgts)
.fill 512*EARLY_DYNAMIC_PAGE_TABLES,8,0
которая будет хранить временные таблицы страниц раннего ядра.
Затем мы инициализируем pgtable_flags, который позже будет использоваться при инициализации записей p*d:
pgtable_flags = _KERNPG_TABLE_NOENC + sme_get_me_mask();
Функция sme_get_me_mask возвращает sme_me_mask, который был инициализирован в функции sme_enable.
Далее мы заполняем две записи pgd с помощью pud плюс pgtable_flags, который мы инициализировали ранее:
i = (physaddr >> PGDIR_SHIFT) % PTRS_PER_PGD;
pgd[i + 0] = (pgdval_t)pud + pgtable_flags;
pgd[i + 1] = (pgdval_t)pud + pgtable_flags;
PGDIR_SHFT обозначает маску для бит глобального каталога страниц в виртуальном адресе. Здесь мы вычисляем по модулю PTRS_PER_PGD (который раскрывается до 512), чтобы не получить доступ к индексу, превышающему 512. Для всех типов каталогов страниц есть свой макрос:
#define PGDIR_SHIFT 39
#define PTRS_PER_PGD 512
#define PUD_SHIFT 30
#define PTRS_PER_PUD 512
#define PMD_SHIFT 21
#define PTRS_PER_PMD 512
Мы делаем почти то же самое:
i = (physaddr >> PUD_SHIFT) % PTRS_PER_PUD;
pud[i + 0] = (pudval_t)pmd + pgtable_flags;
pud[i + 1] = (pudval_t)pmd + pgtable_flags;
Затем мы инициализируем pmd_entry и отфильтровываем неподдерживаемые биты __PAGE_KERNEL_ *:
pmd_entry = __PAGE_KERNEL_LARGE_EXEC & ~_PAGE_GLOBAL;
mask_ptr = fixup_pointer(&__supported_pte_mask, physaddr);
pmd_entry &= *mask_ptr;
pmd_entry += sme_get_me_mask();
pmd_entry += physaddr;
Далее мы заполняем все записи pmd, чтобы покрыть полный размер ядра:
for (i = 0; i < DIV_ROUND_UP(_end - _text, PMD_SIZE); i++) {
int idx = i + (physaddr >> PMD_SHIFT) % PTRS_PER_PMD;
pmd[idx] = pmd_entry + i * PMD_SIZE;
}
Затем мы исправляем виртуальные адреса текста+данных ядра. Обратите внимание, что мы можем записать недопустимые pmd, если ядро было перемещено (функция cleanup_highmap исправляет это вместе с отображениями вне _end).
pmd = fixup_pointer(level2_kernel_pgt, physaddr);
for (i = 0; i < PTRS_PER_PMD; i++) {
if (pmd[i] & _PAGE_PRESENT)
pmd[i] += load_delta;
}
Далее мы удаляем маску шифрования памяти для получения истинного физического адреса (помните, что load_delta включает в себя маску):
*fixup_long(&phys_base, physaddr) += load_delta - sme_get_me_mask();
phys_base должен соответствовать первой записи в level2_kernel_pgt.
В качестве последнего шага функции __startup_64 мы зашифровываем ядро (если активен SME) и возвращаем маску шифрования SME, которая будет использоваться в качестве модификатора для начальной записи каталога страницы, запрограммированной в регистр cr3:
sme_encrypt_kernel(bp);
return sme_get_me_mask();
Теперь вернемся к ассемблерному коду. Мы готовимся к следующему разделу со следующим кодом:
addq $(early_top_pgt - __START_KERNEL_map), %rax
jmp 1f
который добавляет физический адрес early_top_pgt к регистру rax и теперь регистр rax содержит сумму адреса и маски шифрования SME.
На данный момент это всё. Наша ранняя страничная структура настроена и нам нужно совершить последнее приготовление, прежде чем мы перейдем к точке входа в ядро.
Последнее приготовление перед переходом на точку входа в ядро
После перехода на метку 1 мы включаем PAE, PGE (Paging Global Extension) и помещаем содержимое phys_base (см. выше) в регистр rax и заполняем регистр cr3:
1:
movl $(X86_CR4_PAE | X86_CR4_PGE), %ecx
movq %rcx, %cr4
addq phys_base(%rip), %rax
movq %rax, %cr3
На следующем шаге мы проверяем, поддерживает ли процессор бит NX:
movl $0x80000001, %eax
cpuid
movl %edx,%edi
Мы помещаем значение 0x80000001 в eax и выполняем инструкцию cpuid для получения расширенной информации о процессоре и битах. Полученный результат находится в регистре edx, который мы помещаем в edi.
Теперь мы помещаем 0xc0000080 (MSR_EFER) в ecx и вызываем инструкцию rdmsr для чтения моделезависимого регистра.
movl $MSR_EFER, %ecx
rdmsr
Результат находится в edx:eax. Общий вид EFER следующий:
63 32
┌───────────────────────────────────────────────────────────────────────────────┐
│ │
│ Зарезервированный MBZ │
│ │
└───────────────────────────────────────────────────────────────────────────────┘
31 16 15 14 13 12 11 10 9 8 7 1 0
┌──────────────────────────────┬───┬───────┬───────┬────┬───┬───┬───┬───┬───┬───┐
│ │ T │ │ │ │ │ │ │ │ │ │
│ Зарезервированный MBZ │ C │ FFXSR | LMSLE │SVME│NXE│LMA│MBZ│LME│RAZ│SCE│
│ │ E │ │ │ │ │ │ │ │ │ │
└──────────────────────────────┴───┴───────┴───────┴────┴───┴───┴───┴───┴───┴───┘
Здесь мы не увидим все поля, но узнаем об этих и других MSR в специальной части. Когда мы считываем EFER в edx:eax, мы проверяем _EFER_SCE или нулевой бит, являющийся System Call Extensions с инструкцией btsl и устанавливаем его в единицу. С помощью бита SCE мы включаем инструкции SYSCALL и SYSRET. На следующем шаге мы проверяем 20 бит в регистре edi, который хранит результат cpuid (см. выше). Если 20 бит установлен (бит NX), мы просто записываем EFER_SCE в моделезависимый регистр.
btsl $_EFER_SCE, %eax
btl $20,%edi
jnc 1f
btsl $_EFER_NX, %eax
btsq $_PAGE_BIT_NX,early_pmd_flags(%rip)
1: wrmsr
Если бит NX поддерживается, мы включаем _EFER_NX и записываем в него с помощью инструкции wrmsr. После того как бит NX установлен, мы устанавливаем некоторые биты в регистре управления cr0:
movl $CR0_STATE, %eax
movq %rax, %cr0
в частности, следующие биты:
X86_CR0_PE- система в защищённом режиме;X86_CR0_MP- контролирует взаимодействие инструкций WAIT/FWAIT с помощью флага TS в CR0;X86_CR0_ET- на 386 позволяло указать, был ли внешний математический сопроцессор 80287 или 80387;X86_CR0_NE- позволяет включить внутреннюю x87 отчётность об ошибках с плавающей запятой, иначе включает PC-стиль x87 обнаружение ошибок;X86_CR0_WP- если установлен, CPU не может писать в страницы только для чтения, когда уровень привилегий равен 0;X86_CR0_AM- проверка выравнивания включена, если установлен AM и флаг AC (в регистре EFLAGS), а уровень привелигий равен 3;X86_CR0_PG- включает страничную организацию.
Мы уже знаем, что для запуска любого кода и даже большего количества C кода из ассемблера, нам необходимо настроить стек. Как всегда, мы делаем это путём установки указателя стека на корректное место в памяти и сброса регистра флагов:
movq initial_stack(%rip), %rsp
pushq $0
popfq
Самое интересное здесь - initial_stack. Этот символ определён в файле arch/x86/kernel/head_64.S и выглядит следующим образом:
GLOBAL(initial_stack)
.quad init_thread_union + THREAD_SIZE - SIZEOF_PTREGS
Макрос THREAD_SIZE определён в arch/x86/include/asm/page_64_types.h и зависит от значения макроса KASAN_STACK_ORDER:
#ifdef CONFIG_KASAN
#define KASAN_STACK_ORDER 1
#else
#define KASAN_STACK_ORDER 0
#endif
#define THREAD_SIZE_ORDER (2 + KASAN_STACK_ORDER)
#define THREAD_SIZE (PAGE_SIZE << THREAD_SIZE_ORDER)
Мы полагаем, что kasan отключён, а PAGE_SIZE равен 4096 байтам. Таким образом, THREAD_SIZE расширяется до 16 килобайт и представляет собой размер стека потока. Почему потока? Возможно, вы уже знаете, что каждый процесс может иметь родительский процесс и дочерний процессы. На самом деле родительский и дочерний процесс различаются стеком. Для нового процесса выделяется новый стек ядра. В ядре Linux этот стек представлен объединением (union) со структурой thread_info.
init_thread_union представлен thread_union и определён в файле include/linux/sched.h:
union thread_union {
#ifndef CONFIG_ARCH_TASK_STRUCT_ON_STACK
struct task_struct task;
#endif
#ifndef CONFIG_THREAD_INFO_IN_TASK
struct thread_info thread_info;
#endif
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
где CONFIG_THREAD_INFO_IN_TASK - параметр конфигурации ядра, включённый для архитектуры ia64, а CONFIG_THREAD_INFO_IN_TASK - параметр конфигурации ядра, включённый для архитектуры x86_64. Таким образом, структура thread_info будет помещена в структуру task_struct вместо объединения thread_union.
init_thread_union расположен в файле include/asm-generic/vmlinux.lds.h как часть макроса INIT_TASK_DATA:
#define INIT_TASK_DATA(align) \
... \
init_thread_union = .; \
...
Данный макрос используется в arch/x86/kernel/vmlinux.lds.S следующим образом:
.data : AT(ADDR(.data) - LOAD_OFFSET) {
...
INIT_TASK_DATA(THREAD_SIZE)
...
} :data
Теперь мы можем понять это выражение:
GLOBAL(initial_stack)
.quad init_thread_union + THREAD_SIZE - SIZEOF_PTREGS
где символ initial_stack указывает на начало массива thread_union.stack + THREAD_SIZE, который равен 16 килобайтам и - SIZEOF_PTREGS, который является соглашением, помогающим unwinder'у ядра надёжно обнаруживать конец стека.
После настройки начального загрузочного стека, необходимо обновить глобальную таблицу дескрипторов с помощью инструкции lgdt:
lgdt early_gdt_descr(%rip)
где early_gdt_descr определён как:
early_gdt_descr:
.word GDT_ENTRIES*8-1
early_gdt_descr_base:
.quad INIT_PER_CPU_VAR(gdt_page)
Это необходимо, поскольку теперь ядро работает в нижних адресах пользовательского пространства, но вскоре ядро будет работать в своём собственном пространстве.
Теперь давайте посмотрим на определение early_gdt_descr. Макрос GDT_ENTRIES раскрывается до 32, поэтому глобальная таблица дескрипторов содержит 32 записи для кода ядра, данных, сегментов локального хранилища потоков и т.д.
Теперь давайте посмотрим на определение early_gdt_descr_base. Структура gdt_page определена в arch/x86/include/asm/desc.h:
struct gdt_page {
struct desc_struct gdt[GDT_ENTRIES];
} __attribute__((aligned(PAGE_SIZE)));
Она содержит одно поле gdt, которое является массивом структур desc_struct:
struct desc_struct {
union {
struct {
unsigned int a;
unsigned int b;
};
struct {
u16 limit0;
u16 base0;
unsigned base1: 8, type: 4, s: 1, dpl: 2, p: 1;
unsigned limit: 4, avl: 1, l: 1, d: 1, g: 1, base2: 8;
};
};
} __attribute__((packed));
который выглядит знакомым дескриптором GDT. Можно отметить, что структура gdt_page выровнена по PAGE_SIZE, равному 4096 байтам. Это значит, что gdt займёт одну страницу.
Теперь попробуем понять, что такое INIT_PER_CPU_VAR. INIT_PER_CPU_VAR это макрос, определённый в arch/x86/include/asm/percpu.h, который просто совершает конкатенацию init_per_cpu__ с заданным параметром:
#define INIT_PER_CPU_VAR(var) init_per_cpu__##var
После того, как макрос INIT_PER_CPU_VAR будет раскрыт, мы будем иметь init_per_cpu__gdt_page. Мы можем видеть инициализацию init_per_cpu__gdt_page в скрипте компоновщика:
#define INIT_PER_CPU(x) init_per_cpu__##x = x + __per_cpu_load
INIT_PER_CPU(gdt_page);
После того как макросы INIT_PER_CPU_VAR и INIT_PER_CPU будут раскрыты до init_per_cpu__gdt_page мы получим смещение от __per_cpu_load. После этих расчётов мы получим корректный базовый адрес нового GDT.
Переменные, локальные для каждого процессора (per-CPU variables), являются особенностью ядра версии 2.6. Вы уже можете понять что это, исходя из названия. Когда мы создаём per-CPU переменную, каждый процессор будет иметь свою собственную копию этой переменной. Здесь мы создаём per-CPU переменную gdt_page. Существует много преимуществ для переменных этого типа, например, нет блокировок, поскольку каждый процессор работает со своей собственной копией переменной и т.д. Таким образом, каждое ядро на многопроцессорной машине будет иметь свою собственную таблицу GDT и каждая запись в таблице будет представлять сегмент памяти, к которому можно получить доступ из потока, который запускался на ядре. Подробнее о per-CPU переменных можно почитать в статье Concepts/linux-cpu-1.
После загрузки новой глобальной таблицы дескрипторов мы перезагружаем сегменты:
xorl %eax,%eax
movl %eax,%ds
movl %eax,%ss
movl %eax,%es
movl %eax,%fs
movl %eax,%gs
После всех этих шагов мы настраиваем регистр gs, указывающий на irqstack, который представляет собой специальный стек для обработки прерываний:
movl $MSR_GS_BASE,%ecx
movl initial_gs(%rip),%eax
movl initial_gs+4(%rip),%edx
wrmsr
где MSR_GS_BASE:
#define MSR_GS_BASE 0xc0000101
Нам необходимо поместить MSR_GS_BASE в регистр ecx и загрузить данные из eax и edx (которые указывают на initial_gs) с помощью инструкции wrmsr. Мы не используем регистры сегментов cs, fs, ds и ss для адресации в 64-битном режиме, но могут использоваться регистры fs и gs. fs и gs имеют скрытую часть (как мы видели в режиме реальных адресов для cs) и эта часть содержит дескриптор, который отображён на моделезависимый регистр. Таким образом, выше мы можем видеть 0xc0000101 - это MSR-адрес gs.base. В точке входа нет стека ядра, поэтому когда происходит системный вызов или прерывание, значение MSR_GS_BASE будет хранить адрес стека прерываний.
На следующем шаге мы помещаем адрес структуры параметров загрузки режима реальных адресов в регистр rdi (напомним, что rsi содержит указатель на эту структуру с самого начала) и переходим к коду на C:
pushq $.Lafter_lret # поиещает адрес возврата в стек для unwinder'а
xorq %rbp, %rbp # очищает указатель фрейма
movq initial_code(%rip), %rax
pushq $__KERNEL_CS # устанавливает корректный cs
pushq %rax # целевой адрес в отрицательном пространстве
lretq
.Lafter_lret:
Здесь мы помещаем адрес initial_code в rax и помещаем возвращаемый адрес __KERNEL_CS и адрес initial_code в стек. После этого мы видим инструкцию lretq, означающую что после неё адрес возврата будет извлечён из стека (теперь это адрес initial_code) и будет совершён переход по нему. initial_code определён в том же файле исходного кода и выглядит следующим образом:
.balign 8
GLOBAL(initial_code)
.quad x86_64_start_kernel
...
...
...
Как мы видим initial_code содержит адрес x86_64_start_kernel, определённой в arch/x86/kerne/head64.c:
asmlinkage __visible void __init x86_64_start_kernel(char * real_mode_data)
{
...
...
...
}
У неё есть один аргумент - real_mode_data (помните, ранее мы помещали адрес данных режима реальных адресов в регистр rdi).
Далее в start_kernel
Мы увидим последние приготовления, прежде чем сможем перейти к "точке входа в ядро" - к функции start_kernel в файле init/main.c.
Прежде всего в функции x86_64_start_kernel мы видим некоторый проверки:
BUILD_BUG_ON(MODULES_VADDR < __START_KERNEL_map);
BUILD_BUG_ON(MODULES_VADDR - __START_KERNEL_map < KERNEL_IMAGE_SIZE);
BUILD_BUG_ON(MODULES_LEN + KERNEL_IMAGE_SIZE > 2*PUD_SIZE);
BUILD_BUG_ON((__START_KERNEL_map & ~PMD_MASK) != 0);
BUILD_BUG_ON((MODULES_VADDR & ~PMD_MASK) != 0);
BUILD_BUG_ON(!(MODULES_VADDR > __START_KERNEL));
MAYBE_BUILD_BUG_ON(!(((MODULES_END - 1) & PGDIR_MASK) == (__START_KERNEL & PGDIR_MASK)));
BUILD_BUG_ON(__fix_to_virt(__end_of_fixed_addresses) <= MODULES_END);
например, виртуальный адрес пространства модуля не меньше, чем базовый адрес кода ядра (__STAT_KERNEL_map), код ядра с модулями не меньше образа ядра и т.д. BUILD_BUG_ON является макросом и выглядит следующим образом:
#define BUILD_BUG_ON(condition) ((void)sizeof(char[1 - 2*!!(condition)]))
Давайте попробуем понять, как работает этот трюк. Возьмём, например, первое условие: MODULES_VADDR < __START_KERNEL_map. !!conditions тоже самое что и condition != 0. Таким образом, если MODULES_VADDR < __START_KERNEL_map истинно, мы получим 1 в !!(condition) или ноль, если ложно. После 2*!!(condition) мы получим или 2 или 0. В конце вычислений мы можем получить два разных поведения:
- У нас будет ошибка компиляции, поскольку мы попытаемся получить размер
charмассива с отрицательным индексом (вполне возможно, но в нашем случаеMODULES_VADDRне может быть меньше__START_KERNEL_map); - Ошибки компиляции не будет.
На этом всё. Очень интересный C-трюк для получения ошибки компиляции, которая зависит от некоторых констант.
На следующем шаге мы видим вызов функции cr4_init_shadow, которая сохраняет копии cr4 для каждого процессора. Переключения контекста могут изменять биты в cr4, поэтому нам нужно сохранить cr4 для каждого процессора. После этого происходит вызов функции reset_early_page_tables, которая сбрасывает все записи глобального каталога страниц и записывает новый указатель на PGT в cr3:
memset(early_top_pgt, 0, sizeof(pgd_t)*(PTRS_PER_PGD-1));
next_early_pgt = 0;
write_cr3(__sme_pa_nodebug(early_top_pgt));
Вскоре мы создадим новые таблицы страниц. Далее в цикле мы обнуляем все записи глобального каталога страниц. После этого мы устанавливаем next_early_pgt в ноль (подробнее об этом в следующей статье) и записываем физический адрес early_top_pgt в cr3.
После этого мы очищаем _bss от __bss_stop до __bss_start, а также init_top_pgt. init_top_pgt определён в arch/x86/kerne/head_64.S:
NEXT_PGD_PAGE(init_top_pgt)
.fill 512,8,0
.fill PTI_USER_PGD_FILL,8,0
Это то же самое определение, что и early_top_pgt.
Следующим шагом будет настройка начальных обработчиков IDT. Это большой раздел, поэтому мы увидим его в следующей статье.
Заключение
Это конец первой части об инициализации ядра Linux.
В следующей части мы увидим инициализацию начальных обработчиков прерываний, отображение памяти пространства ядра и многое другое.
От переводчика: пожалуйста, имейте в виду, что английский - не мой родной язык, и я очень извиняюсь за возможные неудобства. Если вы найдёте какие-либо ошибки или неточности в переводе, пожалуйста, пришлите pull request в linux-insides-ru.