Если вы читали предыдущие [статьи](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.
В любом случае, если вы только начинаете изучать подобные инструменты, я постараюсь объяснить некоторые детали в этой и последующих частях. Ладно, простое введение закончилось, и теперь можно начать "погружение" в ядро и низкоуровневые вещи.
Я начал писать эту книгу, когда актуальной версией ядра 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) и старше после перезапуска компьютера заполняют регистры следующими предустановленными значениями:
Процессор начинает свою работу в [режиме реальных адресов](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` и прибавить к нему смещение:
что больше первого мегабайта на 65520 байт. Поскольку в режиме реальных адресов доступен только один мегабайт, с отключённой [адресной линией A20](https://en.wikipedia.org/wiki/A20_line) `0x10ffef` становится `0x00ffef`.
Хорошо, теперь мы немного знаем о режиме реальных адресов и адресации памяти в этом режиме. Давайте вернёмся к обсуждению значений регистров после сброса.
В то время как базовый адрес, как правило, формируется путём умножения значения селектора сегмента на 16, во время аппаратного перезапуска в селектор сегмента в регистре `CS` записывается `0xf000`, а в базовый адрес - `0xffff0000`. Процессор использует этот специальный базовый адрес, пока регистр `CS` не изменится.
Мы получили `0xfffffff0`, т.е. 16 байт ниже 4 Гб. По этому адресу располагается так называемый [вектор прерываний](http://en.wikipedia.org/wiki/Reset_vector). Это область памяти, в которой CPU ожидает найти первую инструкцию для выполнения после сброса. Она содержит инструкцию [jump](http://en.wikipedia.org/wiki/JMP_%28x86_instruction%29) (`jmp`), которая обычно указывает на точку входа в BIOS. Например, если мы взглянем на исходный код [coreboot](http://www.coreboot.org/) (`src/cpu/x86/16bit/reset16.inc`), то увидим следующее:
Теперь запускается BIOS; после инициализации и проверки оборудования, BIOS нужно найти загрузочное устройство. Порядок загрузки хранится в конфигурации BIOS, которая определяет, с каких устройств BIOS пытается загрузиться. При попытке загрузиться с жёсткого диска, BIOS пытается найти загрузочный сектор. На размеченных жёстких дисках со схемой разделов MBR, загрузочный сектор расположен в первых 446 байтах первого сектора, размер которого 512 байт. Последние два байта первого сектора - `0x55` и `0xaa`, которые оповещают BIOS о том, что устройство является загрузочным.
Команда оповещает эмулятор [QEMU](http://qemu.org) о необходимости использовать в качестве образа диска созданный только что бинарный файл. Пока последний проверяет, удовлетворяет ли загрузочный сектор всем необходимым требованиям (в origin записывается `0x7c00`, а в конце магическая последовательность), QEMU будет работать с бинарным файлом как с главной загрузочной записью (MBR) образа диска.
В этом примере мы можем видеть, что код будет выполнен в 16-битном режиме реальных адресов и начнёт выполнение с адреса `0x7c00`. После запуска он вызывает прерывание [0x10](http://www.ctyme.com/intr/rb-0106.htm), которое просто печатает символ `!`. Оставшиеся 510 байт заполняются нулями, и код заканчивается двумя магическими байтами `0xaa` и `0x55`.
Реальный загрузочный сектор имеет код для продолжения процесса загрузки и таблицу разделов вместо кучи нулей и восклицательного знака :) С этого момента BIOS передаёт управление загрузчику.
**ЗАМЕЧАНИЕ**: Как уже было упомянуто ранее, CPU находится в режиме реальных адресов; в этом режиме вычисление физического адреса в памяти выполняется следующим образом:
так же, как было упомянуто выше. У нас есть только 16-битные регистры общего назначения; максимальное значение 16-битного регистра - `0xffff`. Поэтому, если мы возьмём максимальное возможное значение, то результат будет следующий:
где `0x10ffef` равен `1 Мб + 64 Кб - 16 байт`. Процессор [8086](https://en.wikipedia.org/wiki/Intel_8086) (который был первым процессором с режимом реальных адресов), в отличии от этого, имеет 20-битную шину адресации. Поскольку `2^20 = 1048576` это 1 Мб, получается, что фактический объём доступной памяти составляет 1 Мб.
В начале статьи я написал, что первая инструкция, выполняемая CPU, расположена по адресу `0xFFFFFFF0`, значение которого намного больше, чем `0xFFFFF` (1 Мб). Каким образом CPU получает доступ к этому участку в режиме реальных адресов? Ответ на этот вопрос находится в документации [coreboot](http://www.coreboot.org/Developer_Manual/Memory_map):
Существует несколько загрузчиков Linux, такие как [GRUB 2](https://www.gnu.org/software/grub/) и [syslinux](http://www.syslinux.org/wiki/index.php/The_Syslinux_Project). Ядро Linux имеет [протокол загрузки](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/Documentation/x86/boot.txt), который определяет требования к загрузчику для реализации поддержки Linux. В этом примере будет описан GRUB 2.
Теперь, когда BIOS выбрал загрузочное устройство и передал контроль управления коду в загрузочном секторе, начинается выполнение [boot.img](http://git.savannah.gnu.org/gitweb/?p=grub.git;a=blob;f=grub-core/boot/i386/pc/boot.S;hb=HEAD). Этот код очень простой в связи с ограниченным количеством свободного пространства и содержит указатель, который используется для перехода к основному образу GRUB 2. Основной образ начинается с [diskboot.img](http://git.savannah.gnu.org/gitweb/?p=grub.git;a=blob;f=grub-core/boot/i386/pc/diskboot.S;hb=HEAD), который обычно располагается сразу после первого сектора в неиспользуемой области перед первым разделом. Приведённый выше код загружает оставшуюся часть основного образа, который содержит ядро и драйверы GRUB 2 для управления файловой системой. После загрузки остальной части основного образа, код выполняет функция [grub_main](http://git.savannah.gnu.org/gitweb/?p=grub.git;a=blob;f=grub-core/kern/main.c).
Функция `grub_main` инициализирует консоль, получает базовый адрес для модулей, устанавливает корневое устройство, загружает/обрабатывает файл настроек grub, загружает модули и т.д. В конце выполнения, `grub_main` переводит grub обратно в нормальный режим. Функция `grub_normal_execute` (из `grub-core/normal/main.c`) завершает последние приготовления и отображает меню выбора операционной системы. Когда мы выбираем один из пунктов меню, запускается функция `grub_menu_execute_entry`, которая в свою очередь запускает команду grub `boot`, загружающую выбранную операционную систему.
Из протокола загрузки видно, что загрузчик должен читать и заполнять некоторые поля в заголовке ядра, который начинается со смещения `0x01f1` в коде настроек. Вы можете посмотреть загрузочный [скрипт компоновщика](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/setup.ld#L16), чтобы убедиться в этом. Заголовок ядра [arch/x86/boot/header.S](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/header.S) начинается с:
Загрузчик должен заполнить этот и другие заголовки (которые помеченные как тип `write` в протоколе загрузки Linux, например, в [данном примере](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/Documentation/x86/boot.txt#L354)) значениями, которые он получил из командной строки или значениями, вычисленными во время загрузки. (Мы не будет вдаваться в подробности и описывать все поля заголовка ядра, но мы сделаем это, когда будем обсуждать как их использует ядро; тем не менее вы можете найти полное описание всех полей в [протоколе загрузки](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/Documentation/x86/boot.txt#L156).)
Сейчас загрузчик поместил ядро Linux в память, заполнил поля заголовка, а затем переключился на него. Теперь мы можем перейти непосредственно к коду настройки ядра.
Наконец, мы находимся в ядре! Технически, ядро ещё не работает; во-первых, часть ядра, отвественная за настройку, должна подготовить декомпрессор, вещи связанные с управлением памятью и т.д. После всех подготовок код настройки ядра должен распаковывать фактическое ядро и совершить переход на него. Выполнение настройки начинается в файле [arch/x86/boot/header.S](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/header.S), начиная с метки [_start](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/header.S#L292). Это немного странно на первый взгляд, так как перед этим есть ещё несколько инструкций.
На самом деле, `header.S` начинается с [MZ](https://en.wikipedia.org/wiki/DOS_MZ_executable) (см. картинку выше), вывода сообщения об ошибке и [PE](https://en.wikipedia.org/wiki/Portable_Executable) заголовка:
Это нужно, чтобы загрузить операционную систему с поддержкой [UEFI](https://en.wikipedia.org/wiki/Unified_Extensible_Firmware_Interface). Мы не будем рассматривать его внутреннюю работу прямо сейчас; мы сделаем это в одной из следующих глав.
Загрузчик (grub2 или другой) знает об этой метке (при смещении `0x200` от `MZ`) и сразу переходит на неё, несмотря на то, что `header.S` начинается с секции `.bstext`, который выводит сообщение об ошибке:
Здесь мы можем видеть опкод инструкции `jmp` (`0xeb`) к метке `start_of_setup-1f`. Нотация `Nf` означает, что `2f` ссылается на следующую локальную метку `2:`; в нашем случае это метка `1`, которая расположена сразу после инструкции jump и содержит оставшуюся часть [заголовка](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/Documentation/x86/boot.txt#L156). Сразу после заголовка настроек мы видим секцию `.entrytext`, которая начинается с метки `start_of_setup`.
Это первый код, который на самом деле запускается (отдельно от предыдущей инструкции jump, конечно). После того как настройщик ядра получил управление от загрузчика, первая инструкция `jmp` располагаетсяь по смещению `0x200` от начала реальных адресов, т.е после первых 512 байт. Об этом можно узнать из протокола загрузки ядра Linux, а также увидеть в исходном коде grub2:
Прежде всего, ядро гарантирует, что сегментные регистры `ds` и `es` указывают на один и тот же адрес. Затем оно сбрасывает флаг направления с помощью инструкции `cld`:
Как я уже писал ранее, `grub2` загружает код настройки ядра по адресу `0x1000` (адрес по умолчанию) и `cs` по адресу `0x1020`, потому что выполнение не начинается с начала файла, ас метки `_start`:
расположеной в `512` байтах от [4d 5a](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/header.S#L46). Нам также необходимо выровнять `cs`с`0x1020` на `0x1000` и остальные сегментные регистры. После этого мы настраиваем стек:
кладём значение `ds` в стек по адресу метки [6](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/header.S#L494) и выполняем инструкцию `lretw`. Когда мы вызываем `lretw`, она загружает адрес метки `6` в регистр [счётчика команд (IP)](https://en.wikipedia.org/wiki/Program_counter), и загружает `cs`со значением `ds`. После этого `ds` и `cs` будут иметь одинаковые значения.
Почти весь код настройки - это подготовка для среды языка C в режиме реальных адресов. Следующим [шагом](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/header.S#L569) является проверка значения регистра `ss` и создание корректного стека, если значение `ss` неверно:
*`ss` имеет верный адрес (`0x1000`). В этом случае мы переходим на метку [2](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/header.S#L583):
Здесь мы видим выравнивание сегмента `dx` (содержащего значение `sp`, полученное загрузчиком) до 4 байт и проверку - является ли полученное значение нулём. Если ноль, то помещаем `0xfffx` (выровненный до `4` байт адрес до максимального значения сегмента в 64 Кб) в `dx`. Если не ноль, продолжаем использовать `sp`, полученный от загрузчика (в моём случае `0xf7f4`). После этого мы помещаем значение `ax` в `ss`, который хранит корректный адрес сегмента `0x1000` и устанавливает корректное значение `sp`. Теперь мы имеем корректный стек:
* Второй сценарий (когда `ss` != `ds`). Во-первых, помещаем значение [_end](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/setup.ld#L52) (адрес окончания кода настройки) в `dx` и проверяем поле заголовка `loadflags` инструкцией `testb`, чтобы понять, можем ли мы использовать кучу (heap). [loadflags](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/header.S#L321) является заголовком с битовой маской, который определён как:
Если бит `CAN_USE_HEAP` установлен, мы помещаем `heap_end_ptr` в `dx` (который указывает на `_end`) и добавляем к нему `STACK_SIZE` (минимальный размер стека, `512` байт). После этого, если `dx` без переноса (будет без переноса, поскольку `dx = _end + 512`), переходим на метку `2` (как в предыдущем случае) и создаём корректный стек.
Последние два шага, которые нужно выполнить перед тем, как мы сможем перейти к основному коду на C, это настройка [BSS](https://en.wikipedia.org/wiki/.bss) и проверка "магических" чисел. Сначала проверка чисел:
Это простое сравнение [setup_sig](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/setup.ld#L39) с магическим числом `0x5a5aaa55`. Если они не равны, сообщается о фатальной ошибке.
Секция BSS используется для хранения статически выделенных, неинициализированных данных. Linux тщательно обнуляет эту область памяти, используя следующий код:
Во-первых, адрес [__bss_start](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/setup.ld#L47) помещается в `di`. Далее, адрес `_end + 3` (+3 - выравнивает до 4 байт) помещается в `cx`. Регистр `eax` очищается (с помощью инструкции `xor`), а размер секции BSS (`cx`-`di`) вычисляется и помещается в `cx`. Затем `cx` делится на 4 (размер 'слова' (англ. word)), а инструкция `stosl` используется повторно, сохраняя значение `eax` (ноль) в адрес, на который указывает `di`, автоматически увеличивая `di` на 4 (это продолжается до тех пор, пока `cx` не достигнет нуля). Эффект от этого кода в том, что теперь все 'слова' в памяти от `__bss_start` до `_end` заполнены нулями:
Функция `main()` находится в файле [arch/x86/boot/main.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/main.c). О том, что она делает, вы сможете узнать в следующей части.
Это конец первой части о внутренностях ядра Linux. В следующей части мы увидим первый код на языке C, который выполняется при настройке ядра Linux, реализацию процедур для работы с памятью, таких как `memset`, `memcpy`, `earlyprintk`, инициализацию консоли и многое другое.
**От переводчика: пожалуйста, имейте в виду, что английский - не мой родной язык, и я очень извиняюсь за возможные неудобства. Если вы найдёте какие-либо ошибки или неточности в переводе, пожалуйста, пришлите pull request в [linux-insides-ru](https://github.com/proninyaroslav/linux-insides-ru).**