You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

552 lines
48 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

Процесс загрузки ядра. Часть 2.
================================================================================
Первые шаги в настройке ядра
--------------------------------------------------------------------------------
Мы начали изучение внутренностей Linux в предыдущей [части](linux-bootstrap-1.md) и увидели начальную часть кода настройки ядра. Мы остановились на вызове функции `main` (это первая функция, написанная на C) из [arch/x86/boot/main.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/main.c).
В этой части мы продолжим исследовать код установки ядра и
* увидим `защищённый режим`,
* некоторую подготовку для перехода в него,
* инициализацию кучи и консоли,
* обнаружение памяти, проверку CPU, инициализацию клавиатуры
* и многое другое.
Итак, давайте начнём.
Защищённый режим
--------------------------------------------------------------------------------
Прежде чем мы сможем перейти к нативному для Intel64 режиму [Long Mode](http://en.wikipedia.org/wiki/Long_mode), ядро должно переключить CPU в защищённый режим.
Что такое [защищённый режим](https://en.wikipedia.org/wiki/Protected_mode)? Защищённый режим был впервые добавлен в архитектуре x86 в 1982 году и был основным режимом процессоров Intel, начиная с [80286](http://en.wikipedia.org/wiki/Intel_80286), пока в Intel64 не появился режим Long Mode.
Основная причина не использовать [режим реальных адресов](http://wiki.osdev.org/Real_Mode) заключается в том, что возможен лишь очень ограниченный доступ к оперативной памяти. Как вы помните из предыдущей части, есть только 2<sup>20</sup> байт или 1 мегабайт, а иногда даже 640 килобайт оперативной памяти, доступной в режиме реальных адресов.
Защищённый режим принёс много изменений, но главным является отличие в управлении памятью. 20-битная адресная шина была заменена на 32-битную. Это позволило обеспечить доступ к 4 Гб памяти против 1 мегабайта в режиме реальных адресов. Также была добавлена поддержка [подкачки страниц](http://en.wikipedia.org/wiki/Paging), про которую вы можете прочитать в следующих разделах.
Управление памятью в защищённом режиме разделяется на две, почти независимые части:
* Сегментация
* Подкачка страниц
Здесь мы будем рассматривать только сегментацию. Подкачка страниц будет обсуждаться в следующих разделах.
Как вы можете помнить из предыдущей части, адреса в режиме реальных адресов состоят из двух частей:
* Базовый адрес сегмента
* Смещение от базового сегмента
И мы можем получить физический адрес, если нам известны эти две части:
```
Физический адрес = Селектор сегмента * 16 + Смещение
```
Сегментация памяти в защищённом режиме была полностью переделана. В нём нет фиксированных 64 килобайтных сегментов. Вместо этого, размер и расположение каждого сегмента описывается структурой данных, называемой ескриптором сегмента_. Дескрипторы сегментов хранятся в структуре данных под названием `глобальная дескрипторная таблица` (GDT).
GDT представляет собой структуру, которая находится в памяти. Она не имеет постоянного места в памяти, поэтому её адрес хранится в специальном регистре `GDTR`. Позже мы увидим загрузку GDT в коде ядра Linux. Там будет операция для её загрузки в память, что-то вроде:
```assembly
lgdt gdt
```
где инструкция `lgdt` загружает базовый адрес и предел (размер) глобальной дескрипторной таблицы в регистр `GDTR`. `GDTR` является 48-битным регистром и состоит из двух частей:
* размер (16 бит) глобальной дескрипторной таблицы;
* адрес (32 бита) глобальной дескрипторной таблицы.
Как упоминалось ранее, GDT содержит `дескрипторы сегментов`, которые описывают сегменты памяти. Каждый дескриптор является 64-битным. Общая схема дескриптора такова:
```
31 24 19 16 7 0
-------------------------------------------------------------------------------
| | |B| |A| | | | |0|E|W|A| |
| БАЗОВЫЙ АДРЕС 31:24 |G|/|L|V| ПРЕДЕЛ |P|DPL|S| ТИП | БАЗОВЫЙ АДРЕС 23:16 | 4
| | |D| |L| 19:16 | | | |1|C|R|A| |
-------------------------------------------------------------------------------
| | |
| БАЗОВЫЙ АДРЕС 15:0 | ПРЕДЕЛ 15:0 | 0
| | |
-------------------------------------------------------------------------------
```
Не волнуйтесь, я знаю, после режима реальных адресов это выглядит немного страшно, но на самом деле всё довольно легко. Например, ПРЕДЕЛ 15:0 означает, что биты 0-15 дескриптора содержат значение предела. Остальная его часть находится в ПРЕДЕЛ 19:16. Таким образом, размер предела составляет 0-19, т.е 20 бит. Давайте внимательно взглянем на структуру дескриптора:
1. Предел (20 бит) находится в пределах 0-15, 16-19 бит. Он определяет `длину_сегмента - 1`. Зависит от бита `G` (гранулярность).
* Если `G` (бит 55) и предел сегмента равен 0, то размер сегмента составляет 1 байт
* Если `G` равен 1, а предел сегмента равен 0, то размер сегмента составляет 4096 байт
* Если `G` равен 0, а предел сегмента равен 0xfffff, то размер сегмента составляет 1 мегабайт
* Если `G` равен 1, а предел сегмента равен 0xfffff, то размер сегмента составляет 4 гигабайта
Таким образом, если
* G равен 0, предел интерпретируется в терминах 1 байта, а максимальный размер сегмента может составлять 1 мегабайт.
* G равен 1, предел интерпретируется в терминах 4096 байт = 4 килобайта = 1 страница, а максимальный размер сегмента может составлять 4 гигабайта. На самом деле, когда G равен 1, значение предела сдвигается на 12 бит влево. Таким образом, 20 бит + 12 бит = 32 бита и 2<sup>32</sup> = 4 гигабайта.
2. Базовый адрес (32 бита) находится в пределах 0-15, 32-39 и 56-63 бит. Он определяет физический адрес начального расположения сегмента.
3. Тип/Атрибут (40-47 бит) определяет тип сегмента и виды доступа к нему.
* Флаг `S` (бит 44) определяет тип дескриптора. Если `S` равен 0, то этот сегмент является системным сегментом, а если `S` равен 1, то этот сегмент является сегментом кода или сегментом данных (сегменты стека являются сегментами данных, которые должны быть сегментами для чтения/записи).
Для того чтобы определить, является ли сегмент сегментом кода или сегментом данных, мы можем проверить атрибут (бит 43), обозначенный как 0 в приведённой выше схеме. Если он равен 0, то сегмент является сегментом данных, в противном случае это сегмент кода.
Сегмент может быть одного из следующих типов:
```
| Поле типа | Тип дескриптора | Описание
|-----------------------------|-----------------|------------------
| Десятичное | |
| 0 E W A | |
| 0 0 0 0 0 | Данные | Только для чтения
| 1 0 0 0 1 | Данные | Только для чтения, было обращение
| 2 0 0 1 0 | Данные | Чтение/запись
| 3 0 0 1 1 | Данные | Чтение/запись, было обращение
| 4 0 1 0 0 | Данные | Только для чтения, растёт вниз
| 5 0 1 0 1 | Данные | Только для чтения, растёт вниз, было обращение
| 6 0 1 1 0 | Данные | Чтение/запись, растёт вниз
| 7 0 1 1 1 | Данные | Чтение/запись, растёт вниз, было обращение
| C R A | |
| 8 1 0 0 0 | Код | Только для выполнения
| 9 1 0 0 1 | Код | Только для выполнения, было обращение
| 10 1 0 1 0 | Код | Выполнение/чтение
| 11 1 0 1 1 | Код | Выполнение/чтение, было обращение
| 12 1 1 0 0 | Код | Только для выполнения, подчинённый
| 14 1 1 0 1 | Код | Только для выполнения, подчинённый, было обращение
| 13 1 1 1 0 | Код | Выполнение/чтение, подчинённый
| 15 1 1 1 1 | Код | Выполнение/чтение, подчинённый, было обращение
```
Как мы можем видеть, первый бит (бит 43) равен `0` для сегмента анных_ и `1` для сегмента ода_. Следующие три бита (40, 41, 42): либо биты `EWA` (бит направления расширения (*E*xpansion), бит записи (*W*ritable), бит обращения (*A*ccessible)), либо `CRA` (бит подчинения (*C*onforming), бит чтения (*R*eadable), бит доступа (*A*ccessible)).
* Если E (бит 42) равен 0, то сегмент растёт вверх, в противном случае растёт вниз. Подробнее [здесь](http://www.sudleyplace.com/dpmione/expanddown.html).
* Если W (бит 41) (для сегмента данных) равен 1, то запись в сегмент разрешена. Обратите внимание, что право на чтение всегда разрешено для сегментов данных.
* A (бит 40) - было ли обращение процессора к сегменту.
* C (бит 43) - бит подчинения (для сегмента кода). Если C равен 1, то сегмент кода может быть выполнен из более низкого уровня привилегий, например, из уровня пользователя. Если C равно 0, то сегмент может быть выполнен только из того же уровня привилегий.
* R (бит 41) (для сегмента кода). Если он равен 1, то чтение сегмента разрешено. Право на запись всегда запрещено для сегмента кода.
4. DPL [2 бита] (уровень привилегий сегмента (Descriptor Privilege Level)) находится в 45-46 битах. Определяет уровень привилегий сегмента от 0 до 3, где 0 является самым привилегированным.
5. Флаг P (бит 47) - указывает на присутствие сегмента в памяти. Если P равен 0, то сегмент является едействительным_ и процессор откажется читать этот сегмент.
6. Флаг AVL (бит 52) - доступный и зарезервированный бит. Игнорируется в Linux.
7. Флаг L (бит 53) - указывает на то, содержит ли сегмент кода нативный 64-битный код. Если он равен 1, то сегмент кода будет выполнен в 64-битном режиме.
8. Флаг D/B (бит 54) - флаг разрядности (Default/Big), определяет размер операнда, т.е 16/32 бит. Если он установлен, то находящиеся в сегменте операнды считаются имеющими размер 32 бита, иначе 16 бит.
Сегментные регистры содержат селекторы сегментов, так же как и в режиме реальных адресов. Тем не менее, в защищённом режиме селектор сегмента обрабатывается иначе. Каждый дескриптор сегмента имеет соответствующий селектор сегмента, который представляет собой 16-битную структуру:
```
15 3 2 1 0
-----------------------------
| Index | TI | RPL |
-----------------------------
```
Где,
* **Index** определяет номер дескриптора в GDT.
* **TI** (указатель таблицы (Table Indicator)) определяет таблицу, в которой нужно искать дескриптор. Если он равен 0, то поиск происходит в глобальной дескрипторной таблице (GDT), в противном случае в локальной дескрипторной таблице (LDT).
* **RPL** определяет уровень привилегий.
Каждый сегментный регистр имеет видимую и скрытую часть.
* Видимая - здесь хранится селектор сегмента
* Скрытая - дескриптор сегмента (базовый адрес, предел, атрибуты, флаги)
Необходимы следующие шаги, чтобы получить физический адрес в защищённом режиме:
* Селектор сегмента должен быть загружен в один из сегментных регистров
* CPU пытается найти дескриптор сегмента по адресу GDT + Index из селектора и загрузить дескриптор в *скрытую* часть сегментного регистра
* Базовый адрес (из дескриптора сегмента) + смещение будет линейным адресом сегмента, который является физическим адресом (если подкачка страниц отключена).
Схематично это будет выглядеть следующим образом:
![линейный адрес](http://oi62.tinypic.com/2yo369v.jpg)
Алгоритм перехода из режима реальных адресов в защищённый режим:
* Отключить прерывания
* Описать и загрузить GDT инструкцией `lgdt`
* Установить бит PE (Protection Enable) в CR0 (регистр управления 0 (Control Register 0))
* Перейти к коду защищённого режима
Полный переход в защищённый режим мы увидим в следующей части, но прежде чем мы сможем перейти в защищённый режим, нужно совершить ещё несколько приготовлений.
Давайте посмотрим на [arch/x86/boot/main.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/main.c). Мы можем видеть некоторые подпрограммы, которые выполняют инициализацию клавиатуры, инициализацию кучи и т.д. Рассмотрим их.
Копирование параметров загрузки в "нулевую страницу" (zeropage)
--------------------------------------------------------------------------------
Мы стартуем из подпрограммы `main` в "main.c". Первая функция, которая вызывается в `main` - [`copy_boot_params(void)`](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/main.c#L30). Она копирует заголовок настройки ядра в поле структуры `boot_params`, которая определена в [arch/x86/include/uapi/asm/bootparam.h](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/include/uapi/asm/bootparam.h#L113).
Структура `boot_params` содержит поле `struct setup_header hdr`. Эта структура содержит те же поля, что и в [протоколе загрузки Linux](https://www.kernel.org/doc/Documentation/x86/boot.txt) и заполняется загрузчиком, а так же во время компиляции/сборки ядра. `copy_boot_params` делает две вещи:
1. Копирует `hdr` из [header.S](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/header.S#L281) в структуру `boot_params` в поле `setup_header`
2. Обновляет указатель на командную строку ядра, если ядро было загружено со старым протоколом командной строки.
Обратите внимание на то, что он копирует `hdr` с помощью функции `memcpy`, которая определена в [copy.S](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/copy.S). Взглянем на неё:
```assembly
GLOBAL(memcpy)
pushw %si
pushw %di
movw %ax, %di
movw %dx, %si
pushw %cx
shrw $2, %cx
rep; movsl
popw %cx
andw $3, %cx
rep; movsb
popw %di
popw %si
retl
ENDPROC(memcpy)
```
Да, мы только что перешли в C-код и снова вернулись к ассемблеру :) Прежде всего мы видим, что `memcpy` и другие подпрограммы, расположенные здесь, начинаются и заканчиваются двумя макросами: `GLOBAL` и `ENDPROC`. Макрос `GLOBAL` описан в [arch/x86/include/asm/linkage.h](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/include/asm/linkage.h) и определяет директиву `globl`, а так же метку для него. `ENDPROC` описан в [include/linux/linkage.h](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/include/linux/linkage.h); отмечает символ `name` в качестве имени функции и заканчивается размером символа `name`.
Реализация `memcpy` достаточно проста. Во-первых, она помещает значения регистров `si` and `di` в стек для их сохранения, так как они будут меняться в течении работы. `memcpy` (как и другие функции в copy.S) использует `fastcall` соглашения о вызовах. Таким образом, она получает свои входные параметры из регистров `ax`, `dx` и `cx`. Вызов `memcpy` выглядит следующим образом:
```c
memcpy(&boot_params.hdr, &hdr, sizeof hdr);
```
Так,
* `ax` будет содержать адрес `boot_params.hdr`
* `dx` будет содержать адрес `hdr`
* `cx` будет содержать размер `hdr` в байтах.
`memcpy` помещает адрес `boot_params.hdr` в `di` и сохраняет размер в стеке. После этого она сдвигается вправо на 2 размера (или делит на 4) и копирует из `si` в `di` по 4 байта. Далее снова восстанавливает размер `hdr`, выравнивает по 4 байта и копирует остальную часть байтов из `si` в `di` побайтово (если они есть). В конце восстанавливает значения `si` и `di` из стека и после этого завершает копирование.
Инициализация консоли
--------------------------------------------------------------------------------
После того как `hdr` скопирован в `boot_params.hdr`, следующим шагом является инициализация консоли с помощью вызова функции `console_init`, определённой в [arch/x86/boot/early_serial_console.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/early_serial_console.c).
Функция пытается найти опцию `earlyprintk` в командной строке и, если поиск завершился успехом, парсит адрес порта, скорость передачи данных и инициализирует последовательный порт. Значение опции `earlyprintk` может быть одним из следующих:
* serial,0x3f8,115200
* serial,ttyS0,115200
* ttyS0,115200
После инициализации последовательного порта мы можем увидеть первый вывод:
```C
if (cmdline_find_option_bool("debug"))
puts("early console in setup code\n");
```
Определение `puts` находится в [tty.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/tty.c). Как мы видим, она печатает символ за символом в цикле, вызывая функцию `putchar`. Давайте посмотрим на реализацию `putchar`:
```C
void __attribute__((section(".inittext"))) putchar(int ch)
{
if (ch == '\n')
putchar('\r');
bios_putchar(ch);
if (early_serial_base != 0)
serial_putchar(ch);
}
```
`__attribute__((section(".inittext")))` означает, что код будет находиться в секции `.inittext`. Мы можем найти его в файле компоновщика [setup.ld](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/setup.ld#L19).
Прежде всего, `putchar` проверяет наличие символа `\n` и, если он найден, печатает перед ним `\r`. После этого она выводит символ на экране VGA, вызвав BIOS с прерыванием `0x10`:
```C
static void __attribute__((section(".inittext"))) bios_putchar(int ch)
{
struct biosregs ireg;
initregs(&ireg);
ireg.bx = 0x0007;
ireg.cx = 0x0001;
ireg.ah = 0x0e;
ireg.al = ch;
intcall(0x10, &ireg, NULL);
}
```
`initregs` принимает структуру `biosregs` и в первую очередь заполняет `biosregs` нулями, используя функцию `memset`, а затем заполняет его значениями регистров:
```C
memset(reg, 0, sizeof *reg);
reg->eflags |= X86_EFLAGS_CF;
reg->ds = ds();
reg->es = ds();
reg->fs = fs();
reg->gs = gs();
```
Давайте посмотри на реализацию [memset](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/copy.S#L36):
```assembly
GLOBAL(memset)
pushw %di
movw %ax, %di
movzbl %dl, %eax
imull $0x01010101,%eax
pushw %cx
shrw $2, %cx
rep; stosl
popw %cx
andw $3, %cx
rep; stosb
popw %di
retl
ENDPROC(memset)
```
Как мы можем видеть, `memset` использует `fastcall` соглашения о вызовах, так же как и `memcpy`: это означает, что функция получает свои параметры из регистров `ax`, `dx` и `cx`.
Как правило, реализация `memset` подобна реализации `memcpy`. Она сохраняет значение регистра `di` в стеке и помещает значение `ax` в `di`, которое является адресом структуры `biosregs`. Далее идёт инструкция `movzbl`, которая копирует значение `dl` в нижние 2 байта регистра `eax`. Оставшиеся 2 верхних байта `eax` будут заполнены нулями.
Следующая инструкция умножает `eax` на `0x01010101`. Это необходимо, так как `memset` будет копировать 4 байта одновременно. Например, нам нужно заполнить структуру значением `0x7` с помощью `memset`. В этом случае `eax` будет содержать значение `0x00000007`. Так что если мы умножим `eax` на `0x01010101`, мы получим `0x07070707` и теперь мы можем скопировать эти 4 байта в структуру. `memset` использует инструкцию `rep; stosl` для копирования `eax` в `es:di`.
Остальная часть `memset` делает почти то же самое, что и `memcpy`.
После того как структура `biosregs` заполнена с помощью `memset`, `bios_putchar` вызывает прерывание [0x10](http://www.ctyme.com/intr/rb-0106.htm) для вывода символа. Затем она проверяет, инициализирован ли последовательный порт, и в случае если он инициализирован, записывает в него символ с помощью инструкций [serial_putchar](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/tty.c#L30) и `inb/outb`.
Инициализация кучи
--------------------------------------------------------------------------------
После подготовки стека и BSS в [header.S](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/header.S) (смотрите предыдущую [часть](linux-bootstrap-1.md)), ядро должно инициализировать [кучу](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/main.c#L116) с помощью функции [`init_heap`](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/main.c#L116).
В первую очередь `init_heap` проверяет флаг [`CAN_USE_HEAP`](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/include/uapi/asm/bootparam.h#L22) в [`loadflags`](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/header.S#L321) в заголовке настройки ядра и если флаг был установлен, вычисляет конец стека:
```C
char *stack_end;
if (boot_params.hdr.loadflags & CAN_USE_HEAP) {
asm("leal %P1(%%esp),%0"
: "=r" (stack_end) : "i" (-STACK_SIZE));
```
другими словами `stack_end = esp - STACK_SIZE`.
Затем идёт расчёт `heap_end`:
```c
heap_end = (char *)((size_t)boot_params.hdr.heap_end_ptr + 0x200);
```
что означает `heap_end_ptr` или `_end` + `512`(`0x200h`). Последняя проверка заключается в сравнении `heap_end` и `stack_end`. Если `heap_end` больше `stack_end`, то присваиваем `stack_end` значение `heap_end`, чтобы сделать их равными.
Теперь куча инициализирована и мы можем использовать её с помощью метода `GET_HEAP`. В следующих постах мы увидим как она используется, как её использовать и как она реализована.
Проверка CPU
--------------------------------------------------------------------------------
Следующим шагом является проверка CPU с помощью `validate_cpu` из [arch/x86/boot/cpu.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/cpu.c).
Она вызывает функцию [`check_cpu`](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/cpucheck.c#L112) и передаёт ей два параметра: уровень CPU и необходимый уровень CPU; `check_cpu` проверяет, запущено ли ядро на нужном уровне CPU.
```c
check_cpu(&cpu_level, &req_level, &err_flags);
if (cpu_level < req_level) {
...
return -1;
}
```
`check_cpu` проверяет флаги CPU, наличие [long mode](http://en.wikipedia.org/wiki/Long_mode) в случае x86_64 (64-битного) CPU, проверяет поставщика процессора и делает специальные подготовки для некоторых производителей, такие как отключение SSE+SSE2 для AMD в случае их отсутствия и т.д.
Обнаружение памяти
--------------------------------------------------------------------------------
Следующим шагом является обнаружение памяти с помощью функции `detect_memory`. `detect_memory` в основном предоставляет карту доступной оперативной памяти для CPU. Она использует различные программные интерфейсы для обнаружения памяти, такие как `0xe820`, `0xe801` и `0x88`. Здесь мы будем рассматривать только реализацию **0xE820**.
Давайте посмотрим на реализацию `detect_memory_e820` в [arch/x86/boot/memory.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/memory.c). Прежде всего, функция `detect_memory_e820` инициализирует структуру `biosregs`, как мы видели выше, и заполняет регистры специальными значениями для вызова `0xe820`:
```assembly
initregs(&ireg);
ireg.ax = 0xe820;
ireg.cx = sizeof buf;
ireg.edx = SMAP;
ireg.di = (size_t)&buf;
```
* `ax` содержит номер функции (в нашем случае 0xe820)
* `cx` содержит размер буфера, который будет содержать данные о памяти
* `edx` должен содержать магическое число `SMAP`
* `es:di` должен содержать адрес буфера, который будет содержать данные из памяти
* `ebx` должен быть равен нулю.
Далее идёт цикл, в котором будут собраны данные о памяти. Он начинается с вызова BIOS прерывания `0x15`, который записывает одну строку из таблицы распределения адресов. Для получения следующей строки мы должны снова вызвать это прерывание (что мы и делаем в цикле). До следующего вызова `ebx` должен содержать значение, возвращённое ранее:
```C
intcall(0x15, &ireg, &oreg);
ireg.ebx = oreg.ebx;
```
В конечном счёте мы делаем итерации в цикле для сбора данных из таблицы распределения адресов и записываем эти данные в массив `e820entry`:
* начало сегмента памяти
* размер сегмента памяти
* тип сегмента памяти (зарезервированый, используемый и т.д).
Вы можете увидеть результат в выводе `dmesg`, что-то вроде:
```
[ 0.000000] e820: BIOS-provided physical RAM map:
[ 0.000000] BIOS-e820: [mem 0x0000000000000000-0x000000000009fbff] usable
[ 0.000000] BIOS-e820: [mem 0x000000000009fc00-0x000000000009ffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000000f0000-0x00000000000fffff] reserved
[ 0.000000] BIOS-e820: [mem 0x0000000000100000-0x000000003ffdffff] usable
[ 0.000000] BIOS-e820: [mem 0x000000003ffe0000-0x000000003fffffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000fffc0000-0x00000000ffffffff] reserved
```
Инициализация клавиатуры
--------------------------------------------------------------------------------
Следующим шагом является инициализация клавиатуры с помощью вызова функции [`keyboard_init()`](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/main.c#L65). Вначале `keyboard_init` инициализирует регистры с помощью функции `initregs` и вызова прерывания [0x16](http://www.ctyme.com/intr/rb-1756.htm) для получения статуса клавиатуры.
```c
initregs(&ireg);
ireg.ah = 0x02; /* Получение статуса клавиатуры */
intcall(0x16, &ireg, &oreg);
boot_params.kbd_status = oreg.al;
```
После этого она ещё раз вызывает [0x16](http://www.ctyme.com/intr/rb-1757.htm) для установки частоты повторения и задержки.
```c
ireg.ax = 0x0305; /* Установка частоты повторения клавиатуры */
intcall(0x16, &ireg, NULL);
```
Выполнение запросов
--------------------------------------------------------------------------------
Следующие несколько шагов - запросы для различных параметров. Мы не будем погружаться в подробности этих запросов, но вернёмся к этому в последующих частях. Давайте коротко взглянем на эти функции:
Функция [query_mca](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/mca.c#L18) вызывает BIOS прерывание [0x15](http://www.ctyme.com/intr/rb-1594.htm) для получения машинного номера модели, номера субмодели, номера ревизии BIOS, а также других, аппаратно-ориентированных атрибутов:
```c
int query_mca(void)
{
struct biosregs ireg, oreg;
u16 len;
initregs(&ireg);
ireg.ah = 0xc0;
intcall(0x15, &ireg, &oreg);
if (oreg.eflags & X86_EFLAGS_CF)
return -1; /* MCA отсутствует */
set_fs(oreg.es);
len = rdfs16(oreg.bx);
if (len > sizeof(boot_params.sys_desc_table))
len = sizeof(boot_params.sys_desc_table);
copy_from_fs(&boot_params.sys_desc_table, oreg.bx, len);
return 0;
}
```
Функция заполняет регистр `ah` значением `0xc0` и вызывает BIOS прерывание `0x15`. После выполнения прерывания она проверяет [флаг переноса](http://en.wikipedia.org/wiki/Carry_flag) и если он установлен в 1, то это означает, что BIOS не поддерживает [**MCA**](https://en.wikipedia.org/wiki/Micro_Channel_architecture). Если флаг переноса установлен в 0, `ES:BX` будет содержать указатель на таблицу системной информации, которая выглядит следующим образом:
```
Смещение Размер Описание
00h СЛОВО количество следующих байт
02h БАЙТ модель (смотрите #00515)
03h БАЙТ субмодель (смотрите #00515)
04h БАЙТ ревизия BIOS: 0 для первой ревизии, 1 для второй и т.д
05h БАЙТ байт свойства 1 (смотрите #00510)
06h БАЙТ байт свойства 2 (смотрите #00511)
07h БАЙТ байт свойства 3 (смотрите #00512)
08h БАЙТ байт свойства 4 (смотрите #00513)
09h БАЙТ байт свойства 5 (смотрите #00514)
---AWARD BIOS---
0Ah N БАЙТ Уведомление об авторских правах AWARD
---Phoenix BIOS---
0Ah БАЙТ ??? (00h)
0Bh БАЙТ мажорная версия
0Ch БАЙТ минорная версия (BCD)
0Dh 4 БАЙТА ASCIZ-строка "PTL" (Phoenix Technologies Ltd)
---Quadram Quad386---
0Ah 17 БАЙТ ASCII-строка подписи "Quadram Quad386XT"
---Toshiba (По крайней мере Satellite Pro 435CDS)---
0Ah 7 БАЙТ подпись "TOSHIBA"
11h БАЙТ ??? (8h)
12h БАЙТ ??? (E7h) ID продукта??? (предположительно)
13h 3 БАЙТА "JPN"
```
Далее мы вызываем функцию `set_fs` и передаём ей значение регистра `es`. Реализация `set_fs` довольно проста:
```c
static inline void set_fs(u16 seg)
{
asm volatile("movw %0,%%fs" : : "rm" (seg));
}
```
Функция содержит ассемблерную вставку, которая получает значение параметра `seg` и помещает его в регистр `fs`. Существует множество функций в [boot.h](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/boot.h), похожих на `set_fs`, например, `set_gs`, `fs`, `gs` для чтения значения в нём и т.д.
В завершении функция `query_mca` просто копирует таблицу, на которую указывает `es:bx`, в `boot_params.sys_desc_table`.
Следующим шагом является получение информации [Intel SpeedStep](http://en.wikipedia.org/wiki/SpeedStep) с помощью вызова функции `query_ist`. В первую очередь она проверяет уровень CPU, и если он верный, вызывает прерывание `0x15` для получения информации и сохраняет результат в `boot_params`.
Следующая функция - [query_apm_bios](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/apm.c#L21) получает из BIOS информацию об [Advanced Power Management](http://en.wikipedia.org/wiki/Advanced_Power_Management). `query_apm_bios` также вызывает BIOS прерывание `0x15`, но с `ah = 0x53` для проверки поддержки `APM`. После выполнения `0x15`, функция `query_apm_bios` проверяет сигнатуру `PM` (она должна быть равна `0x504d`), флаг переноса (он должен быть равен 0, если есть поддержка `APM`) и значение регистра `cx` (оно должено быть равным 0x02, если имеется поддержка защищённого режима).
Далее она снова вызывает `0x15`, но с `ax = 0x5304` для отсоединения от интерфейса `APM` и подключению к интерфейсу 32-битного защищённого режима. В итоге она заполняет `boot_params.apm_bios_info` значениями, полученными из BIOS.
Обратите внимание: `query_apm_bios` будет выполняться только в том случае, если в конфигурационном файле установлен `CONFIG_APM` или `CONFIG_APM_MODULE`:
```C
#if defined(CONFIG_APM) || defined(CONFIG_APM_MODULE)
query_apm_bios();
#endif
```
Последняя функция - [`query_edd`](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/edd.c#L122), запрашивает из BIOS информацию об `Enhanced Disk Drive`. Давайте взглянем на реализацию `query_edd`.
В первую очередь она читает опцию [edd](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/Documentation/kernel-parameters.txt#L1023) из командной строки ядра и если она установлена в `off`, то `query_edd` завершает свою работу.
Если EDD включён, `query_edd` сканирует поддерживаемые BIOS жёсткие диски и запрашивает информацию о EDD в следующем цикле:
```C
for (devno = 0x80; devno < 0x80+EDD_MBR_SIG_MAX; devno++) {
if (!get_edd_info(devno, &ei) && boot_params.eddbuf_entries < EDDMAXNR) {
memcpy(edp, &ei, sizeof ei);
edp++;
boot_params.eddbuf_entries++;
}
...
...
...
}
```
где `0x80` - первый жёсткий диск, а значение макроса `EDD_MBR_SIG_MAX` равно 16. Она собирает данные в массив структур [edd_info](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/include/uapi/linux/edd.h#L172). `get_edd_info` проверяет наличие EDD путём вызова прерывания `0x13` с `ah = 0x41` и если EDD присутствует, `get_edd_info` снова вызывает `0x13`, но с `ah = 0x48` и `si`, содержащим адрес буфера, где будет храниться информация о EDD.
Заключение
--------------------------------------------------------------------------------
Это конец второй части о внутренностях ядра Linux. В следующей части мы увидим настройки режима видео и остальные подготовки перед переходом в защищённый режим и непосредственно переход в него.
**От переводчика: пожалуйста, имейте в виду, что английский - не мой родной язык, и я очень извиняюсь за возможные неудобства. Если вы найдёте какие-либо ошибки или неточности в переводе, пожалуйста, пришлите pull request в [linux-insides-ru](https://github.com/proninyaroslav/linux-insides-ru).**
Ссылки
--------------------------------------------------------------------------------
* [Защищённый режим (Википедия)](http://en.wikipedia.org/wiki/Protected_mode)
* [Защищённый режим (OSDEV)](http://wiki.osdev.org/Protected_Mode)
* [Long mode](http://en.wikipedia.org/wiki/Long_mode)
* [Неплохое объяснение режимов CPU с кодом](http://www.codeproject.com/Articles/45788/The-Real-Protected-Long-mode-assembly-tutorial-for)
* [Как использовать сегменты с ростом вниз на CPU Intel 386 и более поздних](http://www.sudleyplace.com/dpmione/expanddown.html)
* [Документация по earlyprintk](http://lxr.free-electrons.com/source/Documentation/x86/earlyprintk.txt)
* [Параметры ядра](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/Documentation/kernel-parameters.txt)
* [Последовательная консоль](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/Documentation/serial-console.txt)
* [Intel SpeedStep](http://en.wikipedia.org/wiki/SpeedStep)
* [APM](https://en.wikipedia.org/wiki/Advanced_Power_Management)
* [Спецификация EDD](http://www.t13.org/documents/UploadedDocuments/docs2004/d1572r3-EDD3.pdf)
* [Документация TLDP для процесса загрузки Linux](http://www.tldp.org/HOWTO/Linux-i386-Boot-Code-HOWTO/setup.html) (старая)
* [Предыдущая часть](linux-bootstrap-1.md)