mirror of
https://github.com/0xAX/linux-insides.git
synced 2024-11-16 04:59:13 +00:00
495 lines
41 KiB
Markdown
495 lines
41 KiB
Markdown
Процесс загрузки ядра. Часть 1.
|
||
================================================================================
|
||
|
||
От загрузчика к ядру
|
||
--------------------------------------------------------------------------------
|
||
|
||
***От автора:***
|
||
|
||
Если вы читали предыдущие [статьи](https://0xax.github.io/categories/assembler/) моего блога, то могли заметить, что некоторое время назад я начал увлекаться низкоуровневым программированием. Я написал несколько статей о программировании под x86_64 в Linux. В то же время я начал "погружаться" в исходный код Linux. Я испытываю большой интерес к пониманию того, как работают низкоуровневые вещи, как запускаются программы на моем компьютере, как они расположены в памяти, как ядро управляет процессами и памятью, как работает сетевой стек на низком уровне и многие другие вещи. Поэтому я решил написать ещё одну серию статей о ядре Linux для **x86_64**.
|
||
|
||
Замечу, что я не профессиональный хакер ядра и не пишу под него код на работе. Это просто хобби. Мне просто нравятся низкоуровневые вещи и мне интересно наблюдать за тем, как они работают. Так что, если вас что-то будет смущать или у вас появятся вопросы или замечания, пишите мне в твиттер [0xAX](https://twitter.com/0xAX), присылайте письма на [email](anotherworldofworld@gmail.com) или просто создавайте [issue](https://github.com/0xAX/linux-insides/issues/new). Я ценю это.
|
||
|
||
Все статьи также будут доступны в [репозитории Github](https://github.com/0xAX/linux-insides), и, если вы обнаружите какую-нибудь ошибку в содержимом статьи, не стесняйтесь присылать pull request.
|
||
|
||
*Заметьте, что это не официальная документация, а просто материал для обучения и обмена знаниями.*
|
||
|
||
**Требуемые знания**
|
||
|
||
* Понимание кода на языке C
|
||
* Понимание кода на языке ассемблера (AT&T синтаксис)
|
||
|
||
В любом случае, если вы только начинаете изучать подобные инструменты, я постараюсь объяснить некоторые детали в этой и последующих частях. Ладно, простое введение закончилось, и теперь можно начать "погружение" в ядро и низкоуровневые вещи.
|
||
|
||
Я начал писать эту книгу, когда актуальной версией ядра Linux была 3.18, и с этого момента многое могло измениться. При возникновении изменений я буду соответствующим образом обновлять статьи.
|
||
|
||
Магическая кнопка включения, что происходит дальше?
|
||
--------------------------------------------------------------------------------
|
||
|
||
Несмотря на то, что это серия статей о ядре Linux, мы не будем начинать непосредственно с его исходного кода (по крайней мере в этом параграфе). Как только вы нажмёте магическую кнопку включения на вашем ноутбуке или настольном компьютере, он начинает работать. Материнская плата посылает сигнал к [источнику питания](https://en.wikipedia.org/wiki/Power_supply). После получения сигнала, источник питания обеспечивает компьютер надлежащим количеством электричества. После того как материнская плата получает сигнал ["питание в норме" (Power good signal)](https://en.wikipedia.org/wiki/Power_good_signal), она пытается запустить CPU. CPU сбрасывает все остаточные данные в регистрах и записывает предустановленные значения каждого из них.
|
||
|
||
CPU серии [Intel 80386](https://en.wikipedia.org/wiki/Intel_80386) и старше после перезапуска компьютера заполняют регистры следующими предустановленными значениями:
|
||
|
||
```
|
||
IP 0xfff0
|
||
CS selector 0xf000
|
||
CS base 0xffff0000
|
||
```
|
||
Процессор начинает свою работу в [режиме реальных адресов](https://en.wikipedia.org/wiki/Real_mode). Давайте немного задержимся и попытаемся понять сегментацию памяти в этом режиме. Режим реальных адресов поддерживается всеми x86-совместимыми процессорами, от [8086](https://en.wikipedia.org/wiki/Intel_8086) до самых новых 64-битных CPU Intel. Процессор 8086 имел 20-битную шину адреса, т.е. он мог работать с адресным пространством в диапазоне 0-0xFFFFF (1 мегабайт). Но регистры у него были только 16-битные, а в таком случае максимальный размер адресуемой памяти составляет `2^16 - 1` или `0xffff` (64 килобайта).
|
||
|
||
[Сегментация памяти](https://en.wikipedia.org/wiki/Memory_segmentation) используется для того, чтобы задействовать всё доступное адресное пространство. Вся память делится на небольшие, фиксированного размера сегменты по `65536` байт (64 Кб) каждый. Поскольку мы не можем адресовать память свыше `64 Кб` с помощью 16-битных регистров, был придуман альтернативный метод.
|
||
|
||
Адрес состоит из двух частей: селектора сегмента, который содержит базовый адрес, и смещение от этого базового адреса. В режиме реальных адресов базовый адрес селектора сегмента это `Селектор Сегмента * 16`. Таким образом, чтобы получить физический адрес в памяти, нужно умножить селектор сегмента на `16` и прибавить к нему смещение:
|
||
|
||
```
|
||
Физический адрес = Селектор сегмента * 16 + Смещение
|
||
```
|
||
|
||
Например, если `CS:IP` содержит `0x2000:0x0010`, то соответствующий физический адрес будет:
|
||
|
||
```python
|
||
>>> hex((0x2000 << 4) + 0x0010)
|
||
'0x20010'
|
||
```
|
||
|
||
Но если мы возьмём максимально доступный селектор сегментов и смещение: `0xffff:0xffff`, то итоговый адрес будет:
|
||
|
||
```python
|
||
>>> hex((0xffff << 4) + 0xffff)
|
||
'0x10ffef'
|
||
```
|
||
|
||
что больше первого мегабайта на 65520 байт. Поскольку в режиме реальных адресов доступен только один мегабайт, с отключённой [адресной линией A20](https://en.wikipedia.org/wiki/A20_line) `0x10ffef` становится `0x00ffef`.
|
||
|
||
Хорошо, теперь мы немного знаем о режиме реальных адресов и адресации памяти в этом режиме. Давайте вернёмся к обсуждению значений регистров после сброса.
|
||
|
||
Регистр `CS` состоит из двух частей: видимый селектор сегмента и скрытый базовый адрес.
|
||
В то время как базовый адрес, как правило, формируется путём умножения значения селектора сегмента на 16, во время аппаратного перезапуска в селектор сегмента в регистре `CS` записывается `0xf000`, а в базовый адрес - `0xffff0000`. Процессор использует этот специальный базовый адрес, пока регистр `CS` не изменится.
|
||
|
||
Начальный адрес формируется путём добавления базового адреса к значению в регистре `EIP`:
|
||
|
||
```python
|
||
>>> 0xffff0000 + 0xfff0
|
||
'0xfffffff0'
|
||
```
|
||
|
||
Мы получили `0xfffffff0`, т.е. 16 байт ниже 4 Гб. По этому адресу располагается так называемый [вектор прерываний](https://en.wikipedia.org/wiki/Reset_vector). Это область памяти, в которой CPU ожидает найти первую инструкцию для выполнения после сброса. Она содержит инструкцию [jump](https://en.wikipedia.org/wiki/JMP_%28x86_instruction%29) (`jmp`), которая обычно указывает на точку входа в BIOS. Например, если мы взглянем на исходный код [coreboot](https://www.coreboot.org/) (`src/cpu/x86/16bit/reset16.inc`), то увидим следующее:
|
||
|
||
```assembly
|
||
.section ".reset", "ax", %progbits
|
||
.code16
|
||
.globl _start
|
||
_start:
|
||
.byte 0xe9
|
||
.int _start16bit - ( . + 2 )
|
||
...
|
||
```
|
||
|
||
Здесь мы можем видеть [опкод инструкции jmp](http//ref.x86asm.net/coder32.html#xE9) - `0xe9`, и его адрес назначения `_start16bit - ( . + 2)`.
|
||
|
||
Мы также можем видеть, что секция `reset` занимает `16` байт и начинается с `0xfffffff0` (`src/cpu/x86/16bit/reset16.ld`):
|
||
|
||
```
|
||
SECTIONS {
|
||
/* Trigger an error if I have an unuseable start address */
|
||
_bogus = ASSERT(_start16bit >= 0xffff0000, "_start16bit too low. Please report.");
|
||
_ROMTOP = 0xfffffff0;
|
||
. = _ROMTOP;
|
||
.reset . : {
|
||
*(.reset);
|
||
. = 15;
|
||
BYTE(0x00);
|
||
}
|
||
}
|
||
```
|
||
|
||
Теперь запускается BIOS; после инициализации и проверки оборудования, BIOS нужно найти загрузочное устройство. Порядок загрузки хранится в конфигурации BIOS, которая определяет, с каких устройств BIOS пытается загрузиться. При попытке загрузиться с жёсткого диска, BIOS пытается найти загрузочный сектор. На размеченных жёстких дисках со схемой разделов MBR, загрузочный сектор расположен в первых 446 байтах первого сектора, размер которого 512 байт. Последние два байта первого сектора - `0x55` и `0xaa`, которые оповещают BIOS о том, что устройство является загрузочным.
|
||
|
||
Например:
|
||
|
||
```assembly
|
||
;
|
||
; Замечание: этот пример написан с использованием Intel синтаксиса
|
||
;
|
||
[BITS 16]
|
||
|
||
boot:
|
||
mov al, '!'
|
||
mov ah, 0x0e
|
||
mov bh, 0x00
|
||
mov bl, 0x07
|
||
|
||
int 0x10
|
||
jmp $
|
||
|
||
times 510-($-$$) db 0
|
||
|
||
db 0x55
|
||
db 0xaa
|
||
```
|
||
|
||
Собрать и запустить этот код можно таким образом:
|
||
|
||
```
|
||
nasm -f bin boot.nasm && qemu-system-x86_64 boot
|
||
```
|
||
|
||
Команда оповещает эмулятор [QEMU](https://qemu.org) о необходимости использовать в качестве образа диска созданный только что бинарный файл. Пока последний проверяет, удовлетворяет ли загрузочный сектор всем необходимым требованиям (в origin записывается `0x7c00`, а в конце магическая последовательность), QEMU будет работать с бинарным файлом как с главной загрузочной записью (MBR) образа диска.
|
||
|
||
Вы увидите:
|
||
|
||
![Простой загрузчик, который печатает только `!`](http://oi60.tinypic.com/2qbwup0.jpg)
|
||
|
||
В этом примере мы можем видеть, что код будет выполнен в 16-битном режиме реальных адресов и начнёт выполнение с адреса `0x7c00`. После запуска он вызывает прерывание [0x10](http://www.ctyme.com/intr/rb-0106.htm), которое просто печатает символ `!`. Оставшиеся 510 байт заполняются нулями, и код заканчивается двумя магическими байтами `0xaa` и `0x55`.
|
||
|
||
Вы можете увидеть бинарный дамп с помощью утилиты `objdump`:
|
||
|
||
```
|
||
nasm -f bin boot.nasm
|
||
objdump -D -b binary -mi386 -Maddr16,data16,intel boot
|
||
```
|
||
|
||
Реальный загрузочный сектор имеет код для продолжения процесса загрузки и таблицу разделов вместо кучи нулей и восклицательного знака :) С этого момента BIOS передаёт управление загрузчику.
|
||
|
||
**ЗАМЕЧАНИЕ**: Как уже было упомянуто ранее, CPU находится в режиме реальных адресов; в этом режиме вычисление физического адреса в памяти выполняется следующим образом:
|
||
|
||
```
|
||
Физический адрес = Селектор сегмента * 16 + Смещение
|
||
```
|
||
так же, как было упомянуто выше. У нас есть только 16-битные регистры общего назначения; максимальное значение 16-битного регистра - `0xffff`. Поэтому, если мы возьмём максимальное возможное значение, то результат будет следующий:
|
||
|
||
```python
|
||
>>> hex((0xffff * 16) + 0xffff)
|
||
'0x10ffef'
|
||
```
|
||
|
||
где `0x10ffef` равен `1 Мб + 64 Кб - 16 байт`. Процессор [8086](https://en.wikipedia.org/wiki/Intel_8086) (который был первым процессором с режимом реальных адресов), в отличии от этого, имеет 20-битную шину адресации. Поскольку `2^20 = 1048576` это 1 Мб, получается, что фактический объём доступной памяти составляет 1 Мб.
|
||
|
||
В целом, карта разделов в режиме реальных адресов выглядит следующим образом:
|
||
|
||
```
|
||
0x00000000 - 0x000003FF - Таблица векторов прерываний
|
||
0x00000400 - 0x000004FF - Данные BIOS
|
||
0x00000500 - 0x00007BFF - Не используется
|
||
0x00007C00 - 0x00007DFF - Наш загрузчик
|
||
0x00007E00 - 0x0009FFFF - Не используется
|
||
0x000A0000 - 0x000BFFFF - RAM (VRAM) видеопамять
|
||
0x000B0000 - 0x000B7777 - Память для монохромного видео
|
||
0x000B8000 - 0x000BFFFF - Память для цветного видео
|
||
0x000C0000 - 0x000C7FFF - BIOS ROM видеопамяти
|
||
0x000C8000 - 0x000EFFFF - Скрытая область BIOS
|
||
0x000F0000 - 0x000FFFFF - Системная BIOS
|
||
```
|
||
|
||
В начале статьи я написал, что первая инструкция, выполняемая CPU, расположена по адресу `0xFFFFFFF0`, значение которого намного больше, чем `0xFFFFF` (1 Мб). Каким образом CPU получает доступ к этому участку в режиме реальных адресов? Ответ на этот вопрос находится в документации [coreboot](https://www.coreboot.org/Developer_Manual/Memory_map):
|
||
|
||
```
|
||
0xFFFE_0000 - 0xFFFF_FFFF: 128 Кб ROM отображаются на адресное пространство
|
||
```
|
||
В начале выполнения BIOS находится не в RAM, а в ROM.
|
||
|
||
Загрузчик
|
||
--------------------------------------------------------------------------------
|
||
|
||
Существует несколько загрузчиков Linux, такие как [GRUB 2](https://www.gnu.org/software/grub/) и [syslinux](https://www.syslinux.org/wiki/index.php/The_Syslinux_Project). Ядро Linux имеет [протокол загрузки](https://github.com/torvalds/linux/blob/v4.16/Documentation/x86/boot.txt), который определяет требования к загрузчику для реализации поддержки Linux. В этом примере будет описан GRUB 2.
|
||
|
||
Теперь, когда BIOS выбрал загрузочное устройство и передал контроль управления коду в загрузочном секторе, начинается выполнение [boot.img](https://git.savannah.gnu.org/gitweb/?p=grub.git;a=blob;f=grub-core/boot/i386/pc/boot.S;hb=HEAD). Этот код очень простой в связи с ограниченным количеством свободного пространства и содержит указатель, который используется для перехода к основному образу GRUB 2. Основной образ начинается с [diskboot.img](https://git.savannah.gnu.org/gitweb/?p=grub.git;a=blob;f=grub-core/boot/i386/pc/diskboot.S;hb=HEAD), который обычно располагается сразу после первого сектора в неиспользуемой области перед первым разделом. Приведённый выше код загружает оставшуюся часть основного образа, который содержит ядро и драйверы GRUB 2 для управления файловой системой. После загрузки остальной части основного образа, код выполняет функция [grub_main](https://git.savannah.gnu.org/gitweb/?p=grub.git;a=blob;f=grub-core/kern/main.c).
|
||
|
||
Функция `grub_main` инициализирует консоль, получает базовый адрес для модулей, устанавливает корневое устройство, загружает/обрабатывает файл настроек grub, загружает модули и т.д. В конце выполнения, `grub_main` переводит grub обратно в нормальный режим. Функция `grub_normal_execute` (из `grub-core/normal/main.c`) завершает последние приготовления и отображает меню выбора операционной системы. Когда мы выбираем один из пунктов меню, запускается функция `grub_menu_execute_entry`, которая в свою очередь запускает команду grub `boot`, загружающую выбранную операционную систему.
|
||
|
||
Из протокола загрузки видно, что загрузчик должен читать и заполнять некоторые поля в заголовке ядра, который начинается со смещения `0x01f1` в коде настроек. Вы можете посмотреть загрузочный [скрипт компоновщика](https://github.com/torvalds/linux/blob/v4.16/arch/x86/boot/setup.ld#L16), чтобы убедиться в этом. Заголовок ядра [arch/x86/boot/header.S](https://github.com/torvalds/linux/blob/v4.16/arch/x86/boot/header.S) начинается с:
|
||
|
||
```assembly
|
||
.globl hdr
|
||
hdr:
|
||
setup_sects: .byte 0
|
||
root_flags: .word ROOT_RDONLY
|
||
syssize: .long 0
|
||
ram_size: .word 0
|
||
vid_mode: .word SVGA_MODE
|
||
root_dev: .word 0
|
||
boot_flag: .word 0xAA55
|
||
```
|
||
|
||
Загрузчик должен заполнить этот и другие заголовки (которые помеченные как тип `write` в протоколе загрузки Linux, например, в [данном примере](https://github.com/torvalds/linux/blob/v4.16/Documentation/x86/boot.txt#L354)) значениями, которые он получил из командной строки или значениями, вычисленными во время загрузки. (Мы не будет вдаваться в подробности и описывать все поля заголовка ядра, но мы сделаем это, когда будем обсуждать как их использует ядро; тем не менее вы можете найти полное описание всех полей в [протоколе загрузки](https://github.com/torvalds/linux/blob/v4.16/Documentation/x86/boot.txt#L156).)
|
||
|
||
Как мы видим из протокола, после загрузки ядра карта распределения памяти будет выглядеть следующим образом:
|
||
|
||
```shell
|
||
| Ядро защищённого режима |
|
||
100000 +--------------------------+
|
||
| Память I/O |
|
||
0A0000 +--------------------------+
|
||
| Резерв для BIOS | Оставлен максимально допустимый размер
|
||
~ ~
|
||
| Командная строка | (Может быть ниже X+10000)
|
||
X+10000 +--------------------------+
|
||
| Стек/куча | Используется кодом ядра в режиме реальных адресов
|
||
X+08000 +--------------------------+
|
||
| Настройки ядра | Код ядра режима реальных адресов.
|
||
| Загрузочный сектор ядра | Унаследованный загрузочный сектор ядра.
|
||
X +--------------------------+
|
||
| Загрузчик | <- Точка входа загрузочного сектора 0x7C00
|
||
001000 +--------------------------+
|
||
| Резерв для MBR/BIOS |
|
||
000800 +--------------------------+
|
||
| Обычно используется MBR |
|
||
000600 +--------------------------+
|
||
| Используется только BIOS |
|
||
000000 +--------------------------+
|
||
|
||
```
|
||
|
||
Итак, когда загрузчик передаёт управление ядру, он запускается с:
|
||
|
||
```
|
||
X + sizeof(KernelBootSector) + 1
|
||
```
|
||
где `X` - это адрес загруженного сектора загрузки ядра. В моем случае `X` это `0x10000`, как мы можем видеть в дампе памяти:
|
||
|
||
![Первый адрес ядра](http://oi57.tinypic.com/16bkco2.jpg)
|
||
|
||
Сейчас загрузчик поместил ядро Linux в память, заполнил поля заголовка, а затем переключился на него. Теперь мы можем перейти непосредственно к коду настройки ядра.
|
||
|
||
Начальный этап настройки ядра
|
||
--------------------------------------------------------------------------------
|
||
Наконец, мы находимся в ядре! Технически, ядро ещё не работает; во-первых, часть ядра, отвественная за настройку, должна подготовить декомпрессор, вещи связанные с управлением памятью и т.д. После всех подготовок код настройки ядра должен распаковывать фактическое ядро и совершить переход на него. Выполнение настройки начинается в файле [arch/x86/boot/header.S](https://github.com/torvalds/linux/blob/v4.16/arch/x86/boot/header.S), начиная с метки [_start](https://github.com/torvalds/linux/blob/v4.16/arch/x86/boot/header.S#L292). Это немного странно на первый взгляд, так как перед этим есть ещё несколько инструкций.
|
||
|
||
Давным-давно у Linux был свой загрузчик, но сейчас, если вы запустите, например:
|
||
|
||
```
|
||
qemu-system-x86_64 vmlinuz-3.18-generic
|
||
```
|
||
|
||
то увидите:
|
||
|
||
![Попытка использовать vmlinuz в qemu](http://oi60.tinypic.com/r02xkz.jpg)
|
||
|
||
На самом деле, `header.S` начинается с [MZ](https://en.wikipedia.org/wiki/DOS_MZ_executable) (см. картинку выше), вывода сообщения об ошибке и [PE](https://en.wikipedia.org/wiki/Portable_Executable) заголовка:
|
||
|
||
```assembly
|
||
#ifdef CONFIG_EFI_STUB
|
||
# "MZ", MS-DOS header
|
||
.byte 0x4d
|
||
.byte 0x5a
|
||
#endif
|
||
...
|
||
...
|
||
...
|
||
pe_header:
|
||
.ascii "PE"
|
||
.word 0
|
||
```
|
||
|
||
Это нужно, чтобы загрузить операционную систему с поддержкой [UEFI](https://en.wikipedia.org/wiki/Unified_Extensible_Firmware_Interface). Мы не будем рассматривать его внутреннюю работу прямо сейчас; мы сделаем это в одной из следующих глав.
|
||
|
||
Настоящая настройка ядра начинается с:
|
||
|
||
```assembly
|
||
// header.S строка 292
|
||
.globl _start
|
||
_start:
|
||
```
|
||
|
||
Загрузчик (grub2 или другой) знает об этой метке (при смещении `0x200` от `MZ`) и сразу переходит на неё, несмотря на то, что `header.S` начинается с секции `.bstext`, который выводит сообщение об ошибке:
|
||
|
||
```
|
||
//
|
||
// arch/x86/boot/setup.ld
|
||
//
|
||
. = 0; // текущая позиция
|
||
.bstext : { *(.bstext) } // поместить секцию .bstext в позицию 0
|
||
.bsdata : { *(.bsdata) }
|
||
```
|
||
|
||
Точка входа настройки ядра:
|
||
|
||
```assembly
|
||
.globl _start
|
||
_start:
|
||
.byte 0xeb
|
||
.byte start_of_setup-1f
|
||
1:
|
||
//
|
||
// остальная часть заголовка
|
||
//
|
||
```
|
||
|
||
Здесь мы можем видеть опкод инструкции `jmp` (`0xeb`) к метке `start_of_setup-1f`. Нотация `Nf` означает, что `2f` ссылается на следующую локальную метку `2:`; в нашем случае это метка `1`, которая расположена сразу после инструкции jump и содержит оставшуюся часть [заголовка](https://github.com/torvalds/linux/blob/v4.16/Documentation/x86/boot.txt#L156). Сразу после заголовка настроек мы видим секцию `.entrytext`, которая начинается с метки `start_of_setup`.
|
||
|
||
Это первый код, который на самом деле запускается (отдельно от предыдущей инструкции jump, конечно). После того как настройщик ядра получил управление от загрузчика, первая инструкция `jmp` располагается по смещению `0x200` от начала реальных адресов, т.е после первых 512 байт. Об этом можно узнать из протокола загрузки ядра Linux, а также увидеть в исходном коде grub2:
|
||
|
||
```C
|
||
segment = grub_linux_real_target >> 4;
|
||
state.gs = state.fs = state.es = state.ds = state.ss = segment;
|
||
state.cs = segment + 0x20;
|
||
```
|
||
|
||
В моём случае, ядро загружается по адресу `0x10000`. Это означает, что после начала настройки ядра регистры сегмента будут иметь следующие значения:
|
||
|
||
```
|
||
gs = fs = es = ds = ss = 0x1000
|
||
cs = 0x1020
|
||
```
|
||
|
||
После перехода на метку `start_of_setup`, необходимо соблюсти следующие условия:
|
||
|
||
* Убедиться, что все значения всех сегментных регистров равны
|
||
* Правильно настроить стек, если это необходимо
|
||
* Настроить [BSS](https://en.wikipedia.org/wiki/.bss)
|
||
* Перейти к C-коду в [main.c](https://github.com/torvalds/linux/blob/v4.16/arch/x86/boot/main.c)
|
||
|
||
Давайте посмотрим, как эти условия выполняются.
|
||
|
||
Выравнивание сегментных регистров
|
||
--------------------------------------------------------------------------------
|
||
|
||
Прежде всего, ядро гарантирует, что сегментные регистры `ds` и `es` указывают на один и тот же адрес. Затем оно сбрасывает флаг направления с помощью инструкции `cld`:
|
||
|
||
```assembly
|
||
movw %ds, %ax
|
||
movw %ax, %es
|
||
cld
|
||
```
|
||
|
||
Как я уже писал ранее, `grub2` загружает код настройки ядра по адресу `0x1000` (адрес по умолчанию) и `cs` по адресу `0x1020`, потому что выполнение не начинается с начала файла, а с метки `_start`:
|
||
|
||
```assembly
|
||
_start:
|
||
.byte 0xeb
|
||
.byte start_of_setup-1f
|
||
```
|
||
расположеной в `512` байтах от [4d 5a](https://github.com/torvalds/linux/blob/v4.16/arch/x86/boot/header.S#L46). Нам также необходимо выровнять `cs` с `0x1020` на `0x1000` и остальные сегментные регистры. После этого мы настраиваем стек:
|
||
|
||
```assembly
|
||
pushw %ds
|
||
pushw $6f
|
||
lretw
|
||
```
|
||
|
||
кладём значение `ds` в стек по адресу метки [6](https://github.com/torvalds/linux/blob/v4.16/arch/x86/boot/header.S#L494) и выполняем инструкцию `lretw`. Когда мы вызываем `lretw`, она загружает адрес метки `6` в регистр [счётчика команд (IP)](https://en.wikipedia.org/wiki/Program_counter), и загружает `cs` со значением `ds`. После этого `ds` и `cs` будут иметь одинаковые значения.
|
||
|
||
Настройка стека
|
||
--------------------------------------------------------------------------------
|
||
|
||
Почти весь код настройки - это подготовка для среды языка C в режиме реальных адресов. Следующим [шагом](https://github.com/torvalds/linux/blob/v4.16/arch/x86/boot/header.S#L569) является проверка значения регистра `ss` и создание корректного стека, если значение `ss` неверно:
|
||
|
||
```assembly
|
||
movw %ss, %dx
|
||
cmpw %ax, %dx
|
||
movw %sp, %dx
|
||
je 2f
|
||
```
|
||
|
||
Это может привести к трём различны сценариям:
|
||
|
||
* `ss` имеет верное значение `0x1000` (как и все остальные сегментные регистры рядом с `cs`)
|
||
* `ss` является некорректным и установлен флаг `CAN_USE_HEAP` (см. ниже)
|
||
* `ss` является некорректным и флаг `CAN_USE_HEAP` не установлен (см. ниже)
|
||
|
||
Давайте рассмотрим все три сценария:
|
||
|
||
* `ss` имеет верный адрес (`0x1000`). В этом случае мы переходим на метку [2](https://github.com/torvalds/linux/blob/v4.16/arch/x86/boot/header.S#L583):
|
||
|
||
```assembly
|
||
2: andw $~3, %dx
|
||
jnz 3f
|
||
movw $0xfffc, %dx
|
||
3: movw %ax, %ss
|
||
movzwl %dx, %esp
|
||
sti
|
||
```
|
||
|
||
|
||
Здесь мы видим выравнивание сегмента `dx` (содержащего значение `sp`, полученное загрузчиком) до 4 байт и проверку - является ли полученное значение нулём. Если ноль, то помещаем `0xfffx` (выровненный до `4` байт адрес до максимального значения сегмента в 64 Кб) в `dx`. Если не ноль, продолжаем использовать `sp`, полученный от загрузчика (в моём случае `0xf7f4`). После этого мы помещаем значение `ax` в `ss`, который хранит корректный адрес сегмента `0x1000` и устанавливает корректное значение `sp`. Теперь мы имеем корректный стек:
|
||
|
||
![стек](http://oi58.tinypic.com/16iwcis.jpg)
|
||
|
||
* Второй сценарий (когда `ss` != `ds`). Во-первых, помещаем значение [_end](https://github.com/torvalds/linux/blob/v4.16/arch/x86/boot/setup.ld#L52) (адрес окончания кода настройки) в `dx` и проверяем поле заголовка `loadflags` инструкцией `testb`, чтобы понять, можем ли мы использовать кучу (heap). [loadflags](https://github.com/torvalds/linux/blob/v4.16/arch/x86/boot/header.S#L321) является заголовком с битовой маской, который определён как:
|
||
|
||
```C
|
||
#define LOADED_HIGH (1<<0)
|
||
#define QUIET_FLAG (1<<5)
|
||
#define KEEP_SEGMENTS (1<<6)
|
||
#define CAN_USE_HEAP (1<<7)
|
||
```
|
||
|
||
и, как мы можем узнать из протокола загрузки:
|
||
|
||
```
|
||
Имя поля: loadflags
|
||
|
||
Данное поле является битовой маской.
|
||
|
||
Бит 7 (запись): CAN_USE_HEAP
|
||
Бит, установленный в 1, указывает на корректность heap_end_ptr.
|
||
Если поле очищено, то некоторый функционал кода настройки будет отключен.
|
||
```
|
||
Если бит `CAN_USE_HEAP` установлен, мы помещаем `heap_end_ptr` в `dx` (который указывает на `_end`) и добавляем к нему `STACK_SIZE` (минимальный размер стека, `512` байт). После этого, если `dx` без переноса (будет без переноса, поскольку `dx = _end + 512`), переходим на метку `2` (как в предыдущем случае) и создаём корректный стек.
|
||
|
||
![стек](http://oi62.tinypic.com/dr7b5w.jpg)
|
||
|
||
* Если флаг `CAN_USE_HEAP` не установлен, мы просто используем минимальный стек от `_end` до `_end + STACK_SIZE`:
|
||
|
||
![минимальный стек](http://oi60.tinypic.com/28w051y.jpg)
|
||
|
||
Настройка BSS
|
||
--------------------------------------------------------------------------------
|
||
|
||
Последние два шага, которые нужно выполнить перед тем, как мы сможем перейти к основному коду на C, это настройка [BSS](https://en.wikipedia.org/wiki/.bss) и проверка "магических" чисел. Сначала проверка чисел:
|
||
|
||
```assembly
|
||
cmpl $0x5a5aaa55, setup_sig
|
||
jne setup_bad
|
||
```
|
||
Это простое сравнение [setup_sig](https://github.com/torvalds/linux/blob/v4.16/arch/x86/boot/setup.ld#L39) с магическим числом `0x5a5aaa55`. Если они не равны, сообщается о фатальной ошибке.
|
||
|
||
Если магические числа совпадают, зная, что у нас есть набор правильно настроенных сегментных регистров и стек, нам всего лишь нужно настроить BSS.
|
||
|
||
Секция BSS используется для хранения статически выделенных, неинициализированных данных. Linux тщательно обнуляет эту область памяти, используя следующий код:
|
||
|
||
```assembly
|
||
movw $__bss_start, %di
|
||
movw $_end+3, %cx
|
||
xorl %eax, %eax
|
||
subw %di, %cx
|
||
shrw $2, %cx
|
||
rep; stosl
|
||
```
|
||
Во-первых, адрес [__bss_start](https://github.com/torvalds/linux/blob/v4.16/arch/x86/boot/setup.ld#L47) помещается в `di`. Далее, адрес `_end + 3` (+3 - выравнивает до 4 байт) помещается в `cx`. Регистр `eax` очищается (с помощью инструкции `xor`), а размер секции BSS (`cx`-`di`) вычисляется и помещается в `cx`. Затем `cx` делится на 4 (размер 'слова' (англ. word)), а инструкция `stosl` используется повторно, сохраняя значение `eax` (ноль) в адрес, на который указывает `di`, автоматически увеличивая `di` на 4 (это продолжается до тех пор, пока `cx` не достигнет нуля). Эффект от этого кода в том, что теперь все 'слова' в памяти от `__bss_start` до `_end` заполнены нулями:
|
||
|
||
![bss](http://oi59.tinypic.com/29m2eyr.jpg)
|
||
|
||
Переход к основному коду
|
||
--------------------------------------------------------------------------------
|
||
|
||
Вот и все, теперь у нас есть стек и BSS, поэтому мы можем перейти к C-функции `main()`:
|
||
|
||
```assembly
|
||
calll main
|
||
```
|
||
|
||
Функция `main()` находится в файле [arch/x86/boot/main.c](https://github.com/torvalds/linux/blob/v4.16/arch/x86/boot/main.c). О том, что она делает, вы сможете узнать в следующей части.
|
||
|
||
Заключение
|
||
--------------------------------------------------------------------------------
|
||
|
||
Это конец первой части о внутренностях ядра Linux. В следующей части мы увидим первый код на языке C, который выполняется при настройке ядра Linux, реализацию процедур для работы с памятью, таких как `memset`, `memcpy`, `earlyprintk`, инициализацию консоли и многое другое.
|
||
|
||
**От переводчика: пожалуйста, имейте в виду, что английский - не мой родной язык, и я очень извиняюсь за возможные неудобства. Если вы найдёте какие-либо ошибки или неточности в переводе, пожалуйста, пришлите pull request в [linux-insides-ru](https://github.com/proninyaroslav/linux-insides-ru).**
|
||
|
||
Ссылки
|
||
--------------------------------------------------------------------------------
|
||
|
||
* [Справочник программиста Intel 80386 1986](https://css.csail.mit.edu/6.858/2014/readings/i386.pdf)
|
||
* [Минимальный загрузчик для архитектуры Intel®](https://www.cs.cmu.edu/~410/doc/minimal_boot.pdf)
|
||
* [8086](https://en.wikipedia.org/wiki/Intel_8086)
|
||
* [80386](https://en.wikipedia.org/wiki/Intel_80386)
|
||
* [Вектор прерываний](https://en.wikipedia.org/wiki/Reset_vector)
|
||
* [Режим реальных адресов](https://en.wikipedia.org/wiki/Real_mode)
|
||
* [Протокол загрузки ядра Linux](https://www.kernel.org/doc/Documentation/x86/boot.txt)
|
||
* [Справочник разработчика CoreBoot](https://www.coreboot.org/Developer_Manual)
|
||
* [Список прерываний Ральфа Брауна](http://www.ctyme.com/intr/int.htm)
|
||
* [Источник питания](https://en.wikipedia.org/wiki/Power_supply)
|
||
* [Сигнал "Питание в норме"](https://en.wikipedia.org/wiki/Power_good_signal)
|