diff options
Diffstat (limited to 'documentation/content/ru/books/arch-handbook/boot/_index.adoc')
| -rw-r--r-- | documentation/content/ru/books/arch-handbook/boot/_index.adoc | 1351 |
1 files changed, 1351 insertions, 0 deletions
diff --git a/documentation/content/ru/books/arch-handbook/boot/_index.adoc b/documentation/content/ru/books/arch-handbook/boot/_index.adoc new file mode 100644 index 0000000000..43dacd74c7 --- /dev/null +++ b/documentation/content/ru/books/arch-handbook/boot/_index.adoc @@ -0,0 +1,1351 @@ +--- +description: 'Начальная загрузка и инициализация ядра' +next: books/arch-handbook/locking +params: + path: /books/arch-handbook/boot/ +prev: books/arch-handbook/parti +showBookMenu: 'true' +tags: ["boot", "BIOS", "kernel", "MBR", "FreeBSD"] +title: 'Глава 1. Начальная загрузка и инициализация ядра' +weight: 2 +--- + +[[boot]] += Начальная загрузка и инициализация ядра +:doctype: book +:toc: macro +:toclevels: 1 +:icons: font +:sectnums: +:sectnumlevels: 6 +:sectnumoffset: 1 +:partnums: +:source-highlighter: rouge +:experimental: +:images-path: books/arch-handbook/ + +ifdef::env-beastie[] +ifdef::backend-html5[] +:imagesdir: ../../../../images/{images-path} +endif::[] +ifndef::book[] +include::shared/authors.adoc[] +include::shared/mirrors.adoc[] +include::shared/releases.adoc[] +include::shared/attributes/attributes-{{% lang %}}.adoc[] +include::shared/{{% lang %}}/teams.adoc[] +include::shared/{{% lang %}}/mailing-lists.adoc[] +include::shared/{{% lang %}}/urls.adoc[] +toc::[] +endif::[] +ifdef::backend-pdf,backend-epub3[] +include::../../../../../shared/asciidoctor.adoc[] +endif::[] +endif::[] + +ifndef::env-beastie[] +toc::[] +include::../../../../../shared/asciidoctor.adoc[] +endif::[] + +[[boot-synopsis]] +== Обзор + +Эта глава представляет собой обзор процессов загрузки и инициализации системы, начиная с POST в BIOS (микропрограмме) и заканчивая созданием первого пользовательского процесса. Поскольку начальные этапы загрузки системы сильно зависят от архитектуры, в качестве примера используется архитектура IA-32. Однако архитектуры AMD64 и ARM64 гораздо важнее и интереснее, и их следует рассмотреть в ближайшем будущем в соответствии с темой этого документа. + +Процесс загрузки FreeBSD может быть удивительно сложным. После передачи управления от BIOS необходимо выполнить значительный объем низкоуровневой настройки перед загрузкой и выполнением ядра. Эта настройка должна быть выполнена простым и гибким способом, предоставляя пользователю широкие возможности для настройки и адаптации. + +[[boot-overview]] +== Обзор + +Процесс загрузки — это операция, крайне зависимая от оборудования. Не только для каждой архитектуры компьютера должен быть написан код, но также могут существовать различные типы загрузки в рамках одной архитектуры. Например, список файлов в каталоге [.filename]#stand# показывает большое количество кода, зависящего от архитектуры. Для каждой из поддерживаемых архитектур существует отдельный каталог. FreeBSD поддерживает стандарт загрузки CSM (Compatibility Support Module). Таким образом, CSM поддерживается (как с GPT, так и с MBR разметкой), а также загрузка через UEFI (GPT полностью поддерживается, MBR — в основном). Также поддерживается загрузка файлов с ext2fs, MSDOS, UFS и ZFS. FreeBSD поддерживает функцию загрузочного окружения ZFS, которая позволяет основной ОС передавать детали о том, что загружать, выходящие за рамки простого раздела, как это было возможно ранее. Однако в наши дни UEFI более актуален, чем CSM. В следующем примере показана загрузка компьютера x86 с жёсткого диска с MBR-разметкой, где используется мультизагрузчик FreeBSD [.filename]#boot0#, сохранённый в самом первом секторе. Этот загрузочный код запускает трёхэтапный процесс загрузки FreeBSD. + +Ключ к пониманию этого процесса заключается в том, что он состоит из последовательных стадий возрастающей сложности. Эти стадии — [.filename]#boot1#, [.filename]#boot2# и [.filename]#loader# (подробнее см. man:boot[8]). Система загрузки выполняет каждую стадию последовательно. Последняя стадия, [.filename]#loader#, отвечает за загрузку ядра FreeBSD. Каждая стадия рассматривается в следующих разделах. + +Вот пример вывода, сгенерированного на различных этапах загрузки. Фактический вывод может отличаться в зависимости от машины: + +[.informaltable] +[cols="20%,80%", frame="none"] +|=== + +|*Компонент FreeBSD* +|*Вывод (может отличаться)* + +|`boot0` +a| + +[source,bash] +.... +F1 FreeBSD +F2 BSD +F5 Disk 2 +.... + +|`boot2` footnote:[Это приглашение появится, если пользователь нажмет клавишу сразу после выбора ОС для загрузки на этапе boot0.] +a| + +[source,bash] +.... +>>FreeBSD/x86 BOOT +Default: 0:ad(0p4)/boot/loader +boot: +.... + +|[.filename]#loader# +a| + +[source,bash] +.... +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] +.... + +|ядро системы +a| + +[source,bash] +.... +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) +.... + +|=== + +[[boot-bios]] +== 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) или Главная Загрузочная Запись. Остальные секторы на первой дорожке не используются. + +Этот сектор является нашей точкой входа в последовательность загрузки. Как мы увидим, этот сектор содержит копию нашей программы [.filename]#boot0#. BIOS выполняет переход по адресу `0x7c00`, и она начинает выполняться. + +[[boot-boot0]] +== Главная загрузочная запись (`boot0`) + +После получения управления от BIOS по адресу памяти `0x7c00` начинает выполняться [.filename]#boot0#. Это первый код, который управляется FreeBSD. Задача [.filename]#boot0# довольно проста: просканировать таблицу разделов и позволить пользователю выбрать, с какого раздела загружаться. Таблица разделов — это специальная стандартная структура данных, встроенная в MBR (а значит, и в [.filename]#boot0#), которая описывает четыре стандартных PC-раздела. [.filename]#boot0# находится в файловой системе как [.filename]#/boot/boot0#. Это небольшой файл размером 512 байт, и именно его процедура установки FreeBSD записывает в MBR жёсткого диска, если во время установки была выбрана опция "bootmanager". Действительно, [.filename]#boot0# _и есть_ MBR. + +Как упоминалось ранее, мы вызываем прерывание BIOS `INT 0x19` для загрузки MBR ([.filename]#boot0#) в память по адресу `0x7c00`. Исходный файл для [.filename]#boot0# можно найти в [.filename]#stand/i386/boot0/boot0.S# — это впечатляющий фрагмент кода, написанный Робертом Нордье. + +Особая структура, начинающаяся со смещения `0x1be` в MBR, называется _таблицей разделов_. Она содержит четыре записи по 16 байт каждая, называемые _записями разделов_, которые определяют, как разделён жёсткий диск, или, в терминологии FreeBSD, нарезан. Один из этих 16 байт указывает, является ли раздел (срез) загрузочным или нет. Ровно одна запись должна быть с этом установленным флагом, иначе код [.filename]#boot0# откажется продолжать работу. + +Запись о разделе содержит следующие поля: + +* 1-байтовый тип файловой системы +* 1-байтовый флаг загрузки (`bootable`) +* 6-байтовый дескриптор в формате CHS +* 8-байтовый дескриптор в формате LBA + +Дескриптор записи раздела содержит информацию о том, где именно раздел расположен на диске. Оба дескриптора, LBA и CHS, описывают одну и ту же информацию, но разными способами: LBA (Logical Block Addressing) содержит начальный сектор раздела и его длину, тогда как CHS (Cylinder Head Sector) содержит координаты первого и последнего секторов раздела. Таблица разделов завершается специальной сигнатурой `0xaa55`. + +MBR должен помещаться в 512 байт, один сектор диска. Эта программа использует низкоуровневые «трюки», такие как использование побочных эффектов определённых инструкций и повторное использование значений регистров из предыдущих операций, чтобы максимально эффективно использовать минимально возможное количество инструкций. Также необходимо соблюдать осторожность при работе с таблицей разделов, которая встроена в сам MBR. По этим причинам будьте очень внимательны при изменении [.filename]#boot0.S#. + +Обратите внимание, что исходный файл [.filename]#boot0.S# ассемблируется "как есть": инструкции переводятся одна за одной в бинарный код без дополнительной информации (например, без формата файла ELF). Такой низкоуровневый контроль достигается на этапе компоновки с помощью специальных флагов, передаваемых компоновщику. Например, текстовая секция программы располагается по адресу `0x600`. На практике это означает, что [.filename]#boot0# должен быть загружен в память по адресу `0x600` для корректной работы. + +Стоит взглянуть на [.filename]#Makefile# для [.filename]#boot0# ([.filename]#stand/i386/boot0/Makefile#), так как он определяет некоторые аспекты поведения [.filename]#boot0# во время выполнения. Например, если для ввода-вывода используется терминал, подключённый к последовательному порту (COM1), необходимо определить макрос `SIO` (`-DSIO`). `-DPXE` включает загрузку через PXE при нажатии kbd:[F6]. Кроме того, программа определяет набор _флагов_, которые позволяют дополнительно настроить её поведение. Всё это проиллюстрировано в [.filename]#Makefile#. Например, обратите внимание на директивы компоновщика, которые предписывают ему начинать секцию текста с адреса `0x600` и создавать выходной файл "как есть" (удаляя любое форматирование файла): + +[.programlisting] +.... + BOOT_BOOT0_ORG?=0x600 + ORG=${BOOT_BOOT0_ORG} +.... + +.[.filename]#stand/i386/boot0/Makefile# [[boot-boot0-makefile-as-is]] +Приступим к изучению MBR, или [.filename]#boot0#, начиная с точки входа. + +[NOTE] +==== +В некоторые инструкции были внесены изменения для лучшего изложения. Например, некоторые макросы раскрыты, а некоторые проверки макросов опущены, когда результат проверки известен. Это относится ко всем приведённым примерам кода. +==== + +[.programlisting] +.... +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 +.... + +.[.filename]#stand/i386/boot0/boot0.S# [[boot-boot0-entrypoint]] +Этот первый блок кода является точкой входа программы. Именно сюда BIOS передаёт управление. Сначала он гарантирует, что строковые операции автоматически увеличивают указатели операндов (инструкция `cld`) footnote:[В случае сомнений мы отсылаем читателя к официальным руководствам Intel, где описана точная семантика каждой инструкции.]. Затем, не делая предположений о состоянии сегментных регистров, он их инициализирует. Наконец, он устанавливает регистр указателя стека (`%sp`) в ($LOAD = адрес `0x7c00`), чтобы обеспечить работоспособный стек. + +Следующий блок отвечает за перемещение и последующий переход к перемещенному коду. + +[.programlisting] +.... + 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 +.... + +.[.filename]#stand/i386/boot0/boot0.S# [[boot-boot0-relocation]] +Так как [.filename]#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, или тот, что хранится в [.filename]#boot0#. + +[.programlisting] +.... +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 +.... + +.[.filename]#stand/i386/boot0/boot0.S# [[boot-boot0-drivenumber]] +Этот код проверяет бит `SETDRV` (`0x20`) в переменной _flags_. Напомним, что регистр `%bp` указывает на адрес `0x800`, поэтому проверка выполняется для переменной _flags_ по адресу `0x800`-`69`= `0x7bb`. Это пример типа изменений, которые можно внести в [.filename]#boot0#. Флаг `SETDRV` не установлен по умолчанию, но его можно задать в [.filename]#Makefile#. Если он установлен, используется номер диска, сохранённый в MBR, вместо предоставленного BIOS. Мы предполагаем значения по умолчанию и то, что BIOS предоставил корректный номер диска, поэтому переходим к `save_curdrive`. + +Следующий блок сохраняет номер диска, предоставленный BIOS, и вызывает `putn` для вывода новой строки на экран. + +[.programlisting] +.... +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 +.... + +.[.filename]#stand/i386/boot0/boot0.S# [[boot-boot0-savedrivenumber]] +Обратите внимание, что мы предполагаем, что `TEST` не определён, поэтому условный код в нём не собирается и не появится в нашем исполняемом файле [.filename]#boot0#. + +Следующий блок реализует фактическое сканирование таблицы разделов. Он выводит на экран тип раздела для каждой из четырёх записей в таблице разделов. Каждый тип сравнивается со списком известных файловых систем операционных систем. Примерами распознаваемых типов разделов являются NTFS (Windows(R), ID 0x7), `ext2fs` (Linux(R), ID 0x83) и, конечно же, `ffs`/`ufs2` (FreeBSD, ID 0xa5). Реализация довольно проста. + +[.programlisting] +.... + 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 +.... + +.[.filename]#stand/i386/boot0/boot0.S# [[boot-boot0-partition-scan]] +Важно отметить, что флаг активности для каждой записи сбрасывается, поэтому после сканирования _ни одна_ запись о разделе не активна в нашей копии [.filename]#boot0# в памяти. Позже флаг активности будет установлен для выбранного раздела. Это гарантирует, что только один активный раздел существует, если пользователь решит записать изменения обратно на диск. + +Следующий блок проверяет наличие других дисков. При запуске BIOS записывает количество дисков, присутствующих в компьютере, по адресу `0x475`. Если есть другие диски, [.filename]#boot0# выводит текущий диск на экран. Пользователь может позже дать команду [.filename]#boot0# просканировать разделы на другом диске. + +[.programlisting] +.... + 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 +.... + +.[.filename]#stand/i386/boot0/boot0.S# [[boot-boot0-test-drives]] +Мы предполагаем, что присутствует только один диск, поэтому переход к `print_drive` не выполняется. Также мы предполагаем, что ничего необычного не произошло, поэтому переходим к `print_prompt`. + +Следующий блок просто выводит приглашение с последующим вариантом по умолчанию: + +[.programlisting] +.... +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 +.... + +.[.filename]#stand/i386/boot0/boot0.S# [[boot-boot0-prompt]] +Наконец, выполняется переход к `start_input`, где используются сервисы BIOS для запуска таймера и чтения пользовательского ввода с клавиатуры; если таймер истекает, будет выбран вариант по умолчанию: + +[.programlisting] +.... +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 +.... + +.[.filename]#stand/i386/boot0/boot0.S# [[boot-boot0-start-input]] +Прерывание запрашивается с номером `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`. Эта переменная находится в [.filename]#boot0# по смещению `_TICKS` (отрицательное значение) от регистра `%bp` (который, напомним, указывает на `0x800`). Значение этой переменной по умолчанию — `0xb6` (182 в десятичной системе). Идея заключается в том, что [.filename]#boot0# постоянно запрашивает время у BIOS, и когда значение, возвращённое в регистре `%dx`, становится больше значения, хранящегося в `%di`, время истекает и будет сделан выбор по умолчанию. Поскольку RTC тикает 18,2 раза в секунду, это условие выполнится через 10 секунд (это поведение по умолчанию можно изменить в [.filename]#Makefile#). До истечения этого времени [.filename]#boot0# непрерывно опрашивает BIOS на предмет ввода пользователя; это делается через `int 0x16`, аргумент `1` в `%ah`. + +Была нажата клавиша или истекло время, последующий код проверяет выбор. В зависимости от выбора, регистр `%si` устанавливается так, чтобы указывать на соответствующую запись раздела в таблице разделов. Этот новый выбор переопределяет предыдущий выбор по умолчанию. Действительно, он становится новым значением по умолчанию. Наконец, устанавливается флаг ACTIVE выбранного раздела. Если это было разрешено при компиляции, версия [.filename]#boot0# в памяти с этими изменёнными значениями записывается обратно в MBR на диске. Мы оставляем детали этой реализации читателю. + +Мы завершаем наше изучение последним блоком кода из программы [.filename]#boot0#: + +[.programlisting] +.... + 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 +.... + +.[.filename]#stand/i386/boot0/boot0.S# [[boot-boot0-check-bootable]] +Вспомним, что `%si` указывает на выбранную запись раздела. Эта запись сообщает нам, где начинается раздел на диске. Мы предполагаем, конечно, что выбранный раздел действительно является срезом FreeBSD. + +[NOTE] +==== +Отныне мы будем отдавать предпочтение использованию технически более точного термина "слайс" вместо "раздел". +==== + +Буфер передачи установлен в `0x7c00` (регистр `%bx`), и запрос на чтение первого сектора слайса FreeBSD выполняется вызовом `intx13`. Мы предполагаем, что всё прошло успешно, поэтому переход к `beep` не выполняется. В частности, новый прочитанный сектор должен заканчиваться магической последовательностью `0xaa55`. Наконец, значение в `%si` (указатель на выбранную таблицу разделов) сохраняется для использования на следующем этапе, и выполняется переход по адресу `0x7c00`, где начинается выполнение нашего следующего этапа (только что прочитанного блока). + +[[boot-boot1]] +== Этап `boot1` + +До сих пор мы прошли следующую последовательность: + +* BIOS выполнил первоначальную инициализацию оборудования, включая POST. MBR ([.filename]#boot0#) был загружен по адресу `0x7c00` из абсолютного сектора один с диска. Управление выполнением было передано по этому адресу. +* [.filename]#boot0# переместил себя по адресу, по которому он был скомпонован для выполнения (`0x600`), после чего выполнил переход для продолжения выполнения в соответствующем месте. В завершение, [.filename]#boot0# загрузил первый сектор диска из раздела FreeBSD по адресу `0x7c00`. Управление выполнением было передано по этому адресу. + +[.filename]#boot1# — это следующий шаг в последовательности загрузки. Это первая из трех стадий загрузки. Обратите внимание, что до сих пор мы работали исключительно с секторами диска. Действительно, BIOS загружает самый первый сектор, а [.filename]#boot0# загружает первый сектор раздела FreeBSD. Обе загрузки происходят по адресу `0x7c00`. Мы можем концептуально представлять эти секторы диска как содержащие файлы [.filename]#boot0# и [.filename]#boot1#, соответственно, но на самом деле это не совсем верно для [.filename]#boot1#. Строго говоря, в отличие от [.filename]#boot0#, [.filename]#boot1# не является частью загрузочных блоков footnote:[Файл /boot/boot1 существует, но он не записывается в начало раздела FreeBSD. Вместо этого он объединяется с boot2, формируя файл boot, который записывается в начало раздела FreeBSD и считывается во время загрузки.]. Вместо этого, единый полноценный файл [.filename]#boot# ([.filename]#/boot/boot#) в итоге записывается на диск. Этот файл представляет собой комбинацию [.filename]#boot1#, [.filename]#boot2# и `Boot Extender` (или BTX). Этот единый файл превышает размер одного сектора (больше 512 байт). К счастью, [.filename]#boot1# занимает _ровно_ первые 512 байт этого файла, поэтому, когда [.filename]#boot0# загружает первый сектор раздела FreeBSD (512 байт), он фактически загружает [.filename]#boot1# и передаёт ему управление. + +Основная задача [.filename]#boot1# — загрузить следующий этап загрузки. Этот следующий этап несколько сложнее. Он состоит из сервера под названием "Boot Extender" (BTX) и клиента под названием [.filename]#boot2#. Как мы увидим, последний этап загрузки, [.filename]#loader#, также является клиентом сервера BTX. + +Давайте теперь подробно рассмотрим, что именно делает [.filename]#boot1#, начиная, как мы это делали для [.filename]#boot0#, с точки входа: + +[.programlisting] +.... +start: + jmp main +.... + +.[.filename]#stand/i386/boot2/boot1.S# [[boot-boot1-entry]] +Точка входа `start` просто переходит через специальную область данных к метке `main`, которая, в свою очередь, выглядит следующим образом: + +[.programlisting] +.... +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 +.... + +.[.filename]#stand/i386/boot2/boot1.S# [[boot-boot1-main]] +Как и [.filename]#boot0#, этот код перемещает [.filename]#boot1#, на этот раз по адресу `0x700`. Однако, в отличие от [.filename]#boot0#, он не переходит туда. [.filename]#boot1# скомпонован для выполнения по адресу `0x7c00`, фактически там, куда он был изначально загружен. Причина этого перемещения будет рассмотрена далее. + +Далее идет цикл, который ищет слайс FreeBSD. Хотя [.filename]#boot0# загрузил [.filename]#boot1# из слайса FreeBSD, ему не была передана информация об этом footnote:[На самом деле мы передали указатель на адрес слайса в регистре %si. Однако boot1 не предполагает, что он был загружен boot0 (возможно, его загрузил другой MBR и не передал эту информацию), поэтому он ничего не предполагает.], поэтому [.filename]#boot1# должен повторно просканировать таблицу разделов, чтобы найти начало слайса FreeBSD. Для этого он перечитывает MBR: + +[.programlisting] +.... + mov $part4,%si # Partition + cmpb $0x80,%dl # Hard drive? + jb main.4 # No + movb $0x1,%dh # Block count + callw nread # Read MBR +.... + +.[.filename]#stand/i386/boot2/boot1.S# [[boot-boot1-find-freebsd]] +В приведённом выше коде регистр `%dl` содержит информацию о загрузочном устройстве. Эти данные передаются BIOS и сохраняются MBR. Числа `0x80` и выше указывают на то, что мы имеем дело с жёстким диском, поэтому вызывается `nread`, где считывается MBR. Аргументы для `nread` передаются через `%si` и `%dh`. Адрес памяти по метке `part4` копируется в `%si`. Этот адрес памяти содержит "фальшивый раздел", который будет использован `nread`. Ниже приведены данные фальшивого раздела: + +[.programlisting] +.... + part4: + .byte 0x80, 0x00, 0x01, 0x00 + .byte 0xa5, 0xfe, 0xff, 0xff + .byte 0x00, 0x00, 0x00, 0x00 + .byte 0x50, 0xc3, 0x00, 0x00 +.... + +.[.filename]#stand/i386/boot2/boot1.S# [[boot-boot2-make-fake-partition]] +В частности, LBA для этой фиктивной раздела жестко закодирован как ноль. Это используется как аргумент для BIOS при чтении абсолютного сектора один с жесткого диска. Альтернативно, может использоваться адресация CHS. В этом случае, фиктивный раздел содержит цилиндр 0, головку 0 и сектор 1, что эквивалентно абсолютному сектору один. + +Продолжим, рассмотрев `nread`: + +[.programlisting] +.... +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 +.... + +.[.filename]#stand/i386/boot2/boot1.S# [[boot-boot1-nread]] +Напомним, что `%si` указывает на поддельный раздел. Слово footnote:[В контексте 16-битного реального режима слово — это 2 байта.] по смещению `0x8` копируется в регистр `%ax`, а слово по смещению `0xa` — в `%cx`. BIOS интерпретирует их как младшее 4-байтовое значение, обозначающее LBA для чтения (старшие четыре байта предполагаются нулевыми). Регистр `%bx` содержит адрес памяти, куда будет загружен MBR. Инструкция, помещающая `%cs` в стек, очень интересна. В данном контексте она ничего не делает. Однако, как мы скоро увидим, [.filename]#boot2# в сочетании с сервером BTX также использует `xread.1`. Этот механизм будет рассмотрен в следующем разделе. + +Код в `xread.1` далее вызывает функцию `read`, которая фактически обращается к BIOS с запросом на чтение сектора диска: + +[.programlisting] +.... +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 +.... + +.[.filename]#stand/i386/boot2/boot1.S# [[boot-boot1-xread1]] +Обратите внимание на длинную инструкцию возврата в конце этого блока. Эта инструкция извлекает регистр `%cs`, помещённый в стек `nread`, и возвращает управление. В конце `nread` также возвращает управление. + +С загрузкой MBR в память начинается фактический цикл поиска слайса FreeBSD: + +[.programlisting] +.... + 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 +.... + +.[.filename]#stand/i386/boot2/boot1.S# [[boot-boot1-find-part]] +Если обнаружен слайс FreeBSD, выполнение продолжается на метке `main.5`. Обратите внимание, что при обнаружении слайса FreeBSD `%si` указывает на соответствующую запись в таблице разделов, а `%dh` содержит номер раздела. Мы предполагаем, что слайс FreeBSD найден, поэтому продолжаем выполнение на метке `main.5`: + +[.programlisting] +.... +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 +.... + +.[.filename]#stand/i386/boot2/boot1.S# [[boot-boot1-main5]] +Напомним, что в данный момент регистр `%si` указывает на запись среза FreeBSD в таблице разделов MBR, поэтому вызов `nread` фактически прочитает секторы в начале этого раздела. Аргумент, переданный в регистре `%dh`, указывает `nread` прочитать 16 секторов диска. Напомним, что первые 512 байт, или первый сектор слайса FreeBSD, совпадает с программой [.filename]#boot1#. Также напомним, что файл, записанный в начало слайса FreeBSD, это не [.filename]#/boot/boot1#, а [.filename]#/boot/boot#. Давайте посмотрим на размер этих файлов в файловой системе: + +[source, bash] +.... +-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 +.... + +Оба файла [.filename]#boot0# и [.filename]#boot1# имеют размер 512 байт каждый, поэтому они занимают _ровно_ один сектор диска. [.filename]#boot2# значительно больше, так как содержит как сервер BTX, так и клиент [.filename]#boot2#. Наконец, файл под названием просто [.filename]#boot# на 512 байт больше, чем [.filename]#boot2#. Этот файл представляет собой объединение [.filename]#boot1# и [.filename]#boot2#. Как уже отмечалось, [.filename]#boot0# записывается в самый первый сектор диска (MBR), а [.filename]#boot# записывается в первый сектор раздела FreeBSD; [.filename]#boot1# и [.filename]#boot2# _не_ записываются на диск. Команда, используемая для объединения [.filename]#boot1# и [.filename]#boot2# в единый файл [.filename]#boot#, выглядит просто как `cat boot1 boot2 > boot`. + +Итак, [.filename]#boot1# занимает ровно первые 512 байт [.filename]#boot#, и, поскольку [.filename]#boot# записывается в первый сектор слайса FreeBSD, [.filename]#boot1# полностью помещается в этот первый сектор. Когда `nread` читает первые 16 секторов слайса FreeBSD, он фактически читает весь файл [.filename]#boot# footnote:[512*16=8192 байта, ровно размер boot]. Более подробно о том, как [.filename]#boot# формируется из [.filename]#boot1# и [.filename]#boot2#, мы увидим в следующем разделе. + +Напомним, что `nread` использует адрес памяти `0x8c00` в качестве буфера передачи для хранения прочитанных секторов. Этот адрес выбран не случайно. Действительно, поскольку [.filename]#boot1# принадлежит первым 512 байтам, он оказывается в диапазоне адресов `0x8c00`-`0x8dff`. Следующие 512 байт (диапазон `0x8e00`-`0x8fff`) используются для хранения _bsdlabel_ footnote:[Исторически известной как disklabel. Если вам когда-либо было интересно, где FreeBSD хранит эту информацию, она находится в этой области — см. man:bsdlabel[8]]. + +Начиная с адреса `0x9000` находится начало сервера BTX, и сразу за ним следует клиент [.filename]#boot2#. Сервер BTX действует как ядро и выполняется в защищённом режиме с наивысшим уровнем привилегий. В отличие от этого, клиенты BTX (например, [.filename]#boot2#) выполняются в пользовательском режиме. Мы увидим, как это реализовано, в следующем разделе. Код после вызова `nread` находит начало [.filename]#boot2# в буфере памяти и копирует его по адресу `0xc000`. Это связано с тем, что сервер BTX размещает [.filename]#boot2# для выполнения в сегменте, начинающемся с `0xa000`. Мы подробно рассмотрим это в следующем разделе. + +Последний блок кода в [.filename]#boot1# разрешает доступ к памяти выше 1MB footnote:[Это необходимо по историческим причинам.] и завершается переходом к начальной точке сервера BTX: + +[.programlisting] +.... +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 +.... + +.[.filename]#stand/i386/boot2/boot1.S# [[boot-boot1-seta20]] +Обратите внимание, что непосредственно перед переходом прерывания включаются. + +[[btx-server]] +== Сервер BTX + +Далее в нашей последовательности загрузки идёт сервер BTX. Давайте быстро вспомним, как мы сюда попали: + +* BIOS загружает абсолютный сектор один (MBR или [.filename]#boot0#) по адресу `0x7c00` и переходит туда. +* [.filename]#boot0# перемещает себя по адресу `0x600`, по которому он был слинкован для выполнения, и переходит туда. Затем он читает первый сектор среза FreeBSD (который содержит [.filename]#boot1#) в адрес `0x7c00` и переходит туда. +* [.filename]#boot1# загружает первые 16 секторов среза FreeBSD по адресу `0x8c00`. Эти 16 секторов, или 8192 байта, представляют собой весь файл [.filename]#boot#. Файл является объединением [.filename]#boot1# и [.filename]#boot2#. [.filename]#boot2#, в свою очередь, содержит сервер BTX и клиент [.filename]#boot2#. Наконец, выполняется переход по адресу `0x9010`, точке входа сервера BTX. + +Прежде чем изучать сервер BTX подробно, давайте рассмотрим, как создается единый, всеобъемлющий файл [.filename]#boot#. Способ сборки [.filename]#boot# определен в его [.filename]#Makefile# ([.filename]#stand/i386/boot2/Makefile#). Рассмотрим правило, которое создает файл [.filename]#boot#: + +[.programlisting] +.... + boot: boot1 boot2 + cat boot1 boot2 > boot +.... + +.[.filename]#stand/i386/boot2/Makefile# [[boot-boot1-make-boot]] +Это говорит нам, что [.filename]#boot1# и [.filename]#boot2# необходимы, и правило просто объединяет их для создания одного файла с именем [.filename]#boot#. Правила для создания [.filename]#boot1# также довольно просты: + +[.programlisting] +.... + 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 +.... + +.[.filename]#stand/i386/boot2/Makefile# [[boot-boot1-make-boot1]] +Для применения правила создания [.filename]#boot1# необходимо собрать [.filename]#boot1.out#. Это, в свою очередь, зависит от наличия [.filename]#boot1.o#. Последний файл является результатом ассемблирования нашего знакомого [.filename]#boot1.S# без компоновки. Теперь применяется правило создания [.filename]#boot1.out#. Оно указывает, что [.filename]#boot1.o# должен быть скомпонован с точкой входа `start` и начальным адресом `0x7c00`. Наконец, [.filename]#boot1# создается из [.filename]#boot1.out# применением соответствующего правила. Это команда [.filename]#objcopy#, применяемая к [.filename]#boot1.out#. Обратите внимание на флаги, передаваемые [.filename]#objcopy#: `-S` указывает на удаление всей информации о перемещении и символов; `-O binary` указывает формат вывода, то есть простой, неформатированный двоичный файл. + +Имея [.filename]#boot1#, давайте посмотрим, как устроен [.filename]#boot2#: + +[.programlisting] +.... + 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} +.... + +.[.filename]#stand/i386/boot2/Makefile# [[boot-boot1-make-boot2]] +Механизм сборки [.filename]#boot2# гораздо сложнее. Отметим наиболее важные моменты. Список зависимостей выглядит следующим образом: + +[.programlisting] +.... + 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 +.... + +.[.filename]#stand/i386/boot2/Makefile# [[boot-boot1-make-boot2-more]] +Отметим, что изначально файл заголовка [.filename]#boot2.h# отсутствует, но его создание зависит от [.filename]#boot1.out#, который у нас уже есть. Правило его создания немного лаконично, но важно то, что результат, [.filename]#boot2.h#, выглядит примерно так: + +[.programlisting] +.... +#define XREADORG 0x725 +.... + +.[.filename]#stand/i386/boot2/boot2.h# [[boot-boot1-make-boot2h]] +Напомним, что [.filename]#boot1# был перемещён (т.е. скопирован из `0x7c00` в `0x700`). Это перемещение теперь обретает смысл, потому что, как мы увидим, сервер BTX освобождает часть памяти, включая область, куда [.filename]#boot1# был изначально загружен. Однако серверу BTX необходим доступ к функции `xread` из [.filename]#boot1#; согласно выводу [.filename]#boot2.h#, эта функция находится по адресу `0x725`. Действительно, сервер BTX использует функцию `xread` из перемещённого кода [.filename]#boot1#. Теперь эта функция доступна из клиента [.filename]#boot2#. + +Следующее правило указывает компоновщику на необходимость связать различные файлы ([.filename]#ashldi3.o#, [.filename]#boot2.o# и [.filename]#sio.o#). Обратите внимание, что выходной файл [.filename]#boot2.out# компонуется для выполнения по адресу `0x2000` (${ORG2}). Напомним, что [.filename]#boot2# будет выполняться в пользовательском режиме внутри специального пользовательского сегмента, созданного сервером BTX. Этот сегмент начинается с адреса `0xa000`. Также помните, что часть [.filename]#boot2# в [.filename]#boot# была скопирована по адресу `0xc000`, то есть со смещением `0x2000` от начала пользовательского сегмента, поэтому [.filename]#boot2# будет работать корректно при передаче управления на него. Далее, [.filename]#boot2.bin# создается из [.filename]#boot2.out# путем удаления символов и информации о формате; boot2.bin представляет собой _сырой_ бинарный файл. Теперь обратите внимание, что файл [.filename]#boot2.ldr# создается как 512-байтный файл, заполненный нулями. Это пространство зарезервировано для bsdlabel. + +Теперь, когда у нас есть файлы [.filename]#boot1#, [.filename]#boot2.bin# и [.filename]#boot2.ldr#, осталось только добавить сервер BTX перед созданием универсального файла [.filename]#boot#. Сервер BTX находится в [.filename]#stand/i386/btx/btx#; у него есть собственный [.filename]#Makefile# со своим набором правил для сборки. Важно отметить, что он также компилируется как _сырой_ бинарный файл и линкуется для выполнения по адресу `0x9000`. Подробности можно найти в [.filename]#stand/i386/btx/btx/Makefile#. + +Имея файлы, составляющие программу [.filename]#boot#, последним шагом является их _объединение_. Это выполняется специальной программой под названием [.filename]#btxld# (исходный код расположен в [.filename]#/usr/src/usr.sbin/btxld#). Некоторые аргументы этой программы включают имя выходного файла ([.filename]#boot#), его точку входа (`0x2000`) и формат файла (бинарный). Различные файлы окончательно объединяются этой утилитой в файл [.filename]#boot#, который состоит из [.filename]#boot1#, [.filename]#boot2#, `bsdlabel` и сервера BTX. Этот файл, занимающий ровно 16 секторов или 8192 байта, записывается в начало раздела FreeBSD во время установки. Теперь перейдем к изучению программы сервера BTX. + +Сервер BTX подготавливает простое окружение и переключается из 16-битного реального режима в 32-битный защищённый режим, непосредственно перед передачей управления клиенту. Это включает инициализацию и обновление следующих структур данных: + +* Изменяет `Таблицу Векторов Прерываний (IVT)`. IVT предоставляет обработчики исключений и прерываний для кода в Реальном Режиме. +* Создается `Таблица дескрипторов прерываний (IDT)`. В ней предусмотрены записи для исключений процессора, аппаратных прерываний, двух системных вызовов и интерфейса V86. IDT предоставляет обработчики исключений и прерываний для кода в защищенном режиме. +* Создается `Сегмент состояния задачи (TSS)`. Это необходимо, потому что процессор работает на _наименее_ привилегированном уровне при выполнении клиента ([.filename]#boot2#), но на _наиболее_ привилегированном уровне при выполнении сервера BTX. +* Устанавливается GDT (Глобальная Таблица Дескрипторов). Создаются записи (дескрипторы) для кода и данных супервизора, кода и данных пользователя, а также кода и данных реального режима. footnote:[Код и данные реального режима необходимы при переключении обратно в реальный режим из защищённого режима, как указано в руководствах Intel.] + +Приступим к изучению фактической реализации. Напомним, что [.filename]#boot1# выполнил переход на адрес `0x9010` — точку входа сервера BTX. Прежде чем изучать выполнение программы там, обратите внимание, что сервер BTX имеет специальный заголовок в диапазоне адресов `0x9000-0x900f`, непосредственно перед точкой входа. Этот заголовок определён следующим образом: + +[.programlisting] +.... +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 +.... + +.[.filename]#stand/i386/btx/btx/btx.S# [[btx-header]] +Обратите внимание, что первые два байта — это `0xeb` и `0xe`. В архитектуре IA-32 эти два байта интерпретируются как относительный переход за заголовок к точке входа, поэтому теоретически [.filename]#boot1# мог бы перейти сюда (адрес `0x9000`) вместо адреса `0x9010`. Обратите внимание, что последнее поле в заголовке BTX — это указатель на точку входа клиента ([.filename]#boot2#)b2. Это поле исправляется во время компоновки. + +Сразу после заголовка следует точка входа сервера BTX: + +[.programlisting] +.... +/* + * 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 +.... + +.[.filename]#stand/i386/btx/btx/btx.S# [[btx-init]] +Этот код отключает прерывания, устанавливает рабочий стек (начиная с адреса `0x1800`) и очищает флаги в регистре EFLAGS. Обратите внимание, что инструкция `popfl` извлекает двойное слово (4 байта) из стека и помещает его в регистр EFLAGS. Поскольку извлекаемое значение фактически равно `2`, регистр EFLAGS эффективно очищается (IA-32 требует, чтобы бит 2 регистра EFLAGS всегда был равен 1). + +Следующий блок кода очищает (устанавливает в `0`) диапазон памяти `0x5e00-0x8fff`. В этом диапазоне будут созданы различные структуры данных: + +[.programlisting] +.... +/* + * Initialize memory. + */ + mov $MEM_IDT,%di # Memory to initialize + mov $(MEM_ORG-MEM_IDT)/2,%cx # Words to zero + rep # Zero-fill + stosw # memory +.... + +.[.filename]#stand/i386/btx/btx/btx.S# [[btx-clear-mem]] +Напомним, что [.filename]#boot1# изначально загружался по адресу `0x7c00`, поэтому при такой инициализации памяти эта копия фактически исчезла. Однако также напомним, что [.filename]#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: + +[.programlisting] +.... +/* + * 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 +.... + +.[.filename]#stand/i386/btx/btx/btx.S# [[btx-ivt]] +Следующий блок создает IDT (таблицу дескрипторов прерываний). IDT в защищенном режиме аналогична IVT в реальном режиме. То есть, IDT описывает различные обработчики исключений и прерываний, используемые, когда процессор работает в защищенном режиме. По сути, она также состоит из массива пар сегмент/смещение, хотя структура несколько сложнее, поскольку сегменты в защищенном режиме отличаются от реального режима, и применяются различные механизмы защиты: + +[.programlisting] +.... +/* + * 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 +.... + +.[.filename]#stand/i386/btx/btx/btx.S# [[btx-idt]] +Каждая запись в `IDT` имеет длину 8 байт. Помимо информации о сегменте/смещении, они также описывают тип сегмента, уровень привилегий и присутствует ли сегмент в памяти. Структура организована так, что векторы прерываний от `0` до `0xf` (исключения) обрабатываются функцией `intx00`; вектор `0x10` (также исключение) обрабатывается `intx10`; аппаратные прерывания, которые позже настраиваются начиная с вектора `0x20` и до вектора `0x2f`, обрабатываются функцией `intx20`. Наконец, вектор прерывания `0x30`, используемый для системных вызовов, обрабатывается `intx30`, а векторы `0x31` и `0x32` обрабатываются `intx31`. Необходимо отметить, что только дескрипторы для векторов прерываний `0x30`, `0x31` и `0x32` имеют уровень привилегий 3, такой же, как у клиента [.filename]#boot2#, что означает, что клиент может выполнить программно-генерируемое прерывание к этим векторам через инструкцию `int` без ошибки (это способ, которым [.filename]#boot2# использует сервисы, предоставляемые сервером BTX). Также обратите внимание, что _только_ программно-генерируемые прерывания защищены от кода, выполняющегося на более низких уровнях привилегий. Аппаратно-генерируемые прерывания и исключения, генерируемые процессором, _всегда_ обрабатываются корректно, независимо от фактических привилегий. + +Следующий шаг — инициализация TSS (сегмента состояния задачи). TSS — это аппаратная функция, которая помогает операционной системе или исполнительному ПО реализовать многозадачность через абстракцию процессов. Архитектура IA-32 требует создания и использования _как минимум_ одного TSS, если используются механизмы многозадачности или определены различные уровни привилегий. Поскольку клиент [.filename]#boot2# выполняется на уровне привилегий 3, а сервер BTX работает на уровне привилегий 0, необходимо определить TSS: + +[.programlisting] +.... +/* + * 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 +.... + +.[.filename]#stand/i386/btx/btx/btx.S# [[btx-tss]] +Обратите внимание, что в TSS указано значение для указателя стека и сегмента стека уровня привилегий 0. Это необходимо, потому что если прерывание или исключение получено во время выполнения [.filename]#boot2# на уровне привилегий 3, процессор автоматически переключается на уровень привилегий 0, поэтому требуется новый рабочий стек. Наконец, полю базового адреса карты ввода-вывода TSS присваивается значение, которое представляет собой 16-битное смещение от начала TSS до битовой карты разрешений ввода-вывода и битовой карты перенаправления прерываний. + +После создания IDT и TSS процессор готов к переходу в защищённый режим. Это выполняется в следующем блоке: + +[.programlisting] +.... +/* + * 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 +.... + +.[.filename]#stand/i386/btx/btx/btx.S# [[btx-prot]] +Сначала вызывается `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, который мы создали ранее, и настройку окружения пользовательского режима перед передачей управления исполнения клиенту [.filename]#boot2#. + +[.programlisting] +.... +/* + * 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 +.... + +.[.filename]#stand/i386/btx/btx/btx.S# [[btx-end]] +Обратите внимание, что среда клиента включает селектор сегмента стека и указатель стека (регистры `%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. Это значение является указателем на точку входа [.filename]#boot2#. Оно помещается в регистр `%eip` — регистр указателя инструкций. Затем селектор сегмента кода пользователя извлекается и копируется в регистр `%cs`. Помните, что уровень привилегий этого сегмента — 3, наименее привилегированный уровень. Это означает, что мы должны предоставить значения для стека этого уровня привилегий. Именно поэтому процессор, помимо дальнейшего извлечения значения для регистра EFLAGS, выполняет ещё два извлечения из стека. Эти значения попадают в указатель стека (`%esp`) и сегмент стека (`%ss`). Теперь выполнение продолжается с точки входа ``boot0``. + +Важно отметить, как определяется сегмент пользовательского кода. _Базовый адрес_ этого сегмента установлен на `0xa000`. Это означает, что адреса памяти кода являются _относительными_ к адресу 0xa000; если код, который выполняется, извлекается из адреса `0x2000`, _фактический_ адрес в памяти будет `0xa000+0x2000=0xc000`. + +[[boot2]] +== Этап загрузки boot2 + +`boot2` определяет важную структуру, `struct bootinfo`. Эта структура инициализируется `boot2` и передаётся загрузчику, а затем ядру. Некоторые узлы этой структуры устанавливаются `boot2`, остальные — загрузчиком. Эта структура, среди прочей информации, содержит имя файла ядра, геометрию жесткого диска в BIOS, номер диска в BIOS для загрузочного устройства, доступную физическую память, указатель `envp` и т.д. Ее определение выглядит так: + +[.programlisting] +.... +/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()` загружает файл по умолчанию ([.filename]#/boot/loader#). Функции `ino_t lookup(char *filename)` и `int xfsread(ino_t inode, void *buf, size_t nbyte)` используются для чтения содержимого файла в память. [.filename]#/boot/loader# — это ELF-бинарный файл, но с заголовком ELF, перед которым добавлена структура `struct exec` из [.filename]#a.out#. `load()` анализирует ELF-заголовок загрузчика, загружает содержимое [.filename]#/boot/loader# в память и передаёт управление на точку входа загрузчика: + +[.programlisting] +.... +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)); +.... + +[[boot-loader]] +== Этап загрузчика (loader) + +Загрузчик также является клиентом BTX. Я не буду подробно описывать его здесь, существует исчерпывающая man-страница, написанная Майком Смитом: man:loader[8]. Основные механизмы и BTX были рассмотрены выше. + +Основная задача загрузчика — загрузить ядро. Когда ядро загружено в память, загрузчик вызывает его: + +[.programlisting] +.... +stand/common/boot.c: + /* Call the exec handler from the loader matching the kernel */ + file_formats[fp->f_loader]->l_exec(fp); +.... + +[[boot-kernel]] +== Инициализация ядра + +Давайте рассмотрим команду, которая компонует ядро. Это поможет определить точное местоположение, где загрузчик передаёт выполнение ядру. Это местоположение является фактической точкой входа ядра. Данная команда теперь исключена из [.filename]#sys/conf/Makefile.i386#. Интересующее нас содержимое можно найти в [.filename]#/usr/obj/usr/src/i386.i386/sys/GENERIC/#. + +[.programlisting] +.... +/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, но динамический компоновщик для ядра — это [.filename]#/red/herring#, что явно является фиктивным файлом. Во-вторых, взглянув на файл [.filename]#sys/conf/ldscript.i386#, можно понять, какие параметры ld используются при компиляции ядра. Читая первые несколько строк, видим, что строка + +[.programlisting] +.... +sys/conf/ldscript.i386: +ENTRY(btext) +.... + +говорит, что точка входа ядра — это символ `btext`. Этот символ определён в [.filename]#locore.s#: + +[.programlisting] +.... +sys/i386/i386/locore.s: + .text +/********************************************************************** + * + * This is where the bootblocks start us, set the ball rolling... + * + */ +NON_GPROF_ENTRY(btext) +.... + +Сначала регистр EFLAGS устанавливается в предопределённое значение 0x00000002. Затем инициализируются все сегментные регистры: + +[.programlisting] +.... +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()`, которые также определены в [.filename]#locore.s#. Вот описание их функций: + +[.informaltable] +[cols="1,1", frame="none"] +|=== + +|`recover_bootinfo` +|Эта процедура разбирает параметры, переданные ядру при загрузке. +Ядро могло быть загружено тремя способами: загрузчиком (как описано выше), старыми загрузочными блоками диска или по старой процедуре загрузки без диска. +Эта функция определяет метод загрузки и сохраняет структуру `struct bootinfo` в памяти ядра. + +|`identify_cpu` +|Эта функция пытается определить, на каком процессоре она выполняется, сохраняя найденное значение в переменной `_cpu`. +|=== + +Следующие шаги включают активацию VME, если процессор поддерживает эту функцию: + +[.programlisting] +.... +sys/i386/i386/mpboot.s: + testl $CPUID_VME,%edx + jz 3f + orl $CR4_VME,%eax +3: movl %eax,%cr4 +.... + +Затем, включение подкачки: + +[.programlisting] +.... +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! */ +.... + +Следующие три строки кода необходимы, потому что была установлена подкачка, поэтому требуется переход для продолжения выполнения в виртуализированном адресном пространстве: + +[.programlisting] +.... +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()`, и, вызывая её, завершает загрузку: + +[.programlisting] +.... +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 */ +.... + +=== `init386()` + +`init386()` определена в [.filename]#sys/i386/i386/machdep.c# и выполняет низкоуровневую инициализацию, специфичную для чипа i386. Переход в защищённый режим был выполнен загрузчиком. Загрузчик создал самую первую задачу, в которой ядро продолжает работать. Прежде чем рассматривать код, рассмотрим задачи, которые процессор должен выполнить для инициализации выполнения в защищённом режиме: + +* Инициализировать настраиваемые параметры ядра, переданные из загрузочной программы. +* Подготовить GDT. +* Подготовить IDT. +* Инициализировать системную консоль. +* Инициализировать DDB, если он скомпилирован в ядро. +* Инициализировать TSS. +* Подготовить LDT. +* Настройка pcb для thread0. + +`init386()` инициализирует настраиваемые параметры, переданные из bootstrap, устанавливая указатель окружения (envp) и вызывая `init_param1()`. Указатель envp был передан из loader в структуре `bootinfo`: + +[.programlisting] +.... +sys/i386/i386/machdep.c: + /* Init basic tunables, hz etc */ + init_param1(); +.... + +`init_param1()` определена в [.filename]#sys/kern/subr_param.c#. Этот файл содержит ряд sysctl, а также две функции, `init_param1()` и `init_param2()`, которые вызываются из `init386()`: + +[.programlisting] +.... +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` используется для получения значения из окружения: + +[.programlisting] +.... +/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: + +[.programlisting] +.... +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): + +[.programlisting] +.... +sys/i386/i386/machdep.c: +static struct gate_descriptor idt0[NIDT]; +struct gate_descriptor *idt = &idt0[0]; /* interrupt descriptor table */ +.... + +Для каждого прерывания устанавливается соответствующий обработчик. Также настраивается шлюз системного вызова для `INT 0x80`: + +[.programlisting] +.... +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 инициализируются: + +[.programlisting] +.... +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, включая шлюзы системных вызовов, а также селекторы кода и данных пользователя: + +[.programlisting] +.... +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: + +[.programlisting] +.... +sys/i386/i386/machdep.c: +register_t +init386(int first) +{ + /* ... skipped ... */ + + proc_linkup0(&proc0, &thread0); + /* ... skipped ... */ +} +.... + +Структура `struct pcb` является частью структуры proc. Она определена в [.filename]#/usr/include/machine/pcb.h# и содержит информацию процесса, специфичную для архитектуры i386, такую как значения регистров. + +=== `mi_startup()` + +Эта функция выполняет сортировку пузырьком всех объектов инициализации системы, а затем вызывает вход каждого объекта по очереди: + +[.programlisting] +.... +sys/kern/init_main.c: + for (sipp = sysinit; sipp < sysinit_end; sipp++) { + + /* ... skipped ... */ + + /* Call function */ + (*((*sipp)->func))((*sipp)->udata); + /* ... skipped ... */ + } +.... + +Хотя фреймворк sysinit описан в extref:{developers-handbook}[Руководстве разработчика], я рассмотрю его внутреннее устройство. + +Каждый объект инициализации системы (объект sysinit) создается путем вызова макроса SYSINIT(). Возьмем, к примеру, объект sysinit `announce`. Этот объект выводит сообщение об авторских правах: + +[.programlisting] +.... +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`: + +[.programlisting] +.... +/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()`, и именно в этом макросе скрыта вся магия инициализации системы: + +[.programlisting] +.... +/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 для бинарного файла ядра, можно заметить наличие таких небольших разделов: + +[source, bash] +.... +% 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` будет "собрано" в эту переменную: + +[.programlisting] +.... +sys/kern/init_main.c: + SET_DECLARE(sysinit_set, struct sysinit); +.... + +`struct sysinit` определена следующим образом: + +[.programlisting] +.... +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()` сортирует их и вызывает каждый. Самый последний объект — это системный планировщик: + +[.programlisting] +.... +/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 определен в файле [.filename]#sys/vm/vm_glue.c#, а точка входа для этого объекта — `scheduler()`. Эта функция фактически представляет собой бесконечный цикл и описывает процесс с PID 0, известный как процесс swapper. Структура thread0, упомянутая ранее, используется для его описания. + +Первый пользовательский процесс, называемый _init_, создаётся объектом sysinit `init`: + +[.programlisting] +.... +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()`. Эта функция определена в [.filename]#init_main.c#. Она пытается загрузить и выполнить бинарный файл [.filename]#init#, сначала проверяя [.filename]#/sbin/init#, затем [.filename]#/sbin/oinit#, [.filename]#/sbin/init.bak# и, наконец, [.filename]#/rescue/init#: + +[.programlisting] +.... +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 +.... |
