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