Процесс загрузки ядра. Часть 4.
Переход в 64-битный режим
Это четвёртая часть Процесса загрузки ядра
, в которой вы увидите первые шаги в защищённом режиме, такие как проверка поддержки процессором long mode и SSE, страничная организация памяти, инициализация таблиц страниц и в конце мы обсудим переход в long mode.
ЗАМЕЧАНИЕ: данная часть содержит много ассемблерного кода, так что если вы не знакомы с ним, вы можете прочитать соответствующую литературу
В предыдущей части мы остановились на переходе к 32-битной точке входа в arch/x86/boot/pmjump.S:
jmpl *%eax
Вы помните, что регистр eax
содержит адрес 32-битной точки входа. Мы можем прочитать об этом в протоколе загрузки ядра Linux x86:
При использовании 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:
__HEAD
.code32
ENTRY(startup_32)
....
....
....
ENDPROC(startup_32)
Прежде всего, почему директория compressed
? На самом деле, bzimage
является сжатым vmlinux + заголовок + код настройки ядра
. Мы видели код настройки ядра во всех предыдущих частях. Таким образом, главная цель head_64.S
- подготовка перехода в lоng mode, переход в него и декомпрессия ядра. В этой части мы увидим все шаги, вплоть до декомпрессии ядра.
В директории arch/x86/boot/compressed
содержится два файла:
но мы будем рассматривать только head_64.S
, потому что, как вы помните, эта книга только о x86_64
; head_32.S
в нашем случае не используется. Давайте посмотрим на arch/x86/boot/compressed/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, основанном на .config файле:
ifeq ($(CONFIG_X86_32),y)
BITS := 32
...
...
else
BITS := 64
...
...
endif
Перезагрузка сегментов, если это необходимо
Как было отмечено выше, мы начинаем с ассемблерного файла arch/x86/boot/compressed/head_64.S. Во-первых, мы видим определение специального атрибута секции перед определением startup_32
:
__HEAD
.code32
ENTRY(startup_32)
__HEAD
является макросом, определённым в include/linux/init.h и представляет собой следующую секцию:
#define __HEAD .section ".head.text","ax"
с именем .head.text
и флагами ax
. В нашем случае эти флаги означают, что секция является исполняемой или, другими словами, содержит код. Мы можем найти определение этой секции в скрипте компоновщика arch/x86/boot/compressed/vmlinux.lds.S:
SECTIONS
{
. = 0;
.head.text : {
_head = . ;
HEAD_TEXT
_ehead = . ;
}
...
...
...
}
Если вы не знакомы с синтаксисом скриптового языка компоновщика GNU LD
, вы можете найти более подробную информацию в документации. Вкратце, символ .
является специальной переменной компоновщика - счётчиком местоположения. Значение, присвоенное ему - это смещение по отношению к смещению сегмента. В нашем случае мы устанавливаем счётчик местоположения в ноль. Это означает, что наш код слинкован для запуска в памяти со смещения 0
. Кроме того, мы можем найти эту информацию в комментарии:
Be careful parts of head_64.S assume startup_32 is at address 0.
Хорошо, теперь мы знаем, где мы находимся, и сейчас самое время заглянуть внутрь функции startup_32
.
В начале startup_32
мы видим инструкцию cld
, которая очищает бит DF
в регистре флагов. Когда флаг направления очищен, все строковые операции, такие как stos, scas и др. будут инкрементировать индексные регистры esi
или edi
. Нам нужно очистить флаг направления, потому что позже мы будем использовать строковые операции для очистки пространства для таблиц страниц и т.д.
После того как бит DF
очищен, следующим шагом является проверка флага KEEP_SEGMENTS
из поля loadflags
заголовка настройки ядра. Если вы помните, мы уже видели loadflags
в самой первой части книги. Там мы проверяли флаг CAN_USE_HEAP
чтобы узнать, можем ли мы использовать кучу. Теперь нам нужно проверить флаг KEEP_SEGMENTS
. Данный флаг описан в протоколе загрузки:
Бит 6 (запись): KEEP_SEGMENTS
Протокол: 2.07+
- Если 0, перезагрузить регистры сегмента в 32-битной точке входа.
- Если 1, не перезагружать регистры сегмента в 32-битной точке входа.
Предполагается, что %cs %ds %ss %es установлены в плоские сегменты
с базовым адресом 0 (или эквивалент для их среды).
Таким образом, если бит KEEP_SEGMENTS
в loadflags
не установлен, то сегментные регистры ds
, ss
и es
должны быть установлены в индекс сегмента данных с базовым адресом 0
. Что мы и делаем:
testb $KEEP_SEGMENTS, BP_loadflags(%esi)
jnz 1f
cli
movl $(__BOOT_DS), %eax
movl %eax, %ds
movl %eax, %es
movl %eax, %ss
Вы помните, что __BOOT_DS
равен 0x18
(индекс сегмента данных в глобальной таблице дескрипторов). Если KEEP_SEGMENTS
установлен, мы переходим на ближайшую метку 1f
, иначе обновляем сегментные регистры значением __BOOT_DS
. Сделать это довольно легко, но есть один интересный момент. Если вы читали предыдущую часть, то помните, что мы уже обновили сегментные регистры сразу после перехода в защищённый режим в 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, поскольку он поддерживает относительную адресацию с помощью указателя rip
, но в настоящее время мы находимся в защищённом режиме. Для того чтобы узнать адрес startup_32
, мы будем использовать общепринятый шаблон. Нам необходимо определить метку, перейти на эту метку и вытолкнуть вершину стека в регистр:
call label
label: pop %reg
После этого регистр %reg
будет содержать адрес метки. Давайте посмотрим на аналогичный код поиска адреса startup_32
в ядре Linux:
leal (BP_scratch+4)(%esi), %esp
call 1f
1: popl %ebp
subl $1b, %ebp
Как вы помните из предыдущей части, регистр esi
содержит адрес структуры boot_params, которая была заполнена до перехода в защищённый режим. Структура 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 описано, что базовый адрес ядра в защищённом режиме равен 0x100000
. Мы можем проверить это с помощью gdb. Давайте запустим отладчик и поставим точку останова на адресе 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
, мы увидим следующее:
(gdb) nexti
...
...
...
ebp 0x100000 0x100000
...
...
...
Да, всё верно. Адрес startup_32
равен 0x100000
. После того как мы узнали адрес метки startup_32
, мы можем начать подготовку к переходу в long mode. Наша следующая цель - настроить стек и убедится в том, что CPU поддерживает long mode и SSE.
Настройка стека и проверка CPU
Мы не могли настроить стек, пока не знали адрес метки startup_32
. Мы можем представить себе стек как массив, и регистр указателя стека esp
должен указывать на конец этого массива. Конечно, мы можем определить массив в нашем коде, но мы должны знать его фактический адрес, чтобы правильно настроить указатель стека. Давайте посмотрим на код:
movl $boot_stack_end, %eax
addl %ebp, %eax
movl %eax, %esp
Метка boot_stack_end
определена в arch/x86/boot/compressed/head_64.S и расположена в секции .bss:
.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
:
call verify_cpu
testl %eax, %eax
jnz no_longmode
Она определена в arch/x86/kernel/verify_cpu.S и содержит пару вызовов инструкции CPUID. Данная инструкция
используется для получения информации о процессоре. В нашем случае она проверяет поддержку long mode
и SSE
и с помощью регистра eax
возвращает 0
в случае успеха или 1
в случае неудачи.
Если значение eax
не равно нулю, то мы переходим на метку no_longmode
, которая останавливает CPU вызовом инструкции hlt
до тех пор, пока не произойдёт аппаратное прерывание:
no_longmode:
1:
hlt
jmp 1b
Если значение eax
равно нулю, то всё в порядке и мы можем продолжить.
Расчёт адреса релокации
Следующим шагом является вычисление адреса релокации для декомпрессии, если это необходимо. Мы уже знаем, что базовый адрес 32-битной точки входа в ядро Linux - 0x100000
, но это 32-битная точка входа. Базовый адрес ядра по умолчанию определяется значением параметра конфигурации ядра CONFIG_PHYSICAL_START
. Его значение по умолчанию 0x1000000
или 16 Мб
. Основная проблема заключается в том, что если происходит краш ядра, разработчик должен иметь rescue ядро
("спасательное" ядро) для kdump, которое сконфигурировано для загрузки из другого
адреса. Для решения этой проблемы ядро Linux предоставляет специальный параметр конфигурации - CONFIG_RELOCATABLE
. Как вы можете прочесть в документации ядра:
Это создает образ ядра, который сохраняет информацию о релокации
поэтому он может быть загружен где-либо, кроме стандартного 1 Мб.
Примечание: Если CONFIG_RELOCATABLE=y, то ядро запускается с адреса,
на который он был загружен, а физический адрес времени компиляции
(CONFIG_PHYSICAL_START) используется как минимальная локация.
Проще говоря, это означает, что ядро с той же конфигурацией может загружаться с разных адресов. С технической точки зрения это делается путём компиляции декомпрессора как адресно-независимого кода. Если мы посмотрим на arch/x86/boot/compressed/Makefile, то мы увидим, что декомпрессор действительно скомпилирован с флагом -fPIC
:
KBUILD_CFLAGS += -fno-strict-aliasing -fPIC
Когда мы используем адресно-независимый код, адрес получается путём добавления адресного поля инструкции и значения счётчика команд программы. Код, использующий подобную адресацию, возможно загрузить с любого адреса. Вот почему мы должны были получить реальный физический адрес startup_32
. Давайте вернёмся к коду ядра Linux. Наша текущая цель состоит в том, чтобы вычислить адрес, на который мы можем переместить ядро для декомпрессии. Расчёт этого адреса зависит от параметра конфигурации ядра CONFIG_RELOCATABLE
. Давайте посмотрим на код:
#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
Следует помнить, что регистр ebp
содержит физический адрес метки startup_32
. Если параметр CONFIG_RELOCATABLE
включён во время конфигурации ядра, то мы помещаем этот адрес в регистр ebx
, выравниваем по границе, кратной 2 Мб
и сравниваем его со значением LOAD_PHYSICAL_ADDR
. LOAD_PHYSICAL_ADDR
является макросом, определённым в arch/x86/include/asm/boot.h и выглядит следующим образом:
#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
, содержащий адрес, по которому ядро будет перемещено после декомпрессии. Но это еще не конец. Сжатый образ ядра должен быть перемещён в конец буфера декомпрессии, чтобы упростить вычисления местоположения, по которому ядро будет расположено позже:
1:
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. Затем мы вычитаем адрес символа _end
из этого значения и добавляем результат вычитания в регистр ebx
, который хранит базовый адрес для декомпрессии ядра.
Подготовка перед входом в long mode
Теперь, когда у нас есть базовый адрес, на который мы будем перемещать сжатое ядро, нам необходимо сделать последний шаг, прежде чем мы сможем перейти в 64-битный режим. Во-первых, нам необходимо обновить глобальную таблицу дескрипторов с 64-битными сегментами, потому что перемещаемое ядро может быть запущено по любому адресу ниже 512 Гб:
addl %ebp, gdt+2(%ebp)
lgdt gdt(%ebp)
Здесь мы настраиваем базовый адрес глобальной таблицы дескрипторов
на адрес, где мы фактически загружены, и загружаем таблицу с помощью инструкции lgdt
.
Чтобы понять магию смещений gdt
, нам нужно взглянуть на определение глобальной таблицы дескрипторов
. Мы можем найти его определение в том же файле исходного кода:
.data
gdt64:
.word gdt_end - gdt
.long 0
.word 0
.quad 0
gdt:
.word gdt_end - gdt
.long gdt
.word 0
.quad 0x00cf9a000000ffff /* __KERNEL32_CS *//
.quad 0x00af9a000000ffff /* __KERNEL_CS */
.quad 0x00cf92000000ffff /* __KERNEL_DS */
.quad 0x0080890000000000 /* Дескриптор TS */
.quad 0x0000000000000000 /* Продолжение TS */
gdt_end:
Мы видим, что она расположена в секции .data
и содержит пять дескрипторов: 32-битный
дескриптор для сегмента кода ядра, 64-битный
сегмент ядра, сегмент данных ядра и два дескриптора задач.
Мы уже загрузили глобальную таблицу дескрипторов
в предыдущей части, и теперь мы делаем почти то же самое здесь, но теперь дескрипторы с CS.L = 1
и CS.D = 0
для выполнения в 64-битном режиме. Как мы видим, определение gdt
начинается с двух байт: gdt_end - gdt
, который представляет последний байт gdt
или лимит таблицы. Следующие 4 байта содержат базовый адрес gdt
.
После того как глобальная таблица дескрипторов
загружена с помощью инструкции lgdt
, нам необходимо включить режим PAE, поместив значение регистра cr4
в eax
, установить в нём пятый бит и загрузить его снова в cr4
:
movl %cr4, %eax
orl $X86_CR4_PAE, %eax
movl %eax, %cr4
Мы почти закончили все подготовки перед входом в 64-битный режим. Последний шаг заключается в создании таблицы страниц, но прежде чем сделать это, необходимо рассказать о long mode
Long mode
Long mode - нативный режим для процессоров x86_64. Прежде всего посмотрим на некоторые различия между x86_64
и x86
.
64-битный
режим предоставляет следующие особенности:
- 8 новых регистров общего назначения с
r8
поr15
+ все регистры общего назначения теперь 64-битные; - 64-битный указатель инструкции -
RIP
; - Новый режим работы - Long mode;
- 64-битные адреса и операнды;
- Относительная адресация RIP (мы увидим пример этого в следующих частях).
Long mode является расширением унаследованного защищённого режима. Он состоит из двух подрежимов:
- 64-битный режим;
- режим совместимости.
Для переключения в 64-битный
режим необходимо сделать следующее:
- Включить PAE;
- Создать таблицу страниц и загрузить адрес таблицы страниц верхнего уровня в регистр
cr3
; - Включить
EFER.LME
; - Включить страничную организацию памяти.
Мы уже включили PAE
путём установки бита PAE
в регистре управления cr4
. Наша следующая цель - создать структуру для страничной организации. Мы увидим это в следующем параграфе.
Ранняя инициализация таблицы страниц
Итак, мы уже знаем, что прежде чем мы сможем перейти в 64-битный
режим, необходимо создать таблицу страниц. Давайте посмотри на создание ранних 4 гигабайтных
загрузочных таблиц страниц.
ПРИМЕЧАНИЕ: я не буду описывать теорию виртуальной памяти. Если вам необходимо больше информации по виртуальной памяти, см. ссылки в конце этой части.
Ядро Linux использует 4 уровневую
страничную организацию, и в целом мы создадим 6 таблиц страниц:
- Одну таблицу
PML4 (карта страниц 4 уровня, Page Map Level 4)
с одной записью; - Одну таблицу
PDP (указатель директорий страниц, Page Directory Pointer)
с четырьмя записями; - Четыре таблицы директорий страниц с
2048
записями.
Давайте посмотрим на реализацию. Прежде всего, мы очищаем буфер для таблиц страниц в памяти. Каждая таблица имеет размер в 4096
байт, поэтому нам необходимо очистить 24
Кб буфера:
leal pgtable(%ebx), %edi
xorl %eax, %eax
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
(или BOOT_INIT_PGT_SIZE/4
).
Структура pgtable
определена в конце файла arch/x86/boot/compressed/head_64.S:
.section ".pgtable","a",@nobits
.balign 4096
pgtable:
.fill BOOT_PGT_SIZE, 1, 0
Как мы видим, она находится в секции .pgtable
и его размер зависит от опции конфигурации ядра CONFIG_X86_VERBOSE_BOOTUP
:
# 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
- следующим образом:
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
:
leal pgtable + 0x1000(%ebx), %edi
leal 0x1007(%edi), %eax
movl $4, %ecx
1: movl %eax, 0(%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 мегабайтными
страницами:
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 мегабайтных
страниц:
>>> 2048 * 0x00200000
4294967296
или 4 гигабайтную
таблицу страниц. Мы закончили создание нашей ранней структуры таблицы страниц, которая отображает 4
Гб на память и теперь мы можем поместить адрес таблицы страниц верхнего уровня - PML4
- в регистр управления cr3
:
leal pgtable(%ebx), %eax
movl %eax, %cr3
На этом всё. Все подготовки завершены и теперь мы можем перейти в long mode.
Переход в 64-битный режим
В первую очередь нам нужно установить флаг EFER.LME
в MSR, равный 0xC0000080
:
movl $MSR_EFER, %ecx
rdmsr
btsl $_EFER_LME, %eax
wrmsr
Здесь мы помещаем флаг MSR_EFER
(который определён в arch/x86/include/uapi/asm/msr-index.h) в регистр ecx
и вызываем инструкцию rdmsr
, которая считывает регистр MSR. После выполнения rdmsr
, полученные данные будут находится в edx:eax
, которые будут зависеть от значения ecx
. Далее мы проверяем бит EFER_LME
инструкцией btsl
и с помощью инструкции wrmsr
записываем данные из eax
в регистр MSR
.
На следующем шаге мы помещаем адрес сегмента кода ядра в стек (мы определили его в GDT) и помещаем адрес функции startup_64
в eax
.
pushl $__KERNEL_CS
leal startup_64(%ebp), %eax
После этого мы помещаем адрес в стек и включаем поддержку страничной организации путём установки битов PG
и PE
в регистре cr0
:
pushl %eax
movl $(X86_CR0_PG | X86_CR0_PE), %eax
movl %eax, %cr0
и выполняем инструкцию:
lret
Вы должны помнить, что на предыдущем шаге мы поместили адрес функции startup_64
в стек, и после инструкции lret
, CPU извлекает адрес и переходит по нему.
После всего этого, мы, наконец, в 64-битном режиме:
.code64
.org 0x200
ENTRY(startup_64)
....
....
....
На этом всё!
Заключение
Это конец четвёртой части о процессе загрузки ядра Linux. В следующей части мы увидим декомпрессию ядра и многое другое.
От переводчика: пожалуйста, имейте в виду, что английский - не мой родной язык, и я очень извиняюсь за возможные неудобства. Если вы найдёте какие-либо ошибки или неточности в переводе, пожалуйста, пришлите pull request в linux-insides-ru.
Ссылки
- Защищённый режим
- Документация для разработчиков ПО на архитектуре Intel® 64 и IA-32
- GNU компоновщик
- SSE
- Страничная организация памяти (Википедия)
- Моделезависимый регистр
- Инструкция .fill
- Предыдущая часть
- Страничная организация памяти (OSDEV)
- Системы страничной организации памяти
- Пособие по страничной организации на x86