Процесс загрузки ядра. Часть 6.

Введение

Это шестая часть серии Процесса загрузки ядра. В предыдущей части мы увидели конец процесса загрузки ядра. Но мы пропустили некоторые важные дополнительные детали.

Как вы помните, точкой входа ядра Linux является функция start_kernel из файла main.c, которая начинает выполнение по адресу LOAD_PHYSICAL_ADDR. Этот адрес зависит от параметра конфигурации ядра CONFIG_PHYSICAL_START, который по умолчанию равен 0x1000000:

config PHYSICAL_START
    hex "Physical address where the kernel is loaded" if (EXPERT || CRASH_DUMP)
    default "0x1000000"
    ---help---
      This gives the physical address where the kernel is loaded.
      ...
      ...
      ...

Это значение может быть изменено во время конфигурации ядра, но также может быть выбрано случайно. Для этого во время конфигурации ядра должна быть включена опция CONFIG_RANDOMIZE_BASE.

В этом случае будет рандомизирован физический адрес, по которому будет загружен и распакован образ ядра Linux. В этой части рассматривается случай, когда эта опция включена и адрес загрузки образа ядра будет рандомизирован из соображений безопасности.

Инициализация таблиц страниц

Перед тем как декомпрессор ядра начнёт поиск случайного адреса из диапазона, по которому ядро будет распаковано и загружено, таблицы страниц, отображённые 1:1 (identity mapped page tables), должны быть инициализированы. Если загрузчик использует 16-битный или 32-битный протокол загрузки, у нас уже есть таблицы страниц. Но в любом случае нам могут понадобиться новые страницы по требованию, если декомпрессор ядра выберет диапазон памяти за их пределами. Вот почему нам нужно создать новые таблицы таблиц, отображённые 1:1.

Да, создание таблиц является одним из первых шагов во время рандомизации адреса загрузки. Но прежде чем мы это рассмотрим, давайте попробуем вспомнить, откуда мы пришли к этому вопросу.

В предыдущей части, мы увидели переход в long mode и переход к точке входа декомпрессора ядра - функции extract_kernel. Рандомизация начинается с вызова данной функции:

void choose_random_location(unsigned long input,
                            unsigned long input_size,
                            unsigned long *output,
                            unsigned long output_size,
                            unsigned long *virt_addr)
{}

Как мы можем видеть, эта функция принимает следующие пять параметров:

  • input;
  • input_size;
  • output;
  • output_isze;
  • virt_addr.

Попытаемся понять что это за параметры. Первый параметр, input, поступает из параметров функции extract_kernel, расположенной в файле arch/x86/boot/compressed/misc.c:

asmlinkage __visible void *extract_kernel(void *rmode, memptr heap,
                                          unsigned char *input_data,
                                          unsigned long input_len,
                                          unsigned char *output,
                                          unsigned long output_len)
{
  ...
  ...
  ...
  choose_random_location((unsigned long)input_data, input_len,
                         (unsigned long *)&output,
                         max(output_len, kernel_total_size),
                         &virt_addr);
  ...
  ...
  ...
}

Этот параметр передаётся из кода ассемблера:

leaq    input_data(%rip), %rdx

в файле arch/x86/boot/compressed/head_64.S. input_data генерируется маленькой программой mkpiggy. Если вы компилировали ядро Linux своими руками, вы можете найти сгенерированный этой программой файл, расположенный в linux/arch/x86/boot/compressed/piggy.S. В моём случае этот файл выглядит так:

.section ".rodata..compressed","a",@progbits
.globl z_input_len
z_input_len = 6988196
.globl z_output_len
z_output_len = 29207032
.globl input_data, input_data_end
input_data:
.incbin "arch/x86/boot/compressed/vmlinux.bin.gz"
input_data_end:

Как вы можете видеть, он содержит четыре глобальных символа. Первые два, z_input_len и z_output_len, являются размерами сжатого и несжатого vmlinux.bin.gz. Третий - это наш input_data и он указывает на образ ядра Linux в бинарном формате (все отладочные символы, комментарии и информация о релокации удаляются). И последний, input_data_end, указывает на конец сжатого образа ядра.

Таким образом, наш первый параметр функции choose_random_location является указателем на сжатый образ ядра, встроенный в объектный файл piggy.o.

Второй параметр функции choose_random_location - z_input_len, который мы уже видели.

Третий и четвёртый параметры функции choose_random_location - это адрес, по которому размещено распакованное ядро и размер образа распакованного ядра. Адрес, по которому будет размещён образ ядра, получен из arch/x86/boot/compressed/head_64.S и это адрес startup_32, выровненный по границе 2 мегабайт. Размер распакованного ядра также получен из piggy.S, как и z_output_len.

Последним параметром функции choose_random_location является виртуальный адрес физического адреса загрузки ядра. По умолчанию он совпадает с физическим адресом загрузки по умолчанию:

unsigned long virt_addr = LOAD_PHYSICAL_ADDR;

который зависит от конфигурации ядра:

#define LOAD_PHYSICAL_ADDR ((CONFIG_PHYSICAL_START \
                + (CONFIG_PHYSICAL_ALIGN - 1)) \
                & ~(CONFIG_PHYSICAL_ALIGN - 1))

Теперь посмотрим на реализацию функции choose_random_location. Она начинается с проверки опции nokaslr из командной строки ядра:

if (cmdline_find_option_bool("nokaslr")) {
    warn("KASLR disabled: 'nokaslr' on cmdline.");
    return;
}

и если параметр установлен, choose_random_location завершает свою работу и адрес загрузки ядра не будет рандомизрован. Связанные параметры командной строки можно найти в документации ядра:

kaslr/nokaslr [X86]

Включение/выключение базового смещения ASLR ядра и модуля
(рандомизация размещения адресного пространства), если оно встроено в ядро.
Если выбран CONFIG_HIBERNATION, kASLR отключён по умолчанию.
Если kASLR включён, спящий режим будет выключен.

Предположим, что мы не передали nokaslr в командную строку ядра, а также включён параметр конфигурации ядра CONFIG_RANDOMIZE_BASE. В этом случае мы добавляем флаг kASLR к флагам загрузки ядра:

boot_params->hdr.loadflags |= KASLR_FLAG;

и следующим шагом является вызов функции:

initialize_identity_maps();

расположенной в файле arch/x86/boot/compressed/kaslr_64.c. Эта функция начинается с инициализации экземпляра структуры x86_mapping_info:

mapping_info.alloc_pgt_page = alloc_pgt_page;
mapping_info.context = &pgt_data;
mapping_info.page_flag = __PAGE_KERNEL_LARGE_EXEC | sev_me_mask;
mapping_info.kernpg_flag = _KERNPG_TABLE;

Определение структуры x86_mapping_info расположено в файле arch/x86/include/asm/init.h:

struct x86_mapping_info {
    void *(*alloc_pgt_page)(void *);
    void *context;
    unsigned long page_flag;
    unsigned long offset;
    bool direct_gbpages;
    unsigned long kernpg_flag;
};

Эта структура предоставляет информацию об отображениях памяти. Как вы помните из предыдущей части, мы уже настроили начальные страницы с 0 до 4G. На данный момент нам может потребоваться доступ к памяти выше 4G для загрузки ядра в случайном месте. Таким образом, функция initialize_identity_maps выполняет инициализацию области памяти для возможной новой таблицы страниц. Прежде всего, давайте взглянем на определение структуры x86_mapping_info.

alloc_pgt_page - это функция обратного вызова, которая будет вызываться для выделения пространства под запись в таблице страниц. Поле context является экземпляром структурыalloc_pgt_data, которая в нашем случае будет использоваться для отслеживания выделенных таблиц страниц. Поля page_flag иkernpg_flag являются флагами страниц. Первый представляет флаги для записей PMD или PUD. Второе поле kernpg_flag представляет флаги для страниц ядра, которые позже можно переопределить. Поле direct_gbpages представляет поддержку больших страниц, а последнее поле, offset представляет смещение между виртуальными адресами ядра и физическими адресами до уровня PMD.

alloc_pgt_page просто проверяет, есть ли место для новой страницы, и выделяет новую страницу:

entry = pages->pgt_buf + pages->pgt_buf_offset;
pages->pgt_buf_offset += PAGE_SIZE;

в буфере из структуры:

struct alloc_pgt_data {
    unsigned char *pgt_buf;
    unsigned long pgt_buf_size;
    unsigned long pgt_buf_offset;
};

и возвращает адрес новой страницы. Последняя цель функции initialize_identity_maps заключается в инициализации pgdt_buf_size иpgt_buf_offset. Поскольку мы только в фазе инициализации, функция initialze_identity_maps устанавливаетpgt_buf_offset в ноль:

pgt_data.pgt_buf_offset = 0;

и pgt_data.pgt_buf_size будет установлен в 77824 или 69632 в зависимости от того, какой протокол загрузки использует загрузчик (64-битный или 32-битный). Тоже самое и для pgt_data.pgt_buf. Если загрузчик загрузил ядро в startup_32, pgdt_data.pgdt_buf укажет на на конец таблицы страниц, которая уже была инициализирована в arch/x86/boot/compressed/head_64.S:

pgt_data.pgt_buf = _pgtable + BOOT_INIT_PGT_SIZE;

где _pgtable указывает на начало этой таблицы страниц _pgtable. В случае, если загрузчик использовал 64-битный протокол загрузки и загрузил ядро в startup_64, ранние таблицы страниц должны быть созданы самим загрузчиком и _pgtable будет просто перезаписан:

pgt_data.pgt_buf = _pgtable

После инициализации буфера для новых таблиц страниц мы можем вернуться к функции select_random_location.

Избежание зарезервированных диапазонов памяти

После того как таблицы страниц, отображённые 1:1, инициализированы, мы можем начать выбор случайного местоположения, по которому мы поместим распакованный образ ядра. Но, как вы можете догадаться, мы не можем выбрать абсолютно любой адрес. Существует зарезервированные области памяти. Эти адреса занимают некоторые важные вещи, например, initrd, командная строка ядра и т.д. Функция

mem_avoid_init(input, input_size, *output);

поможет нам это сделать. Все небезопасные области памяти будут собраны в массив:

struct mem_vector {
    unsigned long long start;
    unsigned long long size;
};

static struct mem_vector mem_avoid[MEM_AVOID_MAX];

Где MEM_AVOID_MAX находится в перечислении mem_avoid_index, который представляет собой различные типы зарезервированных областей памяти:

enum mem_avoid_index {
    MEM_AVOID_ZO_RANGE = 0,
    MEM_AVOID_INITRD,
    MEM_AVOID_CMDLINE,
    MEM_AVOID_BOOTPARAMS,
    MEM_AVOID_MEMMAP_BEGIN,
    MEM_AVOID_MEMMAP_END = MEM_AVOID_MEMMAP_BEGIN + MAX_MEMMAP_REGIONS - 1,
    MEM_AVOID_MAX,
};

Оба расположены в файле arch/x86/boot/compressed/kaslr.c.

Давайте посмотрим на реализацию функции mem_avoid_init. Основная цель этой функции - хранить информацию о зарезервированных областях памяти, описанных в перечислении mem_avoid_index в массивеmem_avoid, и создавать новые страницы для таких областей в нашем новом буфере, отображённом 1:1. Многочисленные части для функции mem_avoid_index аналогичны, давайте посмотрим на одну из них:

mem_avoid[MEM_AVOID_ZO_RANGE].start = input;
mem_avoid[MEM_AVOID_ZO_RANGE].size = (output + init_size) - input;
add_identity_map(mem_avoid[MEM_AVOID_ZO_RANGE].start,
         mem_avoid[MEM_AVOID_ZO_RANGE].size);

В начале функция mem_avoid_init пытается избежать области памяти, которая используется для текущей декомпрессии ядра. Мы заполняем запись из массива mem_avoid с указанием начала и размера такой области и вызываем функцию add_identity_map, которая должна создать страницы, отображённые 1:1, для этого региона. Функция add_identity_map определена в файле arch/x86/boot/compressed/kaslr_64.c:

void add_identity_map(unsigned long start, unsigned long size)
{
    unsigned long end = start + size;

    start = round_down(start, PMD_SIZE);
    end = round_up(end, PMD_SIZE);
    if (start >= end)
        return;

    kernel_ident_mapping_init(&mapping_info, (pgd_t *)top_level_pgt,
                  start, end);
}

Как мы можем видеть, она выравнивает область памяти по границе 2 мегабайт и проверяет заданные начальные и конечные адреса.

В конце она вызывает функцию kernel_ident_mapping_init из файла arch/x86/mm/ident_map.c и передаёт экземпляр mapping_info, который мы инициализировали ранее, адрес таблицы страниц верхнего уровня и адреса области памяти, для которой необходимо создать новое отображение 1:1.

Функция kernel_ident_mapping_init устанавливает флаги по умолчанию для новых страниц, если они не были заданы:

if (!info->kernpg_flag)
    info->kernpg_flag = _KERNPG_TABLE;

и начинает создание 2 мегабайтных (из-за бита PSE в mapping_info.page_flag) страничных записей (PGD -> P4D -> PUD -> PMD в случае пятиуровневых таблиц страниц или PGD -> PUD -> PMD в случае четырёхуровневых таблиц страниц), относящихся к указанным адресам.

for (; addr < end; addr = next) {
    p4d_t *p4d;

    next = (addr & PGDIR_MASK) + PGDIR_SIZE;
    if (next > end)
        next = end;

    p4d = (p4d_t *)info->alloc_pgt_page(info->context);
    result = ident_p4d_init(info, p4d, addr, next);

    return result;
}

Прежде всего, мы находим следующую запись глобального каталога страниц для данного адреса, и если она больше, чем end данной области памяти, мы устанавливаем её в end. После этого мы выделяем новую страницу с нашим обратным вызовом x86_mapping_info, который мы уже рассмотрели выше, и вызываем функциюident_p4d_init. Функция ident_p4d_init будет делать то же самое, но для низкоуровневых каталогов страниц (p4d -> pud ->pmd).

На этом всё.

Новые страницы, связанные с зарезервированными адресами, находятся в наших таблицах страниц. Это не конец функции mem_avoid_init, но другие части схожи. Они просто создают страницы для initrd, командной строки ядра и т.д.

Теперь мы можем вернуться к функции choose_random_location.

Рандомизация физического адреса

После сохранения зарезервированных областей памяти в массиве mem_avoid и создания для них страниц, отображённых 1:1, мы выбираем минимальный доступный адрес для произвольного выбора области памяти:

min_addr = min(*output, 512UL << 20);

Он должен быть меньше чем 512 мегабайт. Значение 512 мегабайт было выбрано для того, чтобы избежать неизвестных вещей в нижней части памяти.

Следующим шагом будет выбор случайных физических и виртуальных адресов для загрузки ядра. Сначала физические адреса:

random_addr = find_random_phys_addr(min_addr, output_size);

Функция find_random_phys_addr определена в том же файле:

static unsigned long find_random_phys_addr(unsigned long minimum,
                                           unsigned long image_size)
{
    minimum = ALIGN(minimum, CONFIG_PHYSICAL_ALIGN);

    if (process_efi_entries(minimum, image_size))
        return slots_fetch_random();

    process_e820_entries(minimum, image_size);
    return slots_fetch_random();
}

Основная задача process_efi_entries - найти все подходящие диапазоны памяти в доступной для загрузки ядра памяти. Если ядро скомпилировано и запущено на системе без поддержки EFI, поиск областей памяти продолжиться в регионах e820. Все найденные области памяти будут сохранены в массиве:

struct slot_area {
    unsigned long addr;
    int num;
};

#define MAX_SLOT_AREA 100

static struct slot_area slot_areas[MAX_SLOT_AREA];

Для декомпрессии ядро выберет случайный индекс из этого массива. Этот выбор будет выполнен функцией slots_fetch_random. Основная задача функции slots_fetch_random заключается в выборе случайного диапазона памяти из массива slot_areas с помощью функции kaslr_get_random_long:

slot = kaslr_get_random_long("Physical") % slot_max;

Функция kaslr_get_random_long определена в файле arch/x86/lib/kaslr.c и просто возвращает случайное число. Обратите внимание, что случайное число будет получено разными способами, зависящими от конфигурации ядра (выбор случайного числа, основываясь на счётчике времени, rdrand и т.д.).

Рандомизация виртуального адреса

После того как декомпрессором ядра была выбрана случайная область памяти, для неё будут созданы новые страницы, отображённые 1:1

random_addr = find_random_phys_addr(min_addr, output_size);

if (*output != random_addr) {
        add_identity_map(random_addr, output_size);
        *output = random_addr;
}

После этого output будет хранить базовый адрес области памяти, где будет распаковано ядро. Но на данный момент, как вы помните, мы рандомизировали только физический адрес. В случае архитектуры x86_64 виртуальный адрес также должен быть рандомизирован:

if (IS_ENABLED(CONFIG_X86_64))
    random_addr = find_random_virt_addr(LOAD_PHYSICAL_ADDR, output_size);

*virt_addr = random_addr;

В архитектуре, отличной от x86_64, случайный виртуальный адрес будет совпадать со случайным физическим. Функция find_random_virt_addr вычисляет количество диапазонов виртуальной памяти, которые могут содержать образ ядра, и вызывает kaslr_get_random_long, которую мы уже видели ранее, когда пытались найти случайный физический адрес.

Теперь мы имеет как физические базовые случайные адреса (*output), так и виртуальные (*virt_addr) случайные адреса для декомпрессии ядра.

На этом всё.

Заключение

Это конец шестой и последней части процесса загрузки ядра Linux. Мы больше не увидим статей о загрузке ядра (возможны обновления этой и предыдущих статей), но будет много статей о других внутренних компонентах ядра.

Следующая глава посвящена инициализации ядра, и мы увидим первые шаги в коде инициализации ядра Linux.

От переводчика: пожалуйста, имейте в виду, что английский - не мой родной язык, и я очень извиняюсь за возможные неудобства. Если вы найдёте какие-либо ошибки или неточности в переводе, пожалуйста, пришлите pull request в linux-insides-ru.

Ссылки

results matching ""

    No results matching ""