Исправление ошибок в разделе Booting

pull/709/head
proninyaroslav 7 years ago
parent 967cbd3a23
commit 47867dc416

@ -22,16 +22,16 @@
Магическая кнопка включения, что происходит дальше?
--------------------------------------------------------------------------------
Несмотря на то, что это серия статей о ядре Linux, мы не будем начинать с его исходного кода (по крайней мере в этом параграфе). Как только вы нажмёте магическую кнопку включения на вашем ноутбуке или настольном компьютере, он начинает работать. Материнская плата посылает сигнал к [источнику питания](https://en.wikipedia.org/wiki/Power_supply). После получения сигнала, источник питания обеспечивает компьютер надлежащим количеством электричества. После того как материнская плата получает сигнал ["питание в норме" (Power good signal)](https://en.wikipedia.org/wiki/Power_good_signal), она пытается запустить ЦПУ. ЦПУ сбрасывает все остаточные данные в регистрах и записывает предустановленные значения каждого из них.
Несмотря на то, что это серия статей о ядре Linux, мы не будем начинать с его исходного кода (по крайней мере в этом параграфе). Как только вы нажмёте магическую кнопку включения на вашем ноутбуке или настольном компьютере, он начинает работать. Материнская плата посылает сигнал к [источнику питания](https://en.wikipedia.org/wiki/Power_supply). После получения сигнала, источник питания обеспечивает компьютер надлежащим количеством электричества. После того как материнская плата получает сигнал ["питание в норме" (Power good signal)](https://en.wikipedia.org/wiki/Power_good_signal), она пытается запустить CPU. CPU сбрасывает все остаточные данные в регистрах и записывает предустановленные значения каждого из них.
ЦПУ серии [Intel 80386](https://en.wikipedia.org/wiki/Intel_80386) и старше после перезапуска компьютера заполняют регистры следующими предустановленными значениями:
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-битных ЦПУ 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 и прибавить смещение:
Процессор начинает свою работу в [режиме реальных адресов](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 + Смещение
@ -65,7 +65,7 @@ CS base 0xffff0000
'0xfffffff0'
```
Мы получили `0xfffffff0`, т.е. 16 байт ниже 4 Гб. По этому адресу располагается так называемый [вектор прерываний](http://en.wikipedia.org/wiki/Reset_vector). Это область памяти, в которой ЦПУ ожидает найти первую инструкцию для выполнения после сброса. Она содержит инструкцию [jump](http://en.wikipedia.org/wiki/JMP_%28x86_instruction%29) (`jmp`), которая обычно указывает на точку входа в BIOS. Например, если мы взглянем на исходный код [coreboot](http://www.coreboot.org/), то увидим следующее:
Мы получили `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"
@ -138,7 +138,7 @@ objdump -D -b binary -mi386 -Maddr16,data16,intel boot
Реальный загрузочный сектор имеет код для продолжения процесса загрузки и таблицу разделов вместо кучи нулей и восклицательного знака :) С этого момента BIOS передаёт управление загрузчику.
**ЗАМЕЧАНИЕ**: Как уже было упомянуто ранее, ЦПУ находится в режиме реальных адресов; в этом режиме вычисление физического адреса в памяти выполняется следующим образом:
**ЗАМЕЧАНИЕ**: Как уже было упомянуто ранее, CPU находится в режиме реальных адресов; в этом режиме вычисление физического адреса в памяти выполняется следующим образом:
```
Физический адрес = Селектор сегмента * 16 + Смещение
@ -150,7 +150,7 @@ objdump -D -b binary -mi386 -Maddr16,data16,intel boot
'0x10ffef'
```
где `0x10ffef` равен `1 Мб + 64 Кб - 16 байт`. Процессор [8086](https://en.wikipedia.org/wiki/Intel_8086) (который был первым процессором с режимом реальных адресов), в отличии от этого, имеет 20 битную шину адресации. Поскольку `2^20 = 1048576` это 1 Мб, получается, что фактический объём доступной памяти составляет 1 Мб.
где `0x10ffef` равен `1 Мб + 64 Кб - 16 байт`. Процессор [8086](https://en.wikipedia.org/wiki/Intel_8086) (который был первым процессором с режимом реальных адресов), в отличии от этого, имеет 20-битную шину адресации. Поскольку `2^20 = 1048576` это 1 Мб, получается, что фактический объём доступной памяти составляет 1 Мб.
Основная карта разделов в режиме реальных адресов выглядит следующим образом:
@ -168,7 +168,7 @@ objdump -D -b binary -mi386 -Maddr16,data16,intel boot
0x000F0000 - 0x000FFFFF - Системная BIOS
```
В начале статьи я написал, что первая инструкция, выполняемая ЦПУ, расположена по адресу `0xFFFFFFF0`, значение которого намного больше, чем `0xFFFFF` (1 Мб). Каким образом ЦПУ получает доступ к этому участку в режиме реальных адресов? Ответ на этот вопрос находится в документации [coreboot](http://www.coreboot.org/Developer_Manual/Memory_map):
В начале статьи я написал, что первая инструкция, выполняемая CPU, расположена по адресу `0xFFFFFFF0`, значение которого намного больше, чем `0xFFFFF` (1 Мб). Каким образом CPU получает доступ к этому участку в режиме реальных адресов? Ответ на этот вопрос находится в документации [coreboot](http://www.coreboot.org/Developer_Manual/Memory_map):
```
0xFFFE_0000 - 0xFFFF_FFFF: 128 Кб ROM отображаются на адресное пространство
@ -268,7 +268,7 @@ pe_header:
.word 0
```
Это нужно, чтобы загрузить операционную систему с [UEFI](https://en.wikipedia.org/wiki/Unified_Extensible_Firmware_Interface). Мы не будем смотреть на его внутреннюю работу прямо сейчас; это будет в одной из следующих глав.
Это нужно, чтобы загрузить операционную систему с [UEFI](https://en.wikipedia.org/wiki/Unified_Extensible_Firmware_Interface). Мы не будем рассматривать его внутреннюю работу прямо сейчас; мы сделаем это в одной из следующих глав.
Настоящая настройка ядра начинается с:
@ -278,7 +278,7 @@ pe_header:
_start:
```
Загрузчик (grub2 или другой) знает об этой метке (смещение `0x200` от `MZ`) и сразу переходит на неё, несмотря на то, что `header.S` начинается с секции `.bstext`, которая выводит сообщение об ошибке:
Загрузчик (grub2 или другой) знает об этой метке (смещение `0x200` от `MZ`) и сразу переходит на неё, несмотря на то, что `header.S` начинается с секции `.bstext`, который выводит сообщение об ошибке:
```
//
@ -302,9 +302,9 @@ _start:
//
```
Здесь мы можем видеть опкод инструкции `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`.
Здесь мы можем видеть опкод инструкции `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:
Это первый код, который на самом деле запускается (отдельно от предыдущей инструкции jump, конечно). После того как настройщик ядра получил управление от загрузчика, первая инструкция `jmp` располагаетсяь по смещению `0x200` от начала реальных адресов, т.е после первых 512 байт. Об этом можно узнать из протокола загрузки ядра Linux, а также увидеть в исходном коде grub2:
```C
segment = grub_linux_real_target >> 4;
@ -342,14 +342,14 @@ cs = 0x1020
cld
```
Как я уже писал ранее, grub2 загружает код настройки ядра по адресу `0x10000` и `cs` по адресу `0x1020`, потому что исполнение не начинается с начала файла, а с
Как я уже писал ранее, grub2 загружает код настройки ядра по адресу `0x10000` и `cs` по адресу `0x1020`, потому что выполнение не начинается с начала файла, а с инструкции `jump`:
```assembly
_start:
.byte 0xeb
.byte start_of_setup-1f
```
инструкции `jump`, расположение которой смещено на 512 байт от [4d 5a](https://github.com/torvalds/linux/blob/master/arch/x86/boot/header.S#L47). Также необходимо выровнять `cs` с `0x10200` на `0x10000`, а также остальные сегментные регистры. После этого мы настраиваем стек:
расположеной по смещению в 512 байт от [4d 5a](https://github.com/torvalds/linux/blob/master/arch/x86/boot/header.S#L47). Также необходимо выровнять `cs` с `0x10200` на `0x10000`, а также остальные сегментные регистры. После этого мы настраиваем стек:
```assembly
pushw %ds
@ -357,12 +357,12 @@ _start:
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` будут иметь одинаковые значения.
кладём значение `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` неверно:
Почти весь код настройки - это подготовка для среды языка C в режиме реальных адресов. Следующим [шагом](https://github.com/torvalds/linux/blob/master/arch/x86/boot/header.S#L467) является проверка значения регистра `ss` и создание корректного стека, если значение `ss` неверно:
```assembly
movw %ss, %dx
@ -391,11 +391,11 @@ _start:
```
Здесь мы видим выравнивание сегмента `dx` (содержащего значение `sp`, полученное загрузчиком) 4 байтами и проверку - является ли полученное значение нулём. Если ноль, то помещаем `0xfffx` (выровненный 4 байтами адрес до максимального значения сегмента за вычетом 64 Кб) в `dx`. Если не ноль, продолжаем использовать `sp`, полученный от загрузчика (в моём случае 0xf7f4). После этого мы помещаем значение `ax` в `ss`, который хранит правильный адрес сегмента `0x10000` и устанавливает правильное значение `sp`. Теперь мы имеем правильный стек:
Здесь мы видим выравнивание сегмента `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) является заголовком с битовой маской, который определяется как:
* Второй сценарий (когда `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)
@ -407,35 +407,34 @@ _start:
и, как мы можем узнать из протокола загрузки:
```
Field name: loadflags
Имя поля: loadflags
This field is a bitmask.
Данное поле является битовой маской.
Bit 7 (write): CAN_USE_HEAP
Set this bit to 1 to indicate that the value entered in the
heap_end_ptr is valid. If this field is clear, some setup code
functionality will be disabled.
Бит 7 (запись): CAN_USE_HEAP
Бит, установленный в 1, указывает на корректность heap_end_ptr.
Если поле очищено, то некоторый функционал кода настройки будет отключен.
```
Если бит `CAN_USE_HEAP` установлен, мы помещаем `heap_end_ptr` в `dx` (который указывает на `_end`) и добавляем к нему `STACK_SIZE` (минимальный размер стека, 512 байт). После этого, если `dx` без переноса (так и будет, dx = _end + 512), переходим на метку `2` (как в предыдущем случае) и создаём правильный стек.
Если бит `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`:
* Если флаг `CAN_USE_HEAP` не установлен, мы просто используем минимальный стек от `_end` до `_end + STACK_SIZE`:
![минимальный стек](http://oi60.tinypic.com/28w051y.jpg)
Настройка BSS
--------------------------------------------------------------------------------
Последние два шага, которые нужно выполнить перед тем, как мы сможем перейти к основному C-коду, это настройка [BSS](https://en.wikipedia.org/wiki/.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`. Если они не равны, возникает фатальная ошибка.
Это простое сравнение [setup_sig](https://github.com/torvalds/linux/blob/master/arch/x86/boot/setup.ld#L39) с магическим числом `0x5a5aaa55`. Если они не равны, сообщается о фатальной ошибке.
Если магические числа совпадают, зная, что у нас есть набор правильно настроенных сегментных регистров и стек, нам всего лишь нужно настроить BSS, прежде чем перейти к C-коду.
Если магические числа совпадают, зная, что у нас есть набор правильно настроенных сегментных регистров и стек, нам всего лишь нужно настроить BSS.
Секция BSS используется для хранения статически выделенных, неинициализированных данных. Linux тщательно обнуляет эту область памяти, используя следующий код:
@ -467,7 +466,7 @@ Field name: loadflags
Это конец первой части о внутренностях ядра Linux. В следующей части мы увидим первый код на языке C, который выполняется при настройке ядра Linux, реализацию процедур для работы с памятью, таких как `memset`, `memcpy`, `earlyprintk`, инициализацию консоли и многое другое.
**Пожалуйста, имейте в виду, что английский - не мой родной язык, и я очень извиняюсь за возможные неудобства. Если вы найдёте какие-либо ошибки или неточности в переводе, пожалуйста, пришлите pull request в [linux-insides-ru](https://github.com/proninyaroslav/linux-insides-ru).**
**От переводчика: пожалуйста, имейте в виду, что английский - не мой родной язык, и я очень извиняюсь за возможные неудобства. Если вы найдёте какие-либо ошибки или неточности в переводе, пожалуйста, пришлите pull request в [linux-insides-ru](https://github.com/proninyaroslav/linux-insides-ru).**
Ссылки
--------------------------------------------------------------------------------

@ -11,7 +11,7 @@
* увидим `защищённый режим`,
* некоторую подготовку для перехода в него,
* инициализацию кучи и консоли,
* обнаружение памяти, проверку ЦПУ, инициализацию клавиатуры
* обнаружение памяти, проверку CPU, инициализацию клавиатуры
* и многое другое.
Итак, давайте начнём.
@ -19,9 +19,9 @@
Защищённый режим
--------------------------------------------------------------------------------
Прежде чем мы сможем перейти к нативному для Intel 64 режиму [Long Mode](http://en.wikipedia.org/wiki/Long_mode), ядро должно переключить ЦПУ в защищённый режим.
Прежде чем мы сможем перейти к нативному для Intel64 режиму [Long Mode](http://en.wikipedia.org/wiki/Long_mode), ядро должно переключить CPU в защищённый режим.
Что такое [защищённый режим](https://en.wikipedia.org/wiki/Protected_mode)? Защищённый режим был впервые добавлен в архитектуре x86 в 1982 году и был основным режимом процессоров Intel, начиная с [80286](http://en.wikipedia.org/wiki/Intel_80286), пока в Intel 64 не появился режим Long Mode.
Что такое [защищённый режим](https://en.wikipedia.org/wiki/Protected_mode)? Защищённый режим был впервые добавлен в архитектуре x86 в 1982 году и был основным режимом процессоров Intel, начиная с [80286](http://en.wikipedia.org/wiki/Intel_80286), пока в Intel64 не появился режим Long Mode.
Основная причина не использовать [режим реальных адресов](http://wiki.osdev.org/Real_Mode) заключается в том, что возможен лишь очень ограниченный доступ к оперативной памяти. Как вы помните из предыдущей части, есть только 2<sup>20</sup> байт или 1 мегабайт, а иногда даже 640 килобайт оперативной памяти, доступной в режиме реальных адресов.
@ -34,7 +34,7 @@
Здесь мы будем рассматривать только сегментацию. Подкачка страниц будет обсуждаться в следующих разделах.
Как вы можете знать из предыдущей части, адреса в режиме реальных адресов состоят из двух частей:
Как вы можете помнить из предыдущей части, адреса в режиме реальных адресов состоят из двух частей:
* Базовый адрес сегмента
* Смещение от базового сегмента
@ -51,7 +51,7 @@ GDT представляет собой структуру, которая на
```assembly
lgdt gdt
```
где инструкция `lgdt` загружает базовый адрес и ограничение (размер) глобальной дескрипторной таблицы в регистр `GDTR`. `GDTR` является 48-битным регистром и состоит из двух частей:
где инструкция `lgdt` загружает базовый адрес и предел (размер) глобальной дескрипторной таблицы в регистр `GDTR`. `GDTR` является 48-битным регистром и состоит из двух частей:
* размер (16 бит) глобальной дескрипторной таблицы;
* адрес (32 бита) глобальной дескрипторной таблицы.
@ -70,14 +70,14 @@ lgdt gdt
| | |
-------------------------------------------------------------------------------
```
Не волнуйтесь, я знаю, после режима реальных адресов это выглядит немного страшно, но на самом деле это довольно легко. Например, ПРЕДЕЛ 15:0 означает, что биты 0-15 дескриптора содержат значение предела. Остальная его часть находится в ПРЕДЕЛ 19:16. Таким образом, размер предела составляет 0-19, т.е 20 бит. Давайте внимательно взглянем на это:
Не волнуйтесь, я знаю, после режима реальных адресов это выглядит немного страшно, но на самом деле всё довольно легко. Например, ПРЕДЕЛ 15:0 означает, что биты 0-15 дескриптора содержат значение предела. Остальная его часть находится в ПРЕДЕЛ 19:16. Таким образом, размер предела составляет 0-19, т.е 20 бит. Давайте внимательно взглянем на структуру дескриптора:
1. Предел (20 бит) находится в пределах 0-15, 16-19 бит. Он определяет `длину_сегмента - 1`. Зависит от бита `G` (гранулярность).
* Если `G` (бит 55) и предел сегмента равен 0, то размер сегмента составляет 1 байт
* Если `G` равен 1, а предел сегмента равен 0, то размер сегмента составляет 4096 байт
* Если `G` равен 0, а предел сегмента равен 0xfffff, то размер сегмента составляет 1 мегабайт
* Если `G` равен 1, а предел сегмента равен 0xfffff, то размер сегмента составляет 4 гигабайт
* Если `G` равен 1, а предел сегмента равен 0xfffff, то размер сегмента составляет 4 гигабайта
Таким образом, если
@ -89,7 +89,7 @@ lgdt gdt
3. Тип/Атрибут (40-47 бит) определяет тип сегмента и виды доступа к нему.
* Флаг `S` (бит 44) определяет тип дескриптора. Если `S` равен 0, то этот сегмент является системным сегментом, а если `S` равен 1, то этот сегмент является сегментом кода или сегментом данных (сегменты стека являются сегментами данных, которые должны быть сегментами для чтения/записи).
Для того чтобы определить, является ли сегмент сегментом кода или сегментом данных, мы можем проверить атрибут (бит 43), установленный в 0 в приведённой выше схеме. Если он равен 0, то сегмент является сегментом данных, в противном случае это сегмент кода.
Для того чтобы определить, является ли сегмент сегментом кода или сегментом данных, мы можем проверить атрибут (бит 43), обозначенный как 0 в приведённой выше схеме. Если он равен 0, то сегмент является сегментом данных, в противном случае это сегмент кода.
Сегмент может быть одного из следующих типов:
@ -133,7 +133,7 @@ lgdt gdt
7. Флаг L (бит 53) - указывает на то, содержит ли сегмент кода нативный 64-битный код. Если он равен 1, то сегмент кода будет выполнен в 64-битном режиме.
8. Флаг D/B (бит 54) - флаг разрядности (Default/Big, определяет размер операнда, т.е 16/32 бит. Если он установлен, то находящиеся в сегменте операнды считаются имеющими размер 32 бита, иначе 16 бит.
8. Флаг D/B (бит 54) - флаг разрядности (Default/Big), определяет размер операнда, т.е 16/32 бит. Если он установлен, то находящиеся в сегменте операнды считаются имеющими размер 32 бита, иначе 16 бит.
Сегментные регистры содержат селекторы сегментов, так же как и в режиме реальных адресов. Тем не менее, в защищённом режиме селектор сегмента обрабатывается иначе. Каждый дескриптор сегмента имеет соответствующий селектор сегмента, который представляет собой 16-битную структуру:
@ -158,7 +158,7 @@ lgdt gdt
Необходимы следующие шаги, чтобы получить физический адрес в защищённом режиме:
* Селектор сегмента должен быть загружен в один из сегментных регистров
* ЦПУ пытается найти дескриптор сегмента по адресу GDT + Index из селектора и загрузить дескриптор в *скрытую* часть сегментного регистра
* CPU пытается найти дескриптор сегмента по адресу GDT + Index из селектора и загрузить дескриптор в *скрытую* часть сегментного регистра
* Базовый адрес (из дескриптора сегмента) + смещение будет линейным адресом сегмента, который является физическим адресом (если подкачка страниц отключена).
Схематично это будет выглядеть следующим образом:
@ -168,11 +168,11 @@ lgdt gdt
Алгоритм перехода из режима реальных адресов в защищённый режим:
* Отключить прерывания
* Описать и загрузить GDT с инструкцией `lgdt`
* Установить бит PE (Protection Enable) в CR0 (регистр управления (Control Register) 0)
* Описать и загрузить GDT инструкцией `lgdt`
* Установить бит PE (Protection Enable) в CR0 (регистр управления 0 (Control Register 0))
* Перейти к коду защищённого режима
Полный переход в защищённый режим в ядре Linux мы увидим в следующей части, но прежде чем мы сможем перейти в защищённый режим, нужно совершить ещё несколько приготовлений.
Полный переход в защищённый режим мы увидим в следующей части, но прежде чем мы сможем перейти в защищённый режим, нужно совершить ещё несколько приготовлений.
Давайте посмотрим на [arch/x86/boot/main.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/main.c). Мы можем видеть некоторые подпрограммы, которые выполняют инициализацию клавиатуры, инициализацию кучи и т.д. Рассмотрим их.
@ -209,7 +209,7 @@ ENDPROC(memcpy)
Да, мы только что перешли в C-код и снова вернулись к ассемблеру :) Прежде всего мы видим, что `memcpy` и другие подпрограммы, расположенные здесь, начинаются и заканчиваются двумя макросами: `GLOBAL` и `ENDPROC`. Макрос `GLOBAL` описан в [arch/x86/include/asm/linkage.h](https://github.com/torvalds/linux/blob/master/arch/x86/include/asm/linkage.h) и определяет директиву `globl`, а так же метку для него. `ENDPROC` описан в [include/linux/linkage.h](https://github.com/torvalds/linux/blob/master/include/linux/linkage.h); отмечает символ `name` в качестве имени функции и заканчивается размером символа `name`.
Реализация `memcpy` достаточно простая. Во-первых, она помещает значения регистров `si` and `di` в стек для их сохранения, так как они будут меняться в течении работы. `memcpy` (как и другие функции в copy.S) использует `fastcall` соглашения о вызовах. Таким образом, она получает свои входные параметры из регистров `ax`, `dx` и `cx`. Вызов `memcpy` выглядит следующим образом:
Реализация `memcpy` достаточно проста. Во-первых, она помещает значения регистров `si` and `di` в стек для их сохранения, так как они будут меняться в течении работы. `memcpy` (как и другие функции в copy.S) использует `fastcall` соглашения о вызовах. Таким образом, она получает свои входные параметры из регистров `ax`, `dx` и `cx`. Вызов `memcpy` выглядит следующим образом:
```c
memcpy(&boot_params.hdr, &hdr, sizeof hdr);
@ -226,7 +226,7 @@ memcpy(&boot_params.hdr, &hdr, sizeof hdr);
Инициализация консоли
--------------------------------------------------------------------------------
После того, как `hdr` скопирован в `boot_params.hdr`, следующим шагом является инициализация консоли с помощью вызова функции `console_init`, определённой в [arch/x86/boot/early_serial_console.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/early_serial_console.c).
После того как `hdr` скопирован в `boot_params.hdr`, следующим шагом является инициализация консоли с помощью вызова функции `console_init`, определённой в [arch/x86/boot/early_serial_console.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/early_serial_console.c).
Функция пытается найти опцию `earlyprintk` в командной строке и, если поиск завершился успехом, парсит адрес порта, скорость передачи данных и инициализирует последовательный порт. Значение опции `earlyprintk` может быть одним из следующих:
@ -255,9 +255,9 @@ void __attribute__((section(".inittext"))) putchar(int ch)
serial_putchar(ch);
}
```
`__attribute__((section(".inittext")))` означает, что код будет находиться в секции `.inittext`. Мы можем найти его в файле линкёра [setup.ld](https://github.com/torvalds/linux/blob/master/arch/x86/boot/setup.ld#L19).
`__attribute__((section(".inittext")))` означает, что код будет находиться в секции `.inittext`. Мы можем найти его в файле компоновщика [setup.ld](https://github.com/torvalds/linux/blob/master/arch/x86/boot/setup.ld#L19).
Прежде всего, `putchar` проверяет символ `\n` и, если он найден, печатает перед ним `\r`. После этого она выводит символ на экране VGA, вызвав BIOS с прерыванием `0x10`:
Прежде всего, `putchar` проверяет наличие символа `\n` и, если он найден, печатает перед ним `\r`. После этого она выводит символ на экране VGA, вызвав BIOS с прерыванием `0x10`:
```C
static void __attribute__((section(".inittext"))) bios_putchar(int ch)
@ -272,7 +272,7 @@ static void __attribute__((section(".inittext"))) bios_putchar(int ch)
intcall(0x10, &ireg, NULL);
}
```
`initregs` принимает структуру `biosregs` и в первую очередь заполняет `biosregs` нулями, используя функцию `memset`, а затем заполняет его значениями регистра:
`initregs` принимает структуру `biosregs` и в первую очередь заполняет `biosregs` нулями, используя функцию `memset`, а затем заполняет его значениями регистров:
```C
memset(reg, 0, sizeof *reg);
@ -303,13 +303,13 @@ ENDPROC(memset)
```
Как мы можем видеть, `memset` использует `fastcall` соглашения о вызовах, так же как и `memcpy`: это означает, что функция получает свои параметры из регистров `ax`, `dx` и `cx`.
Как правило, реализация `memset` подобна реализации memcpy. Она сохраняет значение регистра `di` в стеке и помещает значение `ax` в `di`, которое является адресом структуры `biosregs`. Далее идёт инструкция `movzbl`, которая копирует значение `dl` в нижние 2 байта регистра `eax`. Оставшиеся 2 верхних байта `eax` будут заполнены нулями.
Как правило, реализация `memset` подобна реализации `memcpy`. Она сохраняет значение регистра `di` в стеке и помещает значение `ax` в `di`, которое является адресом структуры `biosregs`. Далее идёт инструкция `movzbl`, которая копирует значение `dl` в нижние 2 байта регистра `eax`. Оставшиеся 2 верхних байта `eax` будут заполнены нулями.
Следующая инструкция умножает `eax` на `0x01010101`. Это необходимо, так как `memset` будет копировать 4 байта одновременно. Например, нам нужно заполнить структуру значением `0x7` с помощью memset. В этом случае `eax` будет содержать значение `0x00000007`. Так что если мы умножим `eax` на `0x01010101`, мы получим `0x07070707` и теперь мы можем скопировать эти 4 байта в структуру. `memset` использует инструкцию `rep; stosl` для копирования `eax` в `es:di`.
Следующая инструкция умножает `eax` на `0x01010101`. Это необходимо, так как `memset` будет копировать 4 байта одновременно. Например, нам нужно заполнить структуру значением `0x7` с помощью `memset`. В этом случае `eax` будет содержать значение `0x00000007`. Так что если мы умножим `eax` на `0x01010101`, мы получим `0x07070707` и теперь мы можем скопировать эти 4 байта в структуру. `memset` использует инструкцию `rep; stosl` для копирования `eax` в `es:di`.
Остальная часть `memset` делает почти то же самое, что и `memcpy`.
После того, как структура `biosregs` заполнена с помощью `memset`, `bios_putchar` вызывает прерывание [0x10](http://www.ctyme.com/intr/rb-0106.htm) для вывода символа. Затем она проверяет, инициализирован ли последовательный порт, и в случае если он инициализирован, записывает в него символ с помощью инструкций [serial_putchar](https://github.com/torvalds/linux/blob/master/arch/x86/boot/tty.c#L30) и `inb/outb`.
После того как структура `biosregs` заполнена с помощью `memset`, `bios_putchar` вызывает прерывание [0x10](http://www.ctyme.com/intr/rb-0106.htm) для вывода символа. Затем она проверяет, инициализирован ли последовательный порт, и в случае если он инициализирован, записывает в него символ с помощью инструкций [serial_putchar](https://github.com/torvalds/linux/blob/master/arch/x86/boot/tty.c#L30) и `inb/outb`.
Инициализация кучи
--------------------------------------------------------------------------------
@ -337,12 +337,12 @@ ENDPROC(memset)
Теперь куча инициализирована и мы можем использовать её с помощью метода `GET_HEAP`. В следующих постах мы увидим как она используется, как её использовать и как она реализована.
Проверка ЦПУ
Проверка CPU
--------------------------------------------------------------------------------
Следующим шагом является проверка ЦПУ с помощью `validate_cpu` из [arch/x86/boot/cpu.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/cpu.c).
Следующим шагом является проверка CPU с помощью `validate_cpu` из [arch/x86/boot/cpu.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/cpu.c).
Она вызывает функцию [`check_cpu`](https://github.com/torvalds/linux/blob/master/arch/x86/boot/cpucheck.c#L102) и передаёт ей два параметра: уровень ЦПУ и необходимый уровень ЦПУ; `check_cpu` проверяет, запущено ли ядро на нужном уровне ЦПУ.
Она вызывает функцию [`check_cpu`](https://github.com/torvalds/linux/blob/master/arch/x86/boot/cpucheck.c#L102) и передаёт ей два параметра: уровень CPU и необходимый уровень CPU; `check_cpu` проверяет, запущено ли ядро на нужном уровне CPU.
```c
check_cpu(&cpu_level, &req_level, &err_flags);
@ -352,12 +352,12 @@ if (cpu_level < req_level) {
}
```
`check_cpu` проверяет флаги ЦПУ, наличие [long mode](http://en.wikipedia.org/wiki/Long_mode) в случае x86_64 (64-битного) ЦПУ, проверяет поставщика процессора и делает специальные подготовки для некоторых производителей, такие как отключение SSE+SSE2 для AMD в случае их отсутствия и т.д.
`check_cpu` проверяет флаги CPU, наличие [long mode](http://en.wikipedia.org/wiki/Long_mode) в случае x86_64 (64-битного) CPU, проверяет поставщика процессора и делает специальные подготовки для некоторых производителей, такие как отключение SSE+SSE2 для AMD в случае их отсутствия и т.д.
Обнаружение памяти
--------------------------------------------------------------------------------
Следующим шагом является обнаружение памяти с помощью функции `detect_memory`. `detect_memory` в основном предоставляет карту доступной оперативной памяти. Она использует различные программные интерфейсы для обнаружения памяти, такие как `0xe820`, `0xe801` и `0x88`. Здесь мы будем рассматривать только реализацию **0xE820**.
Следующим шагом является обнаружение памяти с помощью функции `detect_memory`. `detect_memory` в основном предоставляет карту доступной оперативной памяти для CPU. Она использует различные программные интерфейсы для обнаружения памяти, такие как `0xe820`, `0xe801` и `0x88`. Здесь мы будем рассматривать только реализацию **0xE820**.
Давайте посмотрим на реализацию `detect_memory_e820` в [arch/x86/boot/memory.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/memory.c). Прежде всего, функция `detect_memory_e820` инициализирует структуру `biosregs`, как мы видели выше, и заполняет регистры специальными значениями для вызова `0xe820`:
@ -487,17 +487,17 @@ static inline void set_fs(u16 seg)
}
```
Функция содержит ассемблерную вставку, которая получает значение параметра `seg` и помещает его в регистр `fs`. Существует много функций в [boot.h](https://github.com/torvalds/linux/blob/master/arch/x86/boot/boot.h), похожих на `set_fs`, например, `set_gs`, `fs`, `gs` для чтения значения в нём и т.д.
Функция содержит ассемблерную вставку, которая получает значение параметра `seg` и помещает его в регистр `fs`. Существует множество функций в [boot.h](https://github.com/torvalds/linux/blob/master/arch/x86/boot/boot.h), похожих на `set_fs`, например, `set_gs`, `fs`, `gs` для чтения значения в нём и т.д.
В конце функция `query_mca` просто копирует таблицу, на которую указывает `es:bx`, в `boot_params.sys_desc_table`.
В завершении функция `query_mca` просто копирует таблицу, на которую указывает `es:bx`, в `boot_params.sys_desc_table`.
Следующим шагом является получение информации [Intel SpeedStep](http://en.wikipedia.org/wiki/SpeedStep) с помощью вызова функции `query_ist`. В первую очередь она проверяет уровень ЦПУ, и если он верный, вызывает прерывание `0x15` для получения информации и сохраняет результат в `boot_params`.
Следующим шагом является получение информации [Intel SpeedStep](http://en.wikipedia.org/wiki/SpeedStep) с помощью вызова функции `query_ist`. В первую очередь она проверяет уровень CPU, и если он верный, вызывает прерывание `0x15` для получения информации и сохраняет результат в `boot_params`.
Следующая функция - [query_apm_bios](https://github.com/torvalds/linux/blob/master/arch/x86/boot/apm.c#L21) получает из BIOS информацию об [Advanced Power Management](http://en.wikipedia.org/wiki/Advanced_Power_Management). `query_apm_bios` также вызывает BIOS прерывание `0x15`, но с `ah = 0x53` для проверки поддержки `APM`. После выполнения `0x15`, функция `query_apm_bios` проверяет сигнатуру `PM` (она должна быть равна `0x504d`), флаг переноса (он должен быть равен 0, если есть поддержка `APM`) и значение регистра `cx` (оно должено быть равно 0x02, если есть поддержка защищённого режима).
Следующая функция - [query_apm_bios](https://github.com/torvalds/linux/blob/master/arch/x86/boot/apm.c#L21) получает из BIOS информацию об [Advanced Power Management](http://en.wikipedia.org/wiki/Advanced_Power_Management). `query_apm_bios` также вызывает BIOS прерывание `0x15`, но с `ah = 0x53` для проверки поддержки `APM`. После выполнения `0x15`, функция `query_apm_bios` проверяет сигнатуру `PM` (она должна быть равна `0x504d`), флаг переноса (он должен быть равен 0, если есть поддержка `APM`) и значение регистра `cx` (оно должено быть равным 0x02, если имеется поддержка защищённого режима).
Далее она снова вызывает `0x15`, но с `ax = 0x5304` для отсоединения от интерфейса `APM` и подключению к интерфейсу 32-битного защищённого режима. В итоге она заполняет `boot_params.apm_bios_info` значениями, полученными из BIOS.
Обратите внимание: `query_apm_bios` будет выполняться только если в конфигурационном файле установлен `CONFIG_APM` или `CONFIG_APM_MODULE`:
Обратите внимание: `query_apm_bios` будет выполняться только в том случае, если в конфигурационном файле установлен `CONFIG_APM` или `CONFIG_APM_MODULE`:
```C
#if defined(CONFIG_APM) || defined(CONFIG_APM_MODULE)
@ -531,7 +531,7 @@ for (devno = 0x80; devno < 0x80+EDD_MBR_SIG_MAX; devno++) {
Это конец второй части о внутренностях ядра Linux. В следующей части мы увидим настройки режима видео и остальные подготовки перед переходом в защищённый режим и непосредственно переход в него.
**Пожалуйста, имейте в виду, что английский - не мой родной язык, и я очень извиняюсь за возможные неудобства. Если вы найдёте какие-либо ошибки или неточности в переводе, пожалуйста, пришлите pull request в [linux-insides-ru](https://github.com/proninyaroslav/linux-insides-ru).**
**От переводчика: пожалуйста, имейте в виду, что английский - не мой родной язык, и я очень извиняюсь за возможные неудобства. Если вы найдёте какие-либо ошибки или неточности в переводе, пожалуйста, пришлите pull request в [linux-insides-ru](https://github.com/proninyaroslav/linux-insides-ru).**
Ссылки
--------------------------------------------------------------------------------
@ -539,8 +539,8 @@ for (devno = 0x80; devno < 0x80+EDD_MBR_SIG_MAX; devno++) {
* [Защищённый режим (Википедия)](http://en.wikipedia.org/wiki/Protected_mode)
* [Защищённый режим (OSDEV)](http://wiki.osdev.org/Protected_Mode)
* [Long mode](http://en.wikipedia.org/wiki/Long_mode)
* [Неплохое объяснение режимов ЦПУ с кодом](http://www.codeproject.com/Articles/45788/The-Real-Protected-Long-mode-assembly-tutorial-for)
* [Как использовать сегменты с ростом вниз на ЦПУ Intel 386 и более поздних](http://www.sudleyplace.com/dpmione/expanddown.html)
* [Неплохое объяснение режимов CPU с кодом](http://www.codeproject.com/Articles/45788/The-Real-Protected-Long-mode-assembly-tutorial-for)
* [Как использовать сегменты с ростом вниз на CPU Intel 386 и более поздних](http://www.sudleyplace.com/dpmione/expanddown.html)
* [Документация по earlyprintk](http://lxr.free-electrons.com/source/Documentation/x86/earlyprintk.txt)
* [Параметры ядра](https://github.com/torvalds/linux/blob/master/Documentation/kernel-parameters.txt)
* [Последовательная консоль](https://github.com/torvalds/linux/blob/master/Documentation/serial-console.txt)

@ -7,10 +7,10 @@
Это третья часть серии `Процесса загрузки ядра`. В предыдущей [части](linux-bootstrap-2.md#kernel-booting-process-part-2) мы остановились прямо перед вызовом функции `set_video` из [main.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/main.c#L181). В этой части мы увидим:
- Инициализацию видеорежима в коде настройки ядра,
- подготовка перед переключением в защищённый режим,
- подготовку перед переключением в защищённый режим,
- переход в защищённый режим
**ПРИМЕЧАНИЕ** Если вы ничего не знаете о защищённом режиме, вы можете найти некоторую информацию о нём в предыдущей [части](linux-bootstrap-2.md#protected-mode). Также есть несколько [ссылок](linux-bootstrap-2.md#links), которые могут вам помочь.
**ПРИМЕЧАНИЕ:** если вы ничего не знаете о защищённом режиме, вы можете найти некоторую информацию о нём в предыдущей [части](linux-bootstrap-2.md#protected-mode). Также есть несколько [ссылок](linux-bootstrap-2.md#links), которые могут вам помочь.
Как я уже писал ранее, мы начнём с функции `set_video`, которая определена в [arch/x86/boot/video.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/video.c#L315). Как мы можем видеть, она начинает работу с получения видеорежима из структуры `boot_params.hdr`:
@ -30,12 +30,12 @@ Offset Proto Name Meaning
```
vga=<mode>
<mode> here is either an integer (in C notation, either
decimal, octal, or hexadecimal) or one of the strings
"normal" (meaning 0xFFFF), "ext" (meaning 0xFFFE) or "ask"
(meaning 0xFFFD). This value should be entered into the
vid_mode field, as it is used by the kernel before the command
line is parsed.
<mode> может быть либо целочисленным значением (в C-нотации,
десятичной, восьмеричной или шестнадцатеричной), либо одной из строк:
"normal" (означает 0xFFFF), "ext" (означает 0xFFFE) или "ask"
(означает 0xFFFD). Это значение должно быть введено в поле
vid_mode field, так как оно используется ядром до
парсинга командной строки.
```
Таким образом, мы можем добавить параметр `vga` в конфигурационный файл GRUB или любого другого загрузчика и он передаст его в командную строку ядра. Как говорится в описании, этот параметр может иметь разные значения. Например, это может быть целым числом `0xFFFD` или `ask`. Если передать `ask` в `vga`, вы увидите примерно такое меню:
@ -59,7 +59,7 @@ vga=<mode>
API кучи
--------------------------------------------------------------------------------
После того, как мы получим `vid_mode` из `boot_params.hdr` в функции `set_video`, мы можем видеть вызов `RESET_HEAP`. `RESET_HEAP` представляет собой макрос, определённый в [boot.h](https://github.com/torvalds/linux/blob/master/arch/x86/boot/boot.h#L199):
После того как мы получим `vid_mode` из `boot_params.hdr` в функции `set_video`, мы можем видеть вызов `RESET_HEAP`. `RESET_HEAP` представляет собой макрос, определённый в [boot.h](https://github.com/torvalds/linux/blob/master/arch/x86/boot/boot.h#L199):
```C
#define RESET_HEAP() ((void *)( HEAP = _end ))
@ -124,15 +124,15 @@ static inline bool heap_free(size_t n)
Настройка видеорежима
--------------------------------------------------------------------------------
Теперь мы можем перейти непосредственно к инициализации видеорежима. Мы остановились на вызове `RESET_HEAP()` в функции `set_video`. Далее идёт вызов `store_mode_params`, который хранит параметры видеорежима в структуре `boot_params.screen_info`, определённой в [include/uapi/linux/screen_info.h](https://github.com/0xAX/linux/blob/master/include/uapi/linux/screen_info.h).
Теперь мы можем перейти непосредственно к инициализации видеорежима. Мы остановились на вызове `RESET_HEAP()` в функции `set_video`. Далее идёт вызов функции `store_mode_params`, которая сохраняет параметры видеорежима в структуре `boot_params.screen_info`, определённой в [include/uapi/linux/screen_info.h](https://github.com/0xAX/linux/blob/master/include/uapi/linux/screen_info.h).
Если мы посмотрим на функцию `store_mode_params`, то увидим, что она начинается с вызова `store_cursor_position`. Как вы можете понять из названия функции, она получает информацию о курсоре и сохраняет её.
В первую очередь `store_cursor_position` инициализирует две переменные, которые имеют тип `biosregs` с `AH = 0x3`, и вызывает BIOS прерывание `0x10`. После того, как прерывание успешно выполнено, она возвращает строку и столбец в регистрах `DL` и `DH`. Строка и столбец будут сохранены в полях `orig_x` и `orig_y` структуры `boot_params.screen_info`.
В первую очередь `store_cursor_position` инициализирует две переменные, которые имеют тип `biosregs` с `AH = 0x3`, и вызывает BIOS прерывание `0x10`. После того как прерывание успешно выполнено, она возвращает строку и столбец в регистрах `DL` и `DH`. Строка и столбец будут сохранены в полях `orig_x` и `orig_y` структуры `boot_params.screen_info`.
После выполнения `store_cursor_position` вызывается функция `store_video_mode`. Она просто получает текущий видеорежим и сохраняет его в `boot_params.screen_info.orig_video_mode`.
После этого она проверяет текущий видеорежим и устанавливает `video_segment`. После того, как BIOS передаёт контроль в загрузочный сектор, для видеопамяти выделяются следующие адреса:
Далее она проверяет текущий видеорежим и устанавливает `video_segment`. После того как BIOS передаёт контроль в загрузочный сектор, для видеопамяти выделяются следующие адреса:
```
0xB000:0x0000 32 Кб Видеопамять для монохромного текста
@ -147,7 +147,7 @@ font_size = rdfs16(0x485);
boot_params.screen_info.orig_video_points = font_size;
```
В первую очередь мы устанавливаем регистр `FS` в 0 с помощью функции `set_fs`. В предыдущей части мы уже видели такие функции, как `set_fs`. Все они определены в [boot.h](https://github.com/0xAX/linux/blob/master/arch/x86/boot/boot.h). Далее мы читаем значение, которое находится по адресу `0x485` (эта область памяти используется для получения размера шрифта) и сохраняет размер шрифта `boot_params.screen_info.orig_video_points`.
В первую очередь мы устанавливаем регистр `FS` в 0 с помощью функции `set_fs`. В предыдущей части мы уже видели такие функции, как `set_fs`. Все они определены в [boot.h](https://github.com/0xAX/linux/blob/master/arch/x86/boot/boot.h). Далее мы читаем значение, которое находится по адресу `0x485` (эта область памяти используется для получения размера шрифта) и сохраняет размер шрифта в `boot_params.screen_info.orig_video_points`.
```
x = rdfs16(0x44a);
@ -214,7 +214,7 @@ struct card_info {
};
```
которая находится в сегменте `.videocards`. Давайте посмотрим в скрипт компоновщика [arch/x86/boot/setup.ld](https://github.com/0xAX/linux/blob/master/arch/x86/boot/setup.ld), в котором мы можем найти:
которая находится в сегменте `.videocards`. Давайте посмотрим на скрипт компоновщика [arch/x86/boot/setup.ld](https://github.com/0xAX/linux/blob/master/arch/x86/boot/setup.ld), в котором мы можем найти:
```
.videocards : {
@ -224,7 +224,7 @@ struct card_info {
}
```
Это значит, что `video_cards` - просто адрес в памяти и все структуры `card_info` размещаются в этом сегменте. Это также означает, что все структуры `card_info` размещаются между `video_cards` и `video_cards_end`, и мы можем воспользоваться этим, чтобы пройтись по ним в цикле. После выполнения `probe_cards` у нас есть все структуры `static __videocard video_vga` с заполненными `nmodes` (число видеорежимов).
Это значит, что `video_cards` - просто адрес в памяти, и все структуры `card_info` размещаются в этом сегменте. Это также означает, что все структуры `card_info` размещаются между `video_cards` и `video_cards_end`, и мы можем воспользоваться этим, чтобы пройтись по ним в цикле. После выполнения `probe_cards` у нас есть все структуры `static __videocard video_vga` с заполненными `nmodes` (число видеорежимов).
После завершения выполнения `probe_cards`, мы переходим в главный цикл функции `set_video`. Это бесконечный цикл, который пытается установить видеорежим с помощью функции `set_mode` и выводит меню, если установлен флаг `vid_mode=ask` командной строки ядра или если видеорежим не определён.
@ -268,7 +268,7 @@ static int vga_set_mode(struct mode_info *mode)
Каждая функция, которая устанавливает видеорежим, просто вызывает BIOS прерывание `0x10` с определённым значением в регистре `AH`.
После того, как мы установили видеорежим, мы передаём его в `boot_params.hdr.vid_mode`.
После того как мы установили видеорежим, мы передаём его в `boot_params.hdr.vid_mode`.
Далее вызывается `vesa_store_edid`. Эта функция сохраняет информацию о [EDID](https://en.wikipedia.org/wiki/Extended_Display_Identification_Data) (**E**xtended **D**isplay **I**dentification **D**ata) для использования ядром. После этого снова вызывается `store_mode_params`. И наконец, если установлен `do_restore`, экран восстанавливается в предыдущее состояние.
@ -291,10 +291,10 @@ outb(0x80, 0x70); /* Выключение NMI */
io_delay();
```
Первой вызывается ассемблерная инструкция `cli`, которая очищает флаг прерывания (`IF`). После этого внешние прерывания отключены. Следующая строка отключает NMI (немаскируемое прерывание).
Первой вызывается ассемблерная инструкция `cli`, которая очищает флаг прерывания (`IF`). После этого внешние прерывания отключены. Следующая строка отключает NMI (немаскируемые прерывания).
Прерывание является сигналом, который отправляется ЦПУ от аппаратного или программного обеспечения.
После получения сигнала, ЦПУ приостанавливает текущую последовательность команд, сохраняет своё состояние и передаёт управление обработчику прерываний. После того, как обработчик прерывания закончил свою работу, он передаёт управление прерванной инструкции. Немаскируемые прерывания (NMI) - это прерывания, которые обрабатываются всегда, независимо от запретов на другие прерывания. Их нельзя игнорировать, и, как правило, они используются для подачи сигнала о невосстанавливаемых аппаратных ошибок. Сейчас мы не будем погружаться в детали прерываний, но обсудим это в следующих постах.
Прерывание является сигналом, который отправляется CPU от аппаратного или программного обеспечения.
После получения сигнала, CPU приостанавливает текущую последовательность команд, сохраняет своё состояние и передаёт управление обработчику прерываний. После того как обработчик прерывания закончил свою работу, он передаёт управление прерванной инструкции. Немаскируемые прерывания (NMI) - это прерывания, которые обрабатываются всегда, независимо от запретов на другие прерывания. Их нельзя игнорировать, и, как правило, они используются для подачи сигнала о невосстанавливаемых аппаратных ошибок. Сейчас мы не будем погружаться в детали прерываний, но обсудим это в следующих постах.
Давайте вернёмся к коду. Мы видим, что вторая строка пишет байт `0x80` (отключённый бит) в `0x70` (регистр CMOS Address). После этого происходит вызов функции `io_delay`. `io_delay` вызывает небольшую задержку и выглядит следующим образом:
@ -336,9 +336,9 @@ static int a20_test(int loops)
В первую очередь мы устанавливаем регистр `FS` в `0x0000` и регистр `GS` в `0xffff`. Далее мы читаем значение по адресу `A20_TEST_ADDR` (`0x200`) и сохраняем его в переменную `saved` и `ctr`.
Далее мы записываем обновлённое значение `ctr` в `fs:gs` с помощью функции `wrfs32`, совершаем задержку в 1 мс, а затем читаем значение из регистра `GS` по адресу `A20_TEST_ADDR+0x10`. Если это не ноль, то линия A20 уже включена. Если линия A20 отключена, мы пытаемся включить её с помощью других методов, которые вы можете найти в `a20.c`. Например, с помощью вызова BIOS прерывания `0x15` с `AH=0x2041` и т.д.
После этого мы записываем обновлённое значение `ctr` в `fs:gs` с помощью функции `wrfs32`, совершаем задержку в 1 мс, а затем читаем значение из регистра `GS` по адресу `A20_TEST_ADDR+0x10`. Если это не ноль, то линия A20 уже включена. Если линия A20 отключена, мы пытаемся включить её с помощью других методов, которые вы можете найти в `a20.c`. Например, с помощью вызова BIOS прерывания `0x15` с `AH=0x2041` и т.д.
Если функция `enabled_a20` завершается неудачей, выводится сообщение об ошибке и вызывается функция `die`. Вы можете вспомнить её из первого файла исходного кода, где мы начали - [arch/x86/boot/header.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/header.S):
Если функция `enabled_a20` завершается неудачей, выводится сообщение об ошибке и вызывается функция `die`. Вы можете вспомнить её из первого файла исходного кода, откуда мы начали - [arch/x86/boot/header.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/header.S):
```assembly
die:
@ -347,7 +347,7 @@ die:
.size die, .-die
```
После того, как шлюз линии A20 успешно включён, вызывается функция `reset_coprocessor`:
После того как шлюз линии A20 успешно включён, вызывается функция `reset_coprocessor`:
```C
outb(0, 0xf0);
@ -439,7 +439,7 @@ Aligned - 16
Здесь `GDT_ENTRY_BOOT_CS` имеет индекс - 2, `GDT_ENTRY_BOOT_DS` является `GDT_ENTRY_BOOT_CS + 1` и т.д. Он начинается с 2, поскольку первый является обязательным нулевым дескриптором (индекс - 0), а второй не используется (индекс - 1).
`GDT_ENTRY` - это макрос, который принимает флаги, базовый адрес, предел и создаёт запись в GDT. Для примера посмотрим на записи сегмента кода. `GDT_ENTRY` принимает следующие значения:
`GDT_ENTRY` - это макрос, который принимает флаги, базовый адрес, предел и создаёт запись в GDT. Для примера посмотрим на запись сегмента кода. `GDT_ENTRY` принимает следующие значения:
* базовый адрес - 0
* предел - 0xfffff
@ -491,7 +491,7 @@ asm volatile("lgdtl %0" : : "m" (gdt));
Фактический переход в защищённый режим
--------------------------------------------------------------------------------
Это конец функции `go_to_protected_mode`. Мы загрузили IDT, GDT, отключили прерывания и теперь можем переключить ЦПУ в защищённый режим. Последний шаг - вызов функции `protected_mode_jump` с двумя параметрами:
Это конец функции `go_to_protected_mode`. Мы загрузили IDT, GDT, отключили прерывания и теперь можем переключить CPU в защищённый режим. Последний шаг - вызов функции `protected_mode_jump` с двумя параметрами:
```C
protected_mode_jump(boot_params.hdr.code32_start, (u32)&boot_params + (ds() << 4));
@ -578,7 +578,7 @@ jmpl *%eax
Это конец третьей части о внутренностях ядра Linux. В следующей части мы рассмотрим первые шаги в защищённом режиме и переход в long mode.
**Пожалуйста, имейте в виду, что английский - не мой родной язык, и я очень извиняюсь за возможные неудобства. Если вы найдёте какие-либо ошибки или неточности в переводе, пожалуйста, пришлите pull request в [linux-insides-ru](https://github.com/proninyaroslav/linux-insides-ru).**
**От переводчика: пожалуйста, имейте в виду, что английский - не мой родной язык, и я очень извиняюсь за возможные неудобства. Если вы найдёте какие-либо ошибки или неточности в переводе, пожалуйста, пришлите pull request в [linux-insides-ru](https://github.com/proninyaroslav/linux-insides-ru).**
Ссылки
--------------------------------------------------------------------------------

@ -4,7 +4,7 @@
Переход в 64-битный режим
--------------------------------------------------------------------------------
Это четвёртая часть `Процесса загрузки ядра`, в которой вы увидите первые шаги в [защищённом режиме](http://en.wikipedia.org/wiki/Protected_mode), такие как проверка поддержки ЦПУ [long mode](http://en.wikipedia.org/wiki/Long_mode) и [SSE](http://en.wikipedia.org/wiki/Streaming_SIMD_Extensions), [подкачка страниц](http://en.wikipedia.org/wiki/Paging), инициализация таблиц страниц и в конце мы обсудим переход в [long mode](https://en.wikipedia.org/wiki/Long_mode).
Это четвёртая часть `Процесса загрузки ядра`, в которой вы увидите первые шаги в [защищённом режиме](http://en.wikipedia.org/wiki/Protected_mode), такие как проверка поддержки процессором [long mode](http://en.wikipedia.org/wiki/Long_mode) и [SSE](http://en.wikipedia.org/wiki/Streaming_SIMD_Extensions), [подкачка страниц](http://en.wikipedia.org/wiki/Paging), инициализация таблиц страниц и в конце мы обсудим переход в [long mode](https://en.wikipedia.org/wiki/Long_mode).
**ЗАМЕЧАНИЕ: данная часть содержит много ассемблерного кода, так что если вы не знакомы с ним, вы можете прочитать соответствующую литературу**
@ -17,7 +17,7 @@ jmpl *%eax
Вы помните, что регистр `eax` содержит адрес 32-битной точки входа. Мы можем прочитать об этом в [протоколе загрузки ядра Linux x86](https://www.kernel.org/doc/Documentation/x86/boot.txt):
```
When using bzImage, the protected-mode kernel was relocated to 0x100000
При использовании bzImage ядро в защищённом режиме перемещается на 0x100000
```
Давайте удостоверимся в том, что это правда, посмотрев на значения регистров в 32-битной точке входа:
@ -127,7 +127,7 @@ Be careful parts of head_64.S assume startup_32 is at address 0.
В начале `startup_32` мы видим инструкцию `cld`, которая очищает бит `DF` в [регистре флагов](https://en.wikipedia.org/wiki/FLAGS_register). Когда флаг направления очищен, все строковые операции, такие как [stos](http://x86.renejeschke.de/html/file_module_x86_id_306.html), [scas](http://x86.renejeschke.de/html/file_module_x86_id_287.html) и др. будут инкрементировать индексные регистры `esi` или `edi`. Нам нужно очистить флаг направления, потому что позже мы будем использовать строковые операции для очистки пространства для таблиц страниц и т.д.
После того, как бит `DF` очищен, следующим шагом является проверка флага `KEEP_SEGMENTS` из поля `loadflags` заголовка настройки ядра. Если вы помните, мы уже видели `loadflags` в самой первой [части](linux-bootstrap-1.md) книги. Там мы проверяли флаг `CAN_USE_HEAP` чтобы узнать, можем ли мы использовать кучу. Теперь нам нужно проверить флаг `KEEP_SEGMENTS`. Данный флаг описан в [протоколе загрузки](https://www.kernel.org/doc/Documentation/x86/boot.txt):
После того как бит `DF` очищен, следующим шагом является проверка флага `KEEP_SEGMENTS` из поля `loadflags` заголовка настройки ядра. Если вы помните, мы уже видели `loadflags` в самой первой [части](linux-bootstrap-1.md) книги. Там мы проверяли флаг `CAN_USE_HEAP` чтобы узнать, можем ли мы использовать кучу. Теперь нам нужно проверить флаг `KEEP_SEGMENTS`. Данный флаг описан в [протоколе загрузки](https://www.kernel.org/doc/Documentation/x86/boot.txt):
```
Бит 6 (запись): KEEP_SEGMENTS
@ -153,7 +153,7 @@ Be careful parts of head_64.S assume startup_32 is at address 0.
Вы помните, что `__BOOT_DS` равен `0x18` (индекс сегмента данных в [глобальной таблице дескрипторов](https://en.wikipedia.org/wiki/Global_Descriptor_Table)). Если `KEEP_SEGMENTS` установлен, мы переходим на ближайшую метку `1f`, иначе обновляем сегментные регистры значением `__BOOT_DS`. Сделать это довольно легко, но есть один интересный момент. Если вы читали предыдущую [часть](linux-bootstrap-3.md), то помните, что мы уже обновили сегментные регистры сразу после перехода в [защищённый режим](https://en.wikipedia.org/wiki/Protected_mode) в [arch/x86/boot/pmjump.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/pmjump.S). Так почему же нам снова нужно обновить значения в сегментных регистрах? Ответ прост. Ядро Linux также имеет 32-битный протокол загрузки и если загрузчик использует его для загрузки ядра, то весь код до `startup_32` будет пропущен. В этом случае `startup_32` будет первой точкой входа в ядро, и нет никаких гарантий, что сегментные регистры будут находиться в ожидаемом состоянии.
После того, как мы проверили флаг `KEEP_SEGMENTS` и установили правильное значение в сегментные регистры, следующим шагом будет вычисление разницы между адресом, по которому мы загружены, и адресом, который был указан во время компиляции. Вы помните, что `setup.ld.S` содержит следующее определение в начале секции: `.head.text`: `. = 0`. Это значит, что код в этой секции скомпилирован для запуска по адресу `0`. Мы можем видеть это в выводе `objdump`:
После того как мы проверили флаг `KEEP_SEGMENTS` и установили правильное значение в сегментные регистры, следующим шагом будет вычисление разницы между адресом, по которому мы загружены, и адресом, который был указан во время компиляции. Вы помните, что `setup.ld.S` содержит следующее определение в начале секции: `.head.text`: `. = 0`. Это значит, что код в этой секции скомпилирован для запуска по адресу `0`. Мы можем видеть это в выводе `objdump`:
```
arch/x86/boot/compressed/vmlinux: file format elf64-x86-64
@ -200,7 +200,7 @@ startup_32 (0x0) +-----------------------+
+-----------------------+
```
`startup_32` слинкован для запуска по адресу `0x0` и это значит, что `1f` имеет адрес `0x0 + смещение 1f`, примерно `0x21` байт. Регистр `ebp` содержит реальный физический адрес метки `1f`. Таким образом, если вычесть `1f` из `ebp`, мы получим реальный физический адрес `startup_32`. В [протоколе загрузки ядра Linux](https://www.kernel.org/doc/Documentation/x86/boot.txt) описано, что базовый адрес ядра в защищённом режиме равен `0x100000`. Мы можем проверить это с помощью [gdb](https://en.wikipedia.org/wiki/GNU_Debugger). Давайте запустим отладчик и поставим точку останова на адресе `1f`, который равен `0x100021`. Если это верно, то мы увидим `0x100021` в регистре `ebp`:
`startup_32` слинкован для запуска по адресу `0x0` и это значит, что `1f` имеет адрес `0x0 + смещение 1f`, примерно `0x21` байт. Регистр `ebp` содержит реальный физический адрес метки `1f`. Таким образом, если вычесть `1f` из `ebp`, мы получим реальный физический адрес `startup_32`. В [протоколе загрузки ядра Linux](https://www.kernel.org/doc/Documentation/x86/boot.txt) описано, что базовый адрес ядра в защищённом режиме равен `0x100000`. Мы можем проверить это с помощью [gdb](https://en.wikipedia.org/wiki/GNU_Debugger). Давайте запустим отладчик и поставим точку останова на адресе `1f`, который равен `0x100021`. Если всё верно, то мы увидим `0x100021` в регистре `ebp`:
```
$ gdb
@ -241,9 +241,9 @@ ebp 0x100000 0x100000
...
```
Да, всё верно. Адрес `startup_32` равен `0x100000`. После того, как мы узнали адрес метки `startup_32`, мы можем начать подготовку к переходу в [long mode](https://en.wikipedia.org/wiki/Long_mode). Наша следующая цель - настроить стек и убедится в том, что ЦПУ поддерживает long mode и [SSE](http://en.wikipedia.org/wiki/Streaming_SIMD_Extensions).
Да, всё верно. Адрес `startup_32` равен `0x100000`. После того как мы узнали адрес метки `startup_32`, мы можем начать подготовку к переходу в [long mode](https://en.wikipedia.org/wiki/Long_mode). Наша следующая цель - настроить стек и убедится в том, что CPU поддерживает long mode и [SSE](http://en.wikipedia.org/wiki/Streaming_SIMD_Extensions).
Настройка стека и проверка ЦПУ
Настройка стека и проверка CPU
--------------------------------------------------------------------------------
Мы не могли настроить стек, пока не знали адрес метки `startup_32`. Мы можем представить себе стек как массив, и регистр указателя стека `esp` должен указывать на конец этого массива. Конечно, мы можем определить массив в нашем коде, но мы должны знать его фактический адрес, чтобы правильно настроить указатель стека. Давайте посмотрим на код:
@ -268,7 +268,7 @@ boot_stack_end:
Прежде всего, мы помещаем адрес `boot_stack_end` в регистр `eax`, т.е регистр `eax` содержит адрес `0x0 + boot_stack_end`. Чтобы получить реальный адрес `boot_stack_end`, нам нужно добавить реальный адрес `startup_32`. Как вы помните, мы нашли этот адрес выше и поместили его в регистр `ebp`. В итоге регистр `eax` будет содержать реальный адрес `boot_stack_end` и нам просто нужно поместить его в указатель стека.
После того, как мы создали стек, следующим шагом является проверка ЦПУ. Так как мы собираемся перейти в `long mode`, нам необходимо проверить, поддерживает ли ЦПУ `long mode` и `SSE`. Мы будем делать это с помощью вызова функции `verify_cpu`:
После того как мы создали стек, следующим шагом является проверка CPU. Так как мы собираемся перейти в `long mode`, нам необходимо проверить, поддерживает ли CPU `long mode` и `SSE`. Мы будем делать это с помощью вызова функции `verify_cpu`:
```assembly
call verify_cpu
@ -279,7 +279,7 @@ boot_stack_end:
Она определена в [arch/x86/kernel/verify_cpu.S](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/verify_cpu.S) и содержит пару вызовов инструкции [CPUID](https://en.wikipedia.org/wiki/CPUID). Данная инструкция
используется для получения информации о процессоре. В нашем случае она проверяет поддержку `long mode` и `SSE` и с помощью регистра `eax` возвращает `0` в случае успеха или `1` в случае неудачи.
Если значение `eax` не равно нулю, то мы переходим на метку `no_longmode`, которая останавливает ЦПУ вызовом инструкции `hlt` до тех пор, пока не произойдёт аппаратное прерывание:
Если значение `eax` не равно нулю, то мы переходим на метку `no_longmode`, которая останавливает CPU вызовом инструкции `hlt` до тех пор, пока не произойдёт аппаратное прерывание:
```assembly
no_longmode:
@ -293,16 +293,16 @@ no_longmode:
Расчёт адреса релокации
--------------------------------------------------------------------------------
Следующим шагом является вычисление адреса релокации для декомпрессии в случае необходимости. Мы уже знаем, что базовый адрес 32-битной точки входа в ядро Linux - `0x100000`, но это не 32-битная точка входа. Базовый адрес ядра по умолчанию определяется значением параметра конфигурации ядра `CONFIG_PHYSICAL_START`. Его значение по умолчанию `0x1000000` или `16 Мб`. Основная проблема заключается в том, что если происходит краш ядра, разработчик должен иметь `rescue ядро` ("спасательное" ядро) для [kdump](https://www.kernel.org/doc/Documentation/kdump/kdump.txt), которое сконфигурировано для загрузки из другого
адреса. Для решения этой проблемы, ядро Linux предоставляет специальный параметр конфигурации - `CONFIG_RELOCATABLE`. Как вы можете прочесть в документации ядра:
Следующим шагом является вычисление адреса релокации для декомпрессии, если это необходимо. Мы уже знаем, что базовый адрес 32-битной точки входа в ядро Linux - `0x100000`, но это 32-битная точка входа. Базовый адрес ядра по умолчанию определяется значением параметра конфигурации ядра `CONFIG_PHYSICAL_START`. Его значение по умолчанию `0x1000000` или `16 Мб`. Основная проблема заключается в том, что если происходит краш ядра, разработчик должен иметь `rescue ядро` ("спасательное" ядро) для [kdump](https://www.kernel.org/doc/Documentation/kdump/kdump.txt), которое сконфигурировано для загрузки из другого
адреса. Для решения этой проблемы ядро Linux предоставляет специальный параметр конфигурации - `CONFIG_RELOCATABLE`. Как вы можете прочесть в документации ядра:
```
This builds a kernel image that retains relocation information
so it can be loaded someplace besides the default 1MB.
Это создает образ ядра, который сохраняет информацию о релокации
поэтому он может быть загружен где-либо, кроме стандартного 1 Мб.
Note: If CONFIG_RELOCATABLE=y, then the kernel runs from the address
it has been loaded at and the compile time physical address
(CONFIG_PHYSICAL_START) is used as the minimum location.
Примечание: Если CONFIG_RELOCATABLE=y, то ядро запускается с адреса,
на который он был загружен, а физический адрес времени компиляции
(CONFIG_PHYSICAL_START) используется как минимальная локация.
```
Проще говоря, это означает, что ядро с той же конфигурацией может загружаться с разных адресов. С технической точки зрения это делается путём компиляции декомпрессора как [адресно-независимого кода](https://en.wikipedia.org/wiki/Position-independent_code). Если мы посмотрим на [arch/x86/boot/compressed/Makefile](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/Makefile), то мы увидим, что декомпрессор действительно скомпилирован с флагом `-fPIC`:
@ -329,7 +329,7 @@ KBUILD_CFLAGS += -fno-strict-aliasing -fPIC
addl $z_extract_offset, %ebx
```
Следует помнить, что регистр `ebp` содержит физический адрес метки `startup_32`. Если параметр `CONFIG_RELOCATABLE` включён во время конфигурации ядра, то мы помещаем этот адрес в регистр `ebx`, выравниваем его до величины, кратной `2 Мб` и сравниваем его со значением `LOAD_PHYSICAL_ADDR`. `LOAD_PHYSICAL_ADDR` является макросом, определённым в [arch/x86/include/asm/boot.h](https://github.com/torvalds/linux/blob/master/arch/x86/include/asm/boot.h) и выглядит следующим образом:
Следует помнить, что регистр `ebp` содержит физический адрес метки `startup_32`. Если параметр `CONFIG_RELOCATABLE` включён во время конфигурации ядра, то мы помещаем этот адрес в регистр `ebx`, выравниваем по границе, кратной `2 Мб` и сравниваем его со значением `LOAD_PHYSICAL_ADDR`. `LOAD_PHYSICAL_ADDR` является макросом, определённым в [arch/x86/include/asm/boot.h](https://github.com/torvalds/linux/blob/master/arch/x86/include/asm/boot.h) и выглядит следующим образом:
```C
#define LOAD_PHYSICAL_ADDR ((CONFIG_PHYSICAL_START \
@ -368,14 +368,14 @@ gdt:
gdt_end:
```
Мы видим, что она расположена в секции `.data` и содержит пять дескрипторов: нулевой дескриптор, сегмент кода ядра, сегмент данных ядра и два дескриптора задач. Мы уже загрузили `глобальную таблицу дескрипторов` в предыдущей [части](https://github.com/0xAX/linux-insides/blob/master/Booting/linux-bootstrap-3.md), и теперь мы делаем почти то же самое здесь, но теперь дескрипторы с `CS.L = 1` и `CS.D = 0` для выполнения в 64-битном режиме. Как мы видим, определение `gdt` начинается с двух байт: `gdt_end - gdt`, который представляет последний байт `gdt` или лимит таблицы. Следующие 4 байта содержат базовый адрес `gdt`. Вы должны помнить, что `глобальная таблица дескрипторов` хранится в 48-битном `GDTR`, который состоит из двух частей:
Мы видим, что она расположена в секции `.data` и содержит пять дескрипторов: нулевой дескриптор, сегмент кода ядра, сегмент данных ядра и два дескриптора задач. Мы уже загрузили `глобальную таблицу дескрипторов` в предыдущей [части](linux-bootstrap-3.md), и теперь мы делаем почти то же самое здесь, но теперь дескрипторы с `CS.L = 1` и `CS.D = 0` для выполнения в 64-битном режиме. Как мы видим, определение `gdt` начинается с двух байт: `gdt_end - gdt`, который представляет последний байт `gdt` или лимит таблицы. Следующие 4 байта содержат базовый адрес `gdt`. Вы должны помнить, что `глобальная таблица дескрипторов` хранится в 48-битном `GDTR`, который состоит из двух частей:
* размер (16-бита) глобальной таблицы дескрипторов;
* адрес (32-бита) глобальной таблицы дескрипторов.
Таким образом, мы помещаем адрес `gdt` в регистр `eax`, а затем помещаем его в `.long gdt` или `gdt+2` в нашем ассемблерном коде. Теперь мы имеем сформированную структуру для регистра `GDTR` и можем загрузить `глобальную таблицу дескрипторов` с помощью инструкции `lgtd`.
После того, как `глобальная таблица дескрипторов` загружена, нам необходимо включить режим [PAE](http://en.wikipedia.org/wiki/Physical_Address_Extension), поместив значение регистра `cr4` в `eax`, установить в нём пятый бит и загрузить его снова в `cr4`:
После того как `глобальная таблица дескрипторов` загружена, нам необходимо включить режим [PAE](http://en.wikipedia.org/wiki/Physical_Address_Extension), поместив значение регистра `cr4` в `eax`, установить в нём пятый бит и загрузить его снова в `cr4`:
```assembly
movl %cr4, %eax
@ -410,14 +410,14 @@ Long mode является расширением унаследованного
* Включить `EFER.LME`;
* Включить подкачку страниц.
Мы уже включили `PAE` путём установки бита `PAE` в регистре управления `cr4`. Наша следующая цель - построить структуру для [подкачки](https://en.wikipedia.org/wiki/Paging). Мы увидим это в следующем параграфе.
Мы уже включили `PAE` путём установки бита `PAE` в регистре управления `cr4`. Наша следующая цель - создать структуру для [подкачки](https://en.wikipedia.org/wiki/Paging). Мы увидим это в следующем параграфе.
Ранняя инициализация таблицы страниц
--------------------------------------------------------------------------------
Итак, мы уже знаем, что прежде чем мы сможем перейти в `64-битный` режим, необходимо создать таблицу страниц, так что давайте посмотри на создание ранних `4 гигабайтных` загрузочных таблиц страниц.
Итак, мы уже знаем, что прежде чем мы сможем перейти в `64-битный` режим, необходимо создать таблицу страниц. Давайте посмотри на создание ранних `4 гигабайтных` загрузочных таблиц страниц.
**ПРИМЕЧАНИЕ: я не буду описывать теорию виртуальной памяти. Если вам необходимо больше знать об этом, см. ссылки в конце этой части.**
**ПРИМЕЧАНИЕ: я не буду описывать теорию виртуальной памяти. Если вам необходимо больше информации по виртуальной памяти, см. ссылки в конце этой части.**
Ядро Linux использует `4 уровневую` подкачку, и в целом мы создадим 6 таблиц страниц:
@ -447,7 +447,7 @@ pgtable:
Как мы видим, она находится в секции `.pgtable` и имеет размер `24` Кб.
После того, как мы получили буфер для `pgtable`, мы можем начать с создания таблицы страниц верхнего уровня - `PML4` - следующим образом:
После того как мы получили буфер для `pgtable`, мы можем начать с создания таблицы страниц верхнего уровня - `PML4` - следующим образом:
```assembly
leal pgtable + 0(%ebx), %edi
@ -455,7 +455,7 @@ pgtable:
movl %eax, 0(%edi)
```
Здесь мы снова помещаем относительный адрес `pgtable` в `ebx` или, другими словами, относительно адреса `startup_32` в регистр `edi`. Далее мы помещаем этот адрес со смещением `0x1007` в регистр `eax`. `0x1007` равен `4096` байтам, который является `PML4 + 7`. `7` здесь представляет флаги записи `PML4`. В нашем случае это флаги `PRESENT+RW+USER`. В конечном счёте мы просто записали адрес первого элемента `PDP` в `PML4`.
Здесь мы снова помещаем относительный адрес `pgtable` в `ebx` или, другими словами, относительный адрес `startup_32` в регистр `edi`. Далее мы помещаем этот адрес со смещением `0x1007` в регистр `eax`. Смещение `0x1007` равно `4096` байтам, которые представляют собой размер `PML4` плюс `7`. `7` здесь представляет флаги `PML4`. В нашем случае это флаги `PRESENT+RW+USER`. В конечном счёте мы просто записали адрес первого элемента `PDP` в `PML4`.
Следующий шаг - создание четырёх записей `директории страниц` в таблице `указателя директорий страниц` с теми же флагами `PRESENT+RW+USE`:
@ -470,7 +470,7 @@ pgtable:
jnz 1b
```
Мы помещаем базовый адрес указателя директорий страниц, который равен `4096` или, другими словами, смещение `0x1000` от таблицы `pgtable` в `edi` и адрес первой записи указателя директорий страниц в регистр `eax`. `4`, помещённое в регистр `ecx`, будет счётчиком в следующем цикле, в котором мы записываем адрес первой записи таблицы указателя директорий страниц в регистр `edi`. После этого `edi` будет содержать адрес первой записи указателя директорий страниц с флагами `0x7`. Далее мы просто вычисляем адрес следующих записей указателя директорий страниц, где каждая запись имеет размер `8` байт, и записываем их адреса в `eax`. Последний шаг в создании структуры подкачки страниц - создание `2048` записей с `2 Мб` страницами:
Мы помещаем базовый адрес указателя директорий страниц, который равен `4096` или, другими словами, смещение `0x1000` от таблицы `pgtable` в `edi`, и адрес первой записи указателя директорий страниц в регистр `eax`. Значение `4`, помещённое в регистр `ecx`, будет счётчиком в следующем цикле, в котором мы записываем адрес первой записи таблицы указателя директорий страниц в регистр `edi`. После этого `edi` будет содержать адрес первой записи указателя директорий страниц с флагами `0x7`. Далее мы просто вычисляем адрес следующих записей указателя директорий страниц, где каждая запись имеет размер `8` байт, и записываем их адреса в `eax`. Последний шаг в создании структуры подкачки страниц - создание `2048` записей с `2 мегабайтными` страницами:
```assembly
leal pgtable + 0x2000(%ebx), %edi
@ -490,7 +490,7 @@ pgtable:
4294967296
```
`4 гигабайтная` таблица страниц. Мы закончили создание нашей ранней структуры таблицы страниц, которая отображает `4` Гб на память и теперь мы можем поместить адрес таблицы страниц верхнего уровня - `PML4` - в регистр управления `cr3`:
или `4 гигабайтную` таблицу страниц. Мы закончили создание нашей ранней структуры таблицы страниц, которая отображает `4` Гб на память и теперь мы можем поместить адрес таблицы страниц верхнего уровня - `PML4` - в регистр управления `cr3`:
```assembly
leal pgtable(%ebx), %eax
@ -533,7 +533,7 @@ pgtable:
lret
```
Вы должны помнить, что на предыдущем шаге мы посметили адрес функции `startup_64` в стек, и после инструкции `lret`, ЦПУ извлекает адрес и переходит по нему.
Вы должны помнить, что на предыдущем шаге мы посметили адрес функции `startup_64` в стек, и после инструкции `lret`, CPU извлекает адрес и переходит по нему.
После всего этого, мы, наконец, в 64-битном режиме:
@ -553,7 +553,7 @@ ENTRY(startup_64)
Это конец четвёртой части о процессе загрузки ядра Linux. В следующей части мы увидим декомпрессию ядра и многое другое.
**Пожалуйста, имейте в виду, что английский - не мой родной язык, и я очень извиняюсь за возможные неудобства. Если вы найдёте какие-либо ошибки или неточности в переводе, пожалуйста, пришлите pull request в [linux-insides-ru](https://github.com/proninyaroslav/linux-insides-ru).**
**От переводчика: пожалуйста, имейте в виду, что английский - не мой родной язык, и я очень извиняюсь за возможные неудобства. Если вы найдёте какие-либо ошибки или неточности в переводе, пожалуйста, пришлите pull request в [linux-insides-ru](https://github.com/proninyaroslav/linux-insides-ru).**
Ссылки
--------------------------------------------------------------------------------

@ -4,7 +4,7 @@
Декомпрессия ядра
--------------------------------------------------------------------------------
Это пятая часть серии `Процесса загрузки ядра`. Мы видели переход в 64-битный режим в предыдущей [части](https://github.com/0xAX/linux-insides/blob/master/Booting/linux-bootstrap-4.md#transition-to-the-long-mode) и в этой части мы продолжим с этого момента. Прежде чем мы перейдём к коду ядра, мы увидим последние шаги: подготовка к декомпрессии ядра, перемещение и, непосредственно, декомпрессия ядра. Итак... давайте снова погрузимся в код ядра.
Это пятая часть серии `Процесса загрузки ядра`. Мы видели переход в 64-битный режим в предыдущей [части](linux-bootstrap-4.md) и в этой части мы продолжим с этого момента. Прежде чем мы перейдём к коду ядра, мы увидим последние шаги: подготовку к декомпрессии ядра, перемещение и, непосредственно, декомпрессию ядра. Итак... давайте снова погрузимся в код ядра.
Подготовка к декомпрессии ядра
--------------------------------------------------------------------------------
@ -24,7 +24,7 @@
lret
```
`startup_64` начинает свою работу. Так как мы загрузили новую глобальную таблицу дескрипторов, и был переход ЦПУ в другой режим (в нашем случае в 64-битный режим), мы можем видеть настройку сегментов данных в начале `startup_64`:
в предыдущей части, `startup_64` начал свою работу. Так как мы загрузили новую глобальную таблицу дескрипторов, и был переход CPU в другой режим (в нашем случае в 64-битный режим), мы можем видеть настройку сегментов данных в начале `startup_64`:
```assembly
.code64
@ -40,7 +40,7 @@ ENTRY(startup_64)
Все сегментные регистры, кроме `cs`, теперь указывают на `ds`, равный `0x18` (если вы не понимаете, почему `0x18`, прочтите предыдущую часть).
Следующий шаг - вычисление разницы между тем, где было скомпилировано ядро, и где оно было загружено:
Следующий шаг - вычисление разницы между адресом, по которому скомпилировано ядро, и адресом, по которому оно было загружено:
```assembly
#ifdef CONFIG_RELOCATABLE
@ -83,7 +83,7 @@ boot_stack_end:
Он расположен в конце секции `.bss`, прямо перед таблицей `.pgtable`. Если вы посмотрите сценарий компоновщика [arch/x86/boot/compressed/vmlinux.lds.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/vmlinux.lds.S), вы найдёте определения `.bss` и `.pgtable`.
После того как стек был настроен, мы можем скопировать сжатое ядро по адресу, который мы получили выше после вычисления адреса перемещения распакованного ядра. Прежде чем перейти к деталям, давайте посмотрим на этот ассемблерный код:
После того как стек был настроен, мы можем скопировать сжатое ядро по адресу, который мы получили выше после вычисления адреса релокации распакованного ядра. Прежде чем перейти к деталям, давайте посмотрим на этот ассемблерный код:
```assembly
pushq %rsi
@ -227,9 +227,9 @@ boot_heap:
где `BOOT_HEAP_SIZE` - это макрос, который раскрывается в `0x8000` (`0x400000` в случае `bzip2` ядра) и представляет собой размер кучи.
После инициализации указателей кучи, следующий шаг - вызов функции `choose_random_location` из [arch/x86/boot/compressed/kaslr.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/kaslr.c#L425). Как можно догадаться из названия функции, она выбирает ячейку памяти, в которой будет разархивирован образ ядра. Может показаться странным, что нам нужно найти или даже `выбрать` место для декомпрессии сжатого образа ядра, но ядро Linux поддерживает [kASLR](https://en.wikipedia.org/wiki/Address_space_layout_randomization), что позволяет загрузить распакованное ядро по случайному адресу из соображений безопасности. Давайте откроем файл [arch/x86/boot/compressed/kaslr.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/kaslr.c#L425) и посмотри на `choose_random_location`.
После инициализации указателей кучи, следующий шаг - вызов функции `choose_random_location` из [arch/x86/boot/compressed/kaslr.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/kaslr.c#L425). Как можно догадаться из названия функции, она выбирает ячейку памяти, в которой будет разархивирован образ ядра. Может показаться странным, что нам нужно найти или даже `выбрать` место для декомпрессии сжатого образа ядра, но ядро Linux поддерживает технологию [kASLR](https://en.wikipedia.org/wiki/Address_space_layout_randomization), которая позволяет загрузить распакованное ядро по случайному адресу из соображений безопасности. Давайте откроем файл [arch/x86/boot/compressed/kaslr.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/kaslr.c#L425) и посмотри на `choose_random_location`.
Во-первых, если `CONFIG_HIBERNATION` установлена, `choose_random_location` пытается найти опцию `kaslr`, в противном случае опцию `nokaslr`:
Во-первых, если опция `CONFIG_HIBERNATION` установлена, `choose_random_location` пытается найти опцию `kaslr`, в противном случае опцию `nokaslr`:
```C
#ifdef CONFIG_HIBERNATION
@ -291,7 +291,7 @@ struct mem_vector {
};
```
Реализация `mem_avoid_init` довольна проста. Давайте посмотрим на часть этой функции:
Реализация `mem_avoid_init` довольна проста. Давайте взглянем на часть этой функции:
```C
...
@ -308,7 +308,7 @@ struct mem_vector {
...
```
Здесь мы видим расчёт начального адреса и размера [initrd](http://en.wikipedia.org/wiki/Initrd). `ext_ramdisk_image` - старшие `32 бита` поля `ramdisk_image` из заголовка настройки, и `ext_ramdisk_size` - старшие 32 бита поля `ramdisk_size` из [протокола загрузки](https://github.com/torvalds/linux/blob/master/Documentation/x86/boot.txt):
Здесь мы видим расчёт начального адреса и размера [initrd](http://en.wikipedia.org/wiki/Initrd). `ext_ramdisk_image` - старшие `32 бита` поля `ramdisk_image` из заголовка настройки и `ext_ramdisk_size` - старшие 32 бита поля `ramdisk_size` из [протокола загрузки](https://github.com/torvalds/linux/blob/master/Documentation/x86/boot.txt):
```
Offset Proto Name Meaning
@ -334,9 +334,9 @@ Offset Proto Name Meaning
...
```
Итак, мы берём `ext_ramdisk_image` и `ext_ramdisk_size`, сдвигаем их влево на `32` (теперь они будут содержать младшие 32 бита в старших битах) и получаем начальный адрес и размер `initrd`. После этого мы сохраняем эти значения в массиве `mem_avoid`.
Итак, мы берём `ext_ramdisk_image` и `ext_ramdisk_size`, сдвигаем их влево на `32` (теперь они будут содержать младшие 32 бита в старших битах) и получаем начальный адрес и размер `initrd`. Далее мы сохраняем их в массиве `mem_avoid`.
Следующим шагом после того, как мы собрали все небезопасные области памяти в массиве `mem_avoid`, будет поиск случайного адреса, который не пересекается с небезопасными областями, используя функцию `find_random_addr`. Прежде всего, мы можем видеть выравнивание выходного адреса в функции `find_random_addr`:
Следующим шагом после того как мы собрали все небезопасные области памяти в массиве `mem_avoid`, будет поиск случайного адреса, который не пересекается с небезопасными областями, используя функцию `find_random_addr`. Прежде всего, мы можем видеть выравнивание выходного адреса в функции `find_random_addr`:
```C
minimum = ALIGN(minimum, CONFIG_PHYSICAL_ALIGN);
@ -350,7 +350,7 @@ for (i = 0; i < real_mode->e820_entries; i++) {
}
```
Напомним, что мы собрали `e820_entries` во [второй части](https://github.com/proninyaroslav/linux-insides-ru/blob/master/Booting/linux-bootstrap-2.md#Обнаружение-памяти). Функция `process_e820_entry` совершает некоторые проверки что область памяти `e820` не является `non-RAM`, что начальный адрес области памяти не больше максимального допустимого смещения `aslr` offset, и что область памяти находится выше минимальной локации загрузки:
Напомним, что мы собрали `e820_entries` во [второй части](https://github.com/proninyaroslav/linux-insides-ru/blob/master/Booting/linux-bootstrap-2.md#Обнаружение-памяти). Функция `process_e820_entry` совершает некоторые проверки: что область памяти `e820` не является `non-RAM`, что начальный адрес области памяти не больше максимального допустимого смещения `aslr` offset, и что область памяти находится выше минимальной локации загрузки:
```C
struct mem_vector region, img;
@ -381,7 +381,7 @@ if (region.start > entry->addr + entry->size)
return;
```
На следующем этапе мы уменьшаем размер области памяти, чтобы не включать отклонённые области в начале, и гарантируем, что последний адрес в области памяти меньше, чем `CONFIG_RANDOMIZE_BASE_MAX_OFFSET`, поэтому конец образа ядра будет меньше чем максимальное смещение `aslr`:
На следующем этапе мы уменьшаем размер области памяти, чтобы не включить отклонённые области в начале, и гарантируем, что последний адрес в области памяти меньше, чем `CONFIG_RANDOMIZE_BASE_MAX_OFFSET`, поэтому конец образа ядра будет меньше чем максимальное смещение `aslr`:
```C
region.size -= region.start - entry->addr;
@ -425,9 +425,9 @@ if (slot_max == 0)
return slots[get_random_long() % slot_max];
```
где функция `get_random_long` проверяет различные флаги ЦПУ, такие как `X86_FEATURE_RDRAND` или `X86_FEATURE_TSC`, и выбирает метод для получения случайного числа (это может быть инструкция RDRAND, счётчик временных меток, программируемый интервальный таймер и т.д.). После извлечения случайного адреса, `choose_random_location` завершает свою работу.
где функция `get_random_long` проверяет различные флаги CPU, такие как `X86_FEATURE_RDRAND` или `X86_FEATURE_TSC`, и выбирает метод для получения случайного числа (это может быть инструкция RDRAND, счётчик временных меток, программируемый интервальный таймер и т.д.). После извлечения случайного адреса, `choose_random_location` завершает свою работу.
Теперь вернёмся к [misc.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/misc.c#L404). После получения адреса для образа ядра мы должны были совершить некоторые проверки, чтобы убедиться, что полученный случайный адрес правильно выровнен и что адрес является корректным.
Теперь вернёмся к [misc.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/misc.c#L404). После получения адреса для образа ядра мы должны были совершить некоторые проверки и убедиться в том, что полученный случайный адрес правильно выровнен и является корректным.
После этого мы увидим знакомое сообщение:
@ -520,14 +520,14 @@ if (ehdr.e_ident[EI_MAG0] != ELFMAG0 ||
output + phdr->p_offset,
phdr->p_filesz);
break;
default: /* Ignore other PT_* */ break;
default: /* Игнорируем остальные PT_* */ break;
}
}
```
С этого момента все загружаемые сегменты находятся в правильном месте. Последняя функция - `handle_relocations` - корректирует адреса в образе ядра и вызывается только в том случае, если `kASLR` был включён во время конфигурации ядра.
После перемещения ядра мы возвращаемся из `decompress_kernel` обратно в [arch/x86/boot/compressed/head_64.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/head_64.S). Адрес ядра находится в регистре `rax` и мы переходим по нему:
После перемещения ядра мы возвращаемся из `decompress_kernel` обратно в [arch/x86/boot/compressed/head_64.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/head_64.S). Адрес ядра находится в регистре `rax` и мы совершаем переход по нему:
```assembly
jmp *%rax
@ -542,7 +542,7 @@ jmp *%rax
Следующая глава посвящена инициализации ядра, и мы увидим первые шаги в коде инициализации ядра Linux.
**Пожалуйста, имейте в виду, что английский - не мой родной язык, и я очень извиняюсь за возможные неудобства. Если вы найдёте какие-либо ошибки или неточности в переводе, пожалуйста, пришлите pull request в [linux-insides-ru](https://github.com/proninyaroslav/linux-insides-ru).**
**От переводчика: пожалуйста, имейте в виду, что английский - не мой родной язык, и я очень извиняюсь за возможные неудобства. Если вы найдёте какие-либо ошибки или неточности в переводе, пожалуйста, пришлите pull request в [linux-insides-ru](https://github.com/proninyaroslav/linux-insides-ru).**
Ссылки
--------------------------------------------------------------------------------

Loading…
Cancel
Save