Инициализация ядра. Часть 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.

Ссылки

results matching ""

    No results matching ""