mirror of
https://github.com/0xAX/linux-insides
synced 2024-11-05 21:20:59 +00:00
554 lines
47 KiB
Markdown
554 lines
47 KiB
Markdown
Процесс загрузки ядра. Часть 2.
|
||
================================================================================
|
||
|
||
Первые шаги в настройке ядра
|
||
--------------------------------------------------------------------------------
|
||
|
||
Мы начали изучение внутренностей Linux в предыдущей [части](linux-bootstrap-1.md) и увидели начальную часть кода настройки ядра. Мы остановились на вызове функции `main` (это первая функция, написанная на C) из [arch/x86/boot/main.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/main.c).
|
||
|
||
В этой части мы продолжим исследовать код установки ядра и
|
||
|
||
* увидим `защищённый режим`,
|
||
* некоторую подготовку для перехода в него,
|
||
* инициализацию кучи и консоли,
|
||
* обнаружение памяти, проверку ЦПУ, инициализацию клавиатуры
|
||
* и многое другое.
|
||
|
||
Итак, давайте начнём.
|
||
|
||
Защищённый режим
|
||
--------------------------------------------------------------------------------
|
||
|
||
Прежде чем мы сможем перейти к нативному для Intel 64 режиму [Long Mode](http://en.wikipedia.org/wiki/Long_mode), ядро должно переключить ЦПУ в защищённый режим.
|
||
|
||
Что такое [защищённый режим](https://en.wikipedia.org/wiki/Protected_mode)? Защищённый режим был впервые добавлен в архитектуре x86 в 1982 году и был основным режимом процессоров Intel начиная с [80286](http://en.wikipedia.org/wiki/Intel_80286), пока в Intel 64 не появился режим 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, 43): либо биты `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** определяет уровень привилегий.
|
||
|
||
Каждый сегментный регистр имеет видимую и скрытую часть.
|
||
|
||
* Видимая - здесь хранится селектор сегмента
|
||
* Скрытая - дескриптор сегмента (базовый адрес, предел, атрибуты, флаги)
|
||
|
||
Необходимы следующие шаги, чтобы получить физический адрес в защищённом режиме:
|
||
|
||
* Селектор сегмента должен быть загружен в один из сегментных регистров
|
||
* ЦПУ пытается найти дескриптор сегмента по адресу GDT + Index из селектора и загрузить дескриптор в *скрытую* часть сегментного регистра
|
||
* Базовый адрес (из дескриптора сегмента) + смещение будет линейным адресом сегмента, который является физическим адресом (если подкачка страниц отключена).
|
||
|
||
Схематично это будет выглядеть следующим образом:
|
||
|
||
![линейный адрес](http://oi62.tinypic.com/2yo369v.jpg)
|
||
|
||
Алгоритм перехода из режима реальных адресов в защищённый режим:
|
||
|
||
* Отключить прерывания
|
||
* Описать и загрузить GDT с инструкцией `lgdt`
|
||
* Установить бит PE (Protection Enable) в CR0 (регистр управления (Control Register) 0)
|
||
* Перейти к коду защищённого режима
|
||
|
||
Полный переход в защищённый режим в ядре Linux мы увидим в следующей части, но прежде чем мы сможем перейти в защищённый режим, нужно совершить ещё несколько приготовлений.
|
||
|
||
Давайте посмотрим на [arch/x86/boot/main.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/main.c). Мы можем видеть некоторые подпрограммы, которые выполняют инициализацию клавиатуры, инициализацию кучи и т.д. Рассмотрим это.
|
||
|
||
Копирование параметров загрузки в "нулевую страницу" (zeropage)
|
||
--------------------------------------------------------------------------------
|
||
|
||
Мы стартуем из подпрограммы `main` в "main.c". Первая функция, которая вызывается в `main` - [`copy_boot_params(void)`](https://github.com/torvalds/linux/blob/master/arch/x86/boot/main.c#L30). Она копирует заголовок настройки ядра в поле структуры `boot_params`, которая определена в [arch/x86/include/uapi/asm/bootparam.h](https://github.com/torvalds/linux/blob/master/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/master/arch/x86/boot/header.S#L281) в структуру `boot_params` в поле `setup_header`
|
||
|
||
2. Обновляет указатель на командную строку ядра, если ядро было загружено со старым протоколом командной строки.
|
||
|
||
Обратите внимание на то что он копирует `hdr` с помощью функции `memcpy`, которая определена в [copy.S](https://github.com/torvalds/linux/blob/master/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/master/arch/x86/include/asm/linkage.h) и определяет директиву `globl`, а так же метку для него. `ENDPROC` описан в [include/linux/linkage.h](https://github.com/torvalds/linux/blob/master/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/master/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/master/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/master/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/master/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/master/arch/x86/boot/tty.c#L30) и `inb/outb`.
|
||
|
||
Инициализация кучи
|
||
--------------------------------------------------------------------------------
|
||
|
||
После подготовки стека и BSS в [header.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/header.S) (смотрите предыдущую [часть](linux-bootstrap-1.md)), ядро должно инициализировать [кучу](https://github.com/torvalds/linux/blob/master/arch/x86/boot/main.c#L116) с помощью функции [`init_heap`](https://github.com/torvalds/linux/blob/master/arch/x86/boot/main.c#L116).
|
||
|
||
В первую очередь `init_heap` проверяет флаг [`CAN_USE_HEAP`](https://github.com/torvalds/linux/blob/master/arch/x86/include/uapi/asm/bootparam.h#L21) в [`loadflags`](https://github.com/torvalds/linux/blob/master/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`. В следующих постах мы увидим как она используется, как её использовать и как она реализуется.
|
||
|
||
Проверка ЦПУ
|
||
--------------------------------------------------------------------------------
|
||
|
||
Следующим шагом является проверка ЦПУ с помощью `validate_cpu` из [arch/x86/boot/cpu.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/cpu.c).
|
||
|
||
Она вызывает функцию [`check_cpu`](https://github.com/torvalds/linux/blob/master/arch/x86/boot/cpucheck.c#L102) и передаёт ей два параметра: уровень ЦПУ и необходимый уровень ЦПУ; `check_cpu` проверяет, запущено ли ядро на нужном уровне ЦПУ.
|
||
|
||
```c
|
||
check_cpu(&cpu_level, &req_level, &err_flags);
|
||
if (cpu_level < req_level) {
|
||
...
|
||
return -1;
|
||
}
|
||
```
|
||
|
||
`check_cpu` проверяет флаги ЦПУ, наличие [long mode](http://en.wikipedia.org/wiki/Long_mode) в случае x86_64 (64-битного) ЦПУ, проверяет поставщика процессора и делает специальные подготовки для некоторых производителей, такие как отключение SSE+SSE2 для AMD в случае их отсутствия и т.д.
|
||
|
||
Обнаружение памяти
|
||
--------------------------------------------------------------------------------
|
||
|
||
Следующим шагом является обнаружение памяти с помощью функции `detect_memory`. `detect_memory` в основном предоставляет карту доступной оперативной памяти. Она использует различные программные интерфейсы для обнаружения памяти, такие как `0xe820`, `0xe801` и `0x88`. Здесь мы будем рассматривать только реализацию **0xE820**.
|
||
|
||
Давайте посмотрим на реализацию `detect_memory_e820` в [arch/x86/boot/memory.c](https://github.com/torvalds/linux/blob/master/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/master/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/master/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/master/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`. В первую очередь она проверяет уровень ЦПУ, и если он верный, вызывает прерывание `0x15` для получения информации и сохраняет результат в `boot_params`.
|
||
|
||
Следующая функция - [query_apm_bios](https://github.com/torvalds/linux/blob/master/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/master/arch/x86/boot/edd.c#L122), которая запрашивает из BIOS информацию об `Enhanced Disk Drive`. Давайте взглянем на реализацию `query_edd`.
|
||
|
||
В первую очередь она читает опцию [edd](https://github.com/torvalds/linux/blob/master/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/master/include/uapi/linux/edd.h#L172). `get_edd_info` проверяет наличие EDD путём вызова прерывания `0x13` с `ah = 0x41` и если EDD присутствует, `get_edd_info` снова вызывает `0x13`, но с `ah = 0x48` и `si`, содержащим адрес буфера, где будет храниться информация о EDD.
|
||
|
||
Заключение
|
||
--------------------------------------------------------------------------------
|
||
|
||
Это конец второй части о внутренностях ядра Linux. В следующей части мы увидим настройки режима видео и остальные подготовки перед переходом в защищённый режим и непосредственно переход в него.
|
||
|
||
Если у вас есть вопросы или предложения, пишите мне в твиттер [0xAX](https://twitter.com/0xAX).
|
||
|
||
**Пожалуйста, имейте в виду, что английский - не мой родной язык, и я очень извиняюсь за возможные неудобства. Если вы найдёте какие-нибудь ошибки, пожалуйста, пришлите pull request в [linux-insides](https://github.com/0xAX/linux-internals).**
|
||
|
||
Ссылки
|
||
--------------------------------------------------------------------------------
|
||
|
||
* [Защищённый режим (Википедия)](http://en.wikipedia.org/wiki/Protected_mode)
|
||
* [Защищённый режим (OSDEV)](http://wiki.osdev.org/Protected_Mode)
|
||
* [Long mode](http://en.wikipedia.org/wiki/Long_mode)
|
||
* [Неплохое объяснение режимов ЦПУ с кодом](http://www.codeproject.com/Articles/45788/The-Real-Protected-Long-mode-assembly-tutorial-for)
|
||
* [Как использовать сегменты с ростом вниз на ЦПУ 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/master/Documentation/kernel-parameters.txt)
|
||
* [Последовательная консоль](https://github.com/torvalds/linux/blob/master/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)
|
||
|