Разворачивал в очередной раз Linux-образ на USB-drive (почему-то им оказался Manjaro, но это совсем другая история), и в голову пробрались странные мысли: BIOS увидел флешку, а дальше-то что? Ну да, там MBR, скорее всего GRUB и… А раз в MBR затесался чей-то кастомный код, значит и простой человек из Адыгеи может запрограммировать что-нибудь на «большом» компьютере, но вне операционной системы.
А так как делать такие штуки на языках высокого уровня слишком жирно, а ассемблеров мы не знаем, будем шпарить прямо на опкодах для 8086.
#
Hello, Habrauser!
Чтобы не докучать домашних грохотом флоповода, тренироваться будем на кошках QEMU. Но, полагаю, желающие смогут всё то же самое нарезать с помощью dd
на флешку и запустить на любой x86-совместимой железяке. Это раз.
Мы будем крушить MBR, так что если вы где-то её еще используете (зачем?) и захотите нарезать наши результаты на живой накопитель (зачем?) — думайте, прежде чем надавить Enter
. Это два.
Автор — не настоящий сварщик, и может нести (и обязательно донесёт!) какую-то ересь. (У автора вообще детство Бейсиком сломано.) Набегите в комментарии и всё исправьте! Это три.
Для наших низменных целей нам достаточно знать следующее:
Bootstrap Area
и финальная сигнатура.адресу 0x7c00
(если вы не счастливый обладатель Compaq).Для начала, что такое опкоды — для тех, кто не знает.
Давным давно, когда компьютеры были большими, а программисты еще не назывались разработчиками, но уже перестали вырезать окошки в перфокартах, они решили писать программы прямо (sic!) на компьютерах. А так как программисты быстро поняли, что делать это в двоичных кодах не очень сподручно (места всё-таки уходит многовато), переводить двоичные числа в шестнадцатеричные может любой дурак, то и листинги писали прямо хексами.
Если видели у бати, а то и деда какой-нибудь «Радио» за 80-е годы или «Моделист-Конструктор» за начало 90-х, то в конце наверняка находили листинги для соответствующих самопайных компьютеров: «РК» или «Специалиста». Там были и «ХО», и клоны Lode Runner, и драйверы для подключения печатной машинки «Консул».
И это только первая страница!
Да-да, всё это вбивали ручками, сверяли контрольные суммы, долго матерясь, искали ошибки, и еще больше матерясь — ждали следующего выпуска с 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 "# Comment\n48 65 6c 6c 6f 2c 20 # First line\n48 61 62 72 21 # Last line" | sed 's/#.*$//g' | xxd -r -p
Hello, Habr!%
(На самом деле такой выхлоп будет и без sed
-а, потому что xxd
просто пропускает то, что не смог распарсить как hex-dump. Но мы ведь не хотим неприятностей?)
В итоге скриптецкий набросать пришлось, но он оказался не таким большим, каким имел шансы быть.
#!/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.
...
# 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, а нас — от лишних ожиданий.
Оно живо, BIOS думает, что эта балалайка его загрузит! Ура, товарищи!
PRINT "#"
Хватит уже мять мышку, пора печатать!
Для нашей первой проказы проделки не понадобится практически ничего, даже думать. Посмотрите сами:
# 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 и вуаля!
Я уверен, мои безграмотные комментарии всё прояснили, но, на всякий случай, давайте еще разок:
B4 0E
— здесь мы отправляем в регистр AH
значение 0E
(нормальные люди написали бы здесь mov ah, 0e
), что укажет одной интересной функции BIOS (о ней ниже), что мы нуждаемся в консольном выводе, то есть просто будет печатать символы на экран.B0 23
— тут всё столь же просто: мы заталкиваем в AL
код символа #
. Где я его взял? Ну что значит «где»? Я же писал выше — в ASCII-таблице из man ascii
!CD 10
— это вообще изян: дергаем BIOS-функцию, отвечающую за вывод всякой ерунды на экран. Она подхватит те аргументы, что мы затолкали в AL
и AH
, ну и сделает то, что мы от неё хотели: напечатает несчастный октоторп.Особо инициативные могут поиграться с шрифтами с кодом, отправляемым в AL
и добиться вывода:
$
(B0 24
)%
(B0 25
)a
(B0 A0
, но возможно мне просто повезло)PRINT "Hello, Habrauser!"
Но все эти одиночные символы, конечно, цветочки. Волчьи ягодки нас ждут впереди.
Давайте же принтанём что-нибудь посерьезнее. Тем более сделать это на опкодах — это вам не 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
И, в принципе, мы добились результата:
Давайте немного очеловечим эту штуку:
Booting from Hard Disk...
.Го смотреть, что получилось в итоге:
# 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-функцию...LODS
!На самом деле я изрядно подпортил себе нервишки, копаясь в документации к командам процессора и подбирая вручную байтики для переходов. Но пара сеансов у психотерапевта всё поправят, не переживайте.
PRINT user_input$
Это всё уже почти похоже на настоящее программирование, но что-то маловато в нашем софте интерактива. Давайте сделаем примитивнейшую печатную машинку: будем с помощью тех же 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
Давайте разбираться:
AH = 00h
скомандуем прерыванию 16h
, которое отвечает за работу с кливиатурой, что нам нужно достать символ нажатой кнопки, который функция окунёт в регистр AL
.OD
(aka CR
aka перевод каретки
), который получаем от нажатия клавиши Return
/Enter
, и напечатаем его, то он у нас только каретку и переведёт (у нас же печатная машинка всё-таки), то есть поставит курсор в начало текущей строки.CR
мы напечатаем не только CR
, но и символ LF
, который провернёт барабан с бумагой на одну строку, сотворив ожидаемое поведение от Enter
.AL
вовсе не OD
, то мы всё это пропускаем, перепрыгивая через шесть байтов к инструкции печати символа.Ух, поразвлекаемся немножко:
Итого, наша штука может:
Backspace
курсор перемещается назад, и мы можем на месте старого символа поставить новый.Но перемещение символов ограничено новой строкой. Что б жизнь emacs
-ом не казалась.
Маленькие дополнения для тех, кто дочитал до конца.
КДПВ родилась из такого выхлопа, который получился из-за неправильного подсчета байтов для джампа:
Я его немного подрихтовал, добавил красивых цветов. В общем, смотрите сами:
# 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
BL
нужный цвет шрифта (чёрный. Да, чёрный.)FE
инкрементируем BL
Вот и весь меджик.
Определенно, самая полезная глава в моём рассказе.
man ascii
тоже помог, молодец.Жена подходит, говорит:
— Хватит работать!
— А я и не работаю.
Заглядывает в экран, видит Sublime Text со всем этим безобразием:
— А-а-а, какой ужас! Это зашифрованная порнуха!
Занавес.
В чём-то ведь она права.
UPD: Спасибо eisaev, Andrew_Pinkerton, MrSmith33, Anthony1025 и Юле, которая не желает регистрироваться на Хабре, за правку моих ошибок и опечаток.
К сожалению, не доступен сервер mySQL