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

45 KiB
Raw Blame History

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

Переход в 64-битный режим

Это четвёртая часть Процесса загрузки ядра, в которой вы увидите первые шаги в защищённом режиме, такие как проверка поддержки ЦПУ long mode и SSE, подкачка страниц, инициализация таблиц страниц и в конце мы обсудим переход в long mode.

ЗАМЕЧАНИЕ: данная часть содержит много ассемблерного кода, так что если вы не знакомы с ним, вы можете прочитать соответствующую литературу

В предыдущей части мы остановились на переходе к 32-битной точке входа в arch/x86/boot/pmjump.S:

jmpl	*%eax

Вы помните, что регистр eax содержит адрес 32-битной точки входа. Мы можем прочитать об этом в протоколе загрузки ядра Linux x86:

When using bzImage, the protected-mode kernel was relocated to 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 $(1 << 6), 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

После этого регистр будет содержать адрес метки. Давайте посмотрим на аналогичный код поиска адреса 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, мы увидим следующее:

nexti
...
ebp            0x100000	0x100000
...

Да, всё верно. Адрес startup_32 равен 0x100000. После того, как мы узнали адрес метки startup_32, мы можем начать подготовку к переходу в long mode. Наша следующая цель - настроить стек и убедится в том, что ЦПУ поддерживает long mode и SSE.

Настройка стека и проверка ЦПУ

Мы не могли настроить стек, пока не знали адрес метки 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 и нам просто нужно поместить его в указатель стека.

После того, как мы создали стек, следующим шагом является проверка ЦПУ. Так как мы собираемся перейти в long mode, нам необходимо проверить, поддерживает ли ЦПУ 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, которая останавливает ЦПУ вызовом инструкции hlt до тех пор, пока не произойдёт аппаратное прерывание:

no_longmode:
1:
	hlt
	jmp     1b

Если значение eax равно нулю, то всё в порядке и мы можем продолжить.

Расчёт адреса релокации

Следующим шагом является вычисление адреса релокации для декомпрессии в случае необходимости. Мы уже знаем, что базовый адрес 32-битной точки входа в ядро Linux - 0x100000, но это не 32-битная точка входа. Базовый адрес ядра по умолчанию определяется значением параметра конфигурации ядра CONFIG_PHYSICAL_START. Его значение по умолчанию 0x1000000 или 16 Мб. Основная проблема заключается в том, что если происходит краш ядра, разработчик должен иметь rescue ядро ("спасательное" ядро) для kdump, которое сконфигурировано для загрузки из другого адреса. Для решения этой проблемы, ядро Linux предоставляет специальный параметр конфигурации - CONFIG_RELOCATABLE. Как вы можете прочесть в документации ядра:

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.

Проще говоря, это означает, что ядро с той же конфигурацией может загружаться с разных адресов. С технической точки зрения это делается путём компиляции декомпрессора как адресно-независимого кода. Если мы посмотрим на 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
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 и выглядит следующим образом:

#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-битный режим. Во-первых, нам необходимо обновить глобальную таблицу дескрипторов:

	leal	gdt(%ebp), %eax
	movl	%eax, gdt+2(%ebp)
	lgdt	gdt(%ebp)

Здесь мы помещаем базовый адрес из регистра ebp со смещением gdt в регистр eax. Далее мы помещаем этот адрес в регистр ebp со смещением gdt+2 и загружаем глобальную таблицу дескрипторов с помощью инструкции lgdt. Чтобы понять магию смещений gdt, нам необходимо посмотреть на определение глобальной таблицы дескрипторов. Мы можем найти его в этом же файле исходного кода:

	.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 и содержит пять дескрипторов: нулевой дескриптор, сегмент кода ядра, сегмент данных ядра и два дескриптора задач. Мы уже загрузили глобальную таблицу дескрипторов в предыдущей части, и теперь мы делаем почти то же самое здесь, но теперь дескрипторы с 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, поместив значение регистра 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	$((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:

	.section ".pgtable","a",@nobits
	.balign 4096
pgtable:
	.fill 6*4096, 1, 0

Как мы видим, она находится в секции .pgtable и имеет размер 24 Кб.

После того, как мы получили буфер для 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, 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 Мб страницами:

	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:

	movl	$(X86_CR0_PG | X86_CR0_PE), %eax
	movl	%eax, %cr0

и выполняем инструкцию:

lret

Вы должны помнить, что на предыдущем шаге мы посметили адрес функции startup_64 в стек, и после инструкции lret, ЦПУ извлекает адрес и переходит на него.

После всего этого, мы, наконец, в 64-битном режиме:

	.code64
	.org 0x200
ENTRY(startup_64)
....
....
....

На этом всё!

Заключение

Это конец четвёртой части о процессе загрузки ядра Linux. В следующей части мы увидим декомпрессию ядра и многое другое.

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

Ссылки