[Перевод] Пишем загрузчик на Ассемблере и С. Часть 2
В предыдущей статье я рассказал о процессе загрузки, а также продемонстрировал написание загрузочного кода на C и ассемблере, в том числе с вложением инструкций последнего в код первого. При этом мы написали несколько простых программ для проверки работоспособности внедренного в загрузочный сектор кода. В этой же статье мы рассмотрим процесс сегментации и чтения данных с дискеты в ходе загрузки, а также их вывод на экран.
Здесь я ограничусь написанием программы на ассемблере и ее копированием в загрузочный сектор образа дискеты 3.5», после чего мы, как и в прошлой статье, протестируем записанный загрузочный код при помощи эмулятора bochs. Для реализации этих задач я задействую службы BIOS, что позволит нам лучше понять их функционирование и более уверенно работать в реальном режиме (Real Mode).
План статьи
• Знакомство с сегментацией
• Среда программирования
• Чтение данных из RAM
• Знакомство с устройствами хранения
• Структура флоппи-диска
• Взаимодействие с флоппи-диском
Знакомство с сегментацией
Прежде чем переходить к рассмотрению чтения флоппи-диска, давайте освежитм в памяти тему сегментации и ее назначения.
Что это такое?
Основная память разделена на сегменты, индексируемые специальными сегментными регистрами CS, DS, SS и ES.
Назначение сегментации
Когда мы указываем 16-битный адрес, ЦПУ автоматически вычисляет начальный адрес соответствующего сегмента. Тем не менее именно программист должен указывать начальный адрес каждого сегмента, особенно при написании такой программы, как загрузчик.
Какие бывают типы сегментов?
В прошлой статье я уже перечислял четыре основных вида. Здесь я еще раз их напомню и приведу для каждого пояснение:
• сегмент кода;
• сегмент данных;
• сегмент стека;
• расширенный сегмент.
Сегмент кода
Один из разделов программы в памяти, содержащий исполняемые инструкции. Если вы загляните в мою предыдущую статью, то увидите метку .text
, под которой мы размещаем исполняемые инструкции. При загрузке программы в память эти инструкции передаются в сегмент кода. В ЦПУ для обращения к этому сегменту мы используем регистр CS.
Сегмент данных
Раздел программы в памяти, содержащий статические и глобальные переменные. Для обращения к нему мы используем регистр DS.
Сегмент стека
Программист может использовать регистры для хранения, изменения и извлечения данных при написании программы. При этом ограниченное количество регистров зачастую приводит к усложнению логики. В итоге у разработчика постоянно возникает потребность в дополнительном пространстве, более гибком в плане хранения, обработки и извлечения данных. С целью решения подобных сложностей в ЦПУ был внедрен специальный сегмент стека. Для хранения в этом сегменте данных и их извлечения используются соответствующие инструкции push
(добавление) и pop
(удаление). Инструкцию push
мы также используем для передачи аргументов в функции. Обращение к сегменту стека производится при помощи регистра SS. При этом важно помнить, что стек растет сверху вниз.
Расширенный сегмент
Обычно этот сегмент используется для загрузки данных, имеющих слишком большой объем для хранения в сегменте данных. Позже вы увидите, как я загружаю данные с дискеты в расширенный сегмент. Для обращения же к нему мы используем регистр ES.
Применение сегментных регистров
Мы не можем устанавливать эти регистры напрямую и вместо этого делаем следующее:
movw $0x07c0, %ax
movw %ax, %ds
Что здесь происходит?
• Копирование данных в регистр общего назначения.
• Их перенос в сегментный регистр.
Мы загружаем в регистр AX значение из 0x07c0
, после чего копируем содержимое AX в DS. Абсолютный же адрес вычисляется так:
DS = 16 * AX
DS = 0x7c00
Далее для перемещения и достижения нужной точки сегмента данных мы используем смещение.
Среда программирования
• Операционная система (GNU Linux)
• Ассемблер (GNU Assembler)
• Компилятор (GNU GCC)
• Компоновщик (GNU linker ld)
• Эмулятор архитектуры x86 для тестирования (bochs).
Чтение данных из RAM
Теперь BIOS загружает нашу программу из
0x7c00
, и та, в свою очередь, начинает поочередно выводить значения. Для обращения к данным в RAM мы устанавливаем сегмент данных со значением 0x7c00
, также указывая смещение.Пример
После загрузки программы из
0x7c00
можно прочесть данные из смещения 3 и 4 и вывести их на экран. Программа: test.S
.code16 # генерирует 16-битный код
.text # расположение исполняемого кода
.globl _start;
_start: # точка входа
jmp _boot # переход к загрузочному коду
data : .byte 'X' # переменная
data1: .byte 'Z' # переменная
_boot:
movw $0x07c0, %ax # установка ax = 0x07c0
movw %ax , %ds # установка ds = 16 * 0x07c0 = 0x7c00
# копируем данные в позиции 3 из 0x7c00:0x0000
#и выводим их на экран
movb 0x02 , %al # копирование данных из 2-й позиции в %al
movb $0x0e , %ah
int $0x10
# копируем данные в позиции 4 из 0x7c00:0x0000
#и выводим их на экран
movb 0x03 , %al # копирование данных из 3-й позиции в %al
movb $0x0e , %ah
int $0x10
#бесконечный цикл
_freeze:
jmp _freeze
. = _start + 510 #переход из позиции 0 к 510-му байту
.byte 0x55 #добавление сигнатуры загрузки
.byte 0xaa #добавление сигнатуры загрузки
Теперь для генерации двоичного файла и копирования кода в загрузочный сектор дискеты в командной строке введите:
• as test.S –o test.o
• ld –Ttext=0x7c00 –oformat=binary boot.o –o boot.bin
• dd if=/dev/zero of=floppy.img bs=512 count=2880
• dd if=boot.bin of=floppy.img
Если открыть файл boot.bin
в hex-редакторе, вы увидите такое окно:
Здесь X
и Z
находятся в третьей и четвертой позиции от начала 0x7c00
.
Для проверки этого кода введите:
• bochs
Пример 2
После того, как BIOS загрузит нашу программу из
0x7c00
, мы прочитаем и выведем завершающуюся нулем строку из смещения 2.Программа: test2.S
.code16 # генерирует 16-битный код
.text # расположение исполняемого кода
.globl _start;
_start: # точка входа
jmp _boot # переход к загрузочному коду
data : .asciz "This is boot loader" # переменная
#вызывает функцию printString, которая
#начинает вывод строки с этой позиции
.macro mprintString start_pos # макрос вывода строки
pushw %si
movw \start_pos, %si
call printString
popw %si
.endm
printString: # функция вывода строки
printStringIn:
lodsb
orb %al , %al
jz printStringOut
movb $0x0e, %ah
int $0x10
jmp printStringIn
printStringOut:
ret
_boot:
movw $0x07c0, %ax # установка значения в ax = 0x07c0
movw %ax , %ds # установка значения в ds = 16 * 0x07c0 = 0x7c00
mprintString $0x02
_freeze:
jmp _freeze
. = _start + 510 # перемещение из 0 позиции к 510-му байту
.byte 0x55 # добавление сигнатуры загрузки
.byte 0xaa # добавление сигнатуры загрузки
Если вы скомпилируете программу и откроете исполняемый файл в эмуляторе, то в качестве вывода увидите строку «This is boot loader».
Знакомство с устройствами хранения
Что это такое?
Это устройство, служащее для хранения и извлечения информации, которое также может использоваться в качестве средства загрузки.
Какие виды устройств хранения бывают?
За время эволюции информационных технологий было разработано множество их видов, включая:
• Магнитные ленты;
• Флоппи-диски;
• CD и DVD-диски;
• Жесткие диски;
• USB-носители;
• …
Нас же в данном случае будет интересовать одна из старейших технологий, а именно флоппи-диски, проще именуемые дискетами. И поскольку для многих это устройство хранения информации может уже быть незнакомым, я вкратце его опишу.
Что такое флоппи-диск?
Так выглядит старая-добрая дискета. Объем этого носителя невелик и составляет всего 1.4 мегабайта, чего для нашей задачи будет вполне достаточно. Дальше я кратко расскажу о способе измерения данных в вычислительных системах.
Что такое мегабайт?
В компьютерах для измерения данных используются следующие величины:
• Бит: может хранить логическое значение 0 или 1.
• Полубайт: 4 бита
• Байт: 8 бит
• Килобайт (Кб): 1024 байта
• Мегабайт (Мб): 1 Кб * 1 Кб = 1,048,576 Байт = 1024 Кб = 1024×1024 байт
• Гигабайт (ГБ): 1,073,741,824 Байт= 2^30 Байт = 1024 Мб = 1,048,576 Кб = 1024×1024 * 1024 байт
• Терабайт (ТБ): 1,099,511,627,776 Байт= 2^40 Байт = 1024 ГБ = 1,048,576 Мб = 1024×1024 * 1024×1024 байт
Существуют и более крупные величины, но мы ограничимся этими.
Структура типичного флоппи-диска
Это общее схематичное строение дискеты, а вот характеристики используемой нами дискеты 3.5»:
• двухсторонняя;
• стороны обозначаются согласно считывающим их магнитным головкам (head0, head1);
• каждая сторона содержит 80 дорожек (Track);
• каждая дорожка разбита на 18 секторов (Sector);
• размер каждого сектора 512 байт.
Как вычислить размер дискеты?
• Общий размер в байтах: кол-во сторон * кол-во дорожек * кол-во секторов в дорожке * байт в секторе.
Пример = 2×80 * 18×512 = 1474560 байт.
• Общий размер в Кб: (кол-во сторон * кол-во дорожек * кол-во секторов в дорожке * байт в секторе)/1024.
Пример = (2×80 * 18×512)/1024 = 1474560/1024 = 1440Кб.
• Общий размер в Мб: ((кол-во сторон * кол-во дорожек * кол-во секторов в дорожке * байт в секторе)/1024)/1024
Пример = ((2×80 * 18×512)/1024)/1024 = (1474560/1024)/1024 = 1440/1024 = 1.4Мб
Где на дискете находится загрузочный сектор?
Загрузочным является первый сектор диска.
Взаимодействие с флоппи-диском
Как считывать с него данные?
Поскольку в нашем случае потребуется считывать дискету в процессе загрузки в реальном режиме, для этого нам будут доступны только службы BIOS.
Какое прерывание мы будем использовать?
Interrupt 0x13
Service code 0x02
Как обратиться к диску с помощью прерывания 0×13?
• Команда BIOS для считывания сектора:
AH = 0x02
• Команда BIOS для считывания «N»-го цилиндра:
CH = ‘N’
• Команда BIOS для считывания «N»-ой головки (стороны):
DH = ‘N’
• Команда BIOS для считывания «N»-го сектора:
CL = ‘N’
• Команда BIOS для считывания «N» секторов:
AL = N
• Команда прерывания:
Int 0x13
Считывание данных с флоппи-диска
Давайте напишем программу для отображения меток нескольких секторов.
Программа: test.S
.code16 # генерирует 16-битный код
.text # расположение исполняемого кода
.globl _start; # точка входа
_start:
jmp _boot # переход к загрузочному коду
msgFail: .asciz "something has gone wrong..." # сообщение об ошибке операции
# макрос вывода строки с завершающим нулем
# этот макрос вызывает функцию PrintString
.macro mPrintString str
leaw \str, %si
call PrintString
.endm
# функция вывода строки с завершающим нулем
PrintString:
lodsb
orb %al , %al
jz PrintStringOut
movb $0x0e, %ah
int $0x10
jmp PrintString
PrintStringOut:
ret
# макрос считывания сектора дискеты
#и его загрузки в расширенный сегмент
.macro mReadSectorFromFloppy num
movb $0x02, %ah # функция чтения диска
movb $0x01, %al # всего секторов для считывания
movb $0x00, %ch # выбор нулевого цилиндра
movb $0x00, %dh # выбор нулевой головки
movb \num, %cl # начало чтения сектора
movb $0x00, %dl # ???номер диска????
int $0x13 # прерывание ЦПУ
jc _failure # при сбое выбросить ошибку
cmpb $0x01, %al # если общее число считываемых секторов != 1,
jne _failure #то выбросить ошибку
.endm
# отображение строки, внедренной в качестве идентификатора сектора
DisplayData:
DisplayDataIn:
movb %es:(%bx), %al
orb %al , %al
jz DisplayDataOut
movb $0x0e , %ah
int $0x10
incw %bx
jmp DisplayDataIn
DisplayDataOut:
ret
_boot:
movw $0x07c0, %ax # инициализация сегмента данных
movw %ax , %ds #с адресом 0x7c00
movw $0x9000, %ax # ax = 0x9000
movw %ax , %es # es = 0x9000 = ax
xorw %bx , %bx # bx = 0
mReadSectorFromFloppy $2 # чтение сектора дискеты
call DisplayData # отображение метки сектора
mReadSectorFromFloppy $3 # чтение 3-го сектора дискеты
call DisplayData # отображение метки сектора
_freeze: # бесконечный цикл
jmp _freeze
_failure:
mPrintString msgFail # вывод сообщения об ошибке и
jmp _freeze #переход к точке остановки
. = _start + 510 # перемещение из 0 позиции к 510-му байту
.byte 0x55 # добавление первой части сигнатуры загрузки
.byte 0xAA # добавление второй части сигнатуры загрузки
_sector2: # второй сектор дискеты
.asciz "Sector: 2\n\r" # запись данных в начало сектора
. = _sector2 + 512 # перемещение в конец второго сектора
_sector3: # третий сектор дискеты
.asciz "Sector: 3\n\r" # запись данный в начало сектора
. = _sector3 + 512 # перемещение в конец третьего сектора
Компиляция кода
•
as test.S -o test.o
•
ld -Ttext=0x0000 --oformat=binary test.o -o test.bin
•
dd if=test.bin of=floppy.img
Если вы откроете test.bin
в hex-редакторе, то увидите, что я вложил метку в сектора 2 и 3, которые выделены на снимке ниже:
После запуска программы через эмулятор bochs
отобразится следующее:
Что делает эта программа?
В ней мы определяем макрос и функции для чтения и отображения содержимого вложенных в каждый сектор строк.
Я вкратце поясню эти макросы и функции.
# макрос вывода строки с завершающим нулем
# этот макрос вызывает функцию PrintString
.macro mPrintString str
leaw \str, %si
call PrintString
.endm
Этот макрос получает в качестве аргумента строку и внутренне вызывает функцию
PrintString
, отвечающую за поочередное отображение символов на экране.# функция вывода строки с завершающим нулем
PrintString:
lodsb
orb %al , %al
jz PrintStringOut
movb $0x0e, %ah
int $0x10
jmp PrintString
PrintStringOut:
Ret
Эту функцию вызывает макрос
mPrintString
для отображения каждого байта строки, заверщшающейся нулем.# макрос для считывания сектора дискеты
#и его загрузки в расширенный сегмент
.macro mReadSectorFromFloppy num
movb $0x02, %ah # функция чтения диска
movb $0x01, %al # всего считываемых секторов
movb $0x00, %ch # выбор нулевого цилиндра
movb $0x00, %dh # выбор нулевой головки
movb \num, %cl # начало чтения сектора
movb $0x00, %dl # номер дисковода
int $0x13 # прерывание ЦПУ
jc _failure # при сбое выбросить ошибку
cmpb $0x01, %al # если общее число считываемых секторов != 1,
jne _failure #выбросить ошибку
.endm
Этот макрос
mReadSectorFromFloppy
считывает сектор и помещает его в расширенный сегмент для дальнейшей обработки. Номер сектора он получает в качестве аргумента.# отображение строки, вставленной в качестве идентификатора сектора
DisplayData:
DisplayDataIn:
movb %es:(%bx), %al
orb %al , %al
jz DisplayDataOut
movb $0x0e , %ah
int $0x10
incw %bx
jmp DisplayDataIn
DisplayDataOut:
Ret
Эта функция отображает каждый байт данных от начала и до завершающего символа.
_boot:
movw $0x07c0, %ax # инициализируем сегмент данных
movw %ax , %ds #с адресом 0x7c00
movw $0x9000, %ax # ax = 0x9000
movw %ax , %es # es = 0x9000 = ax
xorw %bx , %bx # bx = 0
Это основной загрузочный код. Прежде чем начать вывод содержимого диска, мы устанавливаем в сегменте данных значения из
0x7x00
, а в расширенном сегменте из 0x9000
.Зачем определять расширенный сегмент?
Причина в том, что для отображения содержимого сектора сначала мы считываем его в адрес памяти
0x9000
. mReadSectorFromFloppy $2 # чтение сектора дискеты
call DisplayData # отображение метки сектора
mReadSectorFromFloppy $3 # чтение 3-го сектора дискеты
call DisplayData # отображение метки сектора
Мы вызываем макрос для считывания 2-го сектора, а затем отображаем его содержимое, после чего снова вызываем макрос для считывания 3-го сектора, также отображая его содержимое.
_freeze: # бесконечный цикл
jmp _freeze
После вывода содержимого секторов мы переходим к бесконечному циклу, останавливая программу.
_failure:
mPrintString msgFail # вывод сообщения об ошибке и
jmp _freeze # переход к точке остановки
Этот раздел перехода к метке мы определили на случай возникновения сбоя, в следствии чего программа также остановится.
. = _start + 510 # перемещение от позиции 0 к 510-му байту
.byte 0x55 # добавление первой части сигнатуры загрузки
.byte 0xAA # добавление второй части сигнатуры загрузки
Мы переходим к 510-му байту сектора и добавляем сигнатуру загрузки, необходимую для определения дискеты как загрузочной. В противном случае система выбросит ошибку, сообщив о невозможности загрузки.
_sector2: # второй сектор дискеты
.asciz "Sector: 2\n\r" # запись данных в начало сектора
. = _sector2 + 512 # перемещение в конец второго сектора
_sector3: # третий сектор дискеты
.asciz "Sector: 3\n\r" # запись данных в начало сектора
. = _sector3 + 512 # перемещение в конец третьего сектора
Здесь мы добавляем строку в начало 2-го и 3-го сектора.
На этом вторая часть серии завершается. Предлагаю самостоятельно поэкспериментировать с чтением дискеты в реальном режиме и внедрением в загрузчик различной функциональности.
В третьей части серии я коротко расскажу о файловых системах и их важности. В качестве практики мы напишем простейший загрузчик для считывания дискеты в формате FAT12, научимся выполнять чтение/запись такой дискеты, а также создадим загрузчик второй стадии и разберем его назначение.