1
0
mirror of https://github.com/0xAX/linux-insides.git synced 2024-11-16 04:59:13 +00:00
linux-insides/Booting/linux-bootstrap-1.md

496 lines
42 KiB
Markdown
Raw Normal View History

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