Часть Серии Махула.

В этой статье рассматривается, как мы переходим от состояния, в котором нас оставляет GRUB, к рабочей среде C, работающей в 64-битном режиме, сопоставленном с более высокой половиной виртуального адресного пространства. Полное исходное дерево можно найти на нашей ветке начальной загрузки.

В прошлый раз мы говорили о необходимости вручную настроить тип среды, которую обычно предоставляет компилятор C, связывая crt0.o с libc. Для обычной программы это включало бы простую инициализацию стека, сбор всех аргументов командной строки и вызов main(). Для нас есть еще несколько шагов начальной загрузки, прежде чем мы сможем вызвать наш код C. Чтобы понять, что здесь происходит, нам нужно поговорить о паре ключевых понятий: виртуальная память и режимы процессора.

Виртуальная память

Просто пробежимся по основам: каждый байт памяти имеет уникальный адрес, по которому программа может получить к нему доступ. Размер этих адресов ограничен несколькими факторами, одним из которых является размер регистров ЦП. В 32-битных системах адрес памяти использует все 32 бита, поэтому у нас есть 2³² возможных адресов. Теоретически 64-битная система может адресовать 2⁶⁴ байта памяти, но в настоящее время используются только 48 младших значащих битов. AMD указывает, что 16 старших битов являются копиями бита 47. Это означает, что 64-битное адресное пространство пропускается от 0x00007FFF,FFFFFFFF до 0xFFFF8000,00000000.

Теперь все становится сложнее, когда мы понимаем, что для большинства программ используемые нами адреса на самом деле не соответствуют физическому местоположению в памяти. Вместо этого, когда ЦП обращается к объекту через его адрес в памяти (называемый виртуальным адресом), запрос будет преобразован в физический адрес, который действительно представляет, где находится объект. проживает. Перевод между виртуальными и физическими адресами обрабатывается набором структур данных, настроенных операционной системой.

Давайте сначала посмотрим, как это работает в системах i386: память делится на фрагменты фиксированного размера, называемые страницами, которые обычно имеют размер 0x1000 байт (4 КиБ). Адреса для каждой страницы хранятся в массиве из 1024 4-байтовых записей (таблица страниц). Каждый адрес страницы выровнен по 0x1000 байтам, потому что последние 12 бит каждой записи используются для хранения различных флагов. Адрес каждой таблицы страниц также должен быть выровнен таким же образом, чтобы его можно было сохранить в аналогичном массиве, называемом каталогом страниц. Физический адрес каталога страниц хранится в cr3.

Таким образом, в системе i386 с включенным таким типом подкачки всякий раз, когда мы пытаемся получить доступ к ячейке памяти, такой как 0xC2F80032, первые 10 бит (0b1100001011) будут взяты в качестве индекса в каталоге страниц, где хранится таблица страниц. Если для записи не установлен флаг присутствия, то таблицы страниц нет и возникает ошибка страницы. В противном случае мы просматриваем следующие 10 бит (0b1110000000), чтобы найти индекс в таблице страниц, где хранится адрес страницы. Снова произойдет ошибка страницы, если текущий флаг не включен. Наконец, последние 12 бит (0b000000110010) используются для поиска смещения на странице, которую мы только что нашли.

Благодаря этой системе операционная система может отображать любую страницу физической памяти на любой виртуальный адрес, который она хочет. Эти каталоги страниц можно переключать и сохранять, чтобы создать уникальный контекст памяти для каждого запускаемого процесса. Таким образом, мы можем защитить работающие программы друг от друга, предоставив каждому свой собственный каталог страниц. Процесс A не может перезаписывать данные, необходимые процессу B, если каталог страниц B переключается каждый раз, когда A начинает работать.

Еще одно ключевое преимущество заключается в том, что мы можем наметить общую карту памяти для каждого запускаемого процесса. Мы можем, например, пообещать, что каждый процесс будет загружаться по виртуальному адресу 0x0. Затем компоновщик может сделать предположение, что адрес каждой функции в программе всегда будет по одному и тому же адресу. Кроме того, мы можем сопоставить код нашего ядра с одним и тем же адресом в карте памяти для каждого процесса, чтобы упростить системные вызовы. Ядро, отображаемое над программами пользовательского пространства, называется половина ядра высшего уровня.

Расширение адреса страницы

Несмотря на то, что эти таблицы страниц и каталоги должны быть настроены операционной системой, фактическая работа по переводу выполняется за пределами ЦП блоком управления памятью (MMU). Поскольку после включения подкачки физические адреса используются только этим MMU, размер регистров ЦП больше не устанавливает теоретического ограничения на размер адреса физической памяти. Хотя 32-разрядные системы по-прежнему могут ссылаться только на до 4 ГБ виртуальной памяти, страницы могут сопоставляться с физическими адресами за его пределами. Эта идея позволила создать Расширение физических адресов (PAE).

Когда PAE включен в 32-битной системе, кое-что меняется. Во-первых, записи внутри таблицы страниц становятся 8 байтами вместо 4. 12 младших значащих бит по-прежнему используются для хранения тех же флагов, а общий размер структуры данных по-прежнему составляет всего 4 КБ. Конечно, это означает, что одностраничная таблица теперь содержит только 512 целых чисел вместо 1024.

Чтобы компенсировать небольшое количество записей, была добавлена ​​новая структура, называемая Таблица указателей каталога страниц (PDPT). Точно так же, как каталог страниц содержал адреса таблиц страниц, которые, в свою очередь, указывали на фактические страницы, PDPT теперь содержит адреса различных каталогов страниц. PDPT хранит четыре 8-байтовых целого, возвращая нас к тому же количеству записей в таблице страниц, которое мы могли бы хранить без PAE. Поскольку каждая запись имеет размер 8 байт, наши страницы могут быть сопоставлены с физическими адресами за пределами 32-битного адресного пространства.

Мы можем думать о конечном результате как о работе с тремя массивами, каждый из которых является адресом следующего:

uint64_t page_directory_pointer_table[4];
uint64_t page_directory[512];
uint66_t page_table[512];

Теперь, когда мы хотим транслировать виртуальный адрес, нам нужно сначала получить индекс в PDPT из 2 самых значащих битов, затем индекс page_directory из следующих 9, индекс page_table из 9 после этого и, наконец, смещение внутри страница из 12 младших значащих битов.

Режимы процессора

Для обеспечения обратной совместимости процессоры x86 могут работать в разных режимах . При загрузке компьютер работает в реальном режиме, что означает, что он работает только с 16-битными регистрами. Их можно расширить до 32-битных регистров, войдя в защищенный режим. Оба эти режима работают на системах x86_64 так же, как и на i386, но в 64-битных системах добавляется длинный режим. В длинном режиме ЦП может выполнять 32-битный или 64-битный код. 32-битный код работает в основном так же, как в длинном режиме, так и в защищенном режиме с несколькими ключевыми отличиями: одно из которых заключается в том, что требуется разбиение по страницам — и будет работать немного по-другому.

Пейджинг в длинном режиме

Пейджинг в режимах длинного режима очень похож на пейджинг с включенным PAE: таблицы страниц содержат 512 8-байтовых записей и доступны те же флаги. Физические адреса таблиц страниц хранятся в каталогах страниц, на которые, в свою очередь, указывают таблицы указателей каталогов страниц. Однако эти PDPT теперь также могут содержать 512 записей, и не добавлена ​​структура сопоставления страниц четвертого уровня, которая называется уровень сопоставления страниц 4 (PML4).

Как упоминалось выше, при трансляции используются 48 бит виртуального адреса — биты 48–63 должны быть копией бита 47 — биты 0–11 задают смещение в 4-килобайтной странице, 12–20 указывают индекс страницы в таблица страниц. 21–29 показывают индекс каталога страниц, а 30–38 — индекс в PDPT. Это оставляет биты 39–47 для индекса в PML4.

Все это, конечно, предполагает, что мы используем страницы размером 4 КБ. Если бы мы активировали бит 7 в одной из записей в каталоге страниц, то эта страница была бы размером 4 МБ, биты 0–20 виртуального адреса стали бы смещением внутри этой страницы. В более новых системах мы можем активировать тот же бит в записи в PDPT, чтобы указать, что мы используем страницы размером 1 ГБ, и в этом случае виртуальный адрес использует биты 0–30 для отображения смещения.

ГДТ

И последнее замечание перед тем, как перейти к коду начальной загрузки Macchulus: в длинном режиме мы будем использовать структуру, называемую Глобальная таблица дескрипторов (GDT), для переключения с 32-битного на 64-битный код. GDT используются в системах x86 для предоставления информации об определенном разделе памяти. Первоначально это обеспечивало альтернативу пейджингу под названием сегментация, которая отключена в длинном режиме. Вместо этого нам нужно будет использовать GDT, чтобы определить, содержит ли раздел памяти данные, 32-битный код или 64-битный код.

GDT состоит из 4-байтовых записей, которые применяют набор флагов к разделу памяти. После загрузки GDT мы устанавливаем в регистре cs смещение записи в таблице. Смещение должно указывать процессу на дескриптор, указывающий тип выполняемого кода. Смещение дескриптора сегмента данных хранится в регистрах ss, ds, es, fs и gs.

Махул

Итак, напомним: поскольку мы следуем стандарту Multiboot2, мы знаем, что когда загрузчик передает управление нашему ядру, ЦП будет работать в защищенном режиме с отключенным пейджингом. В регистр eax будет помещено магическое число, а ebx будет содержать физический адрес информационной структуры. Наша задача: 1) проверить магическое число; 2) включить пейджинг; 3) войти в длинный режим; и 4) запускаем нашу рабочую среду C. Большая часть работы выполняется в файле x86_64/boot.S, но до того, как мы туда доберемся, в скрипте компоновщика происходят некоторые примечательные вещи.

x86_64/ldscript.S

Чтобы понять, что делает скрипт компоновщика, нам нужно немного подумать о формате файла ELF, который мы используем для Maculus. Изображения ELF разбиты на разделы, имена которых могут быть произвольными. Некоторые из этих разделов являются стандартными, например, код будет находиться в разделе под названием .text, а .rodata — это место, где можно искать данные, доступные только для чтения. Каждый из этих разделов будет связан как с адресом виртуальной памяти (VMA), так и с адресом загрузочной памяти (LMA). Когда программа помещается в память для выполнения, каждый раздел помещается в LMA; однако адреса, используемые внутри программы, будут исходить от VMA. Для нас это означает, что LMA раздела будет указывать его физический адрес, а виртуальный адрес будет исходить от его VMA.

Чтобы сделать этот процесс немного более динамичным, мы собираемся передать ldscript.S в $(CPP), чтобы сгенерировать окончательное ldscript, используемое самим $(LD). Это достигается правилом в x86_64/Makefrag.am

x86_64/ldscript: x86_64/ldscript.S
 $(CPP) $(AM_CPPFLAGS) -P -o $@ $<

Затем мы можем использовать макросы из сценария configure и другие заголовки, чтобы реагировать на определенные настройки. Самый важный макрос здесь — KERNEL_OFFSET, который будет представлять разницу между физическим и виртуальным адресами ядра. 64-битная версия ядра будет иметь смещение 0xFFFFFFFF80000000, а в 32-битных системах смещение будет 0xC0000000. Вы можете узнать 64-битную версию из нашего обсуждения виртуальной памяти выше. Это адрес, где допустимые адреса x86_64 пропускаются, чтобы гарантировать, что биты 48–63 являются копиями бита 47.

Теперь обратимся к x86_64/ldscript.S: мы собираемся загрузить наше ядро ​​по физическому адресу 0x00100000 (1МиБ). Загрузчик обычно рассчитывает, что сможет использовать первый мегабайт памяти, и это предотвращает перезапись нашего ядра чем-либо важным или само перезапись.

. = KERNEL_OFFSET + 0x00100000;

Далее нам нужно разместить наш раздел .text:

.text : AT(ADDR(.text) - KERNEL_OFFSET) {
     KEEP(*(.multiboot_header))
     *(.text)
}

Предложение AT() после двоеточия указывает LMA, и, поскольку мы не добавляли VMA, компоновщик просто поместит его в следующее доступное место в виртуальной памяти. Нам нужно убедиться, что заголовок, требуемый спецификацией Multiboot2, появляется в начале нашего образа ядра, поэтому мы создадим для него специальный раздел, а затем поместим весь этот раздел в начало .text. Функция KEEP() говорит нашему компоновщику включить ее, даже если она больше нигде не упоминается в нашей программе.

Затем мы таким же образом включаем раздел для данных только для чтения:

.rodata ALIGN(4K) : AT(ADDR(.rodata) - KERNEL_OFFSET) {
     *(.rodata)
}

Здесь функция ALIGN() указывает компоновщику разместить его по следующему доступному виртуальному адресу, совмещенному с началом страницы. Мы делаем то же самое с нашими доступными для записи данными (в .data) и нашими неинициализированными данными (в .bss):

.data ALIGN(4K) : AT(ADDR(.data) - KERNEL_OFFSET) {
     *(.boot_gdt)
     *(.boot_paging)
     *(.data)
}
.bss ALIGN(4K) : AT(ADDR(.bss) - KERNEL_OFFSET) {
     *(.bss)
}

Обратите внимание, что мы также создаем пользовательские разделы для нашего исходного GDT и исходных структур подкачки. Эти структуры являются временными, и размещение их в отдельных секциях облегчит их освобождение позже.

Этот скрипт компоновщика будет нормально работать как на 32-, так и на 64-битных сборках, с одной оговоркой: компоновщик иногда будет размещать буферное пространство между разделами в самом образе, чтобы упростить их загрузку. Эти смещения могут стать огромными в 64-битных системах (и испортить расположение нашего заголовка мультизагрузки), поэтому нам нужно отключить эту функцию, передав флаг -n компоновщику. Кроме того, gcc может сделать ложное предположение, что наше ядро ​​работает с теми же младшими виртуальными адресами, что и большинство программ пользовательского пространства. Мы можем отключить это предположение, добавив -mcmodel=kernel к CFLAGS. Этот флаг был добавлен специально для ядра Linux, поэтому работает только для нас, потому что наше 64-битное смещение такое же, как у них. Если бы мы выбрали другой VMA, то нам пришлось бы использовать -mcmodel=large, который отключает все предположения об адресах памяти. Все эти флаги добавлены в x86_64/Makefrag.am:

AM_CFLAGS += -mcmodel=kernel -mno-red-zone
machulus_LINKFLAGS += -n

С установленными флажками и выделенными разделами мы готовы начать программирование!

x86_64/boot.S

Давайте сначала рассмотрим структуры данных, которые мы настроили. Нам понадобятся три из них для начального процесса начальной загрузки. Во-первых, нам нужен GDT, чтобы мы могли переключиться на 64-битный код. Однако, прежде чем мы сможем это сделать, нам нужно войти в длинный режим, для которого требуется, чтобы пейджинг был включен. Я собираюсь настроить пейджинг небрежно, но достаточно хорошо. Вместо того, чтобы точно вычислять, какие страницы должны быть отображены в конкретной системе во время выполнения, я просто собираюсь жестко запрограммировать сопоставления для первого гигабайта памяти. Этого будет более чем достаточно для жизни нашего микроядра. Чтобы упростить этот процесс, я собираюсь использовать страницы размером 1 ГиБ, так что на самом деле я отображаю только одну страницу. Это означает, что вместо четырех структур пейджинга мне нужны только две: PML4 и PDPT.

GDT

Сначала мы рассмотрим GDT. Он установлен в x86_64/boot.S:

.section .boot_gdt
.align 16
boot_gdt:
     /* null descriptor */
     .quad 0
     /* 32-bit code */
     .word 0xFFFF
     .word 0x0
     .byte 0x0
     .byte 0x9a
     .byte 0xcf
     .byte 0x0
     /* 64-bit code */
     .word 0xFFFF
     .word 0x0
     .byte 0x0
     .byte 0x9a
     .byte 0xaf
     .byte 0x0
     /* data segment */
     .word 0xFFFF
     .word 0x0
     .byte 0x0
     .byte 0x92
     .byte 0xcf
     .byte 0x0
boot_gdt_end:

Я расскажу больше о конкретном формате GDT в следующем посте — нам нужно будет быстро заменить этот. На данный момент достаточно знать, что у нас есть массив 8-байтовых записей, первая из которых должна быть 0. Внутри каждой записи у нас есть 32-битный базовый адрес, 20-битный бит limit и различные флаги. Помните, что цель этой структуры — описать кусок памяти, поэтому нам нужен базовый адрес, чтобы сказать нам, где начинается блок, и предел, чтобы сказать нам, насколько он велик. Базовый адрес для всех этих записей — 0x0, а ограничение — 0xFFFFF. Что касается процессора, любая запись, загруженная в регистр cs, может относиться ко всей доступной памяти.

Таким образом, первая запись должна быть 0, но после этого у нас есть два дескриптора кода. Единственная разница между ними находится в битах 52 и 53. Это просто разница между 32-битным и 64-битным кодом. Последние 8 байтов дают нам дескриптор для представления данных — его мы будем использовать для описания нашего начального стека.

Если мы действительно хотим загрузить этот GDT, нам нужен дескриптор GDT, который представляет собой просто структуру данных, сообщающую процессору, как читать сам GDT. Дескриптор должен содержать размер GDT в первых двух байтах, а следующие четыре байта содержат адрес, по которому можно найти GDT. Мы настраиваем это так:

boot_gdt_descr:
     .word boot_gdt - boot_gdt_end
     .long boot_gdt - KERNEL_OFFSET

Нам нужно вычесть KERNEL_OFFSET из адреса boot_gdt, потому что мы загружаем структуру до включения пейджинга. Компоновщик, однако, назначит виртуальные адреса всем нашим меткам, поэтому нам нужно делать это всякий раз, когда мы обращаемся к метке до того, как виртуальная память будет настроена.

НДПТ

Таким образом, мы хотим в конечном итоге отобразить одну страницу, но нам нужно будет отобразить ее дважды: сначала в 0x0, а затем в KERNEL_OFFSET. Пейджинг вступит в силу сразу после того, как мы активируем его в управляющем регистре, но у нас все еще будут физические адреса, загруженные в наш регистр в это время, поэтому нам нужно убедиться, что оба действительны, пока мы не будем действительно готовы. Помните, что запись в любой из структур подкачки — это просто базовый адрес следующей страницы или таблицы, где 12 младших битов заменены флагами. В этом случае наш базовый адрес будет 0x0, поэтому нам просто нужно побеспокоиться о флагах. Нас интересуют флаг присутствия (бит 0), флаг возможности записи (бит 1) и флаг размера (бит 7), которые сигнализируют о том, что мы используем большие страницы. Собрав все это вместе, мы видим, что нам нужно, чтобы каждая из наших записей равнялась 0x83.

Далее, куда мы поместим эти записи? Помните, что индекс PDPT представлен битами 30–38 виртуального адреса. При отображении на адрес 0x0 эти биты явно равны 0. Однако наша старшая половина отображения будет иметь значение 0xFFFFFFFF80000000, поэтому соответствующие биты равны 0b111111110 или 510. Любой другой индекс в нашем PDPT может быть установлен равным 0 (или вообще чему угодно с текущий бит отключен). Мы можем увидеть результат здесь:

.align 0x1000
.type boot_pdpt, @object
boot_pdpt:
     .quad 0x83
     .skip (509 * 8), 0
     .quad 0x83
     .skip (1 * 8), 0

PML4

Далее нам нужно сделать то же самое для PML4. На этот раз нам не нужно включать бит размера, поэтому наши флаги будут просто 0x03, но дополнительно нам нужен фактический базовый адрес для boot_pdpt. Базовый адрес должен быть физическим адресом таблицы, поэтому мы можем установить его равным boot_pdpt — KERNEL_OFFSET, а затем добавить наши флаги. На этот раз индекс хранится в битах 39–47 виртуального адреса, поэтому отображение 0x0 по-прежнему остается первым, но теперь отображение старшей половины находится в индексе 511:

.align 0x1000
.type boot_pml4, @object
boot_pml4:
     .quad ((boot_pdpt - KERNEL_OFFSET) + 0x03)
     .skip (510 * 8), 0
     .quad ((boot_pdpt - KERNEL_OFFSET) + 0x03)

Жестко запрограммированные все эти структуры значительно упрощают фактический код, который нам нужно написать, и в конечном итоге система загружается быстрее (потому что ей не нужно их создавать). Эти структуры могут быть освобождены в доступной памяти, так что на самом деле это не проблема — при этом мы жертвуем переносимостью. Мы теряем возможность настроить начальную подкачку на основе фактической конфигурации произвольной системы. Это то, что меня не очень волнует — помните, что моя цель — просто нацелиться на свои собственные компьютеры, — но хорошо знать о наших ограничениях.

_start

Сама точка входа довольно проста теперь, когда мы инициализировали эти структуры данных. Сразу же нам нужно проверить правильность магического числа в eax:

cmpl $(MULTIBOOT2_BOOTLOADER_MAGIC),%eax
jne err_no_multiboot

Это освобождает регистр для использования в процессе начальной загрузки. Мы знаем, что если мы вообще доберемся до кода C, то значение в ebx будет допустимой структурой multiboot2, так что нет причин сохранять это значение. err_no_multiboot просто печатает сообщение об ошибке и останавливает систему.

После этого нам нужно загрузить наш GDT и активировать его. Это потребует длинного перехода, что является единственным способом установить правильное значение регистра cs. Мы хотим, чтобы он содержал смещение нашего 32-битного дескриптора кода, равное 8. Регистры сегмента данных, в которые мы можем просто переместить правильное значение. Им нужно смещение дескриптора данных: 24. Вот что у нас получилось:

     lgdt boot_gdt_descr - KERNEL_OFFSET
     ljmp $8,$(cs_set - KERNEL_OFFSET)
cs_set:
     movw $0,%ax
     movw %ax,%ds
     movw %ax,%es
     movw %ax,%fs
     movw %ax,%gs
     movw $24,%ax
     movw %ax,%ds
     movw %ax,%es
     movw %ax,%ss

На этом этапе наш GDT настроен и работает — далее нам нужно убедиться, что система, на которой мы работаем, поддерживает как длинный режим, так и страницы размером 1 ГиБ. Делаем это с помощью команды cpuid, но сначала нужно убедиться, что сама команда поддерживается. Это включает в себя проверку возможности изменения бита 21 в eflags. Прежде чем мы будем что-то возиться, нам нужно сохранить исходное состояние флагов, а это значит, что нам нужен стек. Это просто вопрос указания esp на блок памяти:

movl $(boot_stack_top - KERNEL_OFFSET),%esp

Затем мы можем начать нажимать и выталкивать eflags:

pushf
pop %eax
movl %eax,%ecx
xor $(1 << 21),%eax
push %eax
popf
pushf
pop %eax
push %ecx
popf

xor %ecx,%eax
jz err_no_cpuid

Если cpuid не поддерживается, то и длинный режим не поддерживается, и мы можем просто напечатать сообщение и остановить систему. Нам все еще нужно собрать дополнительную информацию, чтобы убедиться, что она поддерживается. При тестировании длинного режима используется cpuid для получения расширенной информации о процессоре. Для этого нам нужно вызвать cpuid с параметром функции 0x80000001 в eax. Если мы сначала вызовем команду, когда eax установлено на 0x80000000, она установит eax на самый высокий допустимый параметр функции в этой системе:

movl $0x80000000,%eax
cpuid
cmpl $0x80000001,%eax
jl err_no_long_mode

Опять же, если мы не можем получить расширенную информацию о процессоре, можно с уверенностью предположить, что длинный режим не поддерживается.

Наконец-то мы можем получить всю искомую информацию — давайте выполним инструкцию:

movl $0x80000001,%eax
cpuid

Теперь все, что мы хотим знать, хранится в виде битового поля в edx. Длинный режим поддерживается, если установлен бит 29, и мы можем посмотреть на бит 26, чтобы убедиться, что мы можем использовать страницы размером 1 ГБ:

test $(1 << 29),%edx
jz err_no_long_mode
test $(1 << 26),%edx
jz err_no_huge_pages

Наша система проверена и все необходимые функции присутствуют! Теперь нам нужно выполнить фактическую работу по переключению в длинный режим. Для этого требуется настроить пейджинг, поэтому мы продолжим и установим cr3 на физический адрес нашего PML4:

movl $(boot_pml4 - KERNEL_OFFSET),%edi
movl %edi,%cr3

Затем нам нужно включить PAE, установив бит 5 в cr4:

movl %cr4,%eax
or $(1 << 5),%eax
movl %eax,%cr4

Наконец, мы можем переключать режимы процессора, но этот шаг будет включать в себя чтение и запись из специфического регистра моделиr. Нам нужны две инструкции: одна для чтения, rdmsr, и одна для записи, wrmsr. Обе эти инструкции используют ecx для указания какого регистра они будут работать. Команда чтения поместит младшие биты 64-битного регистра в eax, а старшие биты в edx, в то время как команда записи читает из тех же регистров. Мы хотим работать с EFER (регистр включения расширенных функций), поэтому ecx нужно установить на 0xC0000080. В EFER мы хотим установить бит 8, чтобы включить длинный режим:

movl $0xc0000080,%ecx
rdmsr
or $(1 << 8),%eax
wrmsr

Теперь безопасно включить пейджинг, установив бит 32 в cr0:

movl %cr0,%eax
or $(1 << 31),%eax
movl %eax,%cr0

Напомним: теперь у нас есть действующий GDT, первый гигабайт памяти сопоставлен как с 0x0, так и с 0xFFFFFFFF80000000, и мы находимся в длинном режиме, выполняя 32-битный код. Мы все еще работаем с младшими адресами памяти, поэтому нам нужно перейти к нашему более высокому полупространству. Однако мы не можем сделать это сразу, потому что наши 32-битные регистры не могут хранить 64-битный адрес памяти. Нам нужно сделать два отдельных прыжка — первый в 64-битный код:

ljmp $16,$(_64bit_entry - KERNEL_OFFSET)

Следующий в старшую половину пространства памяти:

.code64
_64bit_entry:
     movabs $higher_half,%rax
     jmp *%rax
higher_half:

Теперь мы можем начать заменять наши адреса стека на виртуальные, которые мы хотим использовать:

movq %rsp,%rax
addq $KERNEL_OFFSET,%rax
movq %rax,%rsp
movq $boot_stack_bottom,%rbp

Как только это будет сделано, нам больше не нужны сопоставления нижней половины памяти, поэтому давайте избавимся от них, установив соответствующие записи в 0x0:

movq $boot_pml4,%rdi
movq $0x0,(%rdi)
movq $boot_pdpt,%rdi
movq $0x0,(%rdi)

Это недостаточно тихо, потому что наши страницы могли быть сохранены в кеше, называемом TLB. Мы можем позволить системе не очищать кеш этой страницы с помощью инструкции invlpg:

invlpg 0

На этом наша система готова! Все закачано! Мы можем ввести код C и перестать программировать, как пещерные люди!

call kernel_main

Предыдущая: Сборка ядра

Далее: Печать на экран