From af83d52f1efdca77aca09b5dc9af7e3a941c054a Mon Sep 17 00:00:00 2001 From: proninyaroslav Date: Sat, 25 Nov 2017 22:11:47 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D1=87=D0=B0=D1=81=D1=82=D0=B5=D0=B9=20'?= =?UTF-8?q?=D0=9F=D1=80=D0=BE=D1=86=D0=B5=D1=81=D1=81=D0=B0=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B8=20=D1=8F=D0=B4=D1=80=D0=B0?= =?UTF-8?q?'=20=D0=B4=D0=BE=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=D0=B4=D0=BD?= =?UTF-8?q?=D0=B5=D0=B3=D0=BE=20=D1=8F=D0=B4=D1=80=D0=B0=20=D0=B8=D0=B7=20?= =?UTF-8?q?upstream?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Booting/linux-bootstrap-4.md | 70 ++++++++++----- Booting/linux-bootstrap-5.md | 168 ++++++++++++----------------------- 2 files changed, 106 insertions(+), 132 deletions(-) diff --git a/Booting/linux-bootstrap-4.md b/Booting/linux-bootstrap-4.md index 2c14312..3c6ba58 100644 --- a/Booting/linux-bootstrap-4.md +++ b/Booting/linux-bootstrap-4.md @@ -134,11 +134,11 @@ Be careful parts of head_64.S assume startup_32 is at address 0. Протокол: 2.07+ - Если 0, перезагрузить регистры сегмента в 32-битной точке входа. - Если 1, не перезагружать регистры сегмента в 32-битной точке входа. - Предполагается, что %cs %ds %ss %es установлены в плоские сегменты + Предполагается, что %cs %ds %ss %es установлены в плоские сегменты с базовым адресом 0 (или эквивалент для их среды). ``` -Таким образом, если бит `KEEP_SEGMENTS` в `loadflags` не установлен, то сегментные регистры `ds`, `ss` и `es` должны быть сброшены в плоский сегмент с базовым адресом `0`. Что мы и делаем: +Таким образом, если бит `KEEP_SEGMENTS` в `loadflags` не установлен, то сегментные регистры `ds`, `ss` и `es` должны быть установлены в индекс сегмента данных с базовым адресом `0`. Что мы и делаем: ```C testb $(1 << 6), BP_loadflags(%esi) @@ -173,7 +173,7 @@ call label label: pop %reg ``` -После этого регистр будет содержать адрес метки. Давайте посмотрим на аналогичный код поиска адреса `startup_32` в ядре Linux: +После этого регистр `%reg` будет содержать адрес метки. Давайте посмотрим на аналогичный код поиска адреса `startup_32` в ядре Linux: ```assembly leal (BP_scratch+4)(%esi), %esp @@ -182,7 +182,7 @@ label: pop %reg subl $1b, %ebp ``` -Как вы помните из предыдущей части, регистр `esi` содержит адрес структуры [boot_params](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/include/uapi/asm/bootparam.h#L113), которая была заполнена до перехода в защищённый режим. Структура `boot_params` содержит специальное поле `scratch` со смещением `0x1e4`. Это 4 байтное поле будет временным стеком для инструкции `call`. Мы получаем адрес поля `scratch` + 4 байта и помещаем его в регистр `esp`. Мы добавили `4` байта к базовому адресу поля `BP_scratch`, поскольку поле является временным стеком, а стек на архитектуре `x86_64` растёт сверху вниз. Таким образом, наш указатель стека будет указывать на вершину стека. Далее мы видим наш шаблон, который я описал ранее. Мы переходим на метку `1f` и помещаем её адрес в регистр `ebp`, потому что после выполнения инструкции `call` на вершине стека находится адрес возврата. Теперь у нас есть адрес метки `1f` и мы легко сможем получить адрес `startup_32`. Нам просто нужно вычесть адрес метки из адреса, который мы получили из стека: +Как вы помните из предыдущей части, регистр `esi` содержит адрес структуры [boot_params](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/include/uapi/asm/bootparam.h#L113), которая была заполнена до перехода в защищённый режим. Структура `boot_params` содержит специальное поле `scratch` со смещением `0x1e4`. Это 4 байтное поле будет временным стеком для инструкции `call`. Мы получаем адрес поля `scratch` + `4` байта и помещаем его в регистр `esp`. Мы добавили `4` байта к базовому адресу поля `BP_scratch`, поскольку поле является временным стеком, а стек на архитектуре `x86_64` растёт сверху вниз. Таким образом, наш указатель стека будет указывать на вершину стека. Далее мы видим наш шаблон, который я описал ранее. Мы переходим на метку `1f` и помещаем её адрес в регистр `ebp`, потому что после выполнения инструкции `call` на вершине стека находится адрес возврата. Теперь у нас есть адрес метки `1f` и мы легко сможем получить адрес `startup_32`. Нам просто нужно вычесть адрес метки из адреса, который мы получили из стека: ``` startup_32 (0x0) +-----------------------+ @@ -235,10 +235,14 @@ gs 0x18 0x18 Если мы выполним следующую инструкцию, `subl $1b, %ebp`, мы увидим следующее: ``` -nexti +(gdb) nexti +... +... ... ebp 0x100000 0x100000 ... +... +... ``` Да, всё верно. Адрес `startup_32` равен `0x100000`. После того как мы узнали адрес метки `startup_32`, мы можем начать подготовку к переходу в [long mode](https://en.wikipedia.org/wiki/Long_mode). Наша следующая цель - настроить стек и убедится в том, что CPU поддерживает long mode и [SSE](http://en.wikipedia.org/wiki/Streaming_SIMD_Extensions). @@ -300,8 +304,8 @@ no_longmode: Это создает образ ядра, который сохраняет информацию о релокации поэтому он может быть загружен где-либо, кроме стандартного 1 Мб. -Примечание: Если CONFIG_RELOCATABLE=y, то ядро запускается с адреса, -на который он был загружен, а физический адрес времени компиляции +Примечание: Если CONFIG_RELOCATABLE=y, то ядро запускается с адреса, +на который он был загружен, а физический адрес времени компиляции (CONFIG_PHYSICAL_START) используется как минимальная локация. ``` @@ -339,20 +343,29 @@ KBUILD_CFLAGS += -fno-strict-aliasing -fPIC Как мы можем видеть, он просто расширяет адрес до значения выравнивания `CONFIG_PHYSICAL_ALIGN` и представляет собой физический адрес, по которому будет загружено ядро. После сравнения `LOAD_PHYSICAL_ADDR` и значения регистра `ebx`, мы добавляем смещение от `startup_32`, по которому будет происходить декомпрессия образа ядра. Если во время компиляции параметр `CONFIG_RELOCATABLE` не включён, мы просто помещаем адрес по умолчанию и добавляем к нему `z_extract_offset`. -После всех расчётов у нас в распоряжении `ebp`, содержащий адрес, по которому будет происходить загрузка, и `ebx`, содержащий адрес, по которому ядро будет перемещено после декомпрессии. +После всех расчётов у нас в распоряжении `ebp`, содержащий адрес, по которому будет происходить загрузка, и `ebx`, содержащий адрес, по которому ядро будет перемещено после декомпрессии. Но это еще не конец. Сжатый образ ядра должен быть перемещён в конец буфера декомпрессии, чтобы упростить вычисления местоположения, по которому ядро будет расположено позже: + +```assembly +movl BP_init_size(%esi), %eax +subl $_end, %eax +addl %eax, %ebx +``` + +мы помещаем значение из `boot_params.BP_init_size` (или значение заголовка настройки ядра из `hdr.init_size`) в регистр `eax`. `BP_init_size` содержит наибольшее значение между сжатым и распакованным [vmlinux](https://en.wikipedia.org/wiki/Vmlinux). Затем мы вычитаем адрес символа `_end` из этого значения и добавляем результат вычитания в регистр `ebx`, который хранит базовый адрес для декомпрессии ядра. Подготовка перед входом в long mode -------------------------------------------------------------------------------- -Теперь, когда у нас есть базовый адрес, на который мы будем перемещать сжатое ядро, нам необходимо сделать последний шаг, прежде чем мы сможем перейти в 64-битный режим. Во-первых, нам необходимо обновить [глобальную таблицу дескрипторов](https://en.wikipedia.org/wiki/Global_Descriptor_Table): +Теперь, когда у нас есть базовый адрес, на который мы будем перемещать сжатое ядро, нам необходимо сделать последний шаг, прежде чем мы сможем перейти в 64-битный режим. Во-первых, нам необходимо обновить [глобальную таблицу дескрипторов](https://en.wikipedia.org/wiki/Global_Descriptor_Table) с 64-битными сегментами, потому что перемещаемое ядро может быть запущено по любому адресу ниже 512 Гб: ```assembly - leal gdt(%ebp), %eax - movl %eax, gdt+2(%ebp) + addl %ebp, gdt+2(%ebp) lgdt gdt(%ebp) ``` -Здесь мы помещаем базовый адрес из регистра `ebp` со смещением `gdt` в регистр `eax`. Далее мы помещаем этот адрес в регистр `ebp` со смещением `gdt+2` и загружаем `глобальную таблицу дескрипторов` с помощью инструкции `lgdt`. Чтобы понять магию смещений `gdt`, нам необходимо посмотреть на определение `глобальной таблицы дескрипторов`. Мы можем найти его в этом же [файле](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/head_64.S) исходного кода: +Здесь мы настраиваем базовый адрес `глобальной таблицы дескрипторов` на адрес, где мы фактически загружены, и загружаем таблицу с помощью инструкции `lgdt`. + +Чтобы понять магию смещений `gdt`, нам нужно взглянуть на определение `глобальной таблицы дескрипторов`. Мы можем найти его определение в том же [файле](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/head_64.S) исходного кода: ```assembly .data @@ -360,7 +373,7 @@ gdt: .word gdt_end - gdt .long gdt .word 0 - .quad 0x0000000000000000 /* Нулевой дескриптор */ + .quad 0x00cf9a000000ffff /* __KERNEL32_CS *// .quad 0x00af9a000000ffff /* __KERNEL_CS */ .quad 0x00cf92000000ffff /* __KERNEL_DS */ .quad 0x0080890000000000 /* Дескриптор TS */ @@ -368,14 +381,11 @@ gdt: gdt_end: ``` -Мы видим, что она расположена в секции `.data` и содержит пять дескрипторов: нулевой дескриптор, сегмент кода ядра, сегмент данных ядра и два дескриптора задач. Мы уже загрузили `глобальную таблицу дескрипторов` в предыдущей [части](linux-bootstrap-3.md), и теперь мы делаем почти то же самое здесь, но теперь дескрипторы с `CS.L = 1` и `CS.D = 0` для выполнения в 64-битном режиме. Как мы видим, определение `gdt` начинается с двух байт: `gdt_end - gdt`, который представляет последний байт `gdt` или лимит таблицы. Следующие 4 байта содержат базовый адрес `gdt`. Вы должны помнить, что `глобальная таблица дескрипторов` хранится в 48-битном `GDTR`, который состоит из двух частей: +Мы видим, что она расположена в секции `.data` и содержит пять дескрипторов: `32-битный` дескриптор для сегмента кода ядра, `64-битный` сегмент ядра, сегмент данных ядра и два дескриптора задач. -* размер (16-бита) глобальной таблицы дескрипторов; -* адрес (32-бита) глобальной таблицы дескрипторов. +Мы уже загрузили `глобальную таблицу дескрипторов` в предыдущей [части](linux-bootstrap-3.md), и теперь мы делаем почти то же самое здесь, но теперь дескрипторы с `CS.L = 1` и `CS.D = 0` для выполнения в 64-битном режиме. Как мы видим, определение `gdt` начинается с двух байт: `gdt_end - gdt`, который представляет последний байт `gdt` или лимит таблицы. Следующие 4 байта содержат базовый адрес `gdt`. -Таким образом, мы помещаем адрес `gdt` в регистр `eax`, а затем помещаем его в `.long gdt` или `gdt+2` в нашем ассемблерном коде. Теперь мы имеем сформированную структуру для регистра `GDTR` и можем загрузить `глобальную таблицу дескрипторов` с помощью инструкции `lgtd`. - -После того как `глобальная таблица дескрипторов` загружена, нам необходимо включить режим [PAE](http://en.wikipedia.org/wiki/Physical_Address_Extension), поместив значение регистра `cr4` в `eax`, установить в нём пятый бит и загрузить его снова в `cr4`: +После того как `глобальная таблица дескрипторов` загружена с помощью инструкции `lgdt`, нам необходимо включить режим [PAE](http://en.wikipedia.org/wiki/Physical_Address_Extension), поместив значение регистра `cr4` в `eax`, установить в нём пятый бит и загрузить его снова в `cr4`: ```assembly movl %cr4, %eax @@ -430,11 +440,13 @@ Long mode является расширением унаследованного ```assembly leal pgtable(%ebx), %edi xorl %eax, %eax - movl $((4096*6)/4), %ecx + movl $(BOOT_INIT_PGT_SIZE/4), %ecx rep stosl ``` -Мы помещаем адрес `pgtable + ebx` (вы помните, что `ebx` содержит адрес, по которому ядро будет перемещено после декомпрессии) в регистр `edi`, очищаем регистр `eax` и устанавливаем регистр `ecx` в `6144`. Инструкция `rep stosl` записывает значение `eax` в `edi`, увеличивает значение в регистре `edi` на `4` и уменьшает значение в регистре `ecx` на `1`. Эта операция будет повторятся до тех пор, пока значение регистра `ecx` больше нуля. Вот почему мы установили `ecx` в `6144`. +Мы помещаем адрес `pgtable + ebx` (вы помните, что `ebx` содержит адрес, по которому ядро будет перемещено после декомпрессии) в регистр `edi`, очищаем регистр `eax` и устанавливаем регистр `ecx` в `6144`. + +Инструкция `rep stosl` записывает значение `eax` в `edi`, увеличивает значение в регистре `edi` на `4` и уменьшает значение в регистре `ecx` на `1`. Эта операция будет повторятся до тех пор, пока значение регистра `ecx` больше нуля. Вот почему мы установили `ecx` в `6144` (или `BOOT_INIT_PGT_SIZE/4`). Структура `pgtable` определена в конце файла [arch/x86/boot/compressed/head_64.S](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/head_64.S): @@ -442,10 +454,21 @@ Long mode является расширением унаследованного .section ".pgtable","a",@nobits .balign 4096 pgtable: - .fill 6*4096, 1, 0 + .fill BOOT_PGT_SIZE, 1, 0 ``` -Как мы видим, она находится в секции `.pgtable` и имеет размер `24` Кб. +Как мы видим, она находится в секции `.pgtable` и его размер зависит от опции конфигурации ядра `CONFIG_X86_VERBOSE_BOOTUP`: + +```C +# ifdef CONFIG_X86_VERBOSE_BOOTUP +# define BOOT_PGT_SIZE (19*4096) +# else /* !CONFIG_X86_VERBOSE_BOOTUP */ +# define BOOT_PGT_SIZE (17*4096) +# endif +# else /* !CONFIG_RANDOMIZE_BASE */ +# define BOOT_PGT_SIZE BOOT_INIT_PGT_SIZE +# endif +``` После того как мы получили буфер для `pgtable`, мы можем начать с создания таблицы страниц верхнего уровня - `PML4` - следующим образом: @@ -523,6 +546,7 @@ pgtable: После этого мы помещаем адрес в стек и включаем поддержку подкачки страниц путём установки битов `PG` и `PE` в регистре `cr0`: ```assembly + pushl %eax movl $(X86_CR0_PG | X86_CR0_PE), %eax movl %eax, %cr0 ``` diff --git a/Booting/linux-bootstrap-5.md b/Booting/linux-bootstrap-5.md index 30e6459..e9662e7 100644 --- a/Booting/linux-bootstrap-5.md +++ b/Booting/linux-bootstrap-5.md @@ -9,7 +9,7 @@ Подготовка к декомпрессии ядра -------------------------------------------------------------------------------- -Мы остановились прямо перед переходом к 64-битной точке входа - `startup_64`, расположенной в [arch/x86/boot/compressed/head_64.S](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/head_64.S). В предыдущей части мы уже видели переход к `startup_64` в `startup_32`: +Мы остановились прямо перед переходом к `64-битной` точке входа - `startup_64`, расположенной в [arch/x86/boot/compressed/head_64.S](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/head_64.S). В предыдущей части мы уже видели переход к `startup_64` в `startup_32`: ```assembly pushl $__KERNEL_CS @@ -24,7 +24,7 @@ lret ``` -в предыдущей части, `startup_64` начал свою работу. Так как мы загрузили новую глобальную таблицу дескрипторов, и был переход CPU в другой режим (в нашем случае в 64-битный режим), мы можем видеть настройку сегментов данных в начале `startup_64`: +Так как мы загрузили новую `глобальную таблицу дескрипторов`, и был переход CPU в другой режим (в нашем случае в `64-битный` режим), мы можем видеть настройку сегментов данных в начале `startup_64`: ```assembly .code64 @@ -38,7 +38,7 @@ ENTRY(startup_64) movl %eax, %gs ``` -Все сегментные регистры, кроме `cs`, теперь указывают на `ds`, равный `0x18` (если вы не понимаете, почему `0x18`, прочтите предыдущую часть). +Все сегментные регистры, кроме регистра `cs`, теперь сброшены после того как мы перешли в `long mode`. Следующий шаг - вычисление разницы между адресом, по которому скомпилировано ядро, и адресом, по которому оно было загружено: @@ -97,7 +97,7 @@ boot_stack_end: popq %rsi ``` -Прежде всего, мы помещаем `rsi` в стек. Нам нужно сохранить значение `rsi`, потому что теперь этот регистр хранит указатель на `boot_params`, которая является структурой режима реальных адресов, содержащая связанные с загрузкой данные (вы должны помнить эту структуру, мы заполняли её в начале кода настройки ядра). В конце этого кода мы снова восстановим указатель на `boot_params` в `rsi`. +Прежде всего, мы помещаем `rsi` в стек. Нам нужно сохранить значение `rsi`, потому что теперь этот регистр хранит указатель на `boot_params`, которая является структурой режима реальных адресов, содержащая связанные с загрузкой данные (вы должны помнить эту структуру, мы заполняли её в начале кода настройки ядра). В конце этого кода мы снова восстановим указатель на `boot_params` в `rsi`. Следующие две инструкции `leaq` вычисляют эффективные адреса `rip` и `rbx` со смещением `_bss - 8` и помещают их в `rsi` и `rdi`. Зачем мы вычисляем эти адреса? На самом деле сжатый образ ядра находится между этим кодом копирования (от `startup_32` до текущего кода) и кодом декомпрессии. Вы можете проверить это, посмотрев сценарий компоновщика - [arch/x86/boot/compressed/vmlinux.lds.S](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/vmlinux.lds.S): @@ -144,7 +144,7 @@ relocated: ... ``` -`.rodata..compressed` содержит сжатый образ ядра. Таким образом, `rsi` будет содержать абсолютный адрес `_bss - 8`, а `rdi` будет содержать относительный адрес релокации `_bss - 8`. Когда мы сохраняем эти адреса в регистрах, мы помещаем адрес `_bss` в регистр `rcx`. Как вы можете видеть в скрипте компоновщика `vmlinux.lds.S`, он находится в конце всех секций с кодом настройки/ядра. Теперь мы можем начать копирование данных из `rsi` в `rdi` по `8` байт с помощью инструкции `movsq`. +`.rodata..compressed` содержит сжатый образ ядра. Таким образом, `rsi` будет содержать абсолютный адрес `_bss - 8`, а `rdi` будет содержать относительный адрес релокации `_bss - 8`. Когда мы сохраняем эти адреса в регистрах, мы помещаем адрес `_bss` в регистр `rcx`. Как вы можете видеть в скрипте компоновщика `vmlinux.lds.S`, он находится в конце всех секций с кодом настройки/ядра. Теперь мы можем начать копирование данных из `rsi` в `rdi` по `8` байт с помощью инструкции `movsq`. Обратите внимание на инструкцию `std` перед копированием данных: она устанавливает флаг `DF`, означающий, что `rsi` и `rdi` будут уменьшаться. Другими словами, мы будем копировать байты задом наперёд. В конце мы очищаем флаг `DF` с помощью инструкции `cld` и восстанавливаем структуру `boot_params` в `rsi`. @@ -171,24 +171,21 @@ relocated: Нам нужно инициализировать секцию `.bss`, потому что скоро мы перейдём к коду на [C](https://en.wikipedia.org/wiki/C_%28programming_language%29). Здесь мы просто очищаем `eax`, помещаем адрес `_bss` в `rdi` и `_ebss` в `rcx`, и заполняем его нулями с помощью инструкции `rep stosq`. -В конце мы видим вызов функции `decompress_kernel`: +В конце мы видим вызов функции `extract_kernel`: ```assembly pushq %rsi - movq $z_run_size, %r9 - pushq %r9 movq %rsi, %rdi leaq boot_heap(%rip), %rsi leaq input_data(%rip), %rdx movl $z_input_len, %ecx movq %rbp, %r8 movq $z_output_len, %r9 - call decompress_kernel - popq %r9 + call extract_kernel popq %rsi ``` -Мы снова устанавливаем `rdi` в указатель на структуру `boot_params` и вызываем `decompress_kernel` из [arch/x86/boot/compressed/misc.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/misc.c) с семью аргументами: +Мы снова устанавливаем `rdi` в указатель на структуру `boot_params` и сохраняем его в стек. В то же время мы устанавливаем `rsi` для указания на область, которая должа использоваться для распаковки ядра. Последним шагом является подготовка параметров `extract_kernel` и вызов этой функции для распаковки ядра. Функция `extract_kernel` определена в [arch/x86/boot/compressed/misc.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/misc.c) и принимает шесть аргументов: * `rmode` - указатель на структуру [boot_params](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973//arch/x86/include/uapi/asm/bootparam.h#L114), которая заполнена загрузчиком или во время ранней инициализации ядра; * `heap` - указатель на `boot_heap`, представляющий собой начальный адрес ранней загрузочной кучи; @@ -196,14 +193,13 @@ relocated: * `input_len` - размер сжатого ядра; * `output` - начальный адрес будущего распакованного ядра; * `output_len` - размер распакованного ядра; -* `run_size` - объём пространства, необходимый для запуска ядра, включая секции `.bss` и `.brk`. Все аргументы буду передаваться через регистры согласно [двоичному интерфейсу приложений System V (ABI)](http://www.x86-64.org/documentation/abi.pdf). Мы закончили подготовку и переходим к декомпрессии ядра. Декомпрессия ядра -------------------------------------------------------------------------------- -Как мы видели в предыдущем абзаце, функция `decompress_kernel` определена [arch/x86/boot/compressed/misc.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/misc.c) и содержит семь аргументов. Эта функция начинается с инициализации видео/консоли, которую мы уже видели в предыдущих частях. Нам нужно сделать это ещё раз, потому что мы не знаем, находились ли мы в [режиме реальных адресов](https://en.wikipedia.org/wiki/Real_mode), использовался ли загрузчик, или загрузчик использовал 32 или 64-битный протокол загрузки. +Как мы видели в предыдущем абзаце, функция `extract_kernel` определена [arch/x86/boot/compressed/misc.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/misc.c) и содержит шесть аргументов. Эта функция начинается с инициализации видео/консоли, которую мы уже видели в предыдущих частях. Нам нужно сделать это ещё раз, потому что мы не знаем, находились ли мы в [режиме реальных адресов](https://en.wikipedia.org/wiki/Real_mode), использовался ли загрузчик, или загрузчик использовал `32` или `64-битный` протокол загрузки. После первых шагов инициализации мы сохраняем указатели на начало и конец свободной памяти: @@ -212,7 +208,7 @@ free_mem_ptr = heap; free_mem_end_ptr = heap + BOOT_HEAP_SIZE; ``` -где `heap` является вторым параметром функции `decompress_kernel`, который мы получили в [arch/x86/boot/compressed/head_64.S](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/head_64.S): +где `heap` является вторым параметром функции `extract_kernel`, который мы получили в [arch/x86/boot/compressed/head_64.S](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/head_64.S): ```assembly leaq boot_heap(%rip), %rsi @@ -225,34 +221,20 @@ boot_heap: .fill BOOT_HEAP_SIZE, 1, 0 ``` -где `BOOT_HEAP_SIZE` - это макрос, который раскрывается в `0x8000` (`0x400000` в случае `bzip2` ядра) и представляет собой размер кучи. +где `BOOT_HEAP_SIZE` - это макрос, который раскрывается в `0x10000` (`0x400000` в случае `bzip2` ядра) и представляет собой размер кучи. После инициализации указателей кучи, следующий шаг - вызов функции `choose_random_location` из [arch/x86/boot/compressed/kaslr.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/kaslr.c#L425). Как можно догадаться из названия функции, она выбирает ячейку памяти, в которой будет разархивирован образ ядра. Может показаться странным, что нам нужно найти или даже `выбрать` место для декомпрессии сжатого образа ядра, но ядро Linux поддерживает технологию [kASLR](https://en.wikipedia.org/wiki/Address_space_layout_randomization), которая позволяет загрузить распакованное ядро по случайному адресу из соображений безопасности. Давайте откроем файл [arch/x86/boot/compressed/kaslr.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/kaslr.c#L425) и посмотри на `choose_random_location`. -Во-первых, если опция `CONFIG_HIBERNATION` установлена, `choose_random_location` пытается найти опцию `kaslr`, в противном случае опцию `nokaslr`: +Во-первых, если опция `CONFIG_HIBERNATION` установлена, `choose_random_location` пытается найти опцию `nokaslr` в коммандной строке ядра Linux: ```C -#ifdef CONFIG_HIBERNATION - if (!cmdline_find_option_bool("kaslr")) { - debug_putstr("KASLR disabled by default...\n"); - goto out; - } -#else - if (cmdline_find_option_bool("nokaslr")) { - debug_putstr("KASLR disabled by cmdline...\n"); - goto out; - } -#endif +if (cmdline_find_option_bool("nokaslr")) { + debug_putstr("KASLR disabled by cmdline...\n"); + return; +} ``` -Если опция конфигурации ядра `CONFIG_HIBERNATION` включена во время конфигурации ядра и в командной строке отсутствует опция `kaslr`, выводится надпись `KASLR disabled by default...` и совершается переход на метку `out`: - -```C -out: - return (unsigned char *)choice; -``` - -где мы просто возвращаем параметр `output`, который мы передали в `choose_random_location`, без изменений. Если опция `CONFIG_HIBERNATION` выключена и опция `nokaslr` присутствует, мы снова переходим на метку `out`. +и выходим, если опция присутствует. На время предположим, что ядро сконфигурировано с включённой рандомизацией и попытаемся понять, что такое `kASLR`. Мы можем найти информацию об этом в [документации](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/Documentation/kernel-parameters.txt): @@ -260,19 +242,18 @@ out: kaslr/nokaslr [X86] Включение/выключение базового смещения ASLR ядра и модуля -(рандомизация размещения адресного пространства), если оно встроено в ядро. -Если выбран CONFIG_HIBERNATION, kASLR отключён по умолчанию. +(рандомизация размещения адресного пространства), если оно встроено в ядро. +Если выбран CONFIG_HIBERNATION, kASLR отключён по умолчанию. Если kASLR включён, спящий режим будет выключен. ``` Это означает, что мы можем передать опцию `kaslr` в командную строку ядра и получить случайный адрес для распаковки ядра (вы можете прочитать больше о ASLR [здесь](https://en.wikipedia.org/wiki/Address_space_layout_randomization)). Итак, наша текущая цель - найти случайный адрес, где мы сможем `безопасно` распаковать ядро Linux. Повторюсь: `безопасно`. Что это означает в данном контексте? Вы можете помнить, что помимо кода декомпрессора и непосредственно образа ядра в памяти есть несколько небезопасных мест. Например, образ [initrd](https://en.wikipedia.org/wiki/Initrd) также находится в памяти, и мы не должны перекрывать его распакованным ядро. -Следующая функция поможет нам найти безопасное место, где мы можем распаковать ядро. Это функция `mem_avoid_init`. Она определена в том же [файле](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/kaslr.c) исходного кода и принимает 4 аргумента, которые мы видели в функции `decompress_kernel`: +Следующая функция поможет нам создать страницы отображений "один в один" (identity mapping), чтобы избежать небезопасных мест в ОЗУ и распаковывать ядро. И после этого мы должны найти безопасное место, где мы можем распаковать ядро. Это функция `mem_avoid_init`. Она определена в том же [файле](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/kaslr.c) исходного кода и принимает три аргумента, которые мы видели в функции `extract_kernel`: * `input_data` - указатель на начало сжатого ядра, или, другими словами, указатель на `arch/x86/boot/compressed/vmlinux.bin.bz2`; * `input_len` - размер сжатого ядра; * `output` - начальный адрес будущего распакованного ядра; -* `output_len` - размер распакованного ядра. Основной точкой этой функции является заполнение массива структур `mem_vector`: @@ -336,7 +317,9 @@ Offset Proto Name Meaning Итак, мы берём `ext_ramdisk_image` и `ext_ramdisk_size`, сдвигаем их влево на `32` (теперь они будут содержать младшие 32 бита в старших битах) и получаем начальный адрес и размер `initrd`. Далее мы сохраняем их в массиве `mem_avoid`. -Следующим шагом после того как мы собрали все небезопасные области памяти в массиве `mem_avoid`, будет поиск случайного адреса, который не пересекается с небезопасными областями, используя функцию `find_random_addr`. Прежде всего, мы можем видеть выравнивание выходного адреса в функции `find_random_addr`: +Следующим шагом после того как мы собрали все небезопасные области памяти в массиве `mem_avoid`, будет поиск случайного адреса, который не пересекается с небезопасными областями, используя функцию `find_random_phys_addr`. + +Прежде всего, мы можем видеть выравнивание выходного адреса в функции `find_random_addr`: ```C minimum = ALIGN(minimum, CONFIG_PHYSICAL_ALIGN); @@ -345,94 +328,61 @@ minimum = ALIGN(minimum, CONFIG_PHYSICAL_ALIGN); Вы можете помнить опцию конфигурации `CONFIG_PHYSICAL_ALIGN` из предыдущей части. Эта опция предоставляет значение, по которому ядро должно быть выровнено, и по умолчанию оно составляет `0x200000`. После получения выровненного выходного адреса, мы просматриваем области памяти, которые мы получили с помощью BIOS-сервиса [e820](https://en.wikipedia.org/wiki/E820) и собираем подходящие для распакованного образа ядра: ```C -for (i = 0; i < real_mode->e820_entries; i++) { - process_e820_entry(&real_mode->e820_map[i], minimum, size); +process_e820_entry(&real_mode->e820_map[i], minimum, size); +``` + +Напомним, что мы собрали `e820_entries` во [второй части](https://github.com/proninyaroslav/linux-insides-ru/blob/master/Booting/linux-bootstrap-2.md#Обнаружение-памяти). Функция `process_e820_entries` совершает некоторые проверки: что область памяти `e820` не является `non-RAM`, что начальный адрес области памяти не больше максимального допустимого смещения `aslr` offset, и что область памяти находится выше минимальной локации загрузки: + +```C +for (i = 0; i < boot_params->e820_entries; i++) { + ... + ... + ... + process_mem_region(®ion, minimum, image_size); + ... + ... + ... } ``` -Напомним, что мы собрали `e820_entries` во [второй части](https://github.com/proninyaroslav/linux-insides-ru/blob/master/Booting/linux-bootstrap-2.md#Обнаружение-памяти). Функция `process_e820_entry` совершает некоторые проверки: что область памяти `e820` не является `non-RAM`, что начальный адрес области памяти не больше максимального допустимого смещения `aslr` offset, и что область памяти находится выше минимальной локации загрузки: +и вызываем `process_mem_region` для допустимых областей памяти. Функция `process_mem_region` обрабатывает данные области памяти и сохраняет их в массив структур `slot_area` - `slot_areas`. ```C -struct mem_vector region, img; +#define MAX_SLOT_AREA 100 -if (entry->type != E820_RAM) - return; +static struct slot_area slot_areas[MAX_SLOT_AREA]; -if (entry->addr >= CONFIG_RANDOMIZE_BASE_MAX_OFFSET) - return; - -if (entry->addr + entry->size < minimum) - return; +struct slot_area { + unsigned long addr; + int num; +}; ``` -После этого мы сохраняем начальный адрес и размер области памяти `e820` в структуре `mem_vector` (мы видели определение этой структуры выше): +После завершения `process_mem_region` у нас будет массив адресов, безопасных для распакованного ядра. Затем мы вызываем функцию `slots_fetch_random`, чтобы получить случайный элемент из этого массива: ```C -region.start = entry->addr; -region.size = entry->size; -``` +slot = kaslr_get_random_long("Physical") % slot_max; -Во время сохранения значений мы также выравниваем `region.start`, как это делали в функции `find_random_addr` и проверяем, что мы не получили адрес, который находится за пределами области оригинальной памяти: - -```C -region.start = ALIGN(region.start, CONFIG_PHYSICAL_ALIGN); - -if (region.start > entry->addr + entry->size) - return; -``` - -На следующем этапе мы уменьшаем размер области памяти, чтобы не включить отклонённые области в начале, и гарантируем, что последний адрес в области памяти меньше, чем `CONFIG_RANDOMIZE_BASE_MAX_OFFSET`, поэтому конец образа ядра будет меньше чем максимальное смещение `aslr`: - -```C -region.size -= region.start - entry->addr; +for (i = 0; i < slot_area_index; i++) { + if (slot >= slot_areas[i].num) { + slot -= slot_areas[i].num; + continue; + } + return slot_areas[i].addr + slot * CONFIG_PHYSICAL_ALIGN; +}region.size -= region.start - entry->addr; if (region.start + region.size > CONFIG_RANDOMIZE_BASE_MAX_OFFSET) region.size = CONFIG_RANDOMIZE_BASE_MAX_OFFSET - region.start; ``` -И наконец, мы просматриваем все небезопасные области памяти и проверяем, что область не перекрывает небезопасные области, такие как командная строка ядра, initrd и т.д: +где функция `kaslr_get_random_long` проверяет различные флаги CPU, такие как `X86_FEATURE_RDRAND` или `X86_FEATURE_TSC`, и выбирает метод для получения случайного числа (это может быть инструкция RDRAND, счётчик временных меток, программируемый интервальный таймер и т.д.). После извлечения случайного адреса, `choose_random_location` завершает свою работу. -```C -for (img.start = region.start, img.size = image_size ; - mem_contains(®ion, &img) ; - img.start += CONFIG_PHYSICAL_ALIGN) { - if (mem_avoid_overlap(&img)) - continue; - slots_append(img.start); - } -``` - -Если область памяти не перекрывает небезопасные области, мы вызываем функцию `slots_append` с начальным адресом области. Функция `slots_append` просто собирает начальные адреса областей памяти в массив `slots`: - -```C -slots[slot_max++] = addr; -``` - -который определён как: - -```C -static unsigned long slots[CONFIG_RANDOMIZE_BASE_MAX_OFFSET / - CONFIG_PHYSICAL_ALIGN]; -static unsigned long slot_max; -``` - -После завершения `process_e820_entry` у нас будет массив адресов, безопасных для распакованного ядра. Затем мы вызываем функцию `slots_fetch_random` для того, чтобы получить случайный адрес из этого массива: - -```C -if (slot_max == 0) - return 0; - -return slots[get_random_long() % slot_max]; -``` - -где функция `get_random_long` проверяет различные флаги CPU, такие как `X86_FEATURE_RDRAND` или `X86_FEATURE_TSC`, и выбирает метод для получения случайного числа (это может быть инструкция RDRAND, счётчик временных меток, программируемый интервальный таймер и т.д.). После извлечения случайного адреса, `choose_random_location` завершает свою работу. - -Теперь вернёмся к [misc.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/misc.c#L404). После получения адреса для образа ядра мы должны были совершить некоторые проверки и убедиться в том, что полученный случайный адрес правильно выровнен и является корректным. +Теперь мы вернёмся к [misc.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/misc.c#L404). После получения адреса для образа ядра мы должны были совершить некоторые проверки и убедиться в том, что полученный случайный адрес правильно выровнен и является корректным. После этого мы увидим знакомое сообщение: ``` -Decompressing Linux... +Decompressing Linux... ``` и вызываем функцию `__decompress`, которая будет распаковывать ядро. Функция `__decompress` зависит от того, какой алгоритм декомпрессии был выбран во время компиляции: @@ -525,9 +475,9 @@ if (ehdr.e_ident[EI_MAG0] != ELFMAG0 || } ``` -С этого момента все загружаемые сегменты находятся в правильном месте. Последняя функция - `handle_relocations` - корректирует адреса в образе ядра и вызывается только в том случае, если `kASLR` был включён во время конфигурации ядра. +С этого момента все загружаемые сегменты находятся в правильном месте. Реализация последней функции - `handle_relocations` зависит от опции конфигурации ядра `CONFIG_X86_NEED_RELOCS` и если она включена, то эта функция корректирует адреса в образе ядра и вызывается только в том случае, если `kASLR` был включён во время конфигурации ядра. -После перемещения ядра мы возвращаемся из `decompress_kernel` обратно в [arch/x86/boot/compressed/head_64.S](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/head_64.S). Адрес ядра находится в регистре `rax` и мы совершаем переход по нему: +После перемещения ядра мы возвращаемся из `extract_kernel` обратно в [arch/x86/boot/compressed/head_64.S](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/head_64.S). Адрес ядра находится в регистре `rax` и мы совершаем переход по нему: ```assembly jmp *%rax @@ -538,7 +488,7 @@ jmp *%rax Заключение -------------------------------------------------------------------------------- -Это конец пятой и последней части процесса загрузки ядра Linux. Мы больше не увидим статей о загрузке ядра (возможны обновления этой и предыдущих статей), но будет много статей о других внутренних компонентах ядра. +Это конец пятой и последней части процесса загрузки ядра Linux. Мы больше не увидим статей о загрузке ядра (возможны обновления этой и предыдущих статей), но будет много статей о других внутренних компонентах ядра. Следующая глава посвящена инициализации ядра, и мы увидим первые шаги в коде инициализации ядра Linux.