Глава 1. Начальная загрузка и инициализация ядра | FreeBSD Documentation Portal
Description: Начальная загрузка и инициализация ядра
Keywords: boot,BIOS,kernel,MBR,FreeBSD

# Глава 1. Начальная загрузка и инициализация ядра

Этот перевод может быть устаревшим. Для того, чтобы помочь с переводом, пожалуйста, обратитесь к Сервер переводов FreeBSD.

## 1.1. Обзор

Эта глава представляет собой обзор процессов загрузки и инициализации системы, начиная с POST в BIOS (микропрограмме) и заканчивая созданием первого пользовательского процесса. Поскольку начальные этапы загрузки системы сильно зависят от архитектуры, в качестве примера используется архитектура IA-32. Однако архитектуры AMD64 и ARM64 гораздо важнее и интереснее, и их следует рассмотреть в ближайшем будущем в соответствии с темой этого документа.

Процесс загрузки FreeBSD может быть удивительно сложным. После передачи управления от BIOS необходимо выполнить значительный объём низкоуровневой настройки перед загрузкой и выполнением ядра. Эта настройка должна быть выполнена простым и гибким способом, предоставляя пользователю широкие возможности для настройки и адаптации.

## 1.2. Обзор

Процесс загрузки — это операция, крайне зависимая от оборудования. Не только для каждой архитектуры компьютера должен быть написан код, но также могут существовать различные типы загрузки в рамках одной архитектуры. Например, список файлов в каталоге stand показывает большое количество кода, зависящего от архитектуры. Для каждой из поддерживаемых архитектур существует отдельный каталог. FreeBSD поддерживает стандарт загрузки CSM (Compatibility Support Module). Таким образом, CSM поддерживается (как с GPT, так и с MBR разметкой), а также загрузка через UEFI (GPT полностью поддерживается, MBR — в основном). Также поддерживается загрузка файлов с ext2fs, MSDOS, UFS и ZFS. FreeBSD поддерживает функцию загрузочного окружения ZFS, которая позволяет основной ОС передавать детали о том, что загружать, выходящие за рамки простого раздела, как это было возможно ранее. Однако в наши дни UEFI более актуален, чем CSM. В следующем примере показана загрузка компьютера x86 с жёсткого диска с MBR-разметкой, где используется мультизагрузчик FreeBSD boot0, сохранённый в самом первом секторе. Этот загрузочный код запускает трёхэтапный процесс загрузки FreeBSD.

Ключ к пониманию этого процесса заключается в том, что он состоит из последовательных стадий возрастающей сложности. Эти стадии — boot1, boot2 и loader (подробнее см. boot(8)). Система загрузки выполняет каждую стадию последовательно. Последняя стадия, loader, отвечает за загрузку ядра FreeBSD. Каждая стадия рассматривается в следующих разделах.

Вот пример вывода, сгенерированного на различных этапах загрузки. Фактический вывод может отличаться в зависимости от машины:

Компонент FreeBSD | Вывод (может отличаться)
`boot0` |
    F1    FreeBSD
    F2    BSD
    F5    Disk 2
`boot2` [1] |
    >>FreeBSD/x86 BOOT
    Default: 0:ad(0p4)/boot/loader
    boot:
loader |
    BTX loader 1.00 BTX version is 1.02
    Consoles: internal video/keyboard
    BIOS drive C: is disk0
    BIOS 639kB/2096064kB available memory

    FreeBSD/x86 bootstrap loader, Revision 1.1
    Console internal video/keyboard
    (root@releng1.nyi.freebsd.org, Fri Apr  9 04:04:45 UTC 2021)
    Loading /boot/defaults/loader.conf
    /boot/kernel/kernel text=0xed9008 data=0x117d28+0x176650 syms=[0x8+0x137988+0x8+0x1515f8]
ядро системы |
    Copyright (c) 1992-2021 The FreeBSD Project.
    Copyright (c) 1979, 1980, 1983, 1986, 1988, 1989, 1991, 1992, 1993, 1994
            The Regents of the University of California. All rights reserved.
    FreeBSD is a registered trademark of The FreeBSD Foundation.
    FreeBSD 13.0-RELEASE 0 releng/13.0-n244733-ea31abc261f: Fri Apr  9 04:04:45 UTC 2021
        root@releng1.nyi.freebsd.org:/usr/obj/usr/src/i386.i386/sys/GENERIC i386
    FreeBSD clang version 11.0.1 (git@github.com:llvm/llvm-project.git llvmorg-11.0.1-0-g43ff75f2c3fe)

## 1.3. BIOS

При включении компьютера регистры процессора устанавливаются в некоторые предопределённые значения. Один из регистров — это регистр указателя команд, и его значение после включения питания чётко определено: это 32-битное значение `0xfffffff0`. Регистр указателя команд (также известный как Счётчик Команд) указывает на код, который должен быть выполнен процессором. Ещё один важный регистр — это 32-битный управляющий регистр `cr0`, и его значение сразу после перезагрузки равно `0`. Один из битов `cr0`, бит PE (Protection Enabled, Защита Включена), указывает, работает ли процессор в 32-битном защищённом режиме или 16-битном реальном режиме. Поскольку этот бит сброшен при загрузке, процессор запускается в 16-битном реальном режиме. Реальный режим означает, среди прочего, что линейные и физические адреса идентичны. Причина, по которой процессор не запускается сразу в 32-битном защищённом режиме, — это обратная совместимость. В частности, процесс загрузки зависит от услуг, предоставляемых BIOS, а сам BIOS работает в устаревшем 16-битном коде.

Значение `0xfffffff0` немного меньше 4 ГБ, поэтому, если в машине нет 4 ГБ физической памяти, оно не может указывать на действительный адрес памяти. Аппаратное обеспечение компьютера преобразует этот адрес так, чтобы он указывал на блок памяти BIOS.

BIOS (Basic Input Output System) — это микросхема на материнской плате, которая содержит относительно небольшой объём памяти только для чтения (ROM). Эта память включает различные низкоуровневые процедуры, специфичные для оборудования, поставляемого с материнской платой. Процессор сначала переходит по адресу 0xfffffff0, который фактически находится в памяти BIOS. Обычно по этому адресу содержится инструкция перехода к процедурам POST BIOS.

POST (Power On Self Test) — это набор процедур, включающих проверку памяти, проверку системной шины и другую низкоуровневую инициализацию, чтобы процессор мог правильно настроить компьютер. Важным этапом на этой стадии является определение загрузочного устройства. Современные реализации BIOS позволяют выбирать загрузочное устройство, обеспечивая загрузку с дискеты, CD-ROM, жёсткого диска или других устройств.

Самым последним действием в POST является инструкция `INT 0x19`. Обработчик `INT 0x19` считывает 512 байт из первого сектора загрузочного устройства в память по адресу `0x7c00`. Термин первый сектор происходит из архитектуры жёстких дисков, где магнитная пластина разделена на множество цилиндрических дорожек. Дорожки нумеруются, и каждая дорожка разделена на несколько (обычно 64) секторов. Нумерация дорожек начинается с 0, но нумерация секторов начинается с 1. Дорожка 0 находится на внешней стороне магнитной пластины, а сектор 1, первый сектор, имеет особое назначение. Он также называется MBR (Master Boot Record) или Главная Загрузочная Запись. Остальные секторы на первой дорожке не используются.

Этот сектор является нашей точкой входа в последовательность загрузки. Как мы увидим, этот сектор содержит копию нашей программы boot0. BIOS выполняет переход по адресу `0x7c00`, и она начинает выполняться.

## 1.4. Главная загрузочная запись (`boot0`)

После получения управления от BIOS по адресу памяти `0x7c00` начинает выполняться boot0. Это первый код, который управляется FreeBSD. Задача boot0 довольно проста: просканировать таблицу разделов и позволить пользователю выбрать, с какого раздела загружаться. Таблица разделов — это специальная стандартная структура данных, встроенная в MBR (а значит, и в boot0), которая описывает четыре стандартных PC-раздела. boot0 находится в файловой системе как /boot/boot0. Это небольшой файл размером 512 байт, и именно его процедура установки FreeBSD записывает в MBR жёсткого диска, если во время установки была выбрана опция "bootmanager". Действительно, boot0 и есть MBR.

Как упоминалось ранее, мы вызываем прерывание BIOS `INT 0x19` для загрузки MBR (boot0) в память по адресу `0x7c00`. Исходный файл для boot0 можно найти в stand/i386/boot0/boot0.S — это впечатляющий фрагмент кода, написанный Робертом Нордье.

Особая структура, начинающаяся со смещения `0x1be` в MBR, называется таблицей разделов. Она содержит четыре записи по 16 байт каждая, называемые записями разделов, которые определяют, как разделён жёсткий диск, или, в терминологии FreeBSD, нарезан. Один из этих 16 байт указывает, является ли раздел (срез) загрузочным или нет. Ровно одна запись должна быть с этом установленным флагом, иначе код boot0 откажется продолжать работу.

Запись о разделе содержит следующие поля:

- 1-байтовый тип файловой системы

- 1-байтовый флаг загрузки (`bootable`)

- 6-байтовый дескриптор в формате CHS

- 8-байтовый дескриптор в формате LBA

Дескриптор записи раздела содержит информацию о том, где именно раздел расположен на диске. Оба дескриптора, LBA и CHS, описывают одну и ту же информацию, но разными способами: LBA (Logical Block Addressing) содержит начальный сектор раздела и его длину, тогда как CHS (Cylinder Head Sector) содержит координаты первого и последнего секторов раздела. Таблица разделов завершается специальной сигнатурой `0xaa55`.

MBR должен помещаться в 512 байт, один сектор диска. Эта программа использует низкоуровневые «трюки», такие как использование побочных эффектов определённых инструкций и повторное использование значений регистров из предыдущих операций, чтобы максимально эффективно использовать минимально возможное количество инструкций. Также необходимо соблюдать осторожность при работе с таблицей разделов, которая встроена в сам MBR. По этим причинам будьте очень внимательны при изменении boot0.S.

Обратите внимание, что исходный файл boot0.S ассемблируется "как есть": инструкции переводятся одна за одной в бинарный код без дополнительной информации (например, без формата файла ELF). Такой низкоуровневый контроль достигается на этапе компоновки с помощью специальных флагов, передаваемых компоновщику. Например, текстовая секция программы располагается по адресу `0x600`. На практике это означает, что boot0 должен быть загружен в память по адресу `0x600` для корректной работы.

Стоит взглянуть на Makefile для boot0 (stand/i386/boot0/Makefile), так как он определяет некоторые аспекты поведения boot0 во время выполнения. Например, если для ввода-вывода используется терминал, подключённый к последовательному порту (COM1), необходимо определить макрос `SIO` (`-DSIO`). `-DPXE` включает загрузку через PXE при нажатии `F6`. Кроме того, программа определяет набор флагов, которые позволяют дополнительно настроить её поведение. Всё это проиллюстрировано в Makefile. Например, обратите внимание на директивы компоновщика, которые предписывают ему начинать секцию текста с адреса `0x600` и создавать выходной файл "как есть" (удаляя любое форматирование файла):

          BOOT_BOOT0_ORG?=0x600
          ORG=${BOOT_BOOT0_ORG}

stand/i386/boot0/Makefile

Приступим к изучению MBR, или boot0, начиная с точки входа.

| В некоторые инструкции были внесены изменения для лучшего изложения. Например, некоторые макросы раскрыты, а некоторые проверки макросов опущены, когда результат проверки известен. Это относится ко всем приведённым примерам кода.

    start:
          cld			# String ops inc
          xorw %ax,%ax		# Zero
          movw %ax,%es		# Address
          movw %ax,%ds		#  data
          movw %ax,%ss		# Set up
          movw $LOAD,%sp		#  stack

stand/i386/boot0/boot0.S

Этот первый блок кода является точкой входа программы. Именно сюда BIOS передаёт управление. Сначала он гарантирует, что строковые операции автоматически увеличивают указатели операндов (инструкция `cld`) [2]. Затем, не делая предположений о состоянии сегментных регистров, он их инициализирует. Наконец, он устанавливает регистр указателя стека (`%sp`) в ($LOAD = адрес `0x7c00`), чтобы обеспечить работоспособный стек.

Следующий блок отвечает за перемещение и последующий переход к перемещенному коду.

          movw %sp,%si     # Source
          movw $start,%di		# Destination
          movw $0x100,%cx		# Word count
          rep			# Relocate
          movsw			#  code
          movw %di,%bp		# Address variables
          movb $0x8,%cl		# Words to clear
          rep			# Zero
          stosw			#  them
          incb -0xe(%di)		# Set the S field to 1
          jmp main-LOAD+ORIGIN	# Jump to relocated code

stand/i386/boot0/boot0.S

Так как boot0 загружается BIOS по адресу `0x7C00`, он копирует себя по адресу `0x600` и передаёт управление туда (напомним, что он был слинкован для выполнения по адресу `0x600`). Исходный адрес, `0x7c00`, копируется в регистр `%si`. Конечный адрес, `0x600`, — в регистр `%di`. Количество слов для копирования, `256` (размер программы = 512 байт), копируется в регистр `%cx`. Далее инструкция `rep` повторяет следующую за ней инструкцию, то есть `movsw`, количество раз, указанное в регистре `%cx`. Инструкция `movsw` копирует слово, на которое указывает `%si`, по адресу, на который указывает `%di`. Это повторяется ещё 255 раз. При каждом повторении оба регистра, исходный и конечный, `%si` и `%di`, увеличиваются на единицу. Таким образом, по завершении копирования 256 слов (512 байт), `%di` имеет значение `0x600``512`= `0x800`, а `%si` — значение `0x7c00``512`= `0x7e00`; таким образом, мы завершили перемещение кода. С момента последнего обновления этого документа инструкции копирования в коде изменились, поэтому вместо movsb и stosb были введены movsw и stosw, которые копируют 2 байта (1 слово) за одну итерацию.

Затем регистр назначения `%di` копируется в `%bp`. `%bp` получает значение `0x800`. Значение `8` копируется в `%cl` для подготовки новой строковой операции (как в предыдущей `movsw`). Теперь `stosw` выполняется 8 раз. Эта инструкция копирует значение `0` по адресу, на который указывает регистр назначения (`%di`, то есть `0x800`), и увеличивает его. Это повторяется ещё 7 раз, так что `%di` в итоге получает значение `0x810`. Фактически это очищает диапазон адресов `0x800`-`0x80f`. Этот диапазон используется как (фиктивная) таблица разделов для записи MBR обратно на диск. Наконец, полю сектора для CHS-адресации этого фиктивного раздела присваивается значение 1, и выполняется переход к основной функции из перемещённого кода. Обратите внимание, что до этого перехода к перемещённому коду любые ссылки на абсолютные адреса избегались.

Следующий блок кода проверяет, следует ли использовать номер диска, предоставленный BIOS, или тот, что хранится в boot0.

    main:
          testb $SETDRV,_FLAGS(%bp)	# Set drive number?
    #ifndef CHECK_DRIVE	/* disable drive checks */
          jz save_curdrive		# no, use the default
    #else
          jnz disable_update	# Yes
          testb %dl,%dl		# Drive number valid?
          js save_curdrive		# Possibly (0x80 set)
    #endif

stand/i386/boot0/boot0.S

Этот код проверяет бит `SETDRV` (`0x20`) в переменной flags. Напомним, что регистр `%bp` указывает на адрес `0x800`, поэтому проверка выполняется для переменной flags по адресу `0x800`-`69`= `0x7bb`. Это пример типа изменений, которые можно внести в boot0. Флаг `SETDRV` не установлен по умолчанию, но его можно задать в Makefile. Если он установлен, используется номер диска, сохранённый в MBR, вместо предоставленного BIOS. Мы предполагаем значения по умолчанию и то, что BIOS предоставил корректный номер диска, поэтому переходим к `save_curdrive`.

Следующий блок сохраняет номер диска, предоставленный BIOS, и вызывает `putn` для вывода новой строки на экран.

    save_curdrive:
          movb %dl, (%bp)		# Save drive number
          pushw %dx			# Also in the stack
    #ifdef	TEST	/* test code, print internal bios drive */
          rolb $1, %dl
          movw $drive, %si
          call putkey
    #endif
          callw putn		# Print a newline

stand/i386/boot0/boot0.S

Обратите внимание, что мы предполагаем, что `TEST` не определён, поэтому условный код в нём не собирается и не появится в нашем исполняемом файле boot0.

Следующий блок реализует фактическое сканирование таблицы разделов. Он выводит на экран тип раздела для каждой из четырёх записей в таблице разделов. Каждый тип сравнивается со списком известных файловых систем операционных систем. Примерами распознаваемых типов разделов являются NTFS (Windows®, ID 0x7), `ext2fs` (Linux®, ID 0x83) и, конечно же, `ffs`/`ufs2` (FreeBSD, ID 0xa5). Реализация довольно проста.

          movw $(partbl+0x4),%bx	# Partition table (+4)
          xorw %dx,%dx		# Item number

    read_entry:
          movb %ch,-0x4(%bx)	# Zero active flag (ch == 0)
          btw %dx,_FLAGS(%bp)	# Entry enabled?
          jnc next_entry		# No
          movb (%bx),%al		# Load type
          test %al, %al		# skip empty partition
          jz next_entry
          movw $bootable_ids,%di	# Lookup tables
          movb $(TLEN+1),%cl	# Number of entries
          repne			# Locate
          scasb			#  type
          addw $(TLEN-1), %di	# Adjust
          movb (%di),%cl		# Partition
          addw %cx,%di		#  description
          callw putx		# Display it

    next_entry:
          incw %dx			# Next item
          addb $0x10,%bl		# Next entry
          jnc read_entry		# Till done

stand/i386/boot0/boot0.S

Важно отметить, что флаг активности для каждой записи сбрасывается, поэтому после сканирования ни одна запись о разделе не активна в нашей копии boot0 в памяти. Позже флаг активности будет установлен для выбранного раздела. Это гарантирует, что только один активный раздел существует, если пользователь решит записать изменения обратно на диск.

Следующий блок проверяет наличие других дисков. При запуске BIOS записывает количество дисков, присутствующих в компьютере, по адресу `0x475`. Если есть другие диски, boot0 выводит текущий диск на экран. Пользователь может позже дать команду boot0 просканировать разделы на другом диске.

          popw %ax			# Drive number
          subb $0x80-0x1,%al		# Does next
          cmpb NHRDRV,%al		#  drive exist? (from BIOS?)
          jb print_drive		# Yes
          decw %ax			# Already drive 0?
          jz print_prompt		# Yes

stand/i386/boot0/boot0.S

Мы предполагаем, что присутствует только один диск, поэтому переход к `print_drive` не выполняется. Также мы предполагаем, что ничего необычного не произошло, поэтому переходим к `print_prompt`.

Следующий блок просто выводит приглашение с последующим вариантом по умолчанию:

    print_prompt:
          movw $prompt,%si		# Display
          callw putstr		#  prompt
          movb _OPT(%bp),%dl	# Display
          decw %si			#  default
          callw putkey		#  key
          jmp start_input		# Skip beep

stand/i386/boot0/boot0.S

Наконец, выполняется переход к `start_input`, где используются сервисы BIOS для запуска таймера и чтения пользовательского ввода с клавиатуры; если таймер истекает, будет выбран вариант по умолчанию:

    start_input:
          xorb %ah,%ah		# BIOS: Get
          int $0x1a			#  system time
          movw %dx,%di		# Ticks when
          addw _TICKS(%bp),%di	#  timeout
    read_key:
          movb $0x1,%ah		# BIOS: Check
          int $0x16			#  for keypress
          jnz got_key		# Have input
          xorb %ah,%ah		# BIOS: int 0x1a, 00
          int $0x1a			#  get system time
          cmpw %di,%dx		# Timeout?
          jb read_key		# No

stand/i386/boot0/boot0.S

Прерывание запрашивается с номером `0x1a` и аргументом `0` в регистре `%ah`. BIOS имеет предопределённый набор сервисов, запрашиваемых приложениями как программно-генерируемые прерывания через инструкцию `int`, с получением аргументов в регистрах (в данном случае, `%ah`). Здесь, в частности, запрашивается количество тиков часов с момента последней полуночи; это значение вычисляется BIOS через RTC (Real Time Clock). Эти часы могут быть настроены на работу с частотой от 2 Гц до 8192 Гц. BIOS устанавливает их на 18,2 Гц при запуске. Когда запрос выполнен, 32-битный результат возвращается BIOS в регистрах `%cx` и `%dx` (младшие байты в `%dx`). Этот результат (часть `%dx`) копируется в регистр `%di`, и к `%di` добавляется значение переменной `TICKS`. Эта переменная находится в boot0 по смещению `_TICKS` (отрицательное значение) от регистра `%bp` (который, напомним, указывает на `0x800`). Значение этой переменной по умолчанию — `0xb6` (182 в десятичной системе). Идея заключается в том, что boot0 постоянно запрашивает время у BIOS, и когда значение, возвращённое в регистре `%dx`, становится больше значения, хранящегося в `%di`, время истекает и будет сделан выбор по умолчанию. Поскольку RTC тикает 18,2 раза в секунду, это условие выполнится через 10 секунд (это поведение по умолчанию можно изменить в Makefile). До истечения этого времени boot0 непрерывно опрашивает BIOS на предмет ввода пользователя; это делается через `int 0x16`, аргумент `1` в `%ah`.

Была нажата клавиша или истекло время, последующий код проверяет выбор. В зависимости от выбора, регистр `%si` устанавливается так, чтобы указывать на соответствующую запись раздела в таблице разделов. Этот новый выбор переопределяет предыдущий выбор по умолчанию. Действительно, он становится новым значением по умолчанию. Наконец, устанавливается флаг ACTIVE выбранного раздела. Если это было разрешено при компиляции, версия boot0 в памяти с этими изменёнными значениями записывается обратно в MBR на диске. Мы оставляем детали этой реализации читателю.

Мы завершаем наше изучение последним блоком кода из программы boot0:

          movw $LOAD,%bx		# Address for read
          movb $0x2,%ah		# Read sector
          callw intx13		#  from disk
          jc beep			# If error
          cmpw $MAGIC,0x1fe(%bx)	# Bootable?
          jne beep			# No
          pushw %si			# Save ptr to selected part.
          callw putn		# Leave some space
          popw %si			# Restore, next stage uses it
          jmp *%bx			# Invoke bootstrap

stand/i386/boot0/boot0.S

Вспомним, что `%si` указывает на выбранную запись раздела. Эта запись сообщает нам, где начинается раздел на диске. Мы предполагаем, конечно, что выбранный раздел действительно является срезом FreeBSD.

| Отныне мы будем отдавать предпочтение использованию технически более точного термина "слайс" вместо "раздел".

Буфер передачи установлен в `0x7c00` (регистр `%bx`), и запрос на чтение первого сектора слайса FreeBSD выполняется вызовом `intx13`. Мы предполагаем, что всё прошло успешно, поэтому переход к `beep` не выполняется. В частности, новый прочитанный сектор должен заканчиваться магической последовательностью `0xaa55`. Наконец, значение в `%si` (указатель на выбранную таблицу разделов) сохраняется для использования на следующем этапе, и выполняется переход по адресу `0x7c00`, где начинается выполнение нашего следующего этапа (только что прочитанного блока).

## 1.5. Этап `boot1`

До сих пор мы прошли следующую последовательность:

- BIOS выполнил первоначальную инициализацию оборудования, включая POST. MBR (boot0) был загружен по адресу `0x7c00` из абсолютного сектора один с диска. Управление выполнением было передано по этому адресу.

- boot0 переместил себя по адресу, по которому он был скомпонован для выполнения (`0x600`), после чего выполнил переход для продолжения выполнения в соответствующем месте. В завершение, boot0 загрузил первый сектор диска из раздела FreeBSD по адресу `0x7c00`. Управление выполнением было передано по этому адресу.

boot1 — это следующий шаг в последовательности загрузки. Это первая из трёх стадий загрузки. Обратите внимание, что до сих пор мы работали исключительно с секторами диска. Действительно, BIOS загружает самый первый сектор, а boot0 загружает первый сектор раздела FreeBSD. Обе загрузки происходят по адресу `0x7c00`. Мы можем концептуально представлять эти секторы диска как содержащие файлы boot0 и boot1, соответственно, но на самом деле это не совсем верно для boot1. Строго говоря, в отличие от boot0, boot1 не является частью загрузочных блоков [3]. Вместо этого, единый полноценный файл boot (/boot/boot) в итоге записывается на диск. Этот файл представляет собой комбинацию boot1, boot2 и `Boot Extender` (или BTX). Этот единый файл превышает размер одного сектора (больше 512 байт). К счастью, boot1 занимает ровно первые 512 байт этого файла, поэтому, когда boot0 загружает первый сектор раздела FreeBSD (512 байт), он фактически загружает boot1 и передаёт ему управление.

Основная задача boot1 — загрузить следующий этап загрузки. Этот следующий этап несколько сложнее. Он состоит из сервера под названием "Boot Extender" (BTX) и клиента под названием boot2. Как мы увидим, последний этап загрузки, loader, также является клиентом сервера BTX.

Давайте теперь подробно рассмотрим, что именно делает boot1, начиная, как мы это делали для boot0, с точки входа:

    start:
    	jmp main

stand/i386/boot2/boot1.S

Точка входа `start` просто переходит через специальную область данных к метке `main`, которая, в свою очередь, выглядит следующим образом:

    main:
          cld			# String ops inc
          xor %cx,%cx		# Zero
          mov %cx,%es		# Address
          mov %cx,%ds		#  data
          mov %cx,%ss		# Set up
          mov $start,%sp		#  stack
          mov %sp,%si		# Source
          mov $MEM_REL,%di		# Destination
          incb %ch			# Word count
          rep			# Copy
          movsw			#  code

stand/i386/boot2/boot1.S

Как и boot0, этот код перемещает boot1, на этот раз по адресу `0x700`. Однако, в отличие от boot0, он не переходит туда. boot1 скомпонован для выполнения по адресу `0x7c00`, фактически там, куда он был изначально загружен. Причина этого перемещения будет рассмотрена далее.

Далее идёт цикл, который ищет слайс FreeBSD. Хотя boot0 загрузил boot1 из слайса FreeBSD, ему не была передана информация об этом [4], поэтому boot1 должен повторно просканировать таблицу разделов, чтобы найти начало слайса FreeBSD. Для этого он перечитывает MBR:

          mov $part4,%si		# Partition
          cmpb $0x80,%dl		# Hard drive?
          jb main.4			# No
          movb $0x1,%dh		# Block count
          callw nread		# Read MBR

stand/i386/boot2/boot1.S

В приведённом выше коде регистр `%dl` содержит информацию о загрузочном устройстве. Эти данные передаются BIOS и сохраняются MBR. Числа `0x80` и выше указывают на то, что мы имеем дело с жёстким диском, поэтому вызывается `nread`, где считывается MBR. Аргументы для `nread` передаются через `%si` и `%dh`. Адрес памяти по метке `part4` копируется в `%si`. Этот адрес памяти содержит "фальшивый раздел", который будет использован `nread`. Ниже приведены данные фальшивого раздела:

          part4:
    	.byte 0x80, 0x00, 0x01, 0x00
    	.byte 0xa5, 0xfe, 0xff, 0xff
    	.byte 0x00, 0x00, 0x00, 0x00
    	.byte 0x50, 0xc3, 0x00, 0x00

stand/i386/boot2/boot1.S

В частности, LBA для этой фиктивной раздела жёстко закодирован как ноль. Это используется как аргумент для BIOS при чтении абсолютного сектора один с жёсткого диска. Или же может использоваться адресация CHS. В этом случае фиктивный раздел содержит цилиндр 0, головку 0 и сектор 1, что эквивалентно абсолютному сектору один.

Продолжим, рассмотрев `nread`:

    nread:
          mov $MEM_BUF,%bx		# Transfer buffer
          mov 0x8(%si),%ax		# Get
          mov 0xa(%si),%cx		#  LBA
          push %cs			# Read from
          callw xread.1		#  disk
          jnc return		# If success, return

stand/i386/boot2/boot1.S

Напомним, что `%si` указывает на поддельный раздел. Слово [5] по смещению `0x8` копируется в регистр `%ax`, а слово по смещению `0xa` — в `%cx`. BIOS интерпретирует их как младшее 4-байтовое значение, обозначающее LBA для чтения (старшие четыре байта предполагаются нулевыми). Регистр `%bx` содержит адрес памяти, куда будет загружен MBR. Инструкция, помещающая `%cs` в стек, очень интересна. В данном контексте она ничего не делает. Однако, как мы скоро увидим, boot2 в сочетании с сервером BTX также использует `xread.1`. Этот механизм будет рассмотрен в следующем разделе.

Код в `xread.1` далее вызывает функцию `read`, которая фактически обращается к BIOS с запросом на чтение сектора диска:

    xread.1:
    	pushl $0x0		#  absolute
    	push %cx		#  block
    	push %ax		#  number
    	push %es		# Address of
    	push %bx		#  transfer buffer
    	xor %ax,%ax		# Number of
    	movb %dh,%al		#  blocks to
    	push %ax		#  transfer
    	push $0x10		# Size of packet
    	mov %sp,%bp		# Packet pointer
    	callw read		# Read from disk
    	lea 0x10(%bp),%sp	# Clear stack
    	lret			# To far caller

stand/i386/boot2/boot1.S

Обратите внимание на длинную инструкцию возврата в конце этого блока. Эта инструкция извлекает регистр `%cs`, помещённый в стек `nread`, и возвращает управление. В конце `nread` также возвращает управление.

С загрузкой MBR в память начинается фактический цикл поиска слайса FreeBSD:

    	mov $0x1,%cx		 # Two passes
    main.1:
    	mov $MEM_BUF+PRT_OFF,%si # Partition table
    	movb $0x1,%dh		 # Partition
    main.2:
    	cmpb $PRT_BSD,0x4(%si)	 # Our partition type?
    	jne main.3		 # No
    	jcxz main.5		 # If second pass
    	testb $0x80,(%si)	 # Active?
    	jnz main.5		 # Yes
    main.3:
    	add $0x10,%si		 # Next entry
    	incb %dh		 # Partition
    	cmpb $0x1+PRT_NUM,%dh		 # In table?
    	jb main.2		 # Yes
    	dec %cx			 # Do two
    	jcxz main.1		 #  passes

stand/i386/boot2/boot1.S

Если обнаружен слайс FreeBSD, выполнение продолжается на метке `main.5`. Обратите внимание, что при обнаружении слайса FreeBSD `%si` указывает на соответствующую запись в таблице разделов, а `%dh` содержит номер раздела. Мы предполагаем, что слайс FreeBSD найден, поэтому продолжаем выполнение на метке `main.5`:

    main.5:
    	mov %dx,MEM_ARG			   # Save args
    	movb $NSECT,%dh			   # Sector count
    	callw nread			   # Read disk
    	mov $MEM_BTX,%bx			   # BTX
    	mov 0xa(%bx),%si		   # Get BTX length and set
    	add %bx,%si			   #  %si to start of boot2.bin
    	mov $MEM_USR+SIZ_PAG*2,%di			   # Client page 2
    	mov $MEM_BTX+(NSECT-1)*SIZ_SEC,%cx			   # Byte
    	sub %si,%cx			   #  count
    	rep				   # Relocate
    	movsb				   #  client

stand/i386/boot2/boot1.S

Напомним, что в данный момент регистр `%si` указывает на запись среза FreeBSD в таблице разделов MBR, поэтому вызов `nread` фактически прочитает секторы в начале этого раздела. Аргумент, переданный в регистре `%dh`, указывает `nread` прочитать 16 секторов диска. Напомним, что первые 512 байт, или первый сектор слайса FreeBSD, совпадает с программой boot1. Также напомним, что файл, записанный в начало слайса FreeBSD, это не /boot/boot1, а /boot/boot. Давайте посмотрим на размер этих файлов в файловой системе:

    -r--r--r--  1 root  wheel   512B Jan  8 00:15 /boot/boot0
    -r--r--r--  1 root  wheel   512B Jan  8 00:15 /boot/boot1
    -r--r--r--  1 root  wheel   7.5K Jan  8 00:15 /boot/boot2
    -r--r--r--  1 root  wheel   8.0K Jan  8 00:15 /boot/boot

Оба файла boot0 и boot1 имеют размер 512 байт каждый, поэтому они занимают ровно один сектор диска. boot2 значительно больше, так как содержит как сервер BTX, так и клиент boot2. Наконец, файл под названием просто boot на 512 байт больше, чем boot2. Этот файл представляет собой объединение boot1 и boot2. Как уже отмечалось, boot0 записывается в самый первый сектор диска (MBR), а boot записывается в первый сектор раздела FreeBSD; boot1 и boot2 не записываются на диск. Команда, используемая для объединения boot1 и boot2 в единый файл boot, выглядит просто как `cat boot1 boot2 > boot`.

Итак, boot1 занимает ровно первые 512 байт boot, и, поскольку boot записывается в первый сектор слайса FreeBSD, boot1 полностью помещается в этот первый сектор. Когда `nread` читает первые 16 секторов слайса FreeBSD, он фактически читает весь файл boot [6]. Более подробно о том, как boot формируется из boot1 и boot2, мы увидим в следующем разделе.

Напомним, что `nread` использует адрес памяти `0x8c00` в качестве буфера передачи для хранения прочитанных секторов. Этот адрес выбран не случайно. Действительно, поскольку boot1 принадлежит первым 512 байтам, он оказывается в диапазоне адресов `0x8c00`-`0x8dff`. Следующие 512 байт (диапазон `0x8e00`-`0x8fff`) используются для хранения bsdlabel [7].

Начиная с адреса `0x9000` находится начало сервера BTX, и сразу за ним следует клиент boot2. Сервер BTX действует как ядро и выполняется в защищённом режиме с наивысшим уровнем привилегий. В отличие от этого, клиенты BTX (например, boot2) выполняются в пользовательском режиме. Мы увидим, как это реализовано, в следующем разделе. Код после вызова `nread` находит начало boot2 в буфере памяти и копирует его по адресу `0xc000`. Это связано с тем, что сервер BTX размещает boot2 для выполнения в сегменте, начинающемся с `0xa000`. Мы подробно рассмотрим это в следующем разделе.

Последний блок кода в boot1 разрешает доступ к памяти выше 1MB [8] и завершается переходом к начальной точке сервера BTX:

    seta20:
    	cli			# Disable interrupts
    seta20.1:
    	dec %cx			# Timeout?
    	jz seta20.3		# Yes

    	inb $0x64,%al		# Get status
    	testb $0x2,%al		# Busy?
    	jnz seta20.1		# Yes
    	movb $0xd1,%al		# Command: Write
    	outb %al,$0x64		#  output port
    seta20.2:
    	inb $0x64,%al		# Get status
    	testb $0x2,%al		# Busy?
    	jnz seta20.2		# Yes
    	movb $0xdf,%al		# Enable
    	outb %al,$0x60		#  A20
    seta20.3:
    	sti			# Enable interrupts
    	jmp 0x9010		# Start BTX

stand/i386/boot2/boot1.S

Обратите внимание, что непосредственно перед переходом прерывания включаются.

## 1.6. Сервер BTX

Далее в нашей последовательности загрузки идёт сервер BTX. Давайте быстро вспомним, как мы сюда попали:

- BIOS загружает абсолютный сектор один (MBR или boot0) по адресу `0x7c00` и переходит туда.

- boot0 перемещает себя по адресу `0x600`, по которому он был слинкован для выполнения, и переходит туда. Затем он читает первый сектор среза FreeBSD (который содержит boot1) в адрес `0x7c00` и переходит туда.

- boot1 загружает первые 16 секторов среза FreeBSD по адресу `0x8c00`. Эти 16 секторов, или 8192 байта, представляют собой весь файл boot. Файл является объединением boot1 и boot2. boot2, в свою очередь, содержит сервер BTX и клиент boot2. Наконец, выполняется переход по адресу `0x9010`, точке входа сервера BTX.

Прежде чем изучать сервер BTX подробно, давайте рассмотрим, как создаётся единый, всеобъемлющий файл boot. Способ сборки boot определен в его Makefile (stand/i386/boot2/Makefile). Рассмотрим правило, которое создаёт файл boot:

          boot: boot1 boot2
    	cat boot1 boot2 > boot

stand/i386/boot2/Makefile

Это говорит нам, что boot1 и boot2 необходимы, и правило просто объединяет их для создания одного файла с именем boot. Правила для создания boot1 также довольно просты:

          boot1: boot1.out
    	${OBJCOPY} -S -O binary boot1.out ${.TARGET}

          boot1.out: boot1.o
    	${LD} ${LD_FLAGS} -e start --defsym ORG=${ORG1} -T ${LDSCRIPT} -o ${.TARGET} boot1.o

stand/i386/boot2/Makefile

Для применения правила создания boot1 необходимо собрать boot1.out. Это, в свою очередь, зависит от наличия boot1.o. Последний файл является результатом ассемблирования нашего знакомого boot1.S без компоновки. Теперь применяется правило создания boot1.out. Оно указывает, что boot1.o должен быть скомпонован с точкой входа `start` и начальным адресом `0x7c00`. Наконец, boot1 создаётся из boot1.out применением соответствующего правила. Это команда objcopy, применяемая к boot1.out. Обратите внимание на флаги, передаваемые objcopy: `-S` указывает на удаление всей информации о перемещении и символов; `-O binary` указывает формат вывода, то есть простой, неформатированный двоичный файл.

Имея boot1, давайте посмотрим, как устроен boot2:

          boot2: boot2.ld
    	@set -- `ls -l ${.ALLSRC}`; x=$$((${BOOT2SIZE}-$$5)); \
    	    echo "$$x bytes available"; test $$x -ge 0
    	${DD} if=${.ALLSRC} of=${.TARGET} bs=${BOOT2SIZE} conv=sync

          boot2.ld: boot2.ldr boot2.bin ${BTXKERN}
    	btxld -v -E ${ORG2} -f bin -b ${BTXKERN} -l boot2.ldr \
    	    -o ${.TARGET} -P 1 boot2.bin

          boot2.ldr:
    	${DD} if=/dev/zero of=${.TARGET} bs=512 count=1

          boot2.bin: boot2.out
    	${OBJCOPY} -S -O binary boot2.out ${.TARGET}

          boot2.out: ${BTXCRT} boot2.o sio.o ashldi3.o
    	${LD} ${LD_FLAGS} --defsym ORG=${ORG2} -T ${LDSCRIPT} -o ${.TARGET} ${.ALLSRC}

          boot2.h: boot1.out
    	${NM} -t d ${.ALLSRC} | awk '/([0-9])+ T xread/ \
    	    { x = $$1 - ORG1; \
    	    printf("#define XREADORG %#x\n", REL1 + x) }' \
    	    ORG1=`printf "%d" ${ORG1}` \
    	    REL1=`printf "%d" ${REL1}` > ${.TARGET}

stand/i386/boot2/Makefile

Механизм сборки boot2 гораздо сложнее. Отметим наиболее важные моменты. Список зависимостей выглядит следующим образом:

          boot2: boot2.ld
          boot2.ld: boot2.ldr boot2.bin ${BTXDIR}
          boot2.bin: boot2.out
          boot2.out: ${BTXDIR} boot2.o sio.o ashldi3.o
          boot2.h: boot1.out

stand/i386/boot2/Makefile

Отметим, что изначально файл заголовка boot2.h отсутствует, но его создание зависит от boot1.out, который у нас уже есть. Правило его создания немного лаконично, но важно то, что результат, boot2.h, выглядит примерно так:

    #define XREADORG 0x725

stand/i386/boot2/boot2.h

Напомним, что boot1 был перемещён (т.е. скопирован из `0x7c00` в `0x700`). Это перемещение теперь обретает смысл, потому что, как мы увидим, сервер BTX освобождает часть памяти, включая область, куда boot1 был изначально загружен. Однако серверу BTX необходим доступ к функции `xread` из boot1; согласно выводу boot2.h, эта функция находится по адресу `0x725`. Действительно, сервер BTX использует функцию `xread` из перемещённого кода boot1. Теперь эта функция доступна из клиента boot2.

Следующее правило указывает компоновщику на необходимость связать различные файлы (ashldi3.o, boot2.o и sio.o). Обратите внимание, что выходной файл boot2.out компонуется для выполнения по адресу `0x2000` (${ORG2}). Напомним, что boot2 будет выполняться в пользовательском режиме внутри специального пользовательского сегмента, созданного сервером BTX. Этот сегмент начинается с адреса `0xa000`. Также помните, что часть boot2 в boot была скопирована по адресу `0xc000`, то есть со смещением `0x2000` от начала пользовательского сегмента, поэтому boot2 будет работать корректно при передаче управления на него. Далее, boot2.bin создаётся из boot2.out путем удаления символов и информации о формате; boot2.bin представляет собой сырой бинарный файл. Теперь обратите внимание, что файл boot2.ldr создаётся как 512-байтный файл, заполненный нулями. Это пространство зарезервировано для bsdlabel.

Теперь, когда у нас есть файлы boot1, boot2.bin и boot2.ldr, осталось только добавить сервер BTX перед созданием универсального файла boot. Сервер BTX находится в stand/i386/btx/btx; у него есть собственный Makefile со своим набором правил для сборки. Важно отметить, что он также компилируется как сырой бинарный файл и линкуется для выполнения по адресу `0x9000`. Подробности можно найти в stand/i386/btx/btx/Makefile.

Имея файлы, составляющие программу boot, последним шагом является их объединение. Это выполняется специальной программой под названием btxld (исходный код расположен в /usr/src/usr.sbin/btxld). Некоторые аргументы этой программы включают имя выходного файла (boot), его точку входа (`0x2000`) и формат файла (бинарный). Различные файлы окончательно объединяются этой утилитой в файл boot, который состоит из boot1, boot2, `bsdlabel` и сервера BTX. Этот файл, занимающий ровно 16 секторов или 8192 байта, записывается в начало раздела FreeBSD во время установки. Теперь перейдем к изучению программы сервера BTX.

Сервер BTX подготавливает простое окружение и переключается из 16-битного реального режима в 32-битный защищённый режим, непосредственно перед передачей управления клиенту. Это включает инициализацию и обновление следующих структур данных:

- Изменяет `Таблицу Векторов Прерываний (IVT)`. IVT предоставляет обработчики исключений и прерываний для кода в Реальном Режиме.

- Создаётся `Таблица дескрипторов прерываний (IDT)`. В ней предусмотрены записи для исключений процессора, аппаратных прерываний, двух системных вызовов и интерфейса V86. IDT предоставляет обработчики исключений и прерываний для кода в защищенном режиме.

- Создаётся `Сегмент состояния задачи (TSS)`. Это необходимо, потому что процессор работает на наименее привилегированном уровне при выполнении клиента (boot2), но на наиболее привилегированном уровне при выполнении сервера BTX.

- Устанавливается GDT (Глобальная Таблица Дескрипторов). Создаются записи (дескрипторы) для кода и данных супервизора, кода и данных пользователя, а также кода и данных реального режима. [9]

Приступим к изучению фактической реализации. Напомним, что boot1 выполнил переход на адрес `0x9010` — точку входа сервера BTX. Прежде чем изучать выполнение программы там, обратите внимание, что сервер BTX имеет специальный заголовок в диапазоне адресов `0x9000-0x900f`, непосредственно перед точкой входа. Этот заголовок определён следующим образом:

    start:						# Start of code
    /*
     * BTX header.
     */
    btx_hdr:	.byte 0xeb			# Machine ID
    		.byte 0xe			# Header size
    		.ascii "BTX"			# Magic
    		.byte 0x1			# Major version
    		.byte 0x2			# Minor version
    		.byte BTX_FLAGS			# Flags
    		.word PAG_CNT-MEM_ORG>>0xc	# Paging control
    		.word break-start		# Text size
    		.long 0x0			# Entry address

stand/i386/btx/btx/btx.S

Обратите внимание, что первые два байта — это `0xeb` и `0xe`. В архитектуре IA-32 эти два байта интерпретируются как относительный переход за заголовок к точке входа, поэтому теоретически boot1 мог бы перейти сюда (адрес `0x9000`) вместо адреса `0x9010`. Обратите внимание, что последнее поле в заголовке BTX — это указатель на точку входа клиента (boot2)b2. Это поле исправляется во время компоновки.

Сразу после заголовка следует точка входа сервера BTX:

    /*
     * Initialization routine.
     */
    init:		cli				# Disable interrupts
    		xor %ax,%ax			# Zero/segment
    		mov %ax,%ss			# Set up
    		mov $MEM_ESP0,%sp		#  stack
    		mov %ax,%es			# Address
    		mov %ax,%ds			#  data
    		pushl $0x2			# Clear
    		popfl				#  flags

stand/i386/btx/btx/btx.S

Этот код отключает прерывания, устанавливает рабочий стек (начиная с адреса `0x1800`) и очищает флаги в регистре EFLAGS. Обратите внимание, что инструкция `popfl` извлекает двойное слово (4 байта) из стека и помещает его в регистр EFLAGS. Поскольку извлекаемое значение фактически равно `2`, регистр EFLAGS эффективно очищается (IA-32 требует, чтобы бит 2 регистра EFLAGS всегда был равен 1).

Следующий блок кода очищает (устанавливает в `0`) диапазон памяти `0x5e00-0x8fff`. В этом диапазоне будут созданы различные структуры данных:

    /*
     * Initialize memory.
     */
    		mov $MEM_IDT,%di		# Memory to initialize
    		mov $(MEM_ORG-MEM_IDT)/2,%cx	# Words to zero
    		rep				# Zero-fill
    		stosw				#  memory

stand/i386/btx/btx/btx.S

Напомним, что boot1 изначально загружался по адресу `0x7c00`, поэтому при такой инициализации памяти эта копия фактически исчезла. Однако также напомним, что boot1 был перемещён на адрес `0x700`, поэтому эта копия всё ещё находится в памяти, и сервер BTX будет её использовать.

Далее обновляется таблица векторов прерываний (IVT) в реальном режиме. IVT представляет собой массив пар сегмент/смещение для обработчиков исключений и прерываний. BIOS обычно сопоставляет аппаратные прерывания с векторами прерываний `0x8`–`0xf` и `0x70`–`0x77`, но, как будет показано, программируемый контроллер прерываний 8259A, микросхема, управляющая фактическим сопоставлением аппаратных прерываний с векторами прерываний, программируется для переназначения этих векторов прерываний с `0x8`–`0xf` на `0x20`–`0x27` и с `0x70`–`0x77` на `0x28`–`0x2f`. Таким образом, обработчики прерываний предоставляются для векторов прерываний `0x20`–`0x2f`. Причина, по которой обработчики, предоставляемые BIOS, не используются напрямую, заключается в том, что они работают в 16-битном реальном режиме, но не в 32-битном защищённом режиме. Вскоре будет выполнен переход в 32-битный защищённый режим. Однако сервер BTX настраивает механизм для эффективного использования обработчиков, предоставляемых BIOS:

    /*
     * Update real mode IDT for reflecting hardware interrupts.
     */
    		mov $intr20,%bx			# Address first handler
    		mov $0x10,%cx			# Number of handlers
    		mov $0x20*4,%di			# First real mode IDT entry
    init.0:		mov %bx,(%di)			# Store IP
    		inc %di				# Address next
    		inc %di				#  entry
    		stosw				# Store CS
    		add $4,%bx			# Next handler
    		loop init.0			# Next IRQ

stand/i386/btx/btx/btx.S

Следующий блок создаёт IDT (таблицу дескрипторов прерываний). IDT в защищенном режиме аналогична IVT в реальном режиме. То есть, IDT описывает различные обработчики исключений и прерываний, используемые, когда процессор работает в защищенном режиме. По сути, она также состоит из массива пар сегмент/смещение, хотя структура несколько сложнее, поскольку сегменты в защищенном режиме отличаются от реального режима, и применяются различные механизмы защиты:

    /*
     * Create IDT.
     */
    		mov $MEM_IDT,%di		# IDT's address
    		mov $idtctl,%si			# Control string
    init.1:		lodsb				# Get entry
    		cbw				#  count
    		xchg %ax,%cx			#  as word
    		jcxz init.4			# If done
    		lodsb				# Get segment
    		xchg %ax,%dx			#  P:DPL:type
    		lodsw				# Get control
    		xchg %ax,%bx			#  set
    		lodsw				# Get handler offset
    		mov $SEL_SCODE,%dh		# Segment selector
    init.2:		shr %bx				# Handle this int?
    		jnc init.3			# No
    		mov %ax,(%di)			# Set handler offset
    		mov %dh,0x2(%di)		#  and selector
    		mov %dl,0x5(%di)		# Set P:DPL:type
    		add $0x4,%ax			# Next handler
    init.3:		lea 0x8(%di),%di		# Next entry
    		loop init.2			# Till set done
    		jmp init.1			# Continue

stand/i386/btx/btx/btx.S

Каждая запись в `IDT` имеет длину 8 байт. Помимо информации о сегменте/смещении, они также описывают тип сегмента, уровень привилегий и присутствует ли сегмент в памяти. Структура организована так, что векторы прерываний от `0` до `0xf` (исключения) обрабатываются функцией `intx00`; вектор `0x10` (также исключение) обрабатывается `intx10`; аппаратные прерывания, которые позже настраиваются начиная с вектора `0x20` и до вектора `0x2f`, обрабатываются функцией `intx20`. Наконец, вектор прерывания `0x30`, используемый для системных вызовов, обрабатывается `intx30`, а векторы `0x31` и `0x32` обрабатываются `intx31`. Необходимо отметить, что только дескрипторы для векторов прерываний `0x30`, `0x31` и `0x32` имеют уровень привилегий 3, такой же, как у клиента boot2, что означает, что клиент может выполнить программно-генерируемое прерывание к этим векторам через инструкцию `int` без ошибки (это способ, которым boot2 использует сервисы, предоставляемые сервером BTX). Также обратите внимание, что только программно-генерируемые прерывания защищены от кода, выполняющегося на более низких уровнях привилегий. Аппаратно-генерируемые прерывания и исключения, генерируемые процессором, всегда обрабатываются корректно, независимо от фактических привилегий.

Следующий шаг — инициализация TSS (сегмента состояния задачи). TSS — это аппаратная функция, которая помогает операционной системе или исполнительному ПО реализовать многозадачность через абстракцию процессов. Архитектура IA-32 требует создания и использования как минимум одного TSS, если используются механизмы многозадачности или определены различные уровни привилегий. Поскольку клиент boot2 выполняется на уровне привилегий 3, а сервер BTX работает на уровне привилегий 0, необходимо определить TSS:

    /*
     * Initialize TSS.
     */
    init.4:		movb $_ESP0H,TSS_ESP0+1(%di)	# Set ESP0
    		movb $SEL_SDATA,TSS_SS0(%di)	# Set SS0
    		movb $_TSSIO,TSS_MAP(%di)	# Set I/O bit map base

stand/i386/btx/btx/btx.S

Обратите внимание, что в TSS указано значение для указателя стека и сегмента стека уровня привилегий 0. Это необходимо, потому что если прерывание или исключение получено во время выполнения boot2 на уровне привилегий 3, процессор автоматически переключается на уровень привилегий 0, поэтому требуется новый рабочий стек. Наконец, полю базового адреса карты ввода-вывода TSS присваивается значение, которое представляет собой 16-битное смещение от начала TSS до битовой карты разрешений ввода-вывода и битовой карты перенаправления прерываний.

После создания IDT и TSS процессор готов к переходу в защищённый режим. Это выполняется в следующем блоке:

    /*
     * Bring up the system.
     */
    		mov $0x2820,%bx			# Set protected mode
    		callw setpic			#  IRQ offsets
    		lidt idtdesc			# Set IDT
    		lgdt gdtdesc			# Set GDT
    		mov %cr0,%eax			# Switch to protected
    		inc %ax				#  mode
    		mov %eax,%cr0			#
    		ljmp $SEL_SCODE,$init.8		# To 32-bit code
    		.code32
    init.8:		xorl %ecx,%ecx			# Zero
    		movb $SEL_SDATA,%cl		# To 32-bit
    		movw %cx,%ss			#  stack

stand/i386/btx/btx/btx.S

Сначала вызывается `setpic` для программирования 8259A PIC (программируемого контроллера прерываний). Этот чип подключен к нескольким источникам аппаратных прерываний. При получении прерывания от устройства он сигнализирует процессору соответствующим вектором прерывания. Это можно настроить так, чтобы определённые прерывания были связаны с конкретными векторами прерываний, как объяснялось ранее. Затем регистры IDTR (Interrupt Descriptor Table Register) и GDTR (Global Descriptor Table Register) загружаются инструкциями `lidt` и `lgdt` соответственно. Эти регистры загружаются базовым адресом и предельным адресом для IDT и GDT. Следующие три инструкции устанавливают бит Protection Enable (PE) в регистре `%cr0`. Это фактически переключает процессор в 32-битный защищенный режим. Затем выполняется дальний переход на `init.8` с использованием селектора сегмента SEL_SCODE, который выбирает сегмент кода супервизора (Supervisor Code Segment). После этого перехода процессор фактически работает на уровне CPL 0 — наиболее привилегированном уровне. Наконец, для стека выбирается сегмент данных супервизора (Supervisor Data Segment) путем присвоения селектора сегмента SEL_SDATA регистру `%ss`. Этот сегмент данных также имеет уровень привилегий `0`.

Наш последний блок кода отвечает за загрузку TR (Регистра Задач) с селектором сегмента для TSS, который мы создали ранее, и настройку окружения пользовательского режима перед передачей управления исполнения клиенту boot2.

    /*
     * Launch user task.
     */
    		movb $SEL_TSS,%cl		# Set task
    		ltr %cx				#  register
    		movl $MEM_USR,%edx		# User base address
    		movzwl %ss:BDA_MEM,%eax		# Get free memory
    		shll $0xa,%eax			# To bytes
    		subl $ARGSPACE,%eax		# Less arg space
    		subl %edx,%eax			# Less base
    		movb $SEL_UDATA,%cl		# User data selector
    		pushl %ecx			# Set SS
    		pushl %eax			# Set ESP
    		push $0x202			# Set flags (IF set)
    		push $SEL_UCODE			# Set CS
    		pushl btx_hdr+0xc		# Set EIP
    		pushl %ecx			# Set GS
    		pushl %ecx			# Set FS
    		pushl %ecx			# Set DS
    		pushl %ecx			# Set ES
    		pushl %edx			# Set EAX
    		movb $0x7,%cl			# Set remaining
    init.9:		push $0x0			#  general
    		loop init.9			#  registers
    #ifdef BTX_SERIAL
    		call sio_init			# setup the serial console
    #endif
    		popa				#  and initialize
    		popl %es			# Initialize
    		popl %ds			#  user
    		popl %fs			#  segment
    		popl %gs			#  registers
    		iret				# To user mode

stand/i386/btx/btx/btx.S

Обратите внимание, что среда клиента включает селектор сегмента стека и указатель стека (регистры `%ss` и `%esp`). Действительно, как только TR загружается соответствующим селектором сегмента стека (инструкция `ltr`), указатель стека вычисляется и помещается в стек вместе с селектором сегмента стека. Затем значение `0x202` помещается в стек; это значение, которое EFLAGS получит при передаче управления клиенту. Также в стек помещаются селектор сегмента кода пользовательского режима и точка входа клиента. Напомним, что эта точка входа прописывается в заголовке BTX во время компоновки. Наконец, селекторы сегментов (хранящиеся в регистре `%ecx`) для регистров сегментов `%gs, %fs, %ds и %es` помещаются в стек вместе со значением из `%edx` (`0xa000`). Примите во внимание эти значения, помещенные в стек (они скоро будут извлечены). Затем значения для оставшихся регистров общего назначения также помещаются в стек (обратите внимание на цикл `loop`, который помещает значение `0` семь раз). Теперь начнётся извлечение значений из стека. Сначала инструкция `popa` извлекает из стека последние семь помещённых значений. Они сохраняются в регистрах общего назначения в порядке `%edi, %esi, %ebp, %ebx, %edx, %ecx, %eax`. Затем различные селекторы сегментов, помещённые в стек, извлекаются в соответствующие регистры сегментов. В стеке остаются ещё пять значений. Они извлекаются при выполнении инструкции `iret`. Эта инструкция сначала извлекает значение, которое было помещено из заголовка BTX. Это значение является указателем на точку входа boot2. Оно помещается в регистр `%eip` — регистр указателя инструкций. Затем селектор сегмента кода пользователя извлекается и копируется в регистр `%cs`. Помните, что уровень привилегий этого сегмента — 3, наименее привилегированный уровень. Это означает, что мы должны предоставить значения для стека этого уровня привилегий. Именно поэтому процессор, помимо дальнейшего извлечения значения для регистра EFLAGS, выполняет ещё два извлечения из стека. Эти значения попадают в указатель стека (`%esp`) и сегмент стека (`%ss`). Теперь выполнение продолжается с точки входа `boot0`.

Важно отметить, как определяется сегмент пользовательского кода. Базовый адрес этого сегмента установлен на `0xa000`. Это означает, что адреса памяти кода являются относительными к адресу 0xa000; если код, который выполняется, извлекается из адреса `0x2000`, фактический адрес в памяти будет `0xa000+0x2000=0xc000`.

## 1.7. Этап загрузки boot2

`boot2` определяет важную структуру, `struct bootinfo`. Эта структура инициализируется `boot2` и передаётся загрузчику, а затем ядру. Некоторые узлы этой структуры устанавливаются `boot2`, остальные — загрузчиком. Эта структура, среди прочей информации, содержит имя файла ядра, геометрию жёсткого диска в BIOS, номер диска в BIOS для загрузочного устройства, доступную физическую память, указатель `envp` и т.д. Ее определение выглядит так:

    /usr/include/machine/bootinfo.h:
    struct bootinfo {
    	u_int32_t	bi_version;
    	u_int32_t	bi_kernelname;		/* represents a char * */
    	u_int32_t	bi_nfs_diskless;	/* struct nfs_diskless * */
    				/* End of fields that are always present. */
    #define	bi_endcommon	bi_n_bios_used
    	u_int32_t	bi_n_bios_used;
    	u_int32_t	bi_bios_geom[N_BIOS_GEOM];
    	u_int32_t	bi_size;
    	u_int8_t	bi_memsizes_valid;
    	u_int8_t	bi_bios_dev;		/* bootdev BIOS unit number */
    	u_int8_t	bi_pad[2];
    	u_int32_t	bi_basemem;
    	u_int32_t	bi_extmem;
    	u_int32_t	bi_symtab;		/* struct symtab * */
    	u_int32_t	bi_esymtab;		/* struct symtab * */
    				/* Items below only from advanced bootloader */
    	u_int32_t	bi_kernend;		/* end of kernel space */
    	u_int32_t	bi_envp;		/* environment */
    	u_int32_t	bi_modulep;		/* preloaded modules */
    };

`boot2` входит в бесконечный цикл, ожидая ввода пользователя, затем вызывает `load()`. Если пользователь ничего не нажимает, цикл прерывается по таймауту, и `load()` загружает файл по умолчанию (/boot/loader). Функции `ino_t lookup(char *filename)` и `int xfsread(ino_t inode, void *buf, size_t nbyte)` используются для чтения содержимого файла в память. /boot/loader — это ELF-бинарный файл, но с заголовком ELF, перед которым добавлена структура `struct exec` из a.out. `load()` анализирует ELF-заголовок загрузчика, загружает содержимое /boot/loader в память и передаёт управление на точку входа загрузчика:

    stand/i386/boot2/boot2.c:
        __exec((caddr_t)addr, RB_BOOTINFO | (opts & RBX_MASK),
    	   MAKEBOOTDEV(dev_maj[dsk.type], dsk.slice, dsk.unit, dsk.part),
    	   0, 0, 0, VTOP(&bootinfo));

## 1.8. Этап загрузчика (loader)

Загрузчик также является клиентом BTX. Я не буду подробно описывать его здесь, существует исчерпывающая man-страница, написанная Майком Смитом: loader(8). Основные механизмы и BTX были рассмотрены выше.

Основная задача загрузчика — загрузить ядро. Когда ядро загружено в память, загрузчик вызывает его:

    stand/common/boot.c:
        /* Call the exec handler from the loader matching the kernel */
        file_formats[fp->f_loader]->l_exec(fp);

## 1.9. Инициализация ядра

Давайте рассмотрим команду, которая компонует ядро. Это поможет определить точное местоположение, где загрузчик передаёт выполнение ядру. Это местоположение является фактической точкой входа ядра. Данная команда теперь исключена из sys/conf/Makefile.i386. Интересующее нас содержимое можно найти в /usr/obj/usr/src/i386.i386/sys/GENERIC/.

    /usr/obj/usr/src/i386.i386/sys/GENERIC/kernel.meta:
    ld -m elf_i386_fbsd -Bdynamic -T /usr/src/sys/conf/ldscript.i386 --build-id=sha1 --no-warn-mismatch \
    --warn-common --export-dynamic  --dynamic-linker /red/herring -X -o kernel locore.o
    <lots of kernel .o files>

Вот несколько интересных наблюдений. Во-первых, ядро представляет собой динамически связанный бинарный файл ELF, но динамический компоновщик для ядра — это /red/herring, что явно является фиктивным файлом. Во-вторых, взглянув на файл sys/conf/ldscript.i386, можно понять, какие параметры ld используются при компиляции ядра. Читая первые несколько строк, видим, что строка

    sys/conf/ldscript.i386:
    ENTRY(btext)

говорит, что точка входа ядра — это символ `btext`. Этот символ определён в locore.s:

    sys/i386/i386/locore.s:
    	.text
    /**********************************************************************
     *
     * This is where the bootblocks start us, set the ball rolling...
     *
     */
    NON_GPROF_ENTRY(btext)

Сначала регистр EFLAGS устанавливается в предопределённое значение 0x00000002. Затем инициализируются все сегментные регистры:

    sys/i386/i386/locore.s:
    /* Don't trust what the BIOS gives for eflags. */
    	pushl	$PSL_KERNEL
    	popfl

    /*
     * Don't trust what the BIOS gives for %fs and %gs.  Trust the bootstrap
     * to set %cs, %ds, %es and %ss.
     */
    	mov	%ds, %ax
    	mov	%ax, %fs
    	mov	%ax, %gs

btext вызывает подпрограммы `recover_bootinfo()` и `identify_cpu()`, которые также определены в locore.s. Вот описание их функций:

`recover_bootinfo` | Эта процедура разбирает параметры, переданные ядру при загрузке. Ядро могло быть загружено тремя способами: загрузчиком (как описано выше), старыми загрузочными блоками диска или по старой процедуре загрузки без диска. Эта функция определяет метод загрузки и сохраняет структуру `struct bootinfo` в памяти ядра.
`identify_cpu` | Эта функция пытается определить, на каком процессоре она выполняется, сохраняя найденное значение в переменной `_cpu`.

Следующие шаги включают активацию VME, если процессор поддерживает эту функцию:

    sys/i386/i386/mpboot.s:
    	testl	$CPUID_VME,%edx
    	jz	3f
    	orl	$CR4_VME,%eax
    3:	movl	%eax,%cr4

Затем, включение подкачки:

    sys/i386/i386/mpboot.s:
    /* Now enable paging */
    	movl	IdlePTD_nopae, %eax
    	movl	%eax,%cr3			/* load ptd addr into mmu */
    	movl	%cr0,%eax			/* get control word */
    	orl	$CR0_PE|CR0_PG,%eax		/* enable paging */
    	movl	%eax,%cr0			/* and let's page NOW! */

Следующие три строки кода необходимы, потому что была установлена подкачка, поэтому требуется переход для продолжения выполнения в виртуализированном адресном пространстве:

    sys/i386/i386/mpboot.s:
    	pushl	$mp_begin				/* jump to high mem */
    	ret

    /* now running relocated at KERNBASE where the system is linked to run */
    mp_begin:	/* now running relocated at KERNBASE */

Функция `init386()` вызывается с указателем на первую свободную физическую страницу, после чего следует вызов `mi_startup()`. `init386` — это архитектурно-зависимая функция инициализации, а `mi_startup()` — архитектурно-независимая (префикс 'mi_' означает Machine Independent, то есть «независимая от машины»). Ядро никогда не возвращается из `mi_startup()`, и, вызывая её, завершает загрузку:

    sys/i386/i386/locore.s:
    	pushl	physfree			/* value of first for init386(first) */
    	call	init386				/* wire 386 chip for unix operation */
    	addl	$4,%esp
    	movl	%eax,%esp			/* Switch to true top of stack. */
    	call	mi_startup			/* autoconfiguration, mountroot etc */
    	/* NOTREACHED */

### 1.9.1. `init386()`

`init386()` определена в sys/i386/i386/machdep.c и выполняет низкоуровневую инициализацию, специфичную для чипа i386. Переход в защищённый режим был выполнен загрузчиком. Загрузчик создал самую первую задачу, в которой ядро продолжает работать. Прежде чем рассматривать код, рассмотрим задачи, которые процессор должен выполнить для инициализации выполнения в защищённом режиме:

- Инициализировать настраиваемые параметры ядра, переданные из загрузочной программы.

- Подготовить GDT.

- Подготовить IDT.

- Инициализировать системную консоль.

- Инициализировать DDB, если он скомпилирован в ядро.

- Инициализировать TSS.

- Подготовить LDT.

- Настройка pcb для thread0.

`init386()` инициализирует настраиваемые параметры, переданные из bootstrap, устанавливая указатель окружения (envp) и вызывая `init_param1()`. Указатель envp был передан из loader в структуре `bootinfo`:

    sys/i386/i386/machdep.c:
    	/* Init basic tunables, hz etc */
    	init_param1();

`init_param1()` определена в sys/kern/subr_param.c. Этот файл содержит ряд sysctl, а также две функции, `init_param1()` и `init_param2()`, которые вызываются из `init386()`:

    sys/kern/subr_param.c:
    	hz = -1;
    	TUNABLE_INT_FETCH("kern.hz", &hz);
    	if (hz == -1)
    		hz = vm_guest > VM_GUEST_NO ? HZ_VM : HZ;

`TUNABLE_<typename>_FETCH` используется для получения значения из окружения:

    /usr/src/sys/sys/kernel.h:
    #define	TUNABLE_INT_FETCH(path, var)	getenv_int((path), (var))

Sysctl `kern.hz` представляет собой такт системных часов. Кроме того, эти параметры sysctl устанавливаются функцией `init_param1()`: `kern.maxswzone, kern.maxbcache, kern.maxtsiz, kern.dfldsiz, kern.maxdsiz, kern.dflssiz, kern.maxssiz, kern.sgrowsiz`.

Затем `init386()` подготавливает Глобальную Таблицу Дескрипторов (GDT). Каждая задача на x86 выполняется в своём собственном виртуальном адресном пространстве, и это пространство адресуется парой сегмент:смещение. Например, если текущая инструкция, которую должен выполнить процессор, находится по адресу CS:EIP, то линейный виртуальный адрес этой инструкции будет "виртуальный адрес кодового сегмента CS"
EIP. Для удобства сегменты начинаются с виртуального адреса 0 и заканчиваются на границе 4 ГБ. Таким образом, линейный виртуальный адрес инструкции в данном примере будет просто значением EIP. Сегментные регистры, такие как CS, DS и другие, являются селекторами, то есть индексами в GDT (если быть более точным, индекс — это не сам селектор, а поле INDEX в селекторе). GDT в FreeBSD содержит дескрипторы для 15 селекторов на каждый CPU:

    sys/i386/i386/machdep.c:
    union descriptor gdt0[NGDT];	/* initial global descriptor table */
    union descriptor *gdt = gdt0;	/* global descriptor table */

    sys/x86/include/segments.h:
    /*
     * Entries in the Global Descriptor Table (GDT)
     */
    #define	GNULL_SEL	0	/* Null Descriptor */
    #define	GPRIV_SEL	1	/* SMP Per-Processor Private Data */
    #define	GUFS_SEL	2	/* User %fs Descriptor (order critical: 1) */
    #define	GUGS_SEL	3	/* User %gs Descriptor (order critical: 2) */
    #define	GCODE_SEL	4	/* Kernel Code Descriptor (order critical: 1) */
    #define	GDATA_SEL	5	/* Kernel Data Descriptor (order critical: 2) */
    #define	GUCODE_SEL	6	/* User Code Descriptor (order critical: 3) */
    #define	GUDATA_SEL	7	/* User Data Descriptor (order critical: 4) */
    #define	GBIOSLOWMEM_SEL	8	/* BIOS low memory access (must be entry 8) */
    #define	GPROC0_SEL	9	/* Task state process slot zero and up */
    #define	GLDT_SEL	10	/* Default User LDT */
    #define	GUSERLDT_SEL	11	/* User LDT */
    #define	GPANIC_SEL	12	/* Task state to consider panic from */
    #define	GBIOSCODE32_SEL	13	/* BIOS interface (32bit Code) */
    #define	GBIOSCODE16_SEL	14	/* BIOS interface (16bit Code) */
    #define	GBIOSDATA_SEL	15	/* BIOS interface (Data) */
    #define	GBIOSUTIL_SEL	16	/* BIOS interface (Utility) */
    #define	GBIOSARGS_SEL	17	/* BIOS interface (Arguments) */
    #define	GNDIS_SEL	18	/* For the NDIS layer */
    #define	NGDT		19

Обратите внимание, что эти `#defines` не являются самими селекторами, а лишь полем `INDEX` селектора, поэтому они точно соответствуют индексам GDT. Например, реальный селектор для кода ядра (`GCODE_SEL`) имеет значение `0x20`.

Следующий шаг — инициализация таблицы дескрипторов прерываний (IDT). Эта таблица используется процессором при возникновении программного или аппаратного прерывания. Например, чтобы выполнить системный вызов, пользовательское приложение использует инструкцию `INT 0x80`. Это программное прерывание, поэтому аппаратное обеспечение процессора ищет запись с индексом 0x80 в IDT. Эта запись указывает на процедуру обработки данного прерывания, в данном конкретном случае это будет шлюз системных вызовов ядра. IDT может содержать максимум 256 (0x100) записей. Ядро выделяет NIDT записей для IDT, где NIDT — это максимум (256):

    sys/i386/i386/machdep.c:
    static struct gate_descriptor idt0[NIDT];
    struct gate_descriptor *idt = &idt0[0];	/* interrupt descriptor table */

Для каждого прерывания устанавливается соответствующий обработчик. Также настраивается шлюз системного вызова для `INT 0x80`:

    sys/i386/i386/machdep.c:
    	setidt(IDT_SYSCALL, &IDTVEC(int0x80_syscall),
    			SDT_SYS386IGT, SEL_UPL, GSEL(GCODE_SEL, SEL_KPL));

Итак, когда пользовательское приложение выполняет инструкцию `INT 0x80`, управление передаётся функции `_Xint0x80_syscall`, которая находится в сегменте кода ядра и будет выполнена с привилегиями супервизора.

Консоль и DDB инициализируются:

    sys/i386/i386/machdep.c:
    	cninit();
    /* skipped */
      kdb_init();
    #ifdef KDB
    	if (boothowto & RB_KDB)
    		kdb_enter(KDB_WHY_BOOTFLAGS, "Boot flags requested debugger");
    #endif

Сегмент состояния задачи (TSS) — это ещё одна структура защищенного режима x86, используемая оборудованием для хранения информации о задаче при переключении задач.

Локальная таблица дескрипторов (LDT) используется для ссылки на код и данные пользовательского пространства. Определено несколько селекторов, указывающих на LDT, включая шлюзы системных вызовов, а также селекторы кода и данных пользователя:

    sys/x86/include/segments.h:
    #define	LSYS5CALLS_SEL	0	/* forced by intel BCS */
    #define	LSYS5SIGR_SEL	1
    #define	LUCODE_SEL	3
    #define	LUDATA_SEL	5
    #define	NLDT		(LUDATA_SEL + 1)

Далее инициализируется структура Блока Управления Процессом (`struct pcb`) для proc0. proc0 — это структура `struct proc`, описывающая процесс ядра. Она всегда присутствует во время работы ядра, поэтому связана с thread0:

    sys/i386/i386/machdep.c:
    register_t
    init386(int first)
    {
        /* ... skipped ... */

        proc_linkup0(&proc0, &thread0);
        /* ... skipped ... */
    }

Структура `struct pcb` является частью структуры proc. Она определена в /usr/include/machine/pcb.h и содержит информацию процесса, специфичную для архитектуры i386, такую как значения регистров.

### 1.9.2. `mi_startup()`

Эта функция выполняет сортировку пузырьком всех объектов инициализации системы, а затем вызывает вход каждого объекта по очереди:

    sys/kern/init_main.c:
    	for (sipp = sysinit; sipp < sysinit_end; sipp++) {

    		/* ... skipped ... */

    		/* Call function */
    		(*((*sipp)->func))((*sipp)->udata);
    		/* ... skipped ... */
    	}

Хотя фреймворк sysinit описан в Руководстве разработчика, я рассмотрю его внутреннее устройство.

Каждый объект инициализации системы (объект sysinit) создаётся путем вызова макроса SYSINIT(). Возьмем, к примеру, объект sysinit `announce`. Этот объект выводит сообщение об авторских правах:

    sys/kern/init_main.c:
    static void
    print_caddr_t(void *data __unused)
    {
    	printf("%s", (char *)data);
    }
    /* ... skipped ... */
    SYSINIT(announce, SI_SUB_COPYRIGHT, SI_ORDER_FIRST, print_caddr_t, copyright);

Идентификатор подсистемы для этого объекта — SI_SUB_COPYRIGHT (0x0800001). Таким образом, сообщение об авторских правах будет выведено первым, сразу после инициализации консоли.

Давайте рассмотрим, что именно делает макрос `SYSINIT()`. Он раскрывается в макрос `C_SYSINIT()`. Макрос `C_SYSINIT()` затем раскрывается в статическое объявление структуры `struct sysinit` с вызовом другого макроса `DATA_SET`:

    /usr/include/sys/kernel.h:
          #define C_SYSINIT(uniquifier, subsystem, order, func, ident) \
          static struct sysinit uniquifier ## _sys_init = { \ subsystem, \
          order, \ func, \ (ident) \ }; \ DATA_WSET(sysinit_set,uniquifier ##
          _sys_init);

    #define	SYSINIT(uniquifier, subsystem, order, func, ident)	\
    	C_SYSINIT(uniquifier, subsystem, order,			\
    	(sysinit_cfunc_t)(sysinit_nfunc_t)func, (void *)(ident))

Макрос `DATA_SET()` раскрывается в `_MAKE_SET()`, и именно в этом макросе скрыта вся магия инициализации системы:

    /usr/include/linker_set.h:
    #define TEXT_SET(set, sym) _MAKE_SET(set, sym)
    #define DATA_SET(set, sym) _MAKE_SET(set, sym)

После выполнения этих макросов в ядре были созданы различные разделы, включая `set.sysinit_set`. Запустив objdump для бинарного файла ядра, можно заметить наличие таких небольших разделов:

    % llvm-objdump -h /kernel
    Sections:
    Idx Name                               Size     VMA      Type
     10 set_sysctl_set                     000021d4 01827078 DATA
     16 set_kbddriver_set                  00000010 0182a4d0 DATA
     20 set_scterm_set                     0000000c 0182c75c DATA
     21 set_cons_set                       00000014 0182c768 DATA
     33 set_scrndr_set                     00000024 0182c828 DATA
     41 set_sysinit_set                    000014d8 018fabb0 DATA

Это содержимое экрана показывает, что размер раздела set.sysinit_set составляет 0x14d8 байт, поэтому `0x14d8/sizeof(void *)` объектов sysinit скомпилировано в ядро. Другие разделы, такие как `set.sysctl_set`, представляют другие наборы компоновщика.

Определяя переменную типа `struct sysinit`, содержимое раздела `set.sysinit_set` будет "собрано" в эту переменную:

    sys/kern/init_main.c:
      SET_DECLARE(sysinit_set, struct sysinit);

`struct sysinit` определена следующим образом:

    sys/sys/kernel.h:
      struct sysinit {
    	enum sysinit_sub_id	subsystem;	/* subsystem identifier*/
    	enum sysinit_elem_order	order;		/* init order within subsystem*/
    	sysinit_cfunc_t func;			/* function		*/
    	const void	*udata;			/* multiplexer/argument */
    };

Возвращаясь к обсуждению `mi_startup()`, теперь должно быть понятно, как организованы объекты sysinit. Функция `mi_startup()` сортирует их и вызывает каждый. Самый последний объект — это системный планировщик:

    /usr/include/sys/kernel.h:
    enum sysinit_sub_id {
    	SI_SUB_DUMMY		= 0x0000000,	/* not executed; for linker*/
    	SI_SUB_DONE		= 0x0000001,	/* processed*/
    	SI_SUB_TUNABLES		= 0x0700000,	/* establish tunable values */
    	SI_SUB_COPYRIGHT	= 0x0800001,	/* first use of console*/
    ...
    	SI_SUB_LAST		= 0xfffffff	/* final initialization */
    };

Системный планировщик sysinit определен в файле sys/vm/vm_glue.c, а точка входа для этого объекта — `scheduler()`. Эта функция фактически представляет собой бесконечный цикл и описывает процесс с PID 0, известный как процесс swapper. Структура thread0, упомянутая ранее, используется для его описания.

Первый пользовательский процесс, называемый init, создаётся объектом sysinit `init`:

    sys/kern/init_main.c:
    static void
    create_init(const void *udata __unused)
    {
    	struct fork_req fr;
    	struct ucred *newcred, *oldcred;
    	struct thread *td;
    	int error;

    	bzero(&fr, sizeof(fr));
    	fr.fr_flags = RFFDG | RFPROC | RFSTOPPED;
    	fr.fr_procp = &initproc;
    	error = fork1(&thread0, &fr);
    	if (error)
    		panic("cannot fork init: %d\n", error);
    	KASSERT(initproc->p_pid == 1, ("create_init: initproc->p_pid != 1"));
    	/* divorce init's credentials from the kernel's */
    	newcred = crget();
    	sx_xlock(&proctree_lock);
    	PROC_LOCK(initproc);
    	initproc->p_flag |= P_SYSTEM | P_INMEM;
    	initproc->p_treeflag |= P_TREE_REAPER;
    	oldcred = initproc->p_ucred;
    	crcopy(newcred, oldcred);
    #ifdef MAC
    	mac_cred_create_init(newcred);
    #endif
    #ifdef AUDIT
    	audit_cred_proc1(newcred);
    #endif
    	proc_set_cred(initproc, newcred);
    	td = FIRST_THREAD_IN_PROC(initproc);
    	crcowfree(td);
    	td->td_realucred = crcowget(initproc->p_ucred);
    	td->td_ucred = td->td_realucred;
    	PROC_UNLOCK(initproc);
    	sx_xunlock(&proctree_lock);
    	crfree(oldcred);
    	cpu_fork_kthread_handler(FIRST_THREAD_IN_PROC(initproc), start_init, NULL);
    }
    SYSINIT(init, SI_SUB_CREATE_INIT, SI_ORDER_FIRST, create_init, NULL);

Функция `create_init()` выделяет новый процесс, вызывая `fork1()`, но не помечает его как готовый к выполнению. Когда этот новый процесс будет запланирован для выполнения планировщиком, будет вызвана функция `start_init()`. Эта функция определена в init_main.c. Она пытается загрузить и выполнить бинарный файл init, сначала проверяя /sbin/init, затем /sbin/oinit, /sbin/init.bak и, наконец, /rescue/init:

    sys/kern/init_main.c:
    static char init_path[MAXPATHLEN] =
    #ifdef	INIT_PATH
        __XSTRING(INIT_PATH);
    #else
        "/sbin/init:/sbin/oinit:/sbin/init.bak:/rescue/init";
    #endif

1. Это приглашение появится, если пользователь нажмет клавишу сразу после выбора ОС для загрузки на этапе boot0.
2. В случае сомнений мы отсылаем читателя к официальным руководствам Intel, где описана точная семантика каждой инструкции.
3. Файл /boot/boot1 существует, но он не записывается в начало раздела FreeBSD. Вместо этого он объединяется с boot2, формируя файл boot, который записывается в начало раздела FreeBSD и считывается во время загрузки.
4. На самом деле мы передали указатель на адрес слайса в регистре %si. Однако boot1 не предполагает, что он был загружен boot0 (возможно, его загрузил другой MBR и не передал эту информацию), поэтому он ничего не предполагает.
5. В контексте 16-битного реального режима слово — это 2 байта.
6. 512*16=8192 байта, ровно размер boot
7. Исторически известной как disklabel. Если вам когда-либо было интересно, где FreeBSD хранит эту информацию, она находится в этой области — см. bsdlabel(8)
8. Это необходимо по историческим причинам.
9. Код и данные реального режима необходимы при переключении обратно в реальный режим из защищённого режима, как указано в руководствах Intel.

Изменено: 17 мая 2026 г. by Vladlen Popolitov

Назад
Главная
Далее