1
0
mirror of https://github.com/0xAX/linux-insides.git synced 2024-11-18 14:08:09 +00:00
linux-insides/Booting/linux-bootstrap-4.md

572 lines
45 KiB
Markdown
Raw Normal View History

Процесс загрузки ядра. Часть 4.
2015-02-07 18:17:05 +00:00
================================================================================
Переход в 64-битный режим
2015-02-07 18:17:05 +00:00
--------------------------------------------------------------------------------
Это четвёртая часть `Процесса загрузки ядра`, в которой вы увидите первые шаги в [защищённом режиме](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).
2015-02-07 18:17:05 +00:00
**ЗАМЕЧАНИЕ: данная часть содержит много ассемблерного кода, так что если вы не знакомы с ним, вы можете прочитать соответствующую литературу**
2015-02-07 18:17:05 +00:00
В предыдущей [части](linux-bootstrap-3.md) мы остановились на переходе к 32-битной точке входа в [arch/x86/boot/pmjump.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/pmjump.S):
2015-02-07 18:17:05 +00:00
```assembly
jmpl *%eax
```
Вы помните, что регистр `eax` содержит адрес 32-битной точки входа. Мы можем прочитать об этом в [протоколе загрузки ядра Linux x86](https://www.kernel.org/doc/Documentation/x86/boot.txt):
2015-02-07 18:17:05 +00:00
```
When using bzImage, the protected-mode kernel was relocated to 0x100000
```
Давайте удостоверимся в том, что это правда, посмотрев на значения регистров в 32-битной точке входа:
2015-02-07 18:17:05 +00:00
```
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-битной точки входа.
2015-02-07 18:17:05 +00:00
32-битная точка входа
2015-02-07 18:17:05 +00:00
--------------------------------------------------------------------------------
Мы можем найти определение 32-битной точки входа в [arch/x86/boot/compressed/head_64.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/head_64.S):
2015-02-07 18:17:05 +00:00
```assembly
__HEAD
.code32
ENTRY(startup_32)
....
....
....
ENDPROC(startup_32)
```
Прежде всего, почему директория `compressed`? На самом деле, `bzimage` является сжатым `vmlinux + заголовок + код настройки ядра`. Мы видели код настройки ядра во всех предыдущих частях. Таким образом, главная цель `head_64.S` - подготовка перехода в lоng mode, переход в него и декомпрессия ядра. В этой части мы увидим все шаги, вплоть до декомпрессии ядра.
2015-02-07 18:17:05 +00:00
В директории `arch/x86/boot/compressed` содержится два файла:
2015-02-07 18:17:05 +00:00
2016-01-22 19:46:14 +00:00
* [head_32.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/head_32.S)
* [head_64.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/head_64.S)
2015-02-07 18:17:05 +00:00
но мы будем рассматривать только `head_64.S`, потому что, как вы помните, эта книга только о `x86_64`; `head_32.S` в нашем случае не используется. Давайте посмотрим на [arch/x86/boot/compressed/Makefile](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/Makefile). Здесь мы можем увидить следующую цель сборки:
2015-02-07 18:17:05 +00:00
```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/master/arch/x86/Makefile), основанном на .config файле:
2015-02-07 18:17:05 +00:00
```Makefile
ifeq ($(CONFIG_X86_32),y)
BITS := 32
...
2015-02-07 18:17:05 +00:00
...
else
BITS := 64
...
...
2015-02-07 18:17:05 +00:00
endif
```
Перезагрузка сегментов, если это необходимо
2015-02-07 18:17:05 +00:00
--------------------------------------------------------------------------------
Как было отмечено выше, мы начинаем с ассемблерного файла [arch/x86/boot/compressed/head_64.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/head_64.S). Во-первых, мы видим определение специального атрибута секции перед определением `startup_32`:
2015-02-07 18:17:05 +00:00
```assembly
__HEAD
.code32
ENTRY(startup_32)
```
`__HEAD` является макросом, определённым в [include/linux/init.h](https://github.com/torvalds/linux/blob/master/include/linux/init.h) и представляет собой следующую секцию:
2015-02-07 18:17:05 +00:00
```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/master/arch/x86/boot/compressed/vmlinux.lds.S):
2015-02-07 18:17:05 +00:00
```
SECTIONS
{
. = 0;
.head.text : {
_head = . ;
HEAD_TEXT
_ehead = . ;
}
```
Если вы не знакомы с синтаксисом скриптового языка компоновщика `GNU LD`, вы можете найти более подробную информацию в [документации](https://sourceware.org/binutils/docs/ld/Scripts.html#Scripts). Вкратце, символ `.` является специальной переменной компоновщика - счётчиком местоположения. Значение, присвоенное ему - это смещение по отношению к смещению сегмента. В нашем случае мы устанавливаем счётчик местоположения в ноль. Это означает, что наш код слинкован для запуска в памяти со смещения `0`. Кроме того, мы можем найти эту информацию в комментарии:
2015-02-07 18:17:05 +00:00
```
Be careful parts of head_64.S assume startup_32 is at address 0.
```
Хорошо, теперь мы знаем, где мы находимся, и сейчас самое время заглянуть внутрь функции `startup_32`.
2015-02-07 18:17:05 +00:00
В начале `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`. Нам нужно очистить флаг направления, потому что позже мы будем использовать строковые операции для очистки пространства для таблиц страниц и т.д.
2015-02-07 18:17:05 +00:00
После того, как бит `DF` очищен, следующим шагом является проверка флага `KEEP_SEGMENTS` из поля `loadflags` заголовка настройки ядра. Если вы помните, мы уже видели `loadflags` в самой первой [части](linux-bootstrap-1.md) книги. Там мы проверяли флаг `CAN_USE_HEAP` чтобы узнать, можем ли мы использовать кучу. Теперь нам нужно проверить флаг `KEEP_SEGMENTS`. Данный флаг описан в [протоколе загрузки](https://www.kernel.org/doc/Documentation/x86/boot.txt):
2015-02-07 18:17:05 +00:00
```
Бит 6 (запись): KEEP_SEGMENTS
Протокол: 2.07+
- Если 0, перезагрузить регистры сегмента в 32-битной точке входа.
- Если 1, не перезагружать регистры сегмента в 32-битной точке входа.
Предполагается, что %cs %ds %ss %es установлены в плоские сегменты
с базовым адресом 0 (или эквивалент для их среды).
2015-02-07 18:17:05 +00:00
```
2016-01-22 19:46:14 +00:00
Таким образом, если бит `KEEP_SEGMENTS` в `loadflags` не установлен, то сегментные регистры `ds`, `ss` и `es` должны быть сброшены в плоский сегмент с базовым адресом `0`. Что мы и делаем:
2015-02-07 18:17:05 +00:00
```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/master/arch/x86/boot/pmjump.S). Так почему же нам снова нужно обновить значения в сегментных регистрах? Ответ прост. Ядро Linux также имеет 32-битный протокол загрузки и если загрузчик использует его для загрузки ядра, то весь код до `startup_32` будет пропущен. В этом случае `startup_32` будет первой точкой входа в ядро, и нет никаких гарантий, что сегментные регистры будут находиться в ожидаемом состоянии.
2016-01-22 19:46:14 +00:00
После того, как мы проверили флаг `KEEP_SEGMENTS` и установили правильное значение в сегментные регистры, следующим шагом будет вычисление разницы между адресом, по которому мы загружены, и адресом, который был указан во время компиляции. Вы помните, что `setup.ld.S` содержит следующее определение в начале секции: `.head.text`: `. = 0`. Это значит, что код в этой секции скомпилирован для запуска по адресу `0`. Мы можем видеть это в выводе `objdump`:
2016-01-22 19:46:14 +00:00
```
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`, мы будем использовать общепринятый шаблон. Нам необходимо определить метку, перейти на эту метку и вытолкнуть вершину стека в регистр:
2015-02-07 18:17:05 +00:00
2016-01-22 19:46:14 +00:00
```assembly
call label
label: pop %reg
```
2015-02-07 18:17:05 +00:00
После этого регистр будет содержать адрес метки. Давайте посмотрим на аналогичный код поиска адреса `startup_32` в ядре Linux:
2015-02-07 18:17:05 +00:00
```assembly
leal (BP_scratch+4)(%esi), %esp
call 1f
2015-02-07 18:21:34 +00:00
1: popl %ebp
2015-02-07 18:17:05 +00:00
subl $1b, %ebp
```
Как вы помните из предыдущей части, регистр `esi` содержит адрес структуры [boot_params](https://github.com/torvalds/linux/blob/master/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`. Нам просто нужно вычесть адрес метки из адреса, который мы получили из стека:
2016-01-22 19:46:14 +00:00
```
startup_32 (0x0) +-----------------------+
| |
| |
| |
| |
| |
| |
| |
| |
1f (смещение 0x0 + 1f) +-----------------------+ %ebp - реальный физический адрес
| |
| |
+-----------------------+
```
2016-01-22 19:46:14 +00:00
`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`:
2016-01-22 19:46:14 +00:00
```
$ 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`, мы увидим следующее:
2016-01-22 19:46:14 +00:00
```
nexti
...
ebp 0x100000 0x100000
...
```
Да, всё верно. Адрес `startup_32` равен `0x100000`. После того, как мы узнали адрес метки `startup_32`, мы можем начать подготовку к переходу в [long mode](https://en.wikipedia.org/wiki/Long_mode). Наша следующая цель - настроить стек и убедится в том, что ЦПУ поддерживает long mode и [SSE](http://en.wikipedia.org/wiki/Streaming_SIMD_Extensions).
2015-02-07 18:17:05 +00:00
Настройка стека и проверка ЦПУ
2015-02-07 18:17:05 +00:00
--------------------------------------------------------------------------------
Мы не могли настроить стек, пока не знали адрес метки `startup_32`. Мы можем представить себе стек как массив, и регистр указателя стека `esp` должен указывать на конец этого массива. Конечно, мы можем определить массив в нашем коде, но мы должны знать его фактический адрес, чтобы правильно настроить указатель стека. Давайте посмотрим на код:
2015-02-07 18:17:05 +00:00
```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/master/arch/x86/boot/compressed/head_64.S) и расположена в секции [.bss](https://en.wikipedia.org/wiki/.bss):
2015-02-07 18:17:05 +00:00
```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` и нам просто нужно поместить его в указатель стека.
2015-02-07 18:17:05 +00:00
После того, как мы создали стек, следующим шагом является проверка ЦПУ. Так как мы собираемся перейти в `long mode`, нам необходимо проверить, поддерживает ли ЦПУ `long mode` и `SSE`. Мы будем делать это с помощью вызова функции `verify_cpu`:
2015-02-07 18:17:05 +00:00
```assembly
call verify_cpu
testl %eax, %eax
jnz no_longmode
```
Она определена в [arch/x86/kernel/verify_cpu.S](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/verify_cpu.S) и содержит пару вызовов инструкции [CPUID](https://en.wikipedia.org/wiki/CPUID). Данная инструкция
используется для получения информации о процессоре. В нашем случае она проверяет поддержку `long mode` и `SSE` и с помощью регистра `eax` возвращает `0` в случае успеха или `1` в случае неудачи.
2015-02-07 18:17:05 +00:00
Если значение `eax` не равно нулю, то мы переходим на метку `no_longmode`, которая останавливает ЦПУ вызовом инструкции `hlt` до тех пор, пока не произойдёт аппаратное прерывание:
2015-02-07 18:17:05 +00:00
```assembly
no_longmode:
1:
hlt
jmp 1b
```
Если значение `eax` равно нулю, то всё в порядке и мы можем продолжить.
2015-02-07 18:17:05 +00:00
Расчёт адреса релокации
2015-02-07 18:17:05 +00:00
--------------------------------------------------------------------------------
Следующим шагом является вычисление адреса релокации для декомпрессии в случае необходимости. Мы уже знаем, что базовый адрес 32-битной точки входа в ядро Linux - `0x100000`, но это не 32-битная точка входа. Базовый адрес ядра по умолчанию определяется значением параметра конфигурации ядра `CONFIG_PHYSICAL_START`. Его значение по умолчанию `0x1000000` или `16 Мб`. Основная проблема заключается в том, что если происходит краш ядра, разработчик должен иметь `rescue ядро` ("спасательное" ядро) для [kdump](https://www.kernel.org/doc/Documentation/kdump/kdump.txt), которое сконфигурировано для загрузки из другого
адреса. Для решения этой проблемы, ядро Linux предоставляет специальный параметр конфигурации - `CONFIG_RELOCATABLE`. Как вы можете прочесть в документации ядра:
2016-01-22 19:46:14 +00:00
```
This builds a kernel image that retains relocation information
so it can be loaded someplace besides the default 1MB.
Note: If CONFIG_RELOCATABLE=y, then the kernel runs from the address
it has been loaded at and the compile time physical address
(CONFIG_PHYSICAL_START) is used as the minimum location.
```
Проще говоря, это означает, что ядро с той же конфигурацией может загружаться с разных адресов. С технической точки зрения это делается путём компиляции декомпрессора как [адресно-независимого кода](https://en.wikipedia.org/wiki/Position-independent_code). Если мы посмотрим на [arch/x86/boot/compressed/Makefile](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/Makefile), то мы увидим, что декомпрессор действительно скомпилирован с флагом `-fPIC`:
2016-01-22 19:46:14 +00:00
```Makefile
KBUILD_CFLAGS += -fno-strict-aliasing -fPIC
```
Когда мы используем адресно-независимый код, адрес получается путём добавления адресного поля команды и значения счётчика команд программы. Код, использующий подобную адресацию, возможно загрузить с любого адреса. Вот почему мы должны были получить реальный физический адрес `startup_32`. Давайте вернёмся к коду ядра Linux. Наша текущая цель состоит в том, чтобы вычислить адрес, на который мы можем переместить ядро для декомпрессии. Расчёт этого адреса зависит от параметра конфигурации ядра `CONFIG_RELOCATABLE`. Давайте посмотрим на код:
2015-02-07 18:17:05 +00:00
```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/master/arch/x86/include/asm/boot.h) и выглядит следующим образом:
2015-02-07 18:17:05 +00:00
```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`.
2015-02-07 18:17:05 +00:00
После всех расчётов у нас в распоряжении `ebp`, содержащий адрес, по которому будет происходить загрузка, и `ebx`, содержащий адрес, по которому ядро будет перемещено после декомпрессии.
2015-02-07 18:17:05 +00:00
Подготовка перед входом в long mode
2015-02-07 18:17:05 +00:00
--------------------------------------------------------------------------------
Теперь, когда у нас есть базовый адрес, на который мы будем перемещать сжатое ядро, нам необходимо сделать последний шаг, прежде чем мы сможем перейти в 64-битный режим. Во-первых, нам необходимо обновить [глобальную таблицу дескрипторов](https://en.wikipedia.org/wiki/Global_Descriptor_Table):
2015-02-07 18:17:05 +00:00
```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/master/arch/x86/boot/compressed/head_64.S) исходного кода:
2015-02-07 18:17:05 +00:00
```assembly
.data
gdt:
.word gdt_end - gdt
.long gdt
.word 0
.quad 0x0000000000000000 /* Нулевой дескриптор */
2015-02-07 18:17:05 +00:00
.quad 0x00af9a000000ffff /* __KERNEL_CS */
.quad 0x00cf92000000ffff /* __KERNEL_DS */
.quad 0x0080890000000000 /* Дескриптор TS */
.quad 0x0000000000000000 /* Продолжение TS */
2016-01-22 19:46:14 +00:00
gdt_end:
2015-02-07 18:17:05 +00:00
```
Мы видим, что она расположена в секции `.data` и содержит пять дескрипторов: нулевой дескриптор, сегмент кода ядра, сегмент данных ядра и два дескриптора задач. Мы уже загрузили `глобальную таблицу дескрипторов` в предыдущей [части](https://github.com/0xAX/linux-insides/blob/master/Booting/linux-bootstrap-3.md), и теперь мы делаем почти то же самое здесь, но теперь дескрипторы с `CS.L = 1` и `CS.D = 0` для выполнения в 64-битном режиме. Как мы видим, определение `gdt` начинается с двух байт: `gdt_end - gdt`, который представляет последний байт `gdt` или лимит таблицы. Следующие 4 байта содержат базовый адрес `gdt`. Вы должны помнить, что `глобальная таблица дескрипторов` хранится в 48-битном `GDTR`, который состоит из двух частей:
2016-01-22 19:46:14 +00:00
* размер (16-бита) глобальной таблицы дескрипторов;
* адрес (32-бита) глобальной таблицы дескрипторов.
2016-01-22 19:46:14 +00:00
Таким образом, мы помещаем адрес `gdt` в регистр `eax`, а затем помещаем его в `.long gdt` или `gdt+2` в нашем ассемблерном коде. Теперь мы имеем сформированную структуру для регистра `GDTR` и можем загрузить `глобальную таблицу дескрипторов` с помощью инструкции `lgtd`.
2015-02-07 18:17:05 +00:00
После того, как `глобальная таблица дескрипторов` загружена, нам необходимо включить режим [PAE](http://en.wikipedia.org/wiki/Physical_Address_Extension), поместив значение регистра `cr4` в `eax`, установить в нём пятый бит и загрузить его снова в `cr4`:
2015-02-07 18:17:05 +00:00
```assembly
movl %cr4, %eax
orl $X86_CR4_PAE, %eax
movl %eax, %cr4
```
Мы почти закончили все подготовки перед входом в 64-битный режим. Последний шаг заключается в создании таблицы страниц, но прежде чем сделать это, необходимо рассказать о long mode
2015-02-07 18:17:05 +00:00
Long mode
--------------------------------------------------------------------------------
[Long mode](https://en.wikipedia.org/wiki/Long_mode) - нативный режим для процессоров [x86_64](https://en.wikipedia.org/wiki/X86-64). Прежде всего посмотрим на некоторые различия между `x86_64` и `x86`.
2015-02-07 18:17:05 +00:00
`64-битный` режим предоставляет следующие особенности:
2015-02-07 18:17:05 +00:00
* 8 новых регистров общего назначения с `r8` по `r15` + все регистры общего назначения теперь 64-битные;
* 64-битный указатель инструкции - `RIP`;
* Новый режим работы - Long mode;
* 64-битные адреса и операнды;
* Относительная адресация RIP (мы увидим пример этого в следующих частях).
2015-02-07 18:17:05 +00:00
Long mode является расширением унаследованного защищённого режима. Он состоит из двух подрежимов:
2015-02-07 18:17:05 +00:00
* 64-битный режим;
* режим совместимости.
2015-02-07 18:17:05 +00:00
Для переключения в `64-битный` режим необходимо сделать следующее:
2015-02-07 18:17:05 +00:00
* Включить [PAE](https://en.wikipedia.org/wiki/Physical_Address_Extension);
* Создать таблицу страниц и загрузить адрес таблицы страниц верхнего уровня в регистр `cr3`;
* Включить `EFER.LME`;
* Включить подкачку страниц.
2015-02-07 18:17:05 +00:00
Мы уже включили `PAE` путём установки бита `PAE` в регистре управления `cr4`. Наша следующая цель - построить структуру для [подкачки](https://en.wikipedia.org/wiki/Paging). Мы увидим это в следующем параграфе.
2015-02-07 18:17:05 +00:00
Ранняя инициализация таблицы страниц
2015-02-07 18:17:05 +00:00
--------------------------------------------------------------------------------
Итак, мы уже знаем, что прежде чем мы сможем перейти в `64-битный` режим, необходимо создать таблицу страниц, так что давайте посмотри на создание ранних `4 гигабайтных` загрузочных таблиц страниц.
2015-02-07 18:17:05 +00:00
**ПРИМЕЧАНИЕ: я не буду описывать теорию виртуальной памяти. Если вам необходимо больше знать об этом, см. ссылки в конце этой части.**
2015-02-07 18:17:05 +00:00
Ядро Linux использует `4 уровневую` подкачку, и в целом мы создадим 6 таблиц страниц:
2015-02-07 18:17:05 +00:00
* Одну таблицу `PML4 (карта страниц 4 уровня, Page Map Level 4)` с одной записью;
* Одну таблицу `PDP (указатель директорий страниц, Page Directory Pointer)` с четырьмя записями;
* Четыре таблицы директорий страниц с `2048` записями.
2015-02-07 18:17:05 +00:00
Давайте посмотрим на реализацию. Прежде всего, мы очищаем буфер для таблиц страниц в памяти. Каждая таблица имеет размер в `4096` байт, поэтому нам необходимо очистить `24` Кб буфера:
2015-02-07 18:17:05 +00:00
```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/master/arch/x86/boot/compressed/head_64.S):
2015-02-07 18:17:05 +00:00
```assembly
.section ".pgtable","a",@nobits
.balign 4096
pgtable:
.fill 6*4096, 1, 0
```
Как мы видим, она находится в секции `.pgtable` и имеет размер `24` Кб.
2015-02-07 18:17:05 +00:00
После того, как мы получили буфер для `pgtable`, мы можем начать с создания таблицы страниц верхнего уровня - `PML4` - следующим образом:
2015-02-07 18:17:05 +00:00
```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`.
2015-02-07 18:17:05 +00:00
Следующий шаг - создание четырёх записей `директории страниц` в таблице `указателя директорий страниц` с теми же флагами `PRESENT+RW+USE`:
2015-02-07 18:17:05 +00:00
```assembly
leal pgtable + 0x1000(%ebx), %edi
leal 0x1007(%edi), %eax
movl $4, %ecx
2015-02-07 18:21:34 +00:00
1: movl %eax, 0x00(%edi)
2015-02-07 18:17:05 +00:00
addl $0x00001000, %eax
addl $8, %edi
decl %ecx
jnz 1b
```
Мы помещаем базовый адрес указателя директорий страниц, который равен `4096` или, другими словами, смещение `0x1000` от таблицы `pgtable` в `edi` и адрес первой записи указателя директорий страниц в регистр `eax`. `4`, помещённое в регистр `ecx`, будет счётчиком в следующем цикле, в котором мы записываем адрес первой записи таблицы указателя директорий страниц в регистр `edi`. После этого `edi` будет содержать адрес первой записи указателя директорий страниц с флагами `0x7`. Далее мы просто вычисляем адрес следующих записей указателя директорий страниц, где каждая запись имеет размер `8` байт, и записываем их адреса в `eax`. Последний шаг в создании структуры подкачки страниц - создание `2048` записей с `2 Мб` страницами:
2015-02-07 18:17:05 +00:00
```assembly
leal pgtable + 0x2000(%ebx), %edi
movl $0x00000183, %eax
movl $2048, %ecx
2015-02-07 18:21:34 +00:00
1: movl %eax, 0(%edi)
2015-02-07 18:17:05 +00:00
addl $0x00200000, %eax
addl $8, %edi
decl %ecx
jnz 1b
```
Здесь мы делаем почти тоже самое, как и в предыдущем примере; все записи с флагами `$0x00000183`: `PRESENT + WRITE + MBZ`. В итоге мы будем иметь `2048` `2 мегабайтных` страниц:
```python
>>> 2048 * 0x00200000
4294967296
```
2015-02-07 18:17:05 +00:00
`4 гигабайтная` таблица страниц. Мы закончили создание нашей ранней структуры таблицы страниц, которая отображает `4` Гб на память и теперь мы можем поместить адрес таблицы страниц верхнего уровня - `PML4` - в регистр управления `cr3`:
2015-02-07 18:17:05 +00:00
```assembly
leal pgtable(%ebx), %eax
movl %eax, %cr3
```
На этом всё. Все подготовки завершены и теперь мы можем перейти в long mode.
2015-02-07 18:17:05 +00:00
Переход в 64-битный режим
2015-02-07 18:17:05 +00:00
--------------------------------------------------------------------------------
В первую очередь нам нужно установить флаг `EFER.LME` в [MSR](http://en.wikipedia.org/wiki/Model-specific_register), равный `0xC0000080`:
2015-02-07 18:17:05 +00:00
```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/master/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`.
2015-02-07 18:17:05 +00:00
На следующем шаге мы помещаем адрес сегмента кода ядра в стек (мы определили его в GDT) и помещаем адрес функции `startup_64` в `eax`.
2015-02-07 18:17:05 +00:00
```assembly
pushl $__KERNEL_CS
leal startup_64(%ebp), %eax
```
После этого мы помещаем адрес в стек и включаем поддержку подкачки страниц путём установки битов `PG` и `PE` в регистре `cr0`:
2015-02-07 18:17:05 +00:00
```assembly
movl $(X86_CR0_PG | X86_CR0_PE), %eax
movl %eax, %cr0
```
и выполняем инструкцию:
2015-02-07 18:17:05 +00:00
```assembly
lret
```
Вы должны помнить, что на предыдущем шаге мы посметили адрес функции `startup_64` в стек, и после инструкции `lret`, ЦПУ извлекает адрес и переходит по нему.
2015-02-07 18:17:05 +00:00
После всего этого, мы, наконец, в 64-битном режиме:
2015-02-07 18:17:05 +00:00
```assembly
.code64
.org 0x200
ENTRY(startup_64)
....
....
....
```
На этом всё!
2015-02-07 18:17:05 +00:00
Заключение
2015-02-07 18:17:05 +00:00
--------------------------------------------------------------------------------
Это конец четвёртой части о процессе загрузки ядра Linux. В следующей части мы увидим декомпрессию ядра и многое другое.
2015-02-07 18:17:05 +00:00
**Пожалуйста, имейте в виду, что английский - не мой родной язык, и я очень извиняюсь за возможные неудобства. Если вы найдёте какие-либо ошибки или неточности в переводе, пожалуйста, пришлите pull request в [linux-insides-ru](https://github.com/proninyaroslav/linux-insides-ru).**
2015-02-07 18:17:05 +00:00
Ссылки
2015-02-07 18:17:05 +00:00
--------------------------------------------------------------------------------
* [Защищённый режим](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)
2015-02-07 18:17:05 +00:00
* [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/)