- PVSM.RU - https://www.pvsm.ru -
Разворачивал в очередной раз Linux-образ на USB-drive (почему-то им оказался Manjaro, но это совсем другая история), и в голову пробрались странные мысли: BIOS увидел флешку, а дальше-то что? Ну да, там MBR, скорее всего GRUB и… А раз в MBR затесался чей-то кастомный код, значит и простой человек из Адыгеи может запрограммировать что-нибудь на «большом» компьютере, но вне операционной системы.
А так как делать такие штуки на языках высокого уровня слишком жирно, а ассемблеров мы не знаем, будем шпарить прямо на опкодах для 8086.
#
Hello, Habrauser!
Чтобы не докучать домашних грохотом флоповода, тренироваться будем на кошках QEMU. Но, полагаю, желающие смогут всё то же самое нарезать с помощью dd
на флешку и запустить на любой x86-совместимой железяке. Это раз.
Мы будем крушить MBR, так что если вы где-то её еще используете (зачем?) и захотите нарезать наши результаты на живой накопитель (зачем?) — думайте, прежде чем надавить Enter
. Это два.
Автор — не настоящий сварщик, и может нести (и обязательно донесёт!) какую-то ересь. (У автора вообще детство Бейсиком сломано.) Набегите в комментарии и всё исправьте! Это три.
Для наших низменных целей нам достаточно знать следующее:
Bootstrap Area
и финальная сигнатура.адресу 0x7c00
[2] (если вы не счастливый обладатель Compaq).Для начала, что такое опкоды — для тех, кто не знает.
Давным давно, когда компьютеры были большими, а программисты еще не назывались разработчиками, но уже перестали вырезать окошки в перфокартах, они решили писать программы прямо (sic!) на компьютерах. А так как программисты быстро поняли, что делать это в двоичных кодах не очень сподручно (места всё-таки уходит многовато), переводить двоичные числа в шестнадцатеричные может любой дурак, то и листинги писали прямо хексами.
Если видели у бати, а то и деда какой-нибудь «Радио» за 80-е годы или «Моделист-Конструктор» за начало 90-х, то в конце наверняка находили листинги для соответствующих самопайных компьютеров: «РК» или «Специалиста». Там были и «ХО», и клоны Load 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 "# 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. Но мы ведь не хотим неприятностей?)
В итоге скриптецкий набросать пришлось, но он оказался не таким большим, каким имел шансы быть.
#!/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
Скрипт в репо [3]
В нём есть одна недоработка: в конце обязательно должен быть
LF
(akan
), иначе последняя строка обработана не будет. Не могу сказать, что меня это сильно беспокоит, или я думал над тем, как это починить, но если кто-то знает, как это сделать быстро — буду рад помощи.
А теперь — делай, как я!
➜ $ ./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
Болванка в репо [4]
Естественно, наши нули ни к чему хорошему не приведут, и, что 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
Файл целиком [5]
Оживим же нашего Буратино, сменив два финальных байта на валидную сигнатуру:
# MBR Signature 0x1FE:0x1FF (510-511)
55 AA
Что получилось [6]
Запрягаем:
$ 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
Файл в репо [7]
Всё остальное забито теми же нулями и сигнатуркой (следите за тем, чтобы байтов было 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
)á
(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
[12] иtypewriter.mbr
[13].
Эх, да простят меня низкоуровневые программисты:
# 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 [13]
Давайте разбираться:
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
color-printer.mbr [14]
BL
нужный цвет шрифта (чёрный. Да, чёрный.)FE
инкрементируем BL
Вот и весь меджик.
Определенно, самая полезная глава в моём рассказе.
man ascii
тоже помог, молодец.Жена подходит, говорит:
— Хватит работать!
— А я и не работаю.
Заглядывает в экран, видит Sublime Text со всем этим безобразием:
— А-а-а, какой ужас! Это зашифрованная порнуха!
Занавес.
В чём-то ведь она права.
Автор: Дмитрий
Источник [28]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/assembler/350887
Ссылки в тексте:
[1] Структуру: https://en.wikipedia.org/wiki/Master_boot_record#Sector_layout
[2] адресу 0x7c00
: https://wiki.osdev.org/MBR_(x86)#MBR_Bootstrap
[3] Скрипт в репо: https://github.com/dm-logv/ambroxol/blob/master/build
[4] Болванка в репо: https://github.com/dm-logv/ambroxol/blob/922c0e5d6301404d66c13211afefe0995806ef8b/loader.mbr
[5] Файл целиком: https://github.com/dm-logv/ambroxol/blob/e55a0494db5b82347f3a5357fb9c7b6d0b742092/loader.mbr
[6] Что получилось: https://github.com/dm-logv/ambroxol/blob/2167adf9c9c68abe30b040bae40eb215da62ac05/loader.mbr
[7] Файл в репо: https://github.com/dm-logv/ambroxol/blob/3877a30c586d071b44d817e63e4fe46e2d3c11d0/loader.mbr
[8] Да, я это закоммитил: https://github.com/dm-logv/ambroxol/blob/15677382ecddaff80b7ec3b1ec2b8fc486b91296/loader.mbr
[9] Строка целичком и подряд у нас зайдет в отдельный блок: https://github.com/dm-logv/ambroxol/blob/8de7c75216de71ef9f51ffae7c9e96ad3a754fdf/loader.mbr
[10] Бахнем цикл, в котором будем перебирать наши символы и печатать их: https://github.com/dm-logv/ambroxol/blob/805fb566f5ac61db0d3d2317801ed80b210a71ca/loader.mbr
[11] Предварительно почистим экран, чтобы убрать сообщения старичка BIOS'а: https://github.com/dm-logv/ambroxol/blob/6ee9161f5bf96e705e80ac685ad3418cf604289c/loader.mbr
[12] printer.mbr
: https://github.com/dm-logv/ambroxol/blob/master/printer.mbr
[13] typewriter.mbr
: https://github.com/dm-logv/ambroxol/blob/master/typewriter.mbr
[14] color-printer.mbr: https://github.com/dm-logv/ambroxol/blob/master/color-printer.mbr
[15] x86 Opcode Cheat Sheet: https://pnx.tf/files/x86_opcode_structure_and_instruction_overview.pdf
[16] Intel 80386 Reference Programmer's Manual: https://pdos.csail.mit.edu/6.828/2010/readings/i386/toc.htm
[17] Operand Selection: https://pdos.csail.mit.edu/6.828/2010/readings/i386/s02_05.htm
[18] Instruction Format: https://pdos.csail.mit.edu/6.828/2010/readings/i386/s17_02.htm
[19] Opcode Map: https://pdos.csail.mit.edu/6.828/2010/readings/i386/appa.htm
[20] X86 Opcode and Instruction Reference: http://ref.x86asm.net/index.html
[21] x86 Instruction Set Reference: https://c9x.me/x86/
[22] Intel 80x86 Assembly Language OpCodes: http://www.mathemainzel.info/files/x86asmref.html
[23] Ralf Brown's Interrupt List: http://www.ctyme.com/rbrown.htm
[24] оригинал: http://www.cs.cmu.edu/~ralf/files.html
[25] Values for standard video mode: http://www.columbia.edu/~em36/wpdos/videomodes.txt
[26] Онлайн-дебаггер ассемблера: http://asmdebugger.com/
[27] ассемблер-дизассемблер: https://defuse.ca/online-x86-assembler.htm#disassembly
[28] Источник: https://habr.com/ru/post/490094/?utm_source=habrahabr&utm_medium=rss&utm_campaign=490094
Нажмите здесь для печати.