Глава 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 обычно сопоставляет аппаратные прерывания с векторами прерываний 0x80xf и 0x700x77, но, как будет показано, программируемый контроллер прерываний 8259A, микросхема, управляющая фактическим сопоставлением аппаратных прерываний с векторами прерываний, программируется для переназначения этих векторов прерываний с 0x80xf на 0x200x27 и с 0x700x77 на 0x280x2f. Таким образом, обработчики прерываний предоставляются для векторов прерываний 0x200x2f. Причина, по которой обработчики, предоставляемые 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.

Изменено: 14 октября 2025 г. by Vladlen Popolitov