|
|
Процесс загрузки ядра. Часть 1.
|
|
|
================================================================================
|
|
|
|
|
|
От загрузчика к ядру
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
|
|
Если вы читали предыдущие [статьи](http://0xax.blogspot.com/search/label/asm) моего блога, то могли заметить, что некоторое время назад я начал увлекаться низкоуровневым программированием. Я написал несколько статей о программировании под 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). Я ценю это. Все статьи также будут доступны на странице [linux-insides](https://github.com/0xAX/linux-insides), и, если вы обнаружите какую-нибудь ошибку в моём английском или в содержимом статьи, присылайте pull request.
|
|
|
|
|
|
*Заметьте, что это не официальная документация, а просто материал для обучения и обмена знаниями.*
|
|
|
|
|
|
**Требуемые знания**
|
|
|
|
|
|
* Понимание кода на языке C
|
|
|
* Понимание кода на языке ассемблера (AT&T синтаксис)
|
|
|
|
|
|
В любом случае, если вы только начинаете изучать некоторые инструменты, я постараюсь объяснить некоторые детали этой и последующих частей. Ладно, простое введение закончилось, и теперь можно начать "погружение" в ядро и низкоуровневые вещи.
|
|
|
|
|
|
Весь код, представленный здесь, в основном для ядра версии 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 килобайта). [Сегментация памяти](http://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 Гб. По этому адресу располагается так называемый [вектор прерываний](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/), то увидим следующее:
|
|
|
|
|
|
```assembly
|
|
|
.section ".reset"
|
|
|
.code16
|
|
|
.globl reset_vector
|
|
|
reset_vector:
|
|
|
.byte 0xe9
|
|
|
.int _start - ( . + 2 )
|
|
|
...
|
|
|
```
|
|
|
|
|
|
Здесь мы можем видеть [опкод инструкции jmp](http://ref.x86asm.net/coder32.html#xE9) - `0xe9`, и его адрес назначения `_start - ( . + 2)`. Мы также можем видеть, что секция `reset` занимает 16 байт и начинается с `0xfffffff0`:
|
|
|
|
|
|
```
|
|
|
SECTIONS {
|
|
|
_ROMTOP = 0xfffffff0;
|
|
|
. = _ROMTOP;
|
|
|
.reset . : {
|
|
|
*(.reset)
|
|
|
. = 15 ;
|
|
|
BYTE(0x00);
|
|
|
}
|
|
|
}
|
|
|
```
|
|
|
|
|
|
Теперь запускается BIOS; после инициализации и проверки оборудования, BIOS нужно найти загрузочное устройство. Порядок загрузки хранится в конфигурации BIOS, которая определяет, с каких устройств BIOS пытается загрузиться. При попытке загрузиться с жёсткого диска, BIOS пытается найти загрузочный сектор. На размеченных жёстких дисках со схемой разделов MBR, загрузочный сектор расположен в первых 446 байтах первого сектора, размер которого 512 байт. Последние два байта первого сектора - `0x55` и `0xaa`, которые оповещают BIOS о том, что устройство является загрузочным. Например:
|
|
|
|
|
|
```assembly
|
|
|
;
|
|
|
; Замечание: этот пример написан с использованием Intel синтаксиса
|
|
|
;
|
|
|
[BITS 16]
|
|
|
[ORG 0x7c00]
|
|
|
|
|
|
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](http://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](http://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](http://www.syslinux.org/wiki/index.php/The_Syslinux_Project). Ядро Linux имеет [протокол загрузки](https://github.com/torvalds/linux/blob/master/Documentation/x86/boot.txt), который определяет требования к загрузчику для реализации поддержки Linux. В этом примере будет описан GRUB 2.
|
|
|
|
|
|
Теперь, когда 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).
|
|
|
|
|
|
`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/master/arch/x86/boot/setup.ld#L16), чтобы убедиться в этом. Заголовок ядра [arch/x86/boot/header.S](https://github.com/torvalds/linux/blob/master/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/master/Documentation/x86/boot.txt#L354)) значениями, которые он получил из командной строки или вычисленными значениями. (Мы не будет вдаваться в подробности и описывать все поля заголовка ядра, но, когда он будет их использовать, вернёмся к этому; тем не менее вы можете найти полное описание всех полей в [протоколе загрузки](https://github.com/torvalds/linux/blob/master/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/master/arch/x86/boot/header.S), начиная со [_start](https://github.com/torvalds/linux/blob/master/arch/x86/boot/header.S#L293). Это немного странно на первый взгляд, так как перед этим есть ещё несколько инструкций.
|
|
|
|
|
|
Давным-давно у 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/master/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;
|
|
|
```
|
|
|
|
|
|
Это означает, что после начала настройки ядра регистры сегмента будут иметь следующие значения:
|
|
|
|
|
|
```
|
|
|
gs = fs = es = ds = ss = 0x1000
|
|
|
cs = 0x1020
|
|
|
```
|
|
|
|
|
|
|
|
|
В моём случае, ядро загружается в `0x10000`.
|
|
|
|
|
|
После перехода на метку `start_of_setup`, необходимо соблюсти следующие условия:
|
|
|
|
|
|
* Убедиться, что все значения всех сегментных регистров равны
|
|
|
* Правильно настроить стек, если это необходимо
|
|
|
* Настроить [BSS](https://en.wikipedia.org/wiki/.bss)
|
|
|
* Перейти к C-коду в [main.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/main.c)
|
|
|
|
|
|
Давайте посмотрим, как эти условия выполняются.
|
|
|
|
|
|
Выравнивание сегментных регистров
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
|
|
Прежде всего, ядро гарантирует, что сегментные регистры `ds` и `es` указывают на один и тот же адрес. Затем оно сбрасывает флаг направления с помощью инструкции `cld`:
|
|
|
|
|
|
```assembly
|
|
|
movw %ds, %ax
|
|
|
movw %ax, %es
|
|
|
cld
|
|
|
```
|
|
|
|
|
|
Как я уже писал ранее, grub2 загружает код настройки ядра по адресу `0x10000` и `cs` по адресу `0x1020`, потому что выполнение не начинается с начала файла, а с инструкции `jump`:
|
|
|
|
|
|
```assembly
|
|
|
_start:
|
|
|
.byte 0xeb
|
|
|
.byte start_of_setup-1f
|
|
|
```
|
|
|
расположеной по смещению в 512 байт от [4d 5a](https://github.com/torvalds/linux/blob/master/arch/x86/boot/header.S#L47). Также необходимо выровнять `cs` с `0x10200` на `0x10000`, а также остальные сегментные регистры. После этого мы настраиваем стек:
|
|
|
|
|
|
```assembly
|
|
|
pushw %ds
|
|
|
pushw $6f
|
|
|
lretw
|
|
|
```
|
|
|
|
|
|
кладём значение `ds` в стек по адресу метки [6](https://github.com/torvalds/linux/blob/master/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/master/arch/x86/boot/header.S#L467) является проверка значения регистра `ss` и создание корректного стека, если значение `ss` неверно:
|
|
|
|
|
|
```assembly
|
|
|
movw %ss, %dx
|
|
|
cmpw %ax, %dx
|
|
|
movw %sp, %dx
|
|
|
je 2f
|
|
|
```
|
|
|
|
|
|
Это может привести к трём различны сценариям:
|
|
|
|
|
|
* `ss` имеет верное значение `0x10000` (как и все остальные сегментные регистры рядом с `cs`)
|
|
|
* `ss` является некорректным и установлен флаг `CAN_USE_HEAP` (см. ниже)
|
|
|
* `ss` является некорректным и флаг `CAN_USE_HEAP` не установлен (см. ниже)
|
|
|
|
|
|
Давайте рассмотрим все три сценария:
|
|
|
|
|
|
* `ss` имеет верный адрес (`0x10000`). В этом случае мы переходим на метку [2](https://github.com/torvalds/linux/blob/master/arch/x86/boot/header.S#L481):
|
|
|
|
|
|
```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`, который хранит корректный адрес сегмента `0x10000` и устанавливает корректное значение `sp`. Теперь мы имеем корректный стек:
|
|
|
|
|
|
![стек](http://oi58.tinypic.com/16iwcis.jpg)
|
|
|
|
|
|
* Второй сценарий (когда `ss` != `ds`). Во-первых, помещаем значение [_end](https://github.com/torvalds/linux/blob/master/arch/x86/boot/header.S#L481) (адрес окончания кода настройки) в `dx` и проверяем поле заголовка `loadflags` инструкцией `testb`, чтобы понять, можем ли мы использовать кучу (heap). [loadflags](https://github.com/torvalds/linux/blob/master/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/master/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/master/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/master/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](http://css.csail.mit.edu/6.858/2014/readings/i386.pdf)
|
|
|
* [Минимальный загрузчик для архитектуры Intel®](https://www.cs.cmu.edu/~410/doc/minimal_boot.pdf)
|
|
|
* [8086](http://en.wikipedia.org/wiki/Intel_8086)
|
|
|
* [80386](http://en.wikipedia.org/wiki/Intel_80386)
|
|
|
* [Вектор прерываний](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)
|