[Перевод] Пишем загрузчик на Ассемблере и С. Часть 2

hosk3zfyfkpwqpf0f98nezxwyf0.png

В предыдущей статье я рассказал о процессе загрузки, а также продемонстрировал написание загрузочного кода на 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-редакторе, вы увидите такое окно:

ukwbhay9giywieqf-oxgbltgz4s.png

Здесь X и Z находятся в третьей и четвертой позиции от начала 0x7c00.
Для проверки этого кода введите:

bochs

xn8uefosjh0r-ltjm3-e2c6mlpu.png

Пример 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».
oebhckp8j0tqchf3fz5mwytgb50.png

Знакомство с устройствами хранения


Что это такое?


Это устройство, служащее для хранения и извлечения информации, которое также может использоваться в качестве средства загрузки.

Какие виды устройств хранения бывают?


За время эволюции информационных технологий было разработано множество их видов, включая:

• Магнитные ленты;
• Флоппи-диски;
• CD и DVD-диски;
• Жесткие диски;
• USB-носители;
• …

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

Что такое флоппи-диск?

hosk3zfyfkpwqpf0f98nezxwyf0.png

Так выглядит старая-добрая дискета. Объем этого носителя невелик и составляет всего 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 байт

Существуют и более крупные величины, но мы ограничимся этими.

Структура типичного флоппи-диска

nm_twpbhz1bcr2hga-n_e8dzgcq.png

Это общее схематичное строение дискеты, а вот характеристики используемой нами дискеты 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, которые выделены на снимке ниже:

8sktkytn8bufkyyh-b4wyeazqyy.png

После запуска программы через эмулятор bochs отобразится следующее:

dfrx4ud8tgquhdwgmpzd98mjmlo.png

Что делает эта программа?


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

Я вкратце поясню эти макросы и функции.

# макрос вывода строки с завершающим нулем
# этот макрос вызывает функцию 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, научимся выполнять чтение/запись такой дискеты, а также создадим загрузчик второй стадии и разберем его назначение.

oug5kh6sjydt9llengsiebnp40w.png

© Habrahabr.ru