Загрузка компьютера

В статье подробно рассматривается процесс загрузки компьютера, начиная с чтения boot-сектора и заканчивая загрузкой операционной системы и передаче ей управления. В качестве примера в статье разрабатывается "операционная система", которая выводит на экран традиционное приветствие "Hello, world". Загрузка этой ОС происходит через Grub.

[Максим Савенко]

Скачать файлы примеров

1.1 Загрузка компьютера

Процесс загрузки компьютера, на всю кажущуюся простоту, представляет собой достаточно сложный процесс. После включения компьютера блок питания проверяет все необходимые уровни напряжений, если все уровни напряжений будут соответствуют номинальным, то на материнскую плату поступит соответствующий сигнал - PowerGood. В первоначальном состоянии на вход процессора подается сигнал RESET, который удерживает процессор в сброшенном состоянии. Но после получения сигнала PowerGood от блока питания сигнал RESET будет снят и процессор начнет выполнять свои первые инструкции. Таким образом процессор, после теста на питание, стартует от вполне известного состояния: командный регистр CS содержит 0xFFFF, указатель команд - региcтр IP содержит 0, сегментные регистры данных и стека содержат 0. После снятия RESET процессор выполняет инструкцию по адресу FFFF:0000 (физический адрес соответственно - 0xFFFF0), в реальном режиме, где располагается область ROM BIOS. Его размер, очевидно, составляет 16 байт, вплоть до конца максимально адресуемого адресного пространства в реальном режиме - 0xFFFFF. По этому адресу располагается инструкция перехода на реально исполняемый код BIOS. Исполняя код BIOS компьютер проходит стадию само тестирования POST (Power-On Self Test). Тестируется процессор, память, ресурсы ввода/вывода, а так же конфигурируются программно настраиваемые ресурсы системной платы.

После прохождения процедуры тестирования и конфигурации, компьютер переходит к процессу загрузки операционной системы. Программа BIOS считывает с активного загрузочного диска(определяется настройками BIOS) BOOT сектора, который для флоппи диска или винчестера, располагается по адресу Цилиндр=0, Головка=0, Сектор=1 в память компьютера по физическому адресу 0x7C00. Размер сектора равен 512 байтам. После чего происходит проверка, является ли этот сектор загрузочным. Это осуществляется поиском сигнатуры 0x55AA в конце сектора. Если такой последовательности в конце сектора не обнаружено, то BIOS выдаст сообщение что загрузочный сектор не найден.

рис 1

Рассмотрим процесс загрузки на примере традиционной программы HelloWorld. Листинг программы представлен на рис. Код программы является очень простым, тем не менее давайте разберем его основательно:

7-10
Настраиваемся на сегмент 0x7C0 куда загрузит нас BIOS, посредством дальнего перехода на метку boot.
12-14
Настраиваем сегментные регистры данных на тот же сегмент что и CS.
16-19
Настраиваем сегментный регистр стека и его указатель, предварительно отключив прерывания.
25-32
Выводим строку "HelloWorld", используя функцию 0x0E прерывания 0x10 BIOS - вывод символа в режиме телетайп (последовательно от текущей позиции курсора, после вывода символа, курсор автоматически перемещается). Эта функция принимает следующие параметры: в регистр AL размещается ASCII код выводимого символа, в BH должен быть занесен номер видеостраницы, в BL - байт атрибутов и цвета. Таким образом организуем цикл для вывода каждого символа из hello_str пока не встретится 0 символ.
34-37
Чтобы как-то завершить нашу программу, ведь мы работаем на чистом "железе" и никаких операционных систем нет, чтобы мы им могли передать управление, используем инструкцию hlt, предварительно сбросив аппаратные прерывания. Другими словами просто "подвесим" машину.
39-41
Область данных, где мы размещаем выводимую строку "HelloWorld".
42-45
Размещаем по адресу 510 флаг загрузки два байта 0x55,0xAA. Вы наверняка знаете что Intel при размещении чисел в памяти располагает старший байт по старшим адресам, таким образом число .word 0xAA55, будет храниться как последовательность байт 0x55,0xAA, что нам и нужно. Конечно мы могли бы написать и .byte 0x55,0xAA, и результат был бы тем же самым.
Листинг hello.S

 1   #define BOOT_SEG	0x7C0
 2   
 3   .code16
 4   
 5   .global _start
 6   
 7   _start:
 8   	/* Перезагружаем сегменттный регистр комманд */
 9   	ljmp	$BOOT_SEG,$boot
10   boot:
11   	/* Настраиваемся на сегмент BOOT_SEG */
12   	movw	%cs,%ax
13   	movw	%ax,%ds
14   	movw	%ax,%es
15   	/* Большой стек нам не нужен, этого нам вполне хватит */
16   	cli
17   	movw	%ax,%ss
18   	movw	$_start,%sp
19   	sti
20   	
21   	/* 
22   	    Используем функцию 0x0E прерывания 0x10 для вывода 
23   	    символов в режиме телетайпа 
24   	*/
25   	movb	$0xe,%ah
26   	movw	$0x0037,%bx
27   	movw	$hello_str,%si

28   1:	
29   	lodsb	
30   	int	$0x10	
31   	test	%al,%al
32   	jnz	1b
33   
34   	cli
35   1:
36   	hlt
37   	jmp	1b
38   
39   hello_str:
40   	.string		"Hello World!"
41   	.byte		0
42   .org 	510
43   	/* Флаг идентифицирующий загрузочный код */
44   boot_flag:
45   	.word		0xAA55

Давайте рассмотрим процесс сборки нашего приложения. Конечно можно было бы заниматься сборкой, постоянно выполняя одни и те же последовательности команд, но лучше будет автоматизировать этот процесс. Напишем Makefile для сборки нашего проекта. Я надеюсь что читатель хотя бы поверхностно знаком с процессом создания Makefile. Наш Makefile представлен на рис. Разберем его:

1
Объявляем имя нашей главной цели, для того чтобы, в случае если нам это имя не понравиться, мы смогли бы поменять его в одном месте.
3-4
Главное правило, которое будет выполняться после ввода make без параметров. Видно, что сборка нашей цели зависит от правила hello.o в строке 6. Таким образом будет выполнено правило hello.o и после того когда будет построен объектный файл hello.o, будут выполнены действия главного правила - строка 4. В строке 4 указана последовательность действий главного правила - вызов линковщика ld который компонует секцию кода объектного файла начиная с нулевого адреса, формат формируемого файла - бинарный. (переменная $(LD) - это предопределенная в make переменная, которая содержит имя линковщика.)
6
Правило для сборки объектного файла hello.o из промежуточного файла hello.s. Промежуточный файл hello.s это тот же самый файл hello.S после обработки его препроцессором языка C. Зачем это нужно? Конечно для того чтобы воспользоваться всей мощью директив препроцессора языка С, таких как #include, #define, #ifdef, #else и пр. Обратите внимание, что в листинге hello.S в строке 1 мы объявили макрос BOOT_SEG. Если мы передадим такой файл непосредственно компилятору ассемблера, то он нам выдаст ошибку. А если мы прогоним этот файл через препроцессор С, то макрос в коде будет заменен на его значение, и после этого полученный файл hello.s будет передан компилятору ассемблера. Цель таких махинаций - улучшить читаемость кода, и получить возможность пользоваться мощью условной компиляции языка С. Надо заметить что такая практика, используется достаточно часто, и это действительно удобно. Для выполнения правила hello.o, необходимо наличие файла hello.s, для которого есть свое правило в строке 7. Как только файл будет готов, будет произведена его компиляция в файл hello.o.
7
Правило для формирования промежуточного файла hello.s из файла hello.S. Подробное описание см. 6. Надо лишь заметить, что последовательность действий для правил 6 и 7 не указано, однако make сам разберется как построить файл hello.s из hello.S, а затем из hello.s -hello.o. Вы спросите как? Да очень просто, make будет ориентироваться по расширениям файлов. Он знает чтобы сделать из файла .S файл .s надо запустить препроцессор C, а для того чтобы из файла .s сделать файл .o надо запустить компилятор ассемблера, который построит объектный файл. Согласитесь это очень удобно.
10-11
Правило clean, которое будет использоваться для очистки директории нашего проекта, от всех промежуточных файлов, которые появятся в процессе сборки. Таким образом для того чтобы почистить директорию нашего проекта нам нужно будет ввести команду make clean, и это правило будет выполнено.
12-13
Правило для формирования файла .bochsrc для нашей виртуальной машины. Если нам нужно будет изменить параметры, то мы их поправим в файле bochsrc и сделаем make bochsrc, который и сделает нам новый конфигурационный файл для bochs. Конечно этого правила можно было бы и не делать, но мне это показалось удобным ввиду моих настроек "видимости" точечных файлов, которые я менять не захотел.
Листинг Makefile

 1   TARGET:=boot.img
 2   
 3   $(TARGET): hello.o
 4   	$(LD) -Ttext 0x0 -s --oformat binary -o $@ $<
 5   
 6   hello.o: hello.s
 7   hello.s: hello.S
 8   
 9   .PHONY: clean bochs
10   clean:
11   	rm -f *.o *.s *.log $(TARGET)
12   bochs:
13   	cp -f bochsrc .bochsrc

Теперь все готово, для сборки. Осталось перейти в каталог нашего проекта и выполнить сборку проекта:



[maxix@host ~]$ cd src/book/src/helloworld
[maxix@host helloworld]$ make

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

Давайте рассмотрим файл шаблона конфигурации bochsrc:

1
Подключаем типовой файл конфигурации, с общими для всех настройками.
5
Будем эмулировать нашу машину с 64Мб памяти.
8
Указываем что будем использовать в качестве дисковода наш файл boot.img, и устанавливаем его в состояние inserted, т.е. после старта машины "дискета" будет вставлена.
11
Делаем дисковвод с нашей "дискетой" основным устройством загрузки. Именно на нем BIOS будет производить поиск загрузочного сектора.
Листинг bochsrc

 1   #include ../bochsrc.default
 2   
 3   # Объем памяти который будет размещен на нашей виртуальной
 4   # машине
 5   megs: 64
 6   
 7   # Используем для дискеты имя нашего образа
 8   floppya: 1_44=boot.img, status=inserted
 9   
10   # Назначаем устройство загрузки
11   boot: a
12   

Итак, мы приближаемся к кульминационному моменту, создаем конфигурационный файл для bochs:


[maxix@host ~]$ make bochs
И все готово к запуску. Выполним следующую команду

[maxix@host ~]$ bochs -q

Виртуальная машина будет запущена, и сразу же будет остановлена на первой выполняемой инструкции BIOS по физическому адресу 0xFFFF0, по которому содержится инструкция перехода на реально исполняемый код BIOS. Т.к. У нас нет необходимости разбирать код BIOS то мы вводим команду - продолжить с(continue):


<bochs:1> c

И виртуальная машина продолжит свою работу. Этот процесс показан на следующем рис.

рис 2

Работа виртуальной машины показана на скриншоте рис. Как видим после загрузки был с читан наш загрузочный сектор, BIOS передал ему управление и была выведена строка "Hello World!"

У Вас может появиться желание просмотреть каждую выполняемую строчку нашего кода. Bochs имеет встроенный дизассемблер и отладчик что позволяет проводить отладку кода. Для этого, после запуска виртуальной машины надо указать точку останова и выполнить команду продолжить, как только виртуальная машина достигнет этой точки она остановиться и Вы сможете продолжить выполнение кода по-командно. Вот как можно было бы запустить нашу машину для трассировки нашего кода:


<bochs:1> lb 0x7c00
<bochs:2> c

Скриншот показан на рис. Как видно наша виртуальная машина после загрузки бутсектора, передала ему управление по физическому адресу 0x7C00 и остановилась на инструкции jmp 07c0:0005 в ожидании дальнейших распоряжений. Для трассировки каждой последующей команды надо нажимать n (next) либо сделать это один раз а затем нажимать Enter который будет повторно выполнять последнюю введенную команду.

рис 3

Вы так же можете попробовать создать загрузочную дискету с нашим загрузочным сектором и загрузить ваш компьютер. Для этого нужно вставить дискету в компьютер (ВНИМАНИЕ!!! Вся информация на дискете может быть уничтожена!) и выполнить следующую команду:


[maxix@host helloworld]$ dd if=boot.img of=/dev/fd0

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

рис 4

1.2 Загрузка операционных систем

При создании операционной системы, разработчики вынуждены постоянно решать одну и ту же проблему - написание загрузчика для их системы. Иногда "изобретение велосипеда" бывает чрезвычайно утомительным. Зачастую создаются целые цепочки загрузок, т.е. Загрузка системы получается многоуровневой. В идеале все должно быть тривиально, BIOS, как мы видели загружает бутсектор, а он загружает операционную систему в память компьютера. Но не всегда все получается так. Размер бутсектора всего 512 байт, это означает что в него должен поместиться весь необходимый код для осуществления загрузки операционной системы. Уместить в 512 байт весь необходимый код может и не получиться. Тогда прибегают к использованию загрузчика второго уровня. Получается следующая картина - загружается бутсектор, который загружает загрузчик второго уровня способного загрузить всю операционную систему, что он и делает, попутно занимаясь дополнительными обязанностями, которые на него возложит разработчик, например сбор информации о системе в реальном режиме, перевод процессора в защищенный режим, и др. задачи первоначальной инициализации.

Согласитесь это не совсем рационально. Ко всему прочему, пользователь захочет иметь на своем компьютере еще несколько операционных систем, и каждая из них будет иметь свой механизм загрузки, который может противоречить остальным. Для решения этих проблем организация Free Software Foundation вышла с предложением унифицировать загрузку операционных систем. Создать общий , хорошо известный, интерфейс для загрузки операционных систем. Результатом такого, унифицированного, интерфейса должно быть следующее: любой менеджер загрузки реализующий этот интерфейс может загрузить любую операционную систему реализующий этот же интерфейс. Это должно избавить разработчиков операционных систем от головных болей по написанию очередных процедур по загрузке операционных систем, возложив эту обязанность на уже готовых и прекрасно при этом написанных и надежных, менеджеров загрузки, например как GRUB, который кстати уже полностью реализует данный интерфейс.

Следующая глава будет посвящена спецификации на этот интерфейс получившей название Multiboot.

1.3 Спецификация Multiboot

Данная спецификация определяет целевую платформу - PC, как наиболее распространенную, и тем самым самую проблемную, т.к. Для неё существует великое множество операционных систем и менеджеров загрузки. Хотя необходимость такой спецификации и для других архитектур, не отбрасывается. Спецификация рассматривает 32х битные операционные системы, и в качестве основных выделяет Linux, FreeBSD, NetBSD, Mach и VSTa. Так же выражается надежда, на то что разработчики новых операционных систем будут придерживаться этой спецификации с самого начала своей работы над новыми операционными системами (было бы прекрасно если бы разработчики коммерческих операционных систем поступали бы так же).

Предполагается что источник загрузки может быть абсолютно любым, например дискета, жесткий диск, или с какого-то сетевого ресурса. Эти способности отводятся напрямую создателям менеджеров загрузки. Они сами определяют какие возможности сможет предложить их менеджер. Многие операционные системы могут потребовать ту или иную информацию в процессе загрузки от менеджера(например размер свободной памяти), а возможно даже и потребовать от менеджера выполнить какие либо действия по инициализации оборудования, например выставить тот или иной графический режим. Спецификация определяет эти взаимоотношения.

Чтобы облегчить жизнь создателям операционных систем, спецификация ставит за основу тот факт что образ операционной системы должен создаваться очень просто, скорее в формате какого либо исполняемого файла, например ELF, и он должен с легкостью обрабатываться какой нибудь утилитой, например nm или оbjdump, для возможности просмотра таблиц символов или его дизассемблирования. Необходимость перехода в защищенный режим в модуле загрузки операционной системы (для размещения образа операционной системы по адресу выше 1Мб), теперь не должно быть проблемой для разработчиков, т.к. Эту работу выполнит менеджер загрузки. Некоторые операционные системы не могут выполнить всю работу по первоначальной инициализации сами, и требуют загрузки дополнительных модулей в память компьютера. По этой причине в спецификации определяются требования к менеджерам загрузки, дать такую возможность, и описывает механизм передачи информации о загружаемых модулях ядру системы.

Итак спецификация рассматривает три главные задачи

  • Формат образа операционной системы. Как должно выглядеть ядро операционной системы для менеджера загрузки.
  • Состояние компьютера/процессора в момент когда будет передано управление операционной системы.
  • Какая информация должна быть передана операционной системе от менеджера загрузки.

Данная спецификация является достаточно новой и соответственно достаточно "сырой". Но тем не менее попробую преподнести её близко к тексту со своими комментариями и пояснениями.

1.3.1 Формат образа операционной системы

Образ операционной системы должен содержать заголовок multiboot в пределах 8192 байт от начала образа операционной системы в любом удобном месте. Заголовок multiboot должен при этом быть выровнен по границе 4 байт (32 бит). Формат заголовка multiboot приведен в следующей таблице:

Позиция от начала(байт) Размер поля(байт) Символьное имя Комментарии
0 4 magic Обязательно
4 4 flags Обязательно
8 4 checksum Обязательно
12 4 header_addr Если установлен flags[16]
16 4 load_addr Если установлен flags[16]
20 4 load_end_addr Если установлен flags[16]
24 4 bss_end_addr Если установлен flags[16]
28 4 entry_addr Если установлен flags[16]
32 4 mode_type Если установлен flags[2]
36 4 width Если установлен flags[2]
40 4 height Если установлен flags[2]
44 4 depth Если установлен flags[2]
magic
- Поле идентифицирующее заголовок multiboot, это поле должно содержать значение 0x1BADB002.
flags
- Представляет собой набор битов, установка которых определяет ту информацию которую должен передать операционной системе загрузчик, или те действия которые операционная система просит выполнить менеджер загрузки. Биты с 0 по 15 описывает те действия которые менеджер загрузки обязан выполнить, в случае если какой либо из них установлен. Если загрузчик не может понять то что от него требуется или просто не может выполнить этих требований, он обязан сообщить об этом пользователю и прервать загрузку операционной системы. Биты 16-31 определяют опциональные особенности. Если загрузчик увидит установленные биты в этом диапазоне, но не сможет их выполнить, он должен просто проигнорировать их и продолжить загрузку операционной системы.
БитДействие в случае если установлен
0 Если установлен, то все модули, загружаемые вместе с операционной системой обязаны быть выровнены по границе страницы - 4кб. Некоторые операционные системы уже в процессе загрузки включают страничную адресацию, и ожидают возможности организовать страничное обращение к загруженным модулям. По этой причине загружаемые модули должны быть выровнены по границе страницы.
1 Если установлен, то загрузчик должен собрать информацию о доступной памяти и передать ее операционной системе через поле mmap_*.(см. Раздел 1.3.3).
2 Если установлен, то ядру операционной системы должна быть передана информация о доступных видео режимах.(см. Раздел 1.3.3)
16 Если установлен, то поля заголовка по смещению 12-28 заголовка multiboot должны быть использованы загрузчиком, нежели чем он будет просматривать заголовок исполняемого файла операционной системы. Информация в этих полях не обязана быть заполнена операционной системой, если в качестве формата файла операционной системы используется ELF, но если формат файла a.out, или какой то другой, то эта информация обязательно должна быть заполнена.
checksum
- Контрольная сумма, значение которое при добавлении к полям magic и flags должно дать ноль.
header_addr
- Поле используется загрузчиком, если будет установлен бит 16 в поле flags. Содержит физический адрес, ссылающийся на начало заголовка multiboot, т.е. Физический адрес, куда будет загружено поле magic структуры multiboot
load_addr
- Поле используется загрузчиком, если будет установлен бит 16 в поле flags. Содержит физический адрес начала сегмента кода. Смещение в фале образа операционной системы, начиная с которого начнется загрузка, определяется местом где будет найден multiboot заголовок, минус (header_addr-load_addr). load_addr должен быть меньше или равен header_addr.
load_end_addr
- Поле используется загрузчиком, если будет установлен бит 16 в поле flags. Определяет физический адрес конца сегмента данных. (load_end_addr-load_addr ) определяю сколько данных должно быть загружено. В основе этого лежит предположение, что секция кода и секция данных расположены последовательно в образе операционной системы. Это к примеру справедливо для формата a.out. Если значение этого поля - ноль, то загрузчик будет полагать, что сегмент кода и данных занимают весь образ операционной системы.
bss_end_addr
- Поле используется загрузчиком, если будет установлен бит 16 в поле flags. Содержит физический адрес конца сегмента bss (не инициализированных данных). Загрузчик инициализирует эту область данных нулями, и зарезервирует эту область памяти для исключения загрузки дополнительных модулей в эту область. Если значением этого поля является ноль, то загрузчик будет считать что область bss отсутствует в образе операционной системы.
entry_addr
- Поле используется загрузчиком, если будет установлен бит 16 в поле flags. Физический адрес куда загрузчик должен передать управление, после загрузки образа операционной системы.

Перед тем как разобрать остальные поля заголовка multiboot, давайте рассмотрим пример. Предположим мы построили ядро операционной системы в виде какого-то формата файла, отличающегося от ELF, давайте посмотрим как мы должны заполнить вышеописанные поля. Во первых мы должны конечно же установить в поле flags бит 16. Предположим так же что мы хотим загрузить нашу операционную систему по физическому адресу 0x100000 т.е. Начиная с 1Мб. Пусть образ операционной системы имеет следующий вид:

рис 6

Заголовок multiboot расположен не с самого начала, но в пределах 8кб от начала образа операционной системы, как требует спецификация - по смещению 0x100. Справа от каждого сегмента образа, указан его размер. Слева указан физический адрес по которому мы хотим загрузить операционную систему. Исходя из этих условий поля структуры multiboot должны будут содержать следующие значения:

header_addr   = 0x100100
load_addr     = 0x100000
load_end_addr = 0x115130
bss_end_addr  = 0x11A130
entry_addr    = 0x100000

Полю entry_addr передано значение 0x100000 в предположении что начало исполняемого кода находится по этому адресу.

!!! Напомню, что если образ операционной системы выполнен в виде ELF исполняемого файла, то все эти действия излишни. В этом случае не нужно устанавливать бит 16 поля flags и не заполнять все рассмотренные выше поля.

Оставшиеся поля, определяют предпочитаемый операционной системой графический режим. Установка этих значений носит рекомендательный характер. Если этот графический режим поддерживается, то загрузчик обязан его установить, если нет, то перейти в подобный, если это возможно.

mode_type
Поле используется загрузчиком, если будет установлен бит 2 в поле flags. Содержит 0 для линейного графического режима или 1 для стандартного текстового EGA режима. Всё остальное зарезервировано для будущего использования.
width
Поле используется загрузчиком, если будет установлен бит 2 в поле flags. Содержит ширину экрана. Для текстового режима это количество символов, для графического количество пикселей по горизонтали. Значение 0 означает что у операционной системы нет предпочтений на этот счет.
height
Поле используется загрузчиком, если будет установлен бит 2 в поле flags. Содержит высоту экрана. Для текстового режима это количество строк, для графического количество пикселей по вертикали. Значение 0 означает что у операционной системы нет предпочтений на этот счет.
depth
Поле используется загрузчиком, если будет установлен бит 2 в поле flags. Содержит количество бит на пиксел (глубину цвета) в графическом режиме и ноль в текстовом. Значение 0 в графическом режиме означает что у операционной системы нет предпочтений на этот счет.

1.3.2 Состояние процессора после загрузки образа ОС

После загрузки операционной системы, компьютер должен быть переведен в следующее состояние:

Регистр/ресурсСостояние
EAX Должен содержать значение 0x2BADB002, информирующее операционную систему о том что она была загружена менеджером загрузки совместимом со спецификацией multiboot.
EBX Должен содержать 32х битный физический адрес, по которому находится информационная структура multiboot(см. 1.3.3)
CS Должен содержать номер дескриптора сегмента кода доступного для чтения и исполнения, с базовым адресом 0 и размером 0xFFFFFFFF, точное значение номера дескриптора не определено.
DS, ES, FS, GS, SS Должен содержать номер дескриптора данных доступного для чтение и записи, с базовым адресом 0 и размером 0xFFFFFFFF, точное значение номера дескриптора не определено.
A20 Адресная линия A20 должна быть активирована.
CR0 Бит 31(PG) должен быть сброшен.
Бит 0(PE) должен быть установлен.
Состояние остальных битов не определено.
EFLAGS Бит 17(VM) должен быть сброшен.
Бит 9(IF) должен быть сброшен.
Состояние остальных битов не определено.
ESP Операционная система должна сформировать свой стек тогда когда ей это будет необходимо. Состояние не определено.
GDTR Несмотря на то что сегментные регистры настроены так, как описано выше, регистр может содержать не верное значение. Поэтому операционная система не должна загружать сегментные регистры, и даже не должна перегружать теми же значениями пока не сформирует свою собственную таблицу GDT.
IDTR Операционная система должна оставить прерывания отключенными до тех пор пока не сформирует свою таблицу IDT.

Все остальные регистры и биты регистра флагов не определяются данной спецификацией и могут содержать любые значения.

1.3.3 Информация передаваемая от загрузчика

Как было отмечено в разделе 1.3.2 в регистр EBX сразу после загрузки операционной системы будет помещен физический адрес информационной структуры multiboot. Именно через эту структуру и осуществляется передача различных данных от загрузчика операционной системе. Операционная система может использовать эту информацию, а может просто проигнорировать, если она не будет в ней нуждаться. Информационная структура, ровно как и ее подструктуры, может быть размещена загрузчеком в любом месте памяти где ему будет угодно (за исключением конечно памяти зарезервированной за операционной системой и загружаемыми модулями). Весь дальнейший контроль за состоянием этой структуры полностью передан операционной системе.

Формат информационной структуры приведен в следующей таблице:

Позиция от начала(байт) Размер поля(байт) Символьное имя Комментарии
0 4 flags Обязательно
4 4 mem_lower Доступно, если установлен flags[0]
8 4 mem_upper
12 4 boot_device Доступно, если установлен flags[1]
16 4 cmdline Доступно, если установлен flags[2]
20 4 mods_count Доступно, если установлен flags[3]
24 4 mods_addr
28-40 12 syms Доступно, если установлен flags[4] или flags[5]
44 4 mmap_length Доступно, если установлен flags[6]
48 4 mmap_addr
52 4 drives_length Доступно, если установлен flags[7]
56 4 drives_addr
60 4 config_table Доступно, если установлен flags[8]
64 4 boot_loader_name Доступно, если установлен flags[9]
68 4 apm_table Доступно, если установлен flags[10]
72 4 vbe_control_info Доступно, если установлен flags[11]
76 4 vbe_mode_info
80 2 vbe_mode
82 2 vbe_interface_seg
84 2 vbe_interface_off
86 2 vbe_interface_len

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

Бит 0
если установлен, то поля mem_lower и mem_upper содержат размер доступной памяти, нижней и верхней соответственно, выраженной в килобайтах. Нижняя память начинается с нулевого физического адреса, и не превышает 640Кб. Верхняя память начинается после 1 Мб. Значение установленное для верхней памяти будет максимальный физический адрес до первой "дыры" (зарезервированного участка, по тем или иным причинам, памяти) минус 1Мб. Таким образом это значение может не совпадать с максимально доступным размером верхней памяти.
Бит 1
если установлен, то поле boot_device указывает на номер дискового устройства BIOS с которого загрузчик загрузил образ операционной системы. Если операционная система была загружена не с дискового устройства BIOS, то данное поле не должно содержать никаких значений, а управляющий бит(т.е этот) должен быть сброшен. Операционная система может использовать его, например, для определения месторасположения своей корневой файловой системы. Поле boot_device разбивается на четыре однобайтовых поля:

рис 7

Первый байт содержит номер устройства BIOS, такой что использует прерывание BIOS INT13 интерфейса доступа к дисковым устройствам. Например 0x00 для первого привода флоппи диска, или 0x80 для первого жесткого диска.

Три оставшихся байта определяют номер загрузочного раздела. Раздел 1 определяет раздел верхнего уровня. Раздел 2 определяет подраздел раздела верхнего уровня Раздел 1 и т.д. Номера разделов всегда начинаются с нуля. Байты не указывающие номера разделов должны содержать значение 0xFF. Например если диск разбит с использованием простой, одноуровневой схемой DOS, с которой и была загружена операционная система, то байт Раздел 1 будет содержать номер этого раздела, а Раздел 2 и Раздел 3 будут содержать 0xFF. Рассмотрим другой пример. Пусть диск разбит на два раздела DOS, один из которых разбит на несколько BSD разделов используя стратегию disklabel BSD. Если операционная система была загружена с одного из разделов BSD, то Раздел 1 будет содержать номер раздела DOS в котором содержаться BSD разделы, Раздел 2 будет содержать номер раздела BSD с которого была загружена операционная система, а Раздел 3 будет содержать 0xFF.

Расширенные разделы DOS определяются как разделы с номерами начинающимися с 4 го, и увеличиваются в большую сторону. И несмотря на то что такая система несет в себе иерархию при загрузке со второго расширенного раздела DOS Раздел 1 будет содержать номер 5 а Раздел 2 и Раздел 3 будут заполнены значениями 0xFF.

Бит 2
если установлен, то поле cmdline содержит физический адрес командной строки переданной ядру через менеджер загрузки. Командная строка представляет собой стандартную C строку ограниченную ноль символом.
Бит 3
если установлен, то это означает что вместе с образом операционной системы были загружены дополнительные модули. Их количество и место в памяти определяют поля mods_count и mods_addr соответственно. mods_addr содержит физический адрес первой информационной структуры описывающий данные загруженного модуля. mods_count может содержать ноль, несмотря на то что данный бит установлен, что будет означать что ни один модуль не загружался.

Формат информационной структуры для модуля приведен в следующей таблице:

СмещениеРазмерПоле
04mod_start
44mod_end
84string
124reserved

Первые два поля содержат физические адреса начала и конца загруженного модуля. String описывает строку ассоциированную с данным модулем, строка является обычной ASCII строкой ограниченной ноль символом как и командная строка cmdline. String может содержать и ноль, если строка не была ассоциирована с модулем. Поле reserved является зарезервированным и должно быть установлено в 0 загрузчиком и игнорироваться операционной системой.

Бит 4
если установлен, то поле syms содержит таблицу символов для образа операционной системы в формате a.out, и имеет следующий формат:
СмещениеРазмерПоле
04tabsize
44strsize
84addr
124reserved
Бит 5
если установлен, то поле syms содержит таблицу символов для образа операционной системы в формате ELF, и имеет следующий формат:
СмещениеРазмерПоле
04num
44size
84addr
124shndx

Надо отметить что Бит 4 и Бит 5 являются взаимоисключающими.

Бит 6
если установлен, то поле mmap_addr содержит физический адрес буфера содержащего карту памяти которую предоставил BIOS, а поле mmap_length - размер этого буфера. Буфер состоит из одной или более пар размер-структура. Каждая пара (элемент) буфера имеет следующий формат:
СмещениеРазмерПоле
-44size
04base_addr_low
44base_addr_high
84length_low
124length_high
164type

Где поле size содержит размер в байтах структуры с которой оно ассоциировано, которое не может быть меньше или равно минимальному размеру в 20 байт. base_addr_low содержит младшие 32 бита, а base_addr_high старшие 32 бита физического адреса начала участка, описываемого этой структурой, памяти, формируя вместе 64 битовый физический адрес. length_low содержит младшие 32 бита, а length_high старшие 32 бита протяженности этого участка, формируя вместе 64 битовое значение размера участка. Поле type указывает тип описываемого участка памяти, и может содержать различные значения. Значение типа - 1, означает что этот участок памяти доступен для нормального использования операционной системой, все остальные значения означают что данный участок зарезервирован. Описываемая этими структурами карта памяти, гарантированно, будет содержать все доступные участки памяти.

Бит 7
если установлен, то поле drives_addr содержит физический адрес буфера содержащего информацию о дисковых устройствах, а поле drives_length - размер этого буфера. Буфер состоит из одной или более пар размер-структура. Каждая пара (элемент) буфера имеет следующий формат:
СмещениеРазмерПоле
04size
41drive_number
51drive_mode
62drive_cylinders
81drive_heads
91drive_sectors
10...drive_ports

Поле size содержит размер этой структуры, размер изменяется в зависимости от количества портов, поля drive_ports. Заметьте что из за выравнивания размер структуры не может быть равен (10+2*количество портов). drive_number содержит BIOS номер диска, drive_mode содержит режим доступа(используемой адресации) к диску. На данный момент определены следующее режимы:

0
CHS режим (традиционная адресация - цилиндр/головка/сектор)
1
LBA режим (логическая линейная адресация)

Три поля drive_cylinders, drive_heads, drive_sectors передают установленную BIOS геометрию диска, и означают соответственно количество цилиндров, головок и секторов на дорожке.

Поле drive_ports содержит массив номеров портов используемых кодом BIOS. Массив состоит из нуля или более двухбайтовых целых чисел ограниченных нулем с конца. Надо заметить что этот массив может содержать и порты которые вообще-то могут не иметь прямого отношения к диску (например как порты контроллера DMA).

Бит 8
если установлен, то поле config_table будет содержать физический адрес по которому будет расположена конфигурационная таблица ROM возвращаемая функцией BIOS - GET_CONFIGURATION. Если вызов функции был неудачен, то поле размера таблицы будет содержать ноль.
Бит 9
если установлен, то поле boot_loader_name содержит физический адрес строки содержащей имя загрузчика который загрузил операционную систему. Данная строка представляет собой стандартную C строку ограниченную ноль символом.
Бит 10
если установлен, то поле apm_table будет содержать физический адрес таблицы APM следующего формата:
СмещениеРазмерПоле
02version
22cseg
44offset
82cseg_16
102dseg
122flags
142cseg_len
162cseg_16_len
182dseg_len

Поля version, cseg, offset, cseg_16, dseg, flags, cseg_len, cseg_16_len, dseg_len означают соответственно - номер версии, 32х битный сегмент кода в защищенном режиме, смещение точки входа, 16ти битный сегмент кода в защищенном режиме, 16ти битный сегмент данных в защищенном режиме, поле флагов, размер 32х битного сегмента кода в защищенном режиме, размер 16ти битного сегмента кода в защищенном режиме, размер 16ти битного сегмента данных в защищенном режиме. Ознакомится со смыслом этих полей вы сможете в спецификации APM - "Advanced Power Management (APM) BIOS Interface Specification".

Бит 11
если установлен, то поля vbe_* будут заполнены загрузчиком. Данный бит будет установлен, если операционная система запросит у загрузчика установку графического режима через заголовок multiboot(см. 1.3.1). Поля vbe_control_info и vbe_mode_info содержат физический адрес по которому будет расположена VBE информация, которая возвращается функциями VBE 0x00 и 0x01, соответственно. Поле vbe_mode указывает на текущий видео режим, так как определено в спецификации VBE 3.0. Оставшиеся поля vbe_interface_seg, vbe_interface_len содержат таблицу описывающую интерфейс VBE для защищенного режима процессора, как определено в спецификации VBE 2.0+. Если такая информация недоступна, то поля будут содержать 0. Надо заметить, что спецификация VBE 3.0 определяет другой интерфейс для защищенного режима который не совместим с используемым. Если вы хотите использовать новый интерфейс то вам нужно будет получить такую таблицу самостоятельно.

Описываемые поля разработаны для интерфейса VBE, но загрузчики могут эмулировать VBE на не VBE режимах, так как будто это VBE режимы.

1.3.4 Пример использования загрузки Multiboot

Рассмотрим пример загрузки образа нашего приложения с использованием спецификации multiboot. В качестве менеджера загрузки будем использовать GRUB, о котором я уже упомянул в разделе 1.2. GRUB на мой взгляд сейчас является одним из самых мощных загрузчиков с открытым исходным кодом, бесплатный и выходящий под лицензией GNU. Загрузчик GRUB можно встретить в каждом дистрибутиве Linux, xBSD, и в недавно вышедшей версии x86 Open Solaris 10 от фирмы Sun (загрузка которого осуществляется с использованием спецификации multiboot, как это происходит вы сможете посмотреть в файле /usr/src/psm/stand/boot/i386/i86pc/asm.s).

Нашим примером будет приложение, которое по своей функциональности идентично тому что мы рассматривали в разделе 1.1 - "Hello World", но в отличии от него "нас" загрузит менеджер загрузки и мы выведем на экран "Hello Multiboot World" оповестив "всех" о том что "мы" загружены. Образ нашего приложения будет оформлен в виде исполняемого файла формата ELF. Скомпилируем приложение так чтобы код располагался по адресам начиная с 0x100000, что соответствует 1Мб. В этом случае менеджер загрузки прочитав эту информацию из заголовков ELF файла загрузит наше приложение именно по этому адресу. Затем, имея образ нашего приложения мы создадим образ дискеты для нашей виртуальной машины, установим на нее загрузчик GRUB, настроим его на загрузку нашего приложения, скопируем на эту дискету наше приложение и запустив виртуальную машину проанализируем проделанную работу.

Изучение примера начнем с заголовочного файла multiboot.h, описывающего некоторые макросы(символьные константы) и структуры спецификации multiboot, которые мы затем будем использовать в остальных файлах. Листинг multiboot.h приведен на рис. :

Листинг multiboot.h

 1   #ifndef __MULTIBOOT_H
 2   #define __MULTIBOOT_H
 3   
 4   /* Сигнатура заголовка Multiboot. */
 5   #define MULTIBOOT_HEADER_MAGIC          0x1BADB002
 6   
 7   /* Флаги для заголовка Multiboot. */
 8   # define MULTIBOOT_HEADER_FLAGS         0x00000001
 9   
10   /* Размер стека кторый мы будем использовать. */
11   #define STACK_SIZE                      0x4000
12   
13   #ifndef __ASSEMBLY__
14   
15   /* Заголовок таблицы секций ELF. */
16   typedef struct elf_section_header_table
17   {
18   	unsigned long num;
19   	unsigned long size;
20   	unsigned long addr;
21   	unsigned long shndx;
22   } elf_section_header_table_t;
23   
24   /* Информационная структура multiboot. */
25   typedef struct multiboot_info
26   {
27   	unsigned long flags8;
28   	unsigned long mem_lower;
29   	unsigned long mem_upper;
30   	unsigned long boot_device;
31   	unsigned long cmdline;
32   	unsigned long mods_count;
33   	unsigned long mods_addr;
34   	elf_section_header_table_t elf_sec;
35   	unsigned long mmap_length;
36   	unsigned long mmap_addr;
37   } multiboot_info_t;
38   
39   
40   #endif /*!__ASSEMBLY__ */
41   
42   #endif /*__MULTIBOOT_H */

Код multiboot.h достаточно читаем и не труден, поэтому нет нужды разбирать его детально, опишем его кратко. В строке 5, 8 и 11 объявляются константы: первая для сигнатуры multiboot, вторая задает флаги которые мы будем использовать при заполнении заголовка multiboot, и последняя - определяет размер стека который мы будем использовать при его инициализации. Как видно для флагов мы определили значение 0x00000001, что означает установку лишь одного - первого(нулевого) бита т.е., включаем контроль за выравниванием.

Далее в заголовочном файле мы определяем информационную структуру multiboot на языке С, с полями как того требует спецификация (п.1.3.3), и достаточными для нашего приложения. Если мы включим такой файл в исходный текст ассемблера, то очевидно мы получим соответствую ошибку, т.к. Ассемблер не поймет такое определение структуры. Чтобы этого не происходило, мы воспользуемся директивой условной компиляции, и сделаем блок, с кодом понятным только компилятору С - строки 13-40, недоступным для компилятора ассемблера. Исключением этого кода будет управлять директива __ASSEMBLY__. В коде ассемблера при использовании этого файла будем определять директиву __ASSEMBLY__ для исключения кода С, компиляция будет проходить успешно, а мы сможем воспользоваться остальными полезными макросами.

Теперь перейдем непосредственно к нашему приложению. Первый файл который мы рассмотрим это boot.S. В нем расположена точка входа, т.е. точка куда загрузчик передаст управление после загрузки а так же заголовок multiboot. Так же мы настроим стек, а затем передадим управление нашей функции, написанной на языке C для вывода на экран строки.

Программный код boot.S приведен на рис.

Листинг boot.S

 1   #define __ASSEMBLY__
 2   #include <multiboot.h>
 3   
 4   .text
 5   
 6   .globl  _start
 7   
 8   _start:
 9   	jmp     multiboot_entry
10   
11   /* Выравниваем заголовок multiboot по границе 4 байт. */
12   .align  4
13   
14   /* Заголовок Multiboot. */
15   multiboot_header:
16   	/* Сигнатура Multiboot */
17   	.long   MULTIBOOT_HEADER_MAGIC
18   	/* Флаги */
19   	.long   MULTIBOOT_HEADER_FLAGS
20   	/* Контрольная сумма */
21   	.long   -(MULTIBOOT_HEADER_MAGIC + MULTIBOOT_HEADER_FLAGS)
22   	/* Так как мы используем ELF, то остальные поля 
23   	 * определять нет необходимости.
24   	 */
25   
26   multiboot_entry:
27   	/* Инициализируем стек. */
28   	movl    $(stack + STACK_SIZE), %esp
29   
30   	/* Сбрасываем регистр EFLAGS. */
31   	pushl   $0
32   	popf
33   
34   	/* Передаем в функцию multiboot_main 2 параметра:
35   	 * Адрес информационной структуры - ebx
36   	 * Сигнатура multiboot - eax
37   	 */
38   	pushl   %ebx
39   	pushl   %eax
40   
41   	/* Вызываем главную функцию. */
42   	call    multiboot_main
43   
44   loop:   hlt
45   	jmp     loop
46   
47   /* Область нашего стека. */
48   .comm   stack, STACK_SIZE

Рассмотрим внимательно этот файл. Как следует из спецификации в момент когда процессор передаст управление нашему коду, в нашем случае это метка _start в строке 8, процессор будет переведен в защищенный 32х битный режим, и соответственно у нас нет необходимости совершать длинные прыжки для настройки сегментов кода, данных, как мы это делали совершая загрузку в реальный режим самостоятельно, в примере helloworld в главе 1.1. Все сегменты уже настроены загрузчиком. Более того нам в соответствии с той же спецификацией даже запрещено это делать, пока мы не сформируем свою собственную таблицу GDT(защищенный режим мы будем рассматривать в других разделах, сейчас же просто рассматривайте этот код как в реальном 32 битном режиме ). Таким образом в строке 9 мы передаем управление нашему исполняемому коду - строка 26, совершая обычный переход, игнорируя структуру multiboot в строках 11-24, которая нужна лишь загрузчику. Структура multiboot выровнена по границе 4 байт и комплектуется значениями описанными в заголовочном файле multiboot.h рассмотренного ранее. Далее:

28
Проводится инициализация стека на отведенную область данных в строке 48 размером в STACK_SIZE байт. Обратите внимание что производится инициализация только указателя стека - регистр esp, но сегмент стека мы не трогаем, т.к. это запрещено спецификацией, пока мы не сформируем свою таблицу GDT.
30-32
Сбрасываем регистр флагов.
34-42
Вызов нашей главной процедуры, которая делает всю работу по выводу строки на экран - multiboot_main. В строках 38, 39 мы через стек передаем адрес информационной структуры multiboot, а так же сигнатуру multiboot, которые можно будет анализировать в дальнейшем. В соответствии со спецификацией эти значения после загрузки содержаться в регистрах ebx и eax соответственно.
44-45
Когда работа функции multiboot_main будет завершена, мы входим в бесконечный цикл, чтобы как то завершить работу нашего кода, как мы это делали в примере helloworld в 1.1.

Листинг файла main.c в котором находится наша главная функция multiboot_main() представлен представлен на рис.

Листинг main.c

 1   #include <multiboot.h>
 2   
 3   /* Определяем параметры экрана */
 4   #define SCREEN_HEIGHT	25
 5   #define SCREEN_WIDTH	80
 6   #define SCREEN_START	0xb8000
 7   #define SCREEN_SIZE	SCREEN_START+SCREEN_HEIGHT*SCREEN_WIDTH*2
 8   
 9   unsigned long scr_start=SCREEN_START;
10   unsigned long scr_pos=SCREEN_START;
11   /* Строка для вывода */
12   const char* hello_str="Hello Multiboot World!!!";
13   
14   /* Функция для очистки экрана */
15   void clr()
16   {
17       int pos;
18       for(pos=SCREEN_START;pos<SCREEN_SIZE;pos++) {
19   	*((unsigned char*)(pos))=0x0;
20       }
21       scr_pos=scr_start;
22   }
23   
24   /* Главная функция, ей будет передано управление из boot.S */
25   int multiboot_main(unsigned long magic, unsigned long addr)
26   {
27   	multiboot_info_t *mbi=(multiboot_info_t *)addr;
28   	char* str_ptr=(char*)hello_str;
29   	
30   	/* Очищаем экран и устанавливаем курсор в самое его начало */
31   	clr();
32   	/* Выводим строку на экран */	
33   	while(*str_ptr) {
34   	    *((unsigned char*)(scr_pos++))=*str_ptr++;
35   	    *((unsigned char*)(scr_pos++))=0x07;
36   	}
37   	
38   	return 0;    
39   }

Файл main.c написан на языке C и помимо главной функции multiboot_main() содержит вспомогательную функцию clr() - для очистки экрана. В строке 1 мы подключаем файл multiboot.h для доступа к структуре данных multiboot_info_t которая будет передана функции multiboot_main(). Строки 4-7 содержат строковые константы для удобочитаемости и содержат параметры геометрии экрана в текстовом режиме 80x25, адрес начала видеопамяти для этого режима, а также размер этого участка видеопамяти в байтах для отображения одного экрана(напомню что на вывод одного символа в этом режиме используется 2 байта, в одном из них содержится код символа а в другом атрибуты отображения - цвет,фон и др.). Строки 9-12 описывают глобальные переменные для отслеживания позиции курсора и строку для вывода на экран.

Функция clr() очищает экран, не принимает никаких входящих значений. Работа данной функции очень проста, она в цикле проходит по всем адресам видеопамяти от адреса SCREEN_START до SCREEN_START+SCREEN_SIZE обнуляя каждую ячейку памяти. Далее она устанавливает начальную позицию для вывода на экран в значение SCREEN_START - строка 21.

Функция multiboot_main() принимает в качестве параметров адрес структуры multiboot_info_t, и сигнатуру загрузчика, которые вы можете использовать для получения информации передаваемой от загрузчика. В строке 31 - мы вызывая clr() очищаем экран, а в строках 33-36 производим вывод строки hello_str на экран. Цикл 33-36 Проходит по всем символам строки, до ее конца(пока не встретит 0) осуществляя запись каждого из них последовательно по адресам видеопамяти начиная с адреса в scr_pos описывающего текущую позицию. Каждый второй байт в этом цикле заполняется символом атрибутов - 0x07 - что означает вывод символов в белом цвете. Как только будет встречен нулевой символ в строке цикл завершиться и мы покинем функцию multiboot_main уйдя в вечный цикл - см. файл boot.S.

Конструкция:

*((unsigned char*)(scr_pos++))=*str_ptr++;

достаточно сложна для не искушённого в языке С программиста. Здесь в одной строке производятся следующие действия в указанном далее порядке:

  1. scr_pos имеет тип unsigned long и содержит адрес текущей позиции курсора. Чтобы записать по этому адресу символ мы должны привести для начала его в указатель на символ, т.е. в unsigned char*.
  2. Затем полученный указатель unsigned char* путем разименовывания - операция *, используется для записи в него значения текущего байта строки из str_ptr.
  3. После завершения операции присваивания происходит выполнение постфиксных операций ++, т,е, инкрементирование значений текущей позиции курсора scr_pos и str_ptr - переход на следующий символ в строке.

Рассмотрим процесс сборки приложения. Makefile для нашего приложения приведен на рис. Он в целом похож на тот который мы создали для примера helloworld в 1.1 с небольшими различиями.

Во-первых в строке 3 мы используем внутреннюю переменную CPPFLAGS для передачи опций препроцессору: первая опция -stdinc будет говорить препроцессору языка C что мы не собираемся использовать включаемые файлы стандартной библиотеки такие как stdio.h, string.h и пр. Вторая опция _I./ указывает на то что поиск всех заголовочных файлов надо производить в текущей директории.

Во-вторых при окончательной сборке - строка 6, мы указываем линковщику что наш исполняемый код должен располагаться начиная с адреса 0x100000 (выше первого мегабайта) и мы хотим получить обычный исполняемый ELF файл(эта опция не указывается т.к. принята по умолчанию).

В третьих в строке 7, после того как приложение собрано, мы хотим получить таблицу символов, для этого мы воспользовались программой nm. Весь ее вывод будет отсортирован и направлен в файл System.map. Позже после сборки приложения мы его рассмотрим.

Листинг Makefile

 1   TARGET:=boot.elf
 2   
 3   CPPFLAGS:=-nostdinc -I./
 4   
 5   $(TARGET): boot.o main.o
 6   	$(LD) -Ttext 0x100000 -o $@ $^
 7   	nm $(TARGET) | sort > System.map
 8   main.o: main.c
 9   
10   boot.o: boot.s
11   boot.s: boot.S
12   
13   .PHONY: clean bochs
14   clean:
15   	rm -f *.o *.s *.log System.map $(TARGET)
16   bochs:
17   	cp -f bochsrc .bochsrc

Итак для сборки нашего приложения все готово осталось только перейти в его каталог и дать команду на сборку:


[maxix@host ~]$ cd src/book/src/helloworld_multiboot
[maxix@host helloworld_multiboot]$ make

Наше приложение готово осталось настроить загрузчик grub и проверить его на деле. Для начала протестируем наше приложение на совместимость со спецификацией multiboot. Для этого загрузчик grub предлагает утилиту mbchk. Выполним следующую команду:


[maxix@host helloworld_multiboot]$ mbchk boot.elf

Grub выведет на экран следующую информацию:


boot.elf: The Multiboot header is found at the offset 4100.
boot.elf: Page alignment is turned on.
boot.elf: Memory information is turned off.
boot.elf: Address fields is turned off.
boot.elf: All checks passed.

Grub нам сообщил что он нашел заголовок multiboot по смещению 4100, что мы требуем от него выравнивание нашего кода и что мы от него не запрашиваем информации об используемой памяти. В конце он говорит что все тесты пройдены успешно и наш код может быть загружен с его помощью.

Для тестов нашего приложения мы будем использовать виртуальную машину bochs. Её конфигурационный файл приведен на рис. Он ничем не отличается от рассмотренного ранее в примере helloworld за исключением строки 8 в которой мы будем для загрузки использовать образ дискеты floppy.img с загрузчиком grub и нашим приложением. Процесс создания этого образа мы рассмотрим далее.

Листинг bochsrc

 1   #include ../bochsrc.default
 2   
 3   # Объем памяти который будет размещен на нашей виртуальной
 4   # машине
 5   megs: 64
 6   
 7   # Используем для дискеты имя нашего образа
 8   floppya: 1_44=floppy.img, status=inserted
 9   
10   # Назначаем устройство загрузки
11   boot: a
12   

Создадим образ нашей дискеты объемом 1,44Mb. Для этого выполним следующую комманду:

[maxix@host helloworld_multiboot]$ dd if=/dev/zero of=floppy.img bs=1024 count=1440 >

Эта команда создаст пустой образ дискеты. Для того чтобы им можно было воспользоваться нам надо для начала присоединить его к устройству loopback, затем отформатировать. Когда он будет отформатирован мы примонтируем его к нашей файловой системе и установим на него grub вместе с нашим приложением. Для выполнения этих операций вам потребуется доступ суперпользователя root.

Программа dd (diskdup) используется для копирования файлов посредством чтения записи поблочно. Ее часто используют для копирования данных с устройств и на них. Например для того чтобы сохранить образ CD диска на файловой системе надо сделать следующее:

dd if=/dev/cdrom of=cdimage.iso

В результате вы получите ISO образ вашего CD диска.

Для того чтобы потом его можно было использовать нужно назначить этому образу устройство loopback, примонтировать его и затем использовать:


losetup /dev/loop0  cdimage.iso
mkdir /mnt/iso
mount /dev/loop0 /mnt/iso

Теперь все содержание диска можно просматривать в каталоге /mnt/iso, а устройство содержащие диск будет /dev/loop0

После окончания работы с диском нужно отмонтировать каталог и удалить ссылку устройства на наш iso образ:


umount  /mnt/iso
losetup -d /dev/loop0 

Итак первым делом назначим устройству loopback - /dev/loop0 образ нашей дискеты:


[maxix@host helloworld_multiboot]$ su
[root@host helloworld_multiboot]# losetup  /dev/loop0  ./floppy.img

Отформатируем образ нашей дискеты:


[root@host helloworld_multiboot]# mkfs  /dev/loop0

Создадим каталог куда мы будем монтировать образ нашей дискеты и сразу же примонтируем туда наш образ:


[root@host helloworld_multiboot]# mkdir /mnt/fdimage
[root@host helloworld_multiboot]# mount /dev/loop0 /mnt/fdimage

Установим загрузчик grub на наш образ. Для этого создадим каталог на нашем образе /boot/grub(корневая директория естественно относительно /mnt/fdimage):


[root@host helloworld_multiboot]# mkdir -p /mnt/fdimage/boot/grub

Создадим файл конфигурации grub в котором нужно будет описать меню загрузки включающее наше приложение. Конфигурационный файл grub.conf приведен на рис.

Листинг grub.conf

 1   
 2   default 0
 3   
 4   title Multiboot Hello World
 5   root (fd0)
 6   kernel /boot.elf

Файл очень прост: мы объявляем что по умолчанию будет грузится первый пункт меню - строчка 2, далее объявляем заголовок меню - "Multiboot Hello World" - строка 4, для этого пункта меню определяем устройство загрузки /dev/fd0 (строка 5)- дисковод с которого будет производится загрузка и где будет расположен корень файловой системы. В строке 6 мы указываем ядро нашей системы - собственно файл содержащий код который будет загружен в память - т.е. именно наше приложение.

Скопируем файл grub.conf на наш образ в /boot/grub:


[root@host helloworld_multiboot]# cp ./grub.conf  /mnt/fdimage/boot/grub

Нам так же потребуется скопировать 2 файла stage1 и stage2 из каталога grub в вашей системы (у меня они расположены в каталоге /boot/grub) в директорию /boot/grub нашего образа:


[root@host helloworld_multiboot]# cp /boot/grub/stage[12]  /mnt/fdimage/boot/grub

И конечно же копируем наше приложение в корневую директорию нашего образа:


[root@host helloworld_multiboot]# cp boot.elf  /mnt/fdimage

После этих действий отсоединим наш образ от файловой системы:


[root@host helloworld_multiboot]# umount  /mnt/fdimage

Теперь самая важная и ответственная часть - установка grub на наш образ дискеты. Будьте очень внимательны при вводе следующих команд т.к. неправильные действия могут привести к порчи вашего загрузчика, вводите команды в точности как они будут приведены здесь. Если вы хотите поэкспериментировать с grub то вы это будете делать на свой страх и риск. В любом случае подробно ознакомьтесь с руководством пользователя grub.

Для установки grub выполним следующие действия:


[root@host helloworld_multiboot]# grub  --device-map=/dev/null

После выполнения этой команды мы попадем в консоль grub где последовательно введем следующие команды:

grub> device (fd0) /dev/loop1 grub> root (fd0) grub> setup (fd0) grub> quit

После этих операций grub будет установлен на наш образ. Теперь когда все готово мы удалим ссылку устройства /dev/loop0 на наш образ дискеты:


[root@host helloworld_multiboot]# losetup -d /dev/loop0

Давайте насладимся проделанным трудом- покинем супер пользователя и запустим bochs:


[root@host helloworld_multiboot]# exit
[maxix@host helloworld_multiboot]$ bochs -q 

После запуска виртуальной машины сразу же стартует загрузчик grub предлагая нам выбрать загружаемую систему - в нашем случае это только один пункт меню - наше приложение - см. рис.

рис 8

После выбора нашего пункта меню grub загрузит наше приложение и передаст ему управление. На экране высветится результат работы нашего приложение - строка "Hellow Multiboot World" - см. рис.

рис 9

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


[root@host helloworld_multiboot]# dd if=floppy.img of=/dev/fd0

Образ будет записан на дискету. Перезагрузите ваш компьютер, настроив BIOS на загрузку с дискеты. Результат должен быть тем же.

Более того если вы используете этот же загрузчик для запуска вашей операционной системы и вы хотите запустить наше приложение с его помощью, просто отредактируйте файл /boot/grab/grab.conf вашей операционной системы добавив пункт меню для загрузки нашего приложения и выберите его после перезагрузки.

Используя трассировщик bochs вы можете просмотреть детально как работает наше приложение. Большую помощь нам окажет в этом файл System.map который формируется программой nm после сборки приложения(см. Makefile). Файл содержит таблицу символов в которой построчно перечисляются символические имена и адреса в памяти где их можно будет найти, своеобразная "дорожная карта" по коду нашего приложения. Вот например как выглядит файл System.map у меня:

Файл System.map

00100000 T _start
00100004 t multiboot_header
00100010 t multiboot_entry
0010001f t loop
00100028 T clr
00100057 T multiboot_main
001010c4 D scr_start
001010c8 D scr_pos
001010cc D hello_str
001010d0 A __bss_start
001010d0 A _edata
001010d0 B stack
001050d0 A _end

К примеру я хочу протрассировать функцию multiboot_main(). В этом случае я заглянув в файл System.map определю адрес этой функции в памяти, в моем примере это адрес 0x00100057 и установлю в bochs перед стартом точку останова на этот адрес. Как только bochs доберется до первой инструкции по этому адресу он прервет свое выполнение и будет ждать дальнейших распоряжений, что нам и нужно. Далее вы можете пробежаться по каждой инструкции этой функции. Подобные действия мы уже делали в примере helloworld п.1.1, за тем исключением что у нас не было такой "дорожной карты".

Литература:

  1. Евгений КРЕЙДИЧ, "Процесс загрузки компьютера - от включения питания до запуска ОС" http://www.epos.kiev.ua/pubs/pzk.htm
  2. Yoshinori K. Okuji, Bryan Ford, Erich Stefan Boleyn, Kunihiro Ishiguro, "The Multiboot Specification"
  3. Free Software Foundation, Multiboot Specification manual, version 0.6.93. http://www.gnu.org/software/grub/manual/multiboot/multiboot.html
  4. Advanced Power Management (APM) BIOS Interface Specification, Revision 1.2, February 1996, Intel Corporation, Microsoft Corporation http://www.microsoft.com/whdc/archive/amp_12.mspx
  5. VESA BIOS Extension (VBE), Core functions standart, version 3.0, Date September 16, 1998. Video Electronics Standards Assosiation. http://www.vesa.org/public/VBE/vbe3.pdf

[ опубликовано 09/11/2005 ]

Максим Савенко - Загрузка компьютера   Версия для печати