Назад к истокам: рулим компьютером прямо из MBR

в 21:00, , рубрики: mbr, ассемблер, загрузка, КодоБред, ненормальное программирование, низкоуровневое программирование, опкоды, Процессоры, системное программирование

Разворачивал в очередной раз Linux-образ на USB-drive (почему-то им оказался Manjaro, но это совсем другая история), и в голову пробрались странные мысли: BIOS увидел флешку, а дальше-то что? Ну да, там MBR, скорее всего GRUB и… А раз в MBR затесался чей-то кастомный код, значит и простой человек из Адыгеи может запрограммировать что-нибудь на «большом» компьютере, но вне операционной системы.

А так как делать такие штуки на языках высокого уровня слишком жирно, а ассемблеров мы не знаем, будем шпарить прямо на опкодах для 8086.

Назад к истокам: рулим компьютером прямо из MBR - 1

Вступление

План:

  1. Вывести #
  2. Вывести Hello, Habrauser!
  3. Выводить вводимые символы (уже можно детей развлекать).

Предупреждения и отказы от ответственности

Чтобы не докучать домашних грохотом флоповода, тренироваться будем на кошках QEMU. Но, полагаю, желающие смогут всё то же самое нарезать с помощью dd на флешку и запустить на любой x86-совместимой железяке. Это раз.

Мы будем крушить MBR, так что если вы где-то её еще используете (зачем?) и захотите нарезать наши результаты на живой накопитель (зачем?) — думайте, прежде чем надавить Enter. Это два.

Автор — не настоящий сварщик, и может нести (и обязательно донесёт!) какую-то ересь. (У автора вообще детство Бейсиком сломано.) Набегите в комментарии и всё исправьте! Это три.

Немного про MBR

Для наших низменных целей нам достаточно знать следующее:

  • Структуру, а из самой структуры нам нужна только Bootstrap Area и финальная сигнатура.
  • Факт того, что бутстрап загрузится по фиксированному (слава богу) адресу 0x7c00 (если вы не счастливый обладатель Compaq).
  • Ну и то, что работать мы будем в реальном режиме процессора, и доступна нам будет вся память (злобный смех, муа-ха-ха). Ну как вся: все те 640KB, которых всем хватит. (Даже не знаю, чем на это поможет или помешает.)

Опкоды

Для начала, что такое опкоды — для тех, кто не знает.

Давным давно, когда компьютеры были большими, а программисты еще не назывались разработчиками, но уже перестали вырезать окошки в перфокартах, они решили писать программы прямо (sic!) на компьютерах. А так как программисты быстро поняли, что делать это в двоичных кодах не очень сподручно (места всё-таки уходит многовато), переводить двоичные числа в шестнадцатеричные может любой дурак, то и листинги писали прямо хексами.

Если видели у бати, а то и деда какой-нибудь «Радио» за 80-е годы или «Моделист-Конструктор» за начало 90-х, то в конце наверняка находили листинги для соответствующих самопайных компьютеров: «РК» или «Специалиста». Там были и «ХО», и клоны Load Runner, и драйверы для подключения печатной машинки «Консул».

Большой скриншоты «Пещеры» + запись стрима

Назад к истокам: рулим компьютером прямо из MBR - 2

И это только первая страница!

Да-да, всё это вбивали ручками, сверяли контрольные суммы, долго матерясь, искали ошибки, и еще больше матерясь — ждали следующего выпускать с errata.

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

Какие моменты нужно взять на заметку:

  • Проглядеть ассемблеровские мнемоники, все эти MOV, INT, ADD, DIV — это, наверное, и всё, что нам понадобится. Посмотрите, как они работают, какие аргументы принимают, куда складывают результаты.
  • Осознать, что обозначают типы аргументов, которые в интеловской документации выглядят, как: imm8, r16, r/m32, rel8. У меня, вот, довольно много времени (наверное, с час) ушло, чтобы сообразить, как DIV BL превращается в F6 F3 (DIV принимает r/m8, который может указывать, как на регистр, так и адрес памяти — в зависимости от хитросплетений байтов.) и почему опкод F6 — это не только DIV, но и NEG, и еще пара операций (Это зависит от opcode extension — трех байтов в операнде.)

Тулзятина

Решил я по началу писать прямо в файлик, который потом подсовывать сначала эмулятору, а потом и dd, чтобы затолкать на железку, но понял, что так для нас, зумеров, будет решительно неудобно — без красивого оформления, комментариев, да билд-системы. Посему я собрался с духом и накатал себе чудо-скрипт, а вот и он… Хотел было написать я, но подумал что умные дядьки из POSIX наверняка всё сделали за меня, и таки да почти да!

➜  $ echo "48 65 6c 6c 6f 2c 20 48 61 62 72 21" | xxd -r -p 
Hello, Habr!%    

Осталось придумать синтаксис комментариев и стрипать их:

➜  $ echo -e "# Commentn48 65 6c 6c 6f 2c 20  # First linen48 61 62 72 21        # Last line" | sed 's/#.*$//g' | xxd -r -p
Hello, Habr!%  

(На самом деле такой выхлоп будет и без sed-а, потому что xxd просто пропускает то, что не смог распарсить как hex-dump. Но мы ведь не хотим неприятностей?)

В итоге скриптецкий набросать пришлось, но он оказался не таким большим, каким имел шансы быть.

А вот и я, sh-скрипт

#!/bin/sh

IN="${1:-/dev/stdin}"
OUT="${2:-/dev/stdout}"

> $OUT

while read line
do
    echo "$line" | sed 's/#.*$//' | xxd -r -p >> $OUT
done < $IN

Скрипт в репо

В нём есть одна недоработка: в конце обязательно должен быть LF (aka n), иначе последняя строка обработана не будет. Не могу сказать, что меня это сильно беспокоит, или я думал над тем, как это починить, но если кто-то знает, как это сделать быстро — буду рад помощи.

А теперь — делай, как я!

➜  $ ./build loader.mbr loader.img && stat -f %z loader.img
512

512 — именно тот размер, который нас устроит. А как его получить, мы узнаем дальше.

Наступление

Бойлерплейтим

Для начала сделаем болванку, которая сформируется в bin-файлик размером в 512B, забитый исключительно ноликами. «Это можно было сделать с помощью dd и /dev/zero, болван!» — скажете вы и окажетесь правы. Но вы только посмотрите, как красиво я расставил эти нолики по колонкам разделил на блоки и расставил поcчитанные на калькуляторе (ну ладно, в ipython) адреса!

Ничего интересного, просто нолики с адресами

# 0x0000:0x007F (0-127)
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00

00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00

# 0x0080:0x00FF (128-255)
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00

00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00

# 0x0100:0x017F (256-383)
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00

00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00

# 0x0180:0x0200 (384-512)
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00

00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00

Болванка в репо

Естественно, наши нули ни к чему хорошему не приведут, и, что QEMU, что живая железка обругают нас благим Exception'ом.

Назад к истокам: рулим компьютером прямо из MBR - 3

Расчешем деревяшку еще немного, отделив блоки, в которых должны будут описываться разделы (но они нам не пригодятся), и сигнатура MBR.

Немножко интереснее, с заготовкой таблицы разделов

...

# 0x0180:0x0200 (384-445)
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00

# Partition 1     0x01BE:0x01CD (446-461) 
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00

# Partition 2     0x01CE:0x01DD (462-477)
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00

# Partition 3     0x01DE:0x01ED (478-493)
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00

# Partition 4     0x01EE:0x01FD (494-509)
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00

# MBR Signature   0x1FE:0x1FF (510-511)
00 00

Файл целиком

Оживим же нашего Буратино, сменив два финальных байта на валидную сигнатуру:

# MBR Signature   0x1FE:0x1FF (510-511)
55 AA

Что получилось

Запрягаем:

$ qemu-system-i386 -nic none loader.img

-nic none отключит сетевые интерфейсы, что избавит эмулятор от пустых надежд загрузиться через PXE, а нас — от лишних ожиданий.

Назад к истокам: рулим компьютером прямо из MBR - 4

Оно живо, BIOS думает, что эта балалайка его загрузит! Ура, товарищи!

Хватит уже мять мышку, пора печатать!

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

# 0x0000:0x007F (0-127)
B4 0E                      # Set a console output mode
B0 23                      # Set an octothorp sign
CD 10                      # Call a print function

00 00
00 00 00 00 00 00 00 00

Файл в репо

Всё остальное забито теми же нулями и сигнатуркой (следите за тем, чтобы байтов было 512).

Собираем наше ООП, заталкиваем в QEMU и вуаля!

Назад к истокам: рулим компьютером прямо из MBR - 5

Я уверен, мои безграмотные комментарии всё прояснили, но, на всякий случай, давайте еще разок:

  • B4 0E — здесь мы отправляем в регистр AH значение 0E (нормальные люди написали бы здесь mov ah, 0e), что укажет одной интересной функции BIOS (о ней ниже), что мы нуждаемся в консольном выводе, то есть просто будет печатать символы на экран.
  • B0 23 — тут всё столь же просто: мы заталкиваем в AL код символа #. Где я его взял? Ну что значит «где»? Я же писал выше — в ASCII-таблице из man ascii!
  • CD 10 — это вообще изян: дергаем BIOS-функцию, отвечающую за вывод всякой ерунды на экран. Она подхватит те аргументы, что мы затолкали в AL и AH, ну и сделает то, что мы от неё хотели: напечатает несчастный октоторп.

Особо инициативные могут поиграться с шрифтами с кодом, отправляемым в AL и добиться вывода:

  • $ (B0 24)
  • % (B0 25)
  • или даже á (B0 A0, но возможно мне просто повезло)

Но все эти одиночные символы, конечно, цветочки. Волчьи ягодки нас ждут впереди.

Давайте же принтанём что-нибудь посерьезнее. Тем более сделать это на опкодах — это вам не printf('Hell of word') наклепать.

Конечно же, мы можем сделать, как полные удоды:

Мне стыдно показывать, спрячу под спойлер

# 0x0000:0x007F (0-127)
B4 0E    # Set a console output mode

B0 0A    # LF
CD 10

B0 48    # H
CD 10    # print 

B0 65    # e
CD 10

B0 6C    # l
CD 10

B0 6C
CD 10

B0 6F    # o
CD 10

B0 2C    # ,
CD 10

B0 20    # SPC
CD 10

B0 48    # H
CD 10

B0 61    # a
CD 10

B0 62    # b
CD 10

B0 72    # r
CD 10

B0 61    # a
CD 10

B0 75    # u
CD 10

B0 73    # s
CD 10

B0 65    # e
CD 10

B0 72    # r
CD 10

B0 21    # !
CD 10

00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00

Да, я это закоммитил

И, в принципе, мы добились результата:

Назад к истокам: рулим компьютером прямо из MBR - 6

Давайте немного очеловечим эту штуку:

Го смотреть, что получилось в итоге:

# 0x0000:0x007F (0-127)
B8 00 06          # Clear screen
CD 10

B4 0E             # Set a console output mode
BE 80 7C          # Place 0x0080 + 0x7c00 = 0x7c80 into SI
AC                # Load a byte at address SI into AL, increment SI
3C 00             # AL == 00?
74 06             # If yes, go to +6 bytes (to zeroes)
CD 10             # Print a char in AL
EB F7             # Go to -7 bytes (to AC opcode)

00 00 00 00 00
00 00 00 00 00 00 00 00

...

# 0x0080:0x00FF (128-255)
48 65 6C 6C 6F 2C 20 48   # Hello, H
61 62 72 61 75 73 65 72   # abrauser
21                        # !

00 00 00 00 00 00 00
00 00 00 00 00 00 00 00

На деле всё просто:

  • Очистку экрана пропускаем
  • Важная штука: нам пришлось вспомнить, что наша программка грузится в адрес 0x7c00, а значит адреса в нашей программке должны плясать именно от этого значения. Как именно плясать? Мы ручками изобразили data-блок (да просто написали текст отдельно от кода), заботливо посчитали байты (не зря я всё делил на блоки и подписывал их!), заплюсовали с начальным адресом и положили получившуюся позицию в регистр SI — самое то для хранения адреса с данными.
  • Дальше об SI будет заботиться опкод AC (ньюфаги знают его по мнемонике LODS). Этот красавчик не просто вытащит данные из адреса, лежащего в SI и толкнёт его в AL, но и заинкрементит сам SI! Вай, молодец!
  • Теперь нужно подумать об окончании строки. Раз у нас всё забито нулями, пусть ноль и будет терминатором строки. Свежо, ново, не так ли?
  • AL, в котором лежит текущий символ будем сравнивать с нулём, и если оно так — просто выйдем за пределы кода (в нашем случае, нужно сместиться на 6 байт вперед), а 00 процессор выполнять не хочет.
  • Если же наш кремниевый друг обнаружит в AL что-то стоящее, то он вызовет уже знакомую нам BIOS-функцию...
  • … и джампнется на 7 байт назад — как раз к LODS!

На самом деле я изрядно подпортил себе нервишки, копаясь в документации к командам процессора и подбирая вручную байтики для переходов. Но пара сеансов у психотерапевта всё поправят, не переживайте.

Это всё уже почти похоже на настоящее программирование, но что-то маловато в нашем софте интерактива. Давайте сделаем примитивнейшую печатную машинку: будем с помощью тех же BIOS-функций печатать вводимые символы. А сохранять… Ну сфотографируете экран на телефон. Или потом напишем с вами не менее примитивную файловую систему — но уже в другой статье (не забывайте, статьи я пишу раз в десять лет — и изменять этому правилу я не намерен).

Я принял коньяк волевой решение и решил, что печатная машинка достойна отдельного файла. И теперь в репозитории есть printer.mbr и typewriter.mbr.

Эх, да простят меня низкоуровневые программисты:

# 0x0000:0x007F (0-127)
B4 07   # Clear screen
B0 00   #
CD 10   #

B4 00   # Set Get keystroke mode
CD 16   # Read a char -> AL

3C 0D   # AL == 0D? (CR, Return pressed)
75 06   # If no, go to +6 bytes

B4 0E   # Print CR
CD 10   #
B0 0A   # Then print NL

B4 0E   # Print a char
CD 10   # 

EB EC   # Go to -20 bytes

Новый блоб 1

Давайте разбираться:

  • Очистку экрана (которая не работает в QEMU) мы уже видели.
  • Дальше мы с помощью AH = 00h скомандуем прерыванию 16h, которое отвечает за работу с кливиатурой, что нам нужно достать символ нажатой кнопки, который функция окунёт в регистр AL.
  • Далее я натнулся на маленькую траблу, связанную с переводами строк: если мы возьмем символ OD (aka CR aka перевод каретки), который получаем от нажатия клавиши Return/Enter, и напечатаем его, то он у нас только каретку и переведёт (у нас же печатная машинка всё-таки), то есть поставит курсор в начало текущей строки.
  • Поэтому обнаружив CR мы напечатаем не только CR, но и символ LF, который провернёт барабан с бумагой на одну строку, сотворив ожидаемое поведение от Enter.
  • Если же у нас в AL вовсе не OD, то мы всё это пропускаем, перепрыгивая через шесть байтов к инструкции печати символа.
  • Мы молодцы: считали-проверили-напечатали символ, можно повторять сначала! Прыгаем на заботливо посчитанные 20 байт назад.

Ух, поразвлекаемся немножко:

Назад к истокам: рулим компьютером прямо из MBR - 7

Итого, наша штука может:

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

Но перемещение символов ограничено новой строкой. Что б жизнь emacs-ом не казалась.

Отступление

Маленькие дополнения для тех, кто дочитал до конца.

КДПВ

КДПВ родилась из такого выхлопа, который получился из-за неправильного подсчета байтов для джампа:

Назад к истокам: рулим компьютером прямо из MBR - 8

Я его немного подрихтовал, добавил красивых цветов. В общем, смотрите сами:

# 0x0000:0x007F (0-127)
B8 12 00          # Set VGA mode 640x480x16
CD 10

B4 0E             # Set a console output mode
B3 00             # Set FG color to black
FE C3             # Color++

BE 80 7C          # Place 0x0080 + 0x7c00 = 0x7c80 into SI
AC                # Load a byte at address SI into AL, increment SI
3C 00             # AL == 00?
74 F6             # If yes, go to -10 bytes (to FE C3)

CD 10             # Print a char in AL
EB F7             # Go to -9 bytes (to AC)

00
00 00 00 00 00 00 00 00

...

# 0x0080:0x00FF (128-255)
48 65 6C 6C 6F 2C 20 48   # Hello, H
61 62 72 61 68 61 62 72   # abrahabr
21 20                     # ! 

00 00 00 00 00 00
00 00 00 00 00 00 00 00

color-printer.mbr

  • Сперва мы переключаем наш вывод (монитор? видеокарту? BIOS?) на цветной режим, а то не будет цветной красоты,
  • Кладём в BL нужный цвет шрифта (чёрный. Да, чёрный.)
  • С помощью инкрементирующего FE инкрементируем BL
  • Ну а следующий фрагмент вы уже видели: печатаем текст, который лежит отдельно, но по завершению не выходим, а возвращаемся к операции инкремента цвета.

Вот и весь меджик.

Ссылки

Определенно, самая полезная глава в моём рассказе.


Постскриптум

Жена подходит, говорит:
— Хватит работать!
— А я и не работаю.
Заглядывает в экран, видит Sublime Text со всем этим безобразием:
— А-а-а, какой ужас! Это зашифрованная порнуха!
Занавес.

В чём-то ведь она права.

Автор: Дмитрий

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js