Русский
Русский
English
Статистика
Реклама

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



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

Здесь я ограничусь написанием программы на ассемблере и ее копированием в загрузочный сектор образа дискеты 3.5, после чего мы, как и в прошлой статье, протестируем записанный загрузочный код при помощи эмулятора bochs. Для реализации этих задач я задействую службы BIOS, что позволит нам лучше понять их функционирование и более уверенно работать в реальном режиме (Real Mode).

План статьи


Знакомство с сегментацией
Среда программирования
Чтение данных из RAM
Знакомство с устройствами хранения
Структура флоппи-диска
Взаимодействие с флоппи-диском

Знакомство с сегментацией


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

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


Основная память разделена на сегменты, индексируемые специальными сегментными регистрами CS, DS, SS и ES.

Назначение сегментации


Когда мы указываем 16-битный адрес, ЦПУ автоматически вычисляет начальный адрес соответствующего сегмента. Тем не менее именно программист должен указывать начальный адрес каждого сегмента, особенно при написании такой программы, как загрузчик.

Какие бывают типы сегментов?


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

сегмент кода;
сегмент данных;
сегмент стека;
расширенный сегмент.

Сегмент кода
Один из разделов программы в памяти, содержащий исполняемые инструкции. Если вы загляните в мою предыдущую статью, то увидите метку .text, под которой мы размещаем исполняемые инструкции. При загрузке программы в память эти инструкции передаются в сегмент кода. В ЦПУ для обращения к этому сегменту мы используем регистр CS.

Сегмент данных
Раздел программы в памяти, содержащий статические и глобальные переменные. Для обращения к нему мы используем регистр DS.

Сегмент стека
Программист может использовать регистры для хранения, изменения и извлечения данных при написании программы. При этом ограниченное количество регистров зачастую приводит к усложнению логики. В итоге у разработчика постоянно возникает потребность в дополнительном пространстве, более гибком в плане хранения, обработки и извлечения данных. С целью решения подобных сложностей в ЦПУ был внедрен специальный сегмент стека. Для хранения в этом сегменте данных и их извлечения используются соответствующие инструкции push (добавление) и pop (удаление). Инструкцию push мы также используем для передачи аргументов в функции. Обращение к сегменту стека производится при помощи регистра SS. При этом важно помнить, что стек растет сверху вниз.

Расширенный сегмент
Обычно этот сегмент используется для загрузки данных, имеющих слишком большой объем для хранения в сегменте данных. Позже вы увидите, как я загружаю данные с дискеты в расширенный сегмент. Для обращения же к нему мы используем регистр ES.

Применение сегментных регистров


Мы не можем устанавливать эти регистры напрямую и вместо этого делаем следующее:

movw $0x07c0, %axmovw %ax, %ds 


Что здесь происходит?


Копирование данных в регистр общего назначения.
Их перенос в сегментный регистр.

Мы загружаем в регистр AX значение из 0x07c0, после чего копируем содержимое AX в DS. Абсолютный же адрес вычисляется так:

DS = 16 * AXDS = 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


Как обратиться к диску с помощью прерывания 0x13?


Команда 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     # <b>???номер диска????</b>          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  PrintStringPrintStringOut: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  DisplayDataInDisplayDataOut: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, научимся выполнять чтение/запись такой дискеты, а также создадим загрузчик второй стадии и разберем его назначение.

Источник: habr.com
К списку статей
Опубликовано: 09.01.2021 16:11:17
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании ruvds.com

Ruvds_перевод

Assembler

Загрузчик

Bootsector

Mbr

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru