mirror of
https://github.com/0xAX/linux-insides
synced 2024-11-09 13:10:43 +00:00
572 lines
46 KiB
Markdown
572 lines
46 KiB
Markdown
Процесс загрузки ядра. Часть 4.
|
||
================================================================================
|
||
|
||
Переход в 64-битный режим
|
||
--------------------------------------------------------------------------------
|
||
|
||
Это четвёртая часть `Процесса загрузки ядра`, в которой вы увидите первые шаги в [защищённом режиме](http://en.wikipedia.org/wiki/Protected_mode), такие как проверка поддержки процессором [long mode](http://en.wikipedia.org/wiki/Long_mode) и [SSE](http://en.wikipedia.org/wiki/Streaming_SIMD_Extensions), [подкачка страниц](http://en.wikipedia.org/wiki/Paging), инициализация таблиц страниц и в конце мы обсудим переход в [long mode](https://en.wikipedia.org/wiki/Long_mode).
|
||
|
||
**ЗАМЕЧАНИЕ: данная часть содержит много ассемблерного кода, так что если вы не знакомы с ним, вы можете прочитать соответствующую литературу**
|
||
|
||
В предыдущей [части](linux-bootstrap-3.md) мы остановились на переходе к 32-битной точке входа в [arch/x86/boot/pmjump.S](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/pmjump.S):
|
||
|
||
```assembly
|
||
jmpl *%eax
|
||
```
|
||
|
||
Вы помните, что регистр `eax` содержит адрес 32-битной точки входа. Мы можем прочитать об этом в [протоколе загрузки ядра Linux x86](https://www.kernel.org/doc/Documentation/x86/boot.txt):
|
||
|
||
```
|
||
При использовании bzImage ядро в защищённом режиме перемещается на 0x100000
|
||
```
|
||
|
||
Давайте удостоверимся в том, что это правда, посмотрев на значения регистров в 32-битной точке входа:
|
||
|
||
```
|
||
eax 0x100000 1048576
|
||
ecx 0x0 0
|
||
edx 0x0 0
|
||
ebx 0x0 0
|
||
esp 0x1ff5c 0x1ff5c
|
||
ebp 0x0 0x0
|
||
esi 0x14470 83056
|
||
edi 0x0 0
|
||
eip 0x100000 0x100000
|
||
eflags 0x46 [ PF ZF ]
|
||
cs 0x10 16
|
||
ss 0x18 24
|
||
ds 0x18 24
|
||
es 0x18 24
|
||
fs 0x18 24
|
||
gs 0x18 24
|
||
```
|
||
|
||
Мы видим, что регистр `cs` содержит `0x10` (как вы помните из предыдущей части, это второй индекс в глобальной таблице дескрипторов), регистр `eip` содержит `0x100000`, и базовый адрес всех сегментов, в том числе сегмента кода, равен нулю. Таким образом, мы можем получить физический адрес - это будет `0:0x100000` или просто `0x100000`, как указано в протоколе загрузки. Давайте начнём с 32-битной точки входа.
|
||
|
||
32-битная точка входа
|
||
--------------------------------------------------------------------------------
|
||
|
||
Мы можем найти определение 32-битной точки входа в [arch/x86/boot/compressed/head_64.S](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/head_64.S):
|
||
|
||
```assembly
|
||
__HEAD
|
||
.code32
|
||
ENTRY(startup_32)
|
||
....
|
||
....
|
||
....
|
||
ENDPROC(startup_32)
|
||
```
|
||
|
||
Прежде всего, почему директория `compressed`? На самом деле, `bzimage` является сжатым `vmlinux + заголовок + код настройки ядра`. Мы видели код настройки ядра во всех предыдущих частях. Таким образом, главная цель `head_64.S` - подготовка перехода в lоng mode, переход в него и декомпрессия ядра. В этой части мы увидим все шаги, вплоть до декомпрессии ядра.
|
||
|
||
В директории `arch/x86/boot/compressed` содержится два файла:
|
||
|
||
* [head_32.S](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/head_32.S)
|
||
* [head_64.S](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/head_64.S)
|
||
|
||
но мы будем рассматривать только `head_64.S`, потому что, как вы помните, эта книга только о `x86_64`; `head_32.S` в нашем случае не используется. Давайте посмотрим на [arch/x86/boot/compressed/Makefile](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/Makefile). Здесь мы можем увидить следующую цель сборки:
|
||
|
||
```Makefile
|
||
vmlinux-objs-y := $(obj)/vmlinux.lds $(obj)/head_$(BITS).o $(obj)/misc.o \
|
||
$(obj)/string.o $(obj)/cmdline.o \
|
||
$(obj)/piggy.o $(obj)/cpuflags.o
|
||
```
|
||
|
||
Обратите внимание на `$(obj)/head_$(BITS).o`. Это означает, что выбор файла (head_32.o или head_64.o) для линковки будет зависеть от значения `$(BITS)`. `$(BITS)` определён в [arch/x86/Makefile](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/Makefile), основанном на .config файле:
|
||
|
||
```Makefile
|
||
ifeq ($(CONFIG_X86_32),y)
|
||
BITS := 32
|
||
...
|
||
...
|
||
else
|
||
BITS := 64
|
||
...
|
||
...
|
||
endif
|
||
```
|
||
|
||
Перезагрузка сегментов, если это необходимо
|
||
--------------------------------------------------------------------------------
|
||
|
||
Как было отмечено выше, мы начинаем с ассемблерного файла [arch/x86/boot/compressed/head_64.S](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/head_64.S). Во-первых, мы видим определение специального атрибута секции перед определением `startup_32`:
|
||
|
||
```assembly
|
||
__HEAD
|
||
.code32
|
||
ENTRY(startup_32)
|
||
```
|
||
|
||
`__HEAD` является макросом, определённым в [include/linux/init.h](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/include/linux/init.h) и представляет собой следующую секцию:
|
||
|
||
```C
|
||
#define __HEAD .section ".head.text","ax"
|
||
```
|
||
|
||
с именем `.head.text` и флагами `ax`. В нашем случае эти флаги означают, что секция является [исполняемой](https://en.wikipedia.org/wiki/Executable) или, другими словами, содержит код. Мы можем найти определение этой секции в скрипте компоновщика [arch/x86/boot/compressed/vmlinux.lds.S](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/vmlinux.lds.S):
|
||
|
||
```
|
||
SECTIONS
|
||
{
|
||
. = 0;
|
||
.head.text : {
|
||
_head = . ;
|
||
HEAD_TEXT
|
||
_ehead = . ;
|
||
}
|
||
```
|
||
|
||
Если вы не знакомы с синтаксисом скриптового языка компоновщика `GNU LD`, вы можете найти более подробную информацию в [документации](https://sourceware.org/binutils/docs/ld/Scripts.html#Scripts). Вкратце, символ `.` является специальной переменной компоновщика - счётчиком местоположения. Значение, присвоенное ему - это смещение по отношению к смещению сегмента. В нашем случае мы устанавливаем счётчик местоположения в ноль. Это означает, что наш код слинкован для запуска в памяти со смещения `0`. Кроме того, мы можем найти эту информацию в комментарии:
|
||
|
||
```
|
||
Be careful parts of head_64.S assume startup_32 is at address 0.
|
||
```
|
||
|
||
Хорошо, теперь мы знаем, где мы находимся, и сейчас самое время заглянуть внутрь функции `startup_32`.
|
||
|
||
В начале `startup_32` мы видим инструкцию `cld`, которая очищает бит `DF` в [регистре флагов](https://en.wikipedia.org/wiki/FLAGS_register). Когда флаг направления очищен, все строковые операции, такие как [stos](http://x86.renejeschke.de/html/file_module_x86_id_306.html), [scas](http://x86.renejeschke.de/html/file_module_x86_id_287.html) и др. будут инкрементировать индексные регистры `esi` или `edi`. Нам нужно очистить флаг направления, потому что позже мы будем использовать строковые операции для очистки пространства для таблиц страниц и т.д.
|
||
|
||
После того как бит `DF` очищен, следующим шагом является проверка флага `KEEP_SEGMENTS` из поля `loadflags` заголовка настройки ядра. Если вы помните, мы уже видели `loadflags` в самой первой [части](linux-bootstrap-1.md) книги. Там мы проверяли флаг `CAN_USE_HEAP` чтобы узнать, можем ли мы использовать кучу. Теперь нам нужно проверить флаг `KEEP_SEGMENTS`. Данный флаг описан в [протоколе загрузки](https://www.kernel.org/doc/Documentation/x86/boot.txt):
|
||
|
||
```
|
||
Бит 6 (запись): KEEP_SEGMENTS
|
||
Протокол: 2.07+
|
||
- Если 0, перезагрузить регистры сегмента в 32-битной точке входа.
|
||
- Если 1, не перезагружать регистры сегмента в 32-битной точке входа.
|
||
Предполагается, что %cs %ds %ss %es установлены в плоские сегменты
|
||
с базовым адресом 0 (или эквивалент для их среды).
|
||
```
|
||
|
||
Таким образом, если бит `KEEP_SEGMENTS` в `loadflags` не установлен, то сегментные регистры `ds`, `ss` и `es` должны быть сброшены в плоский сегмент с базовым адресом `0`. Что мы и делаем:
|
||
|
||
```C
|
||
testb $(1 << 6), BP_loadflags(%esi)
|
||
jnz 1f
|
||
|
||
cli
|
||
movl $(__BOOT_DS), %eax
|
||
movl %eax, %ds
|
||
movl %eax, %es
|
||
movl %eax, %ss
|
||
```
|
||
|
||
Вы помните, что `__BOOT_DS` равен `0x18` (индекс сегмента данных в [глобальной таблице дескрипторов](https://en.wikipedia.org/wiki/Global_Descriptor_Table)). Если `KEEP_SEGMENTS` установлен, мы переходим на ближайшую метку `1f`, иначе обновляем сегментные регистры значением `__BOOT_DS`. Сделать это довольно легко, но есть один интересный момент. Если вы читали предыдущую [часть](linux-bootstrap-3.md), то помните, что мы уже обновили сегментные регистры сразу после перехода в [защищённый режим](https://en.wikipedia.org/wiki/Protected_mode) в [arch/x86/boot/pmjump.S](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/pmjump.S). Так почему же нам снова нужно обновить значения в сегментных регистрах? Ответ прост. Ядро Linux также имеет 32-битный протокол загрузки и если загрузчик использует его для загрузки ядра, то весь код до `startup_32` будет пропущен. В этом случае `startup_32` будет первой точкой входа в ядро, и нет никаких гарантий, что сегментные регистры будут находиться в ожидаемом состоянии.
|
||
|
||
После того как мы проверили флаг `KEEP_SEGMENTS` и установили правильное значение в сегментные регистры, следующим шагом будет вычисление разницы между адресом, по которому мы загружены, и адресом, который был указан во время компиляции. Вы помните, что `setup.ld.S` содержит следующее определение в начале секции: `.head.text`: `. = 0`. Это значит, что код в этой секции скомпилирован для запуска по адресу `0`. Мы можем видеть это в выводе `objdump`:
|
||
|
||
```
|
||
arch/x86/boot/compressed/vmlinux: file format elf64-x86-64
|
||
|
||
|
||
Disassembly of section .head.text:
|
||
|
||
0000000000000000 <startup_32>:
|
||
0: fc cld
|
||
1: f6 86 11 02 00 00 40 testb $0x40,0x211(%rsi)
|
||
```
|
||
|
||
Утилита `objdump` говорит нам о том, что адрес `startup_32` равен `0`. Но на самом деле это не так. Наша текущая цель состоит в том, чтобы узнать настоящее местоположение. Довольно просто сделать это в [long mode](https://en.wikipedia.org/wiki/Long_mode), поскольку он поддерживает относительную адресацию с помощью указателя `rip`, но в настоящее время мы находимся в [защищённом режиме](https://en.wikipedia.org/wiki/Protected_mode). Для того чтобы узнать адрес `startup_32`, мы будем использовать общепринятый шаблон. Нам необходимо определить метку, перейти на эту метку и вытолкнуть вершину стека в регистр:
|
||
|
||
```assembly
|
||
call label
|
||
label: pop %reg
|
||
```
|
||
|
||
После этого регистр будет содержать адрес метки. Давайте посмотрим на аналогичный код поиска адреса `startup_32` в ядре Linux:
|
||
|
||
```assembly
|
||
leal (BP_scratch+4)(%esi), %esp
|
||
call 1f
|
||
1: popl %ebp
|
||
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`. Нам просто нужно вычесть адрес метки из адреса, который мы получили из стека:
|
||
|
||
```
|
||
startup_32 (0x0) +-----------------------+
|
||
| |
|
||
| |
|
||
| |
|
||
| |
|
||
| |
|
||
| |
|
||
| |
|
||
| |
|
||
1f (смещение 0x0 + 1f) +-----------------------+ %ebp - реальный физический адрес
|
||
| |
|
||
| |
|
||
+-----------------------+
|
||
```
|
||
|
||
`startup_32` слинкован для запуска по адресу `0x0` и это значит, что `1f` имеет адрес `0x0 + смещение 1f`, примерно `0x21` байт. Регистр `ebp` содержит реальный физический адрес метки `1f`. Таким образом, если вычесть `1f` из `ebp`, мы получим реальный физический адрес `startup_32`. В [протоколе загрузки ядра Linux](https://www.kernel.org/doc/Documentation/x86/boot.txt) описано, что базовый адрес ядра в защищённом режиме равен `0x100000`. Мы можем проверить это с помощью [gdb](https://en.wikipedia.org/wiki/GNU_Debugger). Давайте запустим отладчик и поставим точку останова на адресе `1f`, который равен `0x100021`. Если всё верно, то мы увидим `0x100021` в регистре `ebp`:
|
||
|
||
```
|
||
$ gdb
|
||
(gdb)$ target remote :1234
|
||
Remote debugging using :1234
|
||
0x0000fff0 in ?? ()
|
||
(gdb)$ br *0x100022
|
||
Breakpoint 1 at 0x100022
|
||
(gdb)$ c
|
||
Continuing.
|
||
|
||
Breakpoint 1, 0x00100022 in ?? ()
|
||
(gdb)$ i r
|
||
eax 0x18 0x18
|
||
ecx 0x0 0x0
|
||
edx 0x0 0x0
|
||
ebx 0x0 0x0
|
||
esp 0x144a8 0x144a8
|
||
ebp 0x100021 0x100021
|
||
esi 0x142c0 0x142c0
|
||
edi 0x0 0x0
|
||
eip 0x100022 0x100022
|
||
eflags 0x46 [ PF ZF ]
|
||
cs 0x10 0x10
|
||
ss 0x18 0x18
|
||
ds 0x18 0x18
|
||
es 0x18 0x18
|
||
fs 0x18 0x18
|
||
gs 0x18 0x18
|
||
```
|
||
|
||
Если мы выполним следующую инструкцию, `subl $1b, %ebp`, мы увидим следующее:
|
||
|
||
```
|
||
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).
|
||
|
||
Настройка стека и проверка CPU
|
||
--------------------------------------------------------------------------------
|
||
|
||
Мы не могли настроить стек, пока не знали адрес метки `startup_32`. Мы можем представить себе стек как массив, и регистр указателя стека `esp` должен указывать на конец этого массива. Конечно, мы можем определить массив в нашем коде, но мы должны знать его фактический адрес, чтобы правильно настроить указатель стека. Давайте посмотрим на код:
|
||
|
||
```assembly
|
||
movl $boot_stack_end, %eax
|
||
addl %ebp, %eax
|
||
movl %eax, %esp
|
||
```
|
||
|
||
Метка `boot_stack_end` определена в [arch/x86/boot/compressed/head_64.S](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/head_64.S) и расположена в секции [.bss](https://en.wikipedia.org/wiki/.bss):
|
||
|
||
```assembly
|
||
.bss
|
||
.balign 4
|
||
boot_heap:
|
||
.fill BOOT_HEAP_SIZE, 1, 0
|
||
boot_stack:
|
||
.fill BOOT_STACK_SIZE, 1, 0
|
||
boot_stack_end:
|
||
```
|
||
|
||
Прежде всего, мы помещаем адрес `boot_stack_end` в регистр `eax`, т.е регистр `eax` содержит адрес `0x0 + boot_stack_end`. Чтобы получить реальный адрес `boot_stack_end`, нам нужно добавить реальный адрес `startup_32`. Как вы помните, мы нашли этот адрес выше и поместили его в регистр `ebp`. В итоге регистр `eax` будет содержать реальный адрес `boot_stack_end` и нам просто нужно поместить его в указатель стека.
|
||
|
||
После того как мы создали стек, следующим шагом является проверка CPU. Так как мы собираемся перейти в `long mode`, нам необходимо проверить, поддерживает ли CPU `long mode` и `SSE`. Мы будем делать это с помощью вызова функции `verify_cpu`:
|
||
|
||
```assembly
|
||
call verify_cpu
|
||
testl %eax, %eax
|
||
jnz no_longmode
|
||
```
|
||
|
||
Она определена в [arch/x86/kernel/verify_cpu.S](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/kernel/verify_cpu.S) и содержит пару вызовов инструкции [CPUID](https://en.wikipedia.org/wiki/CPUID). Данная инструкция
|
||
используется для получения информации о процессоре. В нашем случае она проверяет поддержку `long mode` и `SSE` и с помощью регистра `eax` возвращает `0` в случае успеха или `1` в случае неудачи.
|
||
|
||
Если значение `eax` не равно нулю, то мы переходим на метку `no_longmode`, которая останавливает CPU вызовом инструкции `hlt` до тех пор, пока не произойдёт аппаратное прерывание:
|
||
|
||
```assembly
|
||
no_longmode:
|
||
1:
|
||
hlt
|
||
jmp 1b
|
||
```
|
||
|
||
Если значение `eax` равно нулю, то всё в порядке и мы можем продолжить.
|
||
|
||
Расчёт адреса релокации
|
||
--------------------------------------------------------------------------------
|
||
|
||
Следующим шагом является вычисление адреса релокации для декомпрессии, если это необходимо. Мы уже знаем, что базовый адрес 32-битной точки входа в ядро Linux - `0x100000`, но это 32-битная точка входа. Базовый адрес ядра по умолчанию определяется значением параметра конфигурации ядра `CONFIG_PHYSICAL_START`. Его значение по умолчанию `0x1000000` или `16 Мб`. Основная проблема заключается в том, что если происходит краш ядра, разработчик должен иметь `rescue ядро` ("спасательное" ядро) для [kdump](https://www.kernel.org/doc/Documentation/kdump/kdump.txt), которое сконфигурировано для загрузки из другого
|
||
адреса. Для решения этой проблемы ядро Linux предоставляет специальный параметр конфигурации - `CONFIG_RELOCATABLE`. Как вы можете прочесть в документации ядра:
|
||
|
||
```
|
||
Это создает образ ядра, который сохраняет информацию о релокации
|
||
поэтому он может быть загружен где-либо, кроме стандартного 1 Мб.
|
||
|
||
Примечание: Если CONFIG_RELOCATABLE=y, то ядро запускается с адреса,
|
||
на который он был загружен, а физический адрес времени компиляции
|
||
(CONFIG_PHYSICAL_START) используется как минимальная локация.
|
||
```
|
||
|
||
Проще говоря, это означает, что ядро с той же конфигурацией может загружаться с разных адресов. С технической точки зрения это делается путём компиляции декомпрессора как [адресно-независимого кода](https://en.wikipedia.org/wiki/Position-independent_code). Если мы посмотрим на [arch/x86/boot/compressed/Makefile](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/Makefile), то мы увидим, что декомпрессор действительно скомпилирован с флагом `-fPIC`:
|
||
|
||
```Makefile
|
||
KBUILD_CFLAGS += -fno-strict-aliasing -fPIC
|
||
```
|
||
|
||
Когда мы используем адресно-независимый код, адрес получается путём добавления адресного поля команды и значения счётчика команд программы. Код, использующий подобную адресацию, возможно загрузить с любого адреса. Вот почему мы должны были получить реальный физический адрес `startup_32`. Давайте вернёмся к коду ядра Linux. Наша текущая цель состоит в том, чтобы вычислить адрес, на который мы можем переместить ядро для декомпрессии. Расчёт этого адреса зависит от параметра конфигурации ядра `CONFIG_RELOCATABLE`. Давайте посмотрим на код:
|
||
|
||
```assembly
|
||
#ifdef CONFIG_RELOCATABLE
|
||
movl %ebp, %ebx
|
||
movl BP_kernel_alignment(%esi), %eax
|
||
decl %eax
|
||
addl %eax, %ebx
|
||
notl %eax
|
||
andl %eax, %ebx
|
||
cmpl $LOAD_PHYSICAL_ADDR, %ebx
|
||
jge 1f
|
||
#endif
|
||
movl $LOAD_PHYSICAL_ADDR, %ebx
|
||
1:
|
||
addl $z_extract_offset, %ebx
|
||
```
|
||
|
||
Следует помнить, что регистр `ebp` содержит физический адрес метки `startup_32`. Если параметр `CONFIG_RELOCATABLE` включён во время конфигурации ядра, то мы помещаем этот адрес в регистр `ebx`, выравниваем по границе, кратной `2 Мб` и сравниваем его со значением `LOAD_PHYSICAL_ADDR`. `LOAD_PHYSICAL_ADDR` является макросом, определённым в [arch/x86/include/asm/boot.h](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/include/asm/boot.h) и выглядит следующим образом:
|
||
|
||
```C
|
||
#define LOAD_PHYSICAL_ADDR ((CONFIG_PHYSICAL_START \
|
||
+ (CONFIG_PHYSICAL_ALIGN - 1)) \
|
||
& ~(CONFIG_PHYSICAL_ALIGN - 1))
|
||
```
|
||
|
||
Как мы можем видеть, он просто расширяет адрес до значения выравнивания `CONFIG_PHYSICAL_ALIGN` и представляет собой физический адрес, по которому будет загружено ядро. После сравнения `LOAD_PHYSICAL_ADDR` и значения регистра `ebx`, мы добавляем смещение от `startup_32`, по которому будет происходить декомпрессия образа ядра. Если во время компиляции параметр `CONFIG_RELOCATABLE` не включён, мы просто помещаем адрес по умолчанию и добавляем к нему `z_extract_offset`.
|
||
|
||
После всех расчётов у нас в распоряжении `ebp`, содержащий адрес, по которому будет происходить загрузка, и `ebx`, содержащий адрес, по которому ядро будет перемещено после декомпрессии.
|
||
|
||
Подготовка перед входом в long mode
|
||
--------------------------------------------------------------------------------
|
||
|
||
Теперь, когда у нас есть базовый адрес, на который мы будем перемещать сжатое ядро, нам необходимо сделать последний шаг, прежде чем мы сможем перейти в 64-битный режим. Во-первых, нам необходимо обновить [глобальную таблицу дескрипторов](https://en.wikipedia.org/wiki/Global_Descriptor_Table):
|
||
|
||
```assembly
|
||
leal gdt(%ebp), %eax
|
||
movl %eax, 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) исходного кода:
|
||
|
||
```assembly
|
||
.data
|
||
gdt:
|
||
.word gdt_end - gdt
|
||
.long gdt
|
||
.word 0
|
||
.quad 0x0000000000000000 /* Нулевой дескриптор */
|
||
.quad 0x00af9a000000ffff /* __KERNEL_CS */
|
||
.quad 0x00cf92000000ffff /* __KERNEL_DS */
|
||
.quad 0x0080890000000000 /* Дескриптор TS */
|
||
.quad 0x0000000000000000 /* Продолжение TS */
|
||
gdt_end:
|
||
```
|
||
|
||
Мы видим, что она расположена в секции `.data` и содержит пять дескрипторов: нулевой дескриптор, сегмент кода ядра, сегмент данных ядра и два дескриптора задач. Мы уже загрузили `глобальную таблицу дескрипторов` в предыдущей [части](linux-bootstrap-3.md), и теперь мы делаем почти то же самое здесь, но теперь дескрипторы с `CS.L = 1` и `CS.D = 0` для выполнения в 64-битном режиме. Как мы видим, определение `gdt` начинается с двух байт: `gdt_end - gdt`, который представляет последний байт `gdt` или лимит таблицы. Следующие 4 байта содержат базовый адрес `gdt`. Вы должны помнить, что `глобальная таблица дескрипторов` хранится в 48-битном `GDTR`, который состоит из двух частей:
|
||
|
||
* размер (16-бита) глобальной таблицы дескрипторов;
|
||
* адрес (32-бита) глобальной таблицы дескрипторов.
|
||
|
||
Таким образом, мы помещаем адрес `gdt` в регистр `eax`, а затем помещаем его в `.long gdt` или `gdt+2` в нашем ассемблерном коде. Теперь мы имеем сформированную структуру для регистра `GDTR` и можем загрузить `глобальную таблицу дескрипторов` с помощью инструкции `lgtd`.
|
||
|
||
После того как `глобальная таблица дескрипторов` загружена, нам необходимо включить режим [PAE](http://en.wikipedia.org/wiki/Physical_Address_Extension), поместив значение регистра `cr4` в `eax`, установить в нём пятый бит и загрузить его снова в `cr4`:
|
||
|
||
```assembly
|
||
movl %cr4, %eax
|
||
orl $X86_CR4_PAE, %eax
|
||
movl %eax, %cr4
|
||
```
|
||
|
||
Мы почти закончили все подготовки перед входом в 64-битный режим. Последний шаг заключается в создании таблицы страниц, но прежде чем сделать это, необходимо рассказать о long mode
|
||
|
||
Long mode
|
||
--------------------------------------------------------------------------------
|
||
|
||
[Long mode](https://en.wikipedia.org/wiki/Long_mode) - нативный режим для процессоров [x86_64](https://en.wikipedia.org/wiki/X86-64). Прежде всего посмотрим на некоторые различия между `x86_64` и `x86`.
|
||
|
||
`64-битный` режим предоставляет следующие особенности:
|
||
|
||
* 8 новых регистров общего назначения с `r8` по `r15` + все регистры общего назначения теперь 64-битные;
|
||
* 64-битный указатель инструкции - `RIP`;
|
||
* Новый режим работы - Long mode;
|
||
* 64-битные адреса и операнды;
|
||
* Относительная адресация RIP (мы увидим пример этого в следующих частях).
|
||
|
||
Long mode является расширением унаследованного защищённого режима. Он состоит из двух подрежимов:
|
||
|
||
* 64-битный режим;
|
||
* режим совместимости.
|
||
|
||
Для переключения в `64-битный` режим необходимо сделать следующее:
|
||
|
||
* Включить [PAE](https://en.wikipedia.org/wiki/Physical_Address_Extension);
|
||
* Создать таблицу страниц и загрузить адрес таблицы страниц верхнего уровня в регистр `cr3`;
|
||
* Включить `EFER.LME`;
|
||
* Включить подкачку страниц.
|
||
|
||
Мы уже включили `PAE` путём установки бита `PAE` в регистре управления `cr4`. Наша следующая цель - создать структуру для [подкачки страниц](https://en.wikipedia.org/wiki/Paging). Мы увидим это в следующем параграфе.
|
||
|
||
Ранняя инициализация таблицы страниц
|
||
--------------------------------------------------------------------------------
|
||
|
||
Итак, мы уже знаем, что прежде чем мы сможем перейти в `64-битный` режим, необходимо создать таблицу страниц. Давайте посмотри на создание ранних `4 гигабайтных` загрузочных таблиц страниц.
|
||
|
||
**ПРИМЕЧАНИЕ: я не буду описывать теорию виртуальной памяти. Если вам необходимо больше информации по виртуальной памяти, см. ссылки в конце этой части.**
|
||
|
||
Ядро Linux использует `4 уровневую` подкачку страниц, и в целом мы создадим 6 таблиц страниц:
|
||
|
||
* Одну таблицу `PML4 (карта страниц 4 уровня, Page Map Level 4)` с одной записью;
|
||
* Одну таблицу `PDP (указатель директорий страниц, Page Directory Pointer)` с четырьмя записями;
|
||
* Четыре таблицы директорий страниц с `2048` записями.
|
||
|
||
Давайте посмотрим на реализацию. Прежде всего, мы очищаем буфер для таблиц страниц в памяти. Каждая таблица имеет размер в `4096` байт, поэтому нам необходимо очистить `24` Кб буфера:
|
||
|
||
```assembly
|
||
leal pgtable(%ebx), %edi
|
||
xorl %eax, %eax
|
||
movl $((4096*6)/4), %ecx
|
||
rep stosl
|
||
```
|
||
|
||
Мы помещаем адрес `pgtable + ebx` (вы помните, что `ebx` содержит адрес, по которому ядро будет перемещено после декомпрессии) в регистр `edi`, очищаем регистр `eax` и устанавливаем регистр `ecx` в `6144`. Инструкция `rep stosl` записывает значение `eax` в `edi`, увеличивает значение в регистре `edi` на `4` и уменьшает значение в регистре `ecx` на `1`. Эта операция будет повторятся до тех пор, пока значение регистра `ecx` больше нуля. Вот почему мы установили `ecx` в `6144`.
|
||
|
||
Структура `pgtable` определена в конце файла [arch/x86/boot/compressed/head_64.S](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/head_64.S):
|
||
|
||
```assembly
|
||
.section ".pgtable","a",@nobits
|
||
.balign 4096
|
||
pgtable:
|
||
.fill 6*4096, 1, 0
|
||
```
|
||
|
||
Как мы видим, она находится в секции `.pgtable` и имеет размер `24` Кб.
|
||
|
||
После того как мы получили буфер для `pgtable`, мы можем начать с создания таблицы страниц верхнего уровня - `PML4` - следующим образом:
|
||
|
||
```assembly
|
||
leal pgtable + 0(%ebx), %edi
|
||
leal 0x1007 (%edi), %eax
|
||
movl %eax, 0(%edi)
|
||
```
|
||
|
||
Здесь мы снова помещаем относительный адрес `pgtable` в `ebx` или, другими словами, относительный адрес `startup_32` в регистр `edi`. Далее мы помещаем этот адрес со смещением `0x1007` в регистр `eax`. Смещение `0x1007` равно `4096` байтам, которые представляют собой размер `PML4` плюс `7`. `7` здесь представляет флаги `PML4`. В нашем случае это флаги `PRESENT+RW+USER`. В конечном счёте мы просто записали адрес первого элемента `PDP` в `PML4`.
|
||
|
||
Следующий шаг - создание четырёх записей `директории страниц` в таблице `указателя директорий страниц` с теми же флагами `PRESENT+RW+USE`:
|
||
|
||
```assembly
|
||
leal pgtable + 0x1000(%ebx), %edi
|
||
leal 0x1007(%edi), %eax
|
||
movl $4, %ecx
|
||
1: movl %eax, 0x00(%edi)
|
||
addl $0x00001000, %eax
|
||
addl $8, %edi
|
||
decl %ecx
|
||
jnz 1b
|
||
```
|
||
|
||
Мы помещаем базовый адрес указателя директорий страниц, который равен `4096` или, другими словами, смещение `0x1000` от таблицы `pgtable` в `edi`, и адрес первой записи указателя директорий страниц в регистр `eax`. Значение `4`, помещённое в регистр `ecx`, будет счётчиком в следующем цикле, в котором мы записываем адрес первой записи таблицы указателя директорий страниц в регистр `edi`. После этого `edi` будет содержать адрес первой записи указателя директорий страниц с флагами `0x7`. Далее мы просто вычисляем адрес следующих записей указателя директорий страниц, где каждая запись имеет размер `8` байт, и записываем их адреса в `eax`. Последний шаг в создании структуры подкачки страниц - создание `2048` записей с `2 мегабайтными` страницами:
|
||
|
||
```assembly
|
||
leal pgtable + 0x2000(%ebx), %edi
|
||
movl $0x00000183, %eax
|
||
movl $2048, %ecx
|
||
1: movl %eax, 0(%edi)
|
||
addl $0x00200000, %eax
|
||
addl $8, %edi
|
||
decl %ecx
|
||
jnz 1b
|
||
```
|
||
|
||
Здесь мы делаем почти тоже самое, как и в предыдущем примере; все записи с флагами `$0x00000183`: `PRESENT + WRITE + MBZ`. В итоге мы будем иметь `2048` `2 мегабайтных` страниц:
|
||
|
||
```python
|
||
>>> 2048 * 0x00200000
|
||
4294967296
|
||
```
|
||
|
||
или `4 гигабайтную` таблицу страниц. Мы закончили создание нашей ранней структуры таблицы страниц, которая отображает `4` Гб на память и теперь мы можем поместить адрес таблицы страниц верхнего уровня - `PML4` - в регистр управления `cr3`:
|
||
|
||
```assembly
|
||
leal pgtable(%ebx), %eax
|
||
movl %eax, %cr3
|
||
```
|
||
|
||
На этом всё. Все подготовки завершены и теперь мы можем перейти в long mode.
|
||
|
||
Переход в 64-битный режим
|
||
--------------------------------------------------------------------------------
|
||
|
||
В первую очередь нам нужно установить флаг `EFER.LME` в [MSR](http://en.wikipedia.org/wiki/Model-specific_register), равный `0xC0000080`:
|
||
|
||
```assembly
|
||
movl $MSR_EFER, %ecx
|
||
rdmsr
|
||
btsl $_EFER_LME, %eax
|
||
wrmsr
|
||
```
|
||
|
||
Здесь мы помещаем флаг `MSR_EFER` (который определён в [arch/x86/include/uapi/asm/msr-index.h](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/include/uapi/asm/msr-index.h#L7)) в регистр `ecx` и вызываем инструкцию `rdmsr`, которая считывает регистр [MSR](http://en.wikipedia.org/wiki/Model-specific_register). После выполнения `rdmsr`, полученные данные будут находится в `edx:eax`, которые будут зависеть от значения `ecx`. Далее мы проверяем бит `EFER_LME` инструкцией `btsl` и с помощью инструкции `wrmsr` записываем данные из `eax` в регистр `MSR`.
|
||
|
||
На следующем шаге мы помещаем адрес сегмента кода ядра в стек (мы определили его в GDT) и помещаем адрес функции `startup_64` в `eax`.
|
||
|
||
```assembly
|
||
pushl $__KERNEL_CS
|
||
leal startup_64(%ebp), %eax
|
||
```
|
||
|
||
После этого мы помещаем адрес в стек и включаем поддержку подкачки страниц путём установки битов `PG` и `PE` в регистре `cr0`:
|
||
|
||
```assembly
|
||
movl $(X86_CR0_PG | X86_CR0_PE), %eax
|
||
movl %eax, %cr0
|
||
```
|
||
|
||
и выполняем инструкцию:
|
||
|
||
```assembly
|
||
lret
|
||
```
|
||
|
||
Вы должны помнить, что на предыдущем шаге мы посметили адрес функции `startup_64` в стек, и после инструкции `lret`, CPU извлекает адрес и переходит по нему.
|
||
|
||
После всего этого, мы, наконец, в 64-битном режиме:
|
||
|
||
```assembly
|
||
.code64
|
||
.org 0x200
|
||
ENTRY(startup_64)
|
||
....
|
||
....
|
||
....
|
||
```
|
||
|
||
На этом всё!
|
||
|
||
Заключение
|
||
--------------------------------------------------------------------------------
|
||
|
||
Это конец четвёртой части о процессе загрузки ядра Linux. В следующей части мы увидим декомпрессию ядра и многое другое.
|
||
|
||
**От переводчика: пожалуйста, имейте в виду, что английский - не мой родной язык, и я очень извиняюсь за возможные неудобства. Если вы найдёте какие-либо ошибки или неточности в переводе, пожалуйста, пришлите pull request в [linux-insides-ru](https://github.com/proninyaroslav/linux-insides-ru).**
|
||
|
||
Ссылки
|
||
--------------------------------------------------------------------------------
|
||
|
||
* [Защищённый режим](http://en.wikipedia.org/wiki/Protected_mode)
|
||
* [Документация для разработчиков ПО на архитектуре Intel® 64 и IA-32](http://www.intel.com/content/www/us/en/processors/architectures-software-developer-manuals.html)
|
||
* [GNU компоновщик](http://www.eecs.umich.edu/courses/eecs373/readings/Linker.pdf)
|
||
* [SSE](http://en.wikipedia.org/wiki/Streaming_SIMD_Extensions)
|
||
* [Подкачка страниц (Википедия)](http://en.wikipedia.org/wiki/Paging)
|
||
* [Моделезависимый регистр](http://en.wikipedia.org/wiki/Model-specific_register)
|
||
* [Инструкция .fill](http://www.chemie.fu-berlin.de/chemnet/use/info/gas/gas_7.html)
|
||
* [Предыдущая часть](linux-bootstrap-3.md)
|
||
* [Подкачка страниц (OSDEV)](http://wiki.osdev.org/Paging)
|
||
* [Системы подкачки страниц](https://www.cs.rutgers.edu/~pxk/416/notes/09a-paging.html)
|
||
* [Пособие по подкачке страниц на x86](http://www.cirosantilli.com/x86-paging/)
|