ассемблер
Утилита «s-tools»
Вчера перед сном вспоминал, что ещё я такого программировал в детстве, о чём можно упомянуть, кроме программы «Паррот», о которой я рассказывал около месяца назад. Вспомнил про утилиту s-tools, которую мы писали вдвоём с братишкой на ассемблере x86 под ДОС.
Название придумал я. Расшифровывалось оно как «Stepanischevs’ tools», а сокращение намеренно было сделано похожим на «stools» — «табуретки», поэтому я нередко так эту утилиту и называл.
Это был набор утилит в одном файле и самый объёмный код на ассемблере, который мы
Утилиты вызывались, если я ничего не путаю, при помощи номерного ключа, после которого шли параметры самой утилиты. К каждой из них прилагалось описание на русском языке. Часть программ была «резидентами» (TSR), и некоторые из них умели загружаться в HMA.
В ДОСе были специальные вызовы, при помощи которых можно было выделить себе в HMA кусочек памяти. Но поскольку это был один сегмент, программам приходилось модифицировать себя, чтобы поменять у себя внутри адреса ячеек памяти и использовать только ближние вызовы (jmp near или call near).
Я попытался вспомнить что именно умели «табуретки», но вспомнил только малую часть, разумеется. Утилиты в моём списке идут не по порядку следования, я его просто не помню.
Первое, что я вспомнил — в отдельном файле s-tools.ovl лежал шрифт, который мы вынули
Вторая утилита умела поднимать тайминги памяти на PC XT, идея была подсмотрена
Третья утилита решала проблему буфера клавиатуры. Компьютеры в те времена работали так медленно, что не успевали обрабатывать весь ввод пользователя, он попадал в специальный буфер, а уже оттуда его забирали программы. Если вы печатали достаточно быстро, вы вполне могли упереться в размер буфера и услышать звуковой сигнал, означающий, что вам надо подождать.
Буфер можно было переместить, но недалеко — в пределах, если ничего не путаю, первых 64 килобайт ОЗУ. Я тогда нашёл в ДОС два неиспользуемых зарезервированных участка памяти, куда, на выбор, и переставлял этот буфер. Второй из этих участков
Четвёртая утилита меняла палитру экрана. Если верно помню,
Следующая утилита выводила по нажатию
Шестая утилита умела записывать в файл нажатые клавиши, а потом их «проигрывать» — это было удобно для автоматизации
Седьмую утилиту я написал уже когда учился в Университете. Как я уже упоминал, я тогда работал лаборантом и, как и все лаборанты, нередко
Позже
Восьмую утилиту, возможно, следовало бы вспомнить первой. Не исключено, что с неё и начались «табуретки», но это не точно. В те времена мы много работали с дискетами и, если на них портился сектор на нулевой дорожке, где помещалось оглавление файловой системы, то её можно было выбрасывать — несмотря на то, что другие сектора в таких случаях помечались как «плохие» и просто не использовались, тот же фокус с нулевой дорожкой не проходил.
Один из наших соотечественников написал драйвер pu_1700, позволявший выбрать другую дорожку для хранения оглавления. Было только одно неудобство — драйвер приходилось запускать с разными параметрами руками, чтобы работать с разными дискетами.
В «табуретках» была резидентная программа, которая по горячей клавише патчила память pu_1700, меняя дорожку с оглавлением. Это позволяло быстро переключаться между стандартными и нестандартными дискетами.
Больше никаких подробностей я чётко не помню.
Смутно припоминаю, что была некая утилита для сканирования файловой системы, которую точно писал братишка, но что она делала вспомнить не могу. Кроме того, было
Программа с именем 0xFF
В ДОСе, как может
Поэтому странно, что пробельных символов в ней два, один из них — обычный пробел с кодом 32, именно его операционная система считает пробелом, второй — с кодом 255, он выводится как пробел, но считается обычным символом.
Не помню, использовалось ли это свойство
А 255 символ запретили? 30 лет назад, еще под MSDOS, переименовали утилиту переворота экрана в 255 символ и вставили в autoexec.bat. Хозяин компа уже психовать начал и монитор переворачивать :)) Показали ему прикол…
Ну то есть переименовали программу так, чтобы её имя выглядело как пробельный символ и добавили её в скрипт автозагрузки. Хозяин компьютера, видимо, проверял скрипт, ничего не находил и не понимал что происходит.
Мне стало интересно проверить — действительно ли это работает? Благо ДОС можно сейчас запустить даже в браузере.
Тут надо пояснить, что «настоящий» пробел для этого не годится — ДОС просто не распознает его в автозагрузке как имя файла для запуска.
Я не знаю как раньше вводили символ с кодом 255 в командную строку, поэтому просто набросал программу на ассемблере, которая переименовывает файл mem.exe (которая обычно есть в составе ДОСа) в .com:
org 100h
section .data
oldName db 'MEM.EXE', 0
newName db 255, '.COM', 0
section .text
global _start
_start:
mov ah, 56h
lea dx, oldName
lea di, newName
int 21h
mov ax, 4C00h
int 21hНеобходимое пояснение: расширение обязательно, иначе ДОС не поймёт, что файл запускаемый. При запуске его можно не указывать — операционка догадается сама что именно подставить.
На следующем шаге я скомпилировал программу у себя прямо в «МакОСи» и вывел в шестнадцатеричном виде:
$ hexdump -C <(nasm -f bin rename.asm -o /dev/stdout)
00000000 b4 56 8d 16 14 01 8d 3e 1c 01 cd 21 b8 00 4c cd |.V.....>...!..L.|
00000010 21 00 00 00 4d 45 4d 2e 45 58 45 00 ff 2e 43 4f |!...MEM.EXE...CO|
00000020 4d 00 |M.|
00000022Далее набрал её в кодах при помощи команды DEBUG и запустил. В текущей директории появилась программа с нужным именем:
Для последнего шага мне нужно было
Попробовал запустить, сработало!
Apple I ★
Заметил позитивную тенденцию — меня снова стали интересовать статьи на «Хабре», вплоть до зуда
Не знаю, что ребята сделали, но направление верное, молодцы!
В общем, прочитал недавно там статью про самодельный «Эпл I» и сегодня не спал до двух ночи.
Нет, свой компьютер я не собрал, да и начиналось всё невинно — сначала скачал несколько эмуляторов, попробовал
Для этой цели мне очень подошёл «ОупенЭмулятор»
Первая моя программа умела совсем мало — только бесконечно выводить на экран таблицу символов. Она такая короткая (всего восемь байт), так как для самого сложного — печати на экран, используется вызов ECHO, встроенной в «Эпл I» программы «Woz Monitor», — по всей видимости её писал Возняк, поэтому она так называется.
В ассемблере 6502 особо не развернёшься — три восьмибитных регистра (A, X, Y), 56 команд (не считая нескольких недокументированных), только целые числа, нет деления и умножения. Я совсем не помню свой первый ассемблер
Тем интереснее было написать
Кстати,
Там всего несколько страничек, вникнуть просто и сейчас (о ужас!) мне даже удобно.
Выделение памяти (ассемблер под Линукс)
До переезда блога на новый движок я писал аналог команды tee на ассемблере. Тогда передо мной стояла проблема выделения памяти — в команду может быть передано несколько имён файлов, я не знаю сколько, их нужно будет открыть, а под открытые файлы нужно место.
Программу я закончил,
Как я уже упоминал, есть два способа выделить память под Линуксом — sys_brk и sys_mmap. Второй способ используется, когда памяти надо выделить много, очевидно это не мой случай, поэтому я использовал вызов sys_brk. Этот вызов двигает границу так называемой «кучи» (Runtime heap на картинке) вверх (к старшим адресам) и в получившемся пространстве можно
Код выглядит так:
mov rax, 12 ; sys_brk
xor rdi, rdi ; в rdi записываем ноль (получить текущий адрес)
syscall
cmp rax, 0
jl memory_error ; обработка выделения памяти
; в rax у нас указатель на текущую границу кучи
; новый адрес нужно записать в регистр rdi
mov rcx, size ; количество хидеров файлов, которые надо хранить
lea rdi, [rcx * 8 + rax] ; каждый хидер занимает восемь байт
mov rax, 12 ; sys_brk
syscallТо есть получаем старый адрес, прибавляем сколько надо, устанавливаем новый адрес. Все менеджеры памяти Линукса (включая известнейший вызов malloc с Си), используют один из этих двух вызовов или их комбинацию.
Сложнейшие из них умеют сочетать различные стратегии выделения и управлять памятью внутри уже выделенных кусков, разбивая память на более мелкие регионы под нужды приложения, а так же бороться с фрагментацией, которая неизбежно появляется в процессе.
Моно-но аварэ
Вчера нашёл в себе силы оторваться от вечернего сериальчика и сесть за ассемблер, в итоге моя неповторимая поделочка уже может почти всё, что и жалкий оригинал.
Пока писал, обнаружилась несколько занятных вещей. Ниже серьёзный тон и если вам не нравится программирование, можете смело пропустить этот пост, да и картиночек не будет, совсем печально.
В прошлой версии своей утилиты я сначала пытался файл открыть, если не получалось, то создать. Вообще, есть такой флаг O_CREAT, который можно указать в качестве режима открытия и если файл не существует, он создаётся. У меня не создавался. Я не понимал почему и понял лишь когда запустил strace. Оказалось, что константы, которые я взял
Вторая вещь — я очень давно не программирую на ассемблере и растерял большинство лучших практик. Вот например я не помню как лучше — когда подпрограмма сохраняет регистры, которая она портит или наоборот. Вроде первый вариант выглядит
Ну и последнее, самое интересное. Утилита работает с файлами, а файловые хендлеры надо
Получаются всего несколько вызовов — мне нужен sys_mmap или sys_brk, первый работает с памятью от четырёх килобайт, второй и с менее значительными кусками. Хотя первый вызов мне интереснее, но выглядит он как попытка выстрелить из пушки по воробьям, попробую использовать второй.
Вообще всё это конечно очень непрактично, зато очень увлекательно —
Ассемблер под Линукс ★
Заметка про полноту по Тьюрингу команды присваивания в ассемблере натолкнула меня на мысль, что я
Тогда не довелось, решил вчера попробовать. Теорию в очень общих чертах я знал — есть системные вызовы (сисколы), которые можно дёргать
MOV RAX, 3 ; sys_read
MOV RBX, 0 ; stdin
MOV RCX, string ; адрес строки, которую выведем на экран
MOV RDX, len ; длина строки
INT 0x80Но я
MOV RAX, 0 ; sys_read
MOV RDI, 0 ; stdin
MOV RSI, string ; адрес строки, которую выведем на экран
MOV RDX, len ; длина строки
SYSCALLК тому времени меня посетила мысль сделать простенький аналог линуксовой утилитки tee, но для этого нужно было
Так как системный вызов для открытия файла (sys_open) как раз требует на вход сишную строку, я передал указатель, который взял со стека, в сискол, ожидая, что всё заработает. Это было моей ошибкой. Я потратил не меньше часа, разбираясь, что я сделал не так, перебирал способы передачи параметров, ползал по стеку при помощи отладчика gdb, ничего не помогало, пока не забрезжила догадка, что сами параметры командной строки тоже лежат на стеке и системному вызову это может не нравится.
Попробовал скопировать строку оттуда в данные и всё заработало. Не знаю что это было, пока не гуглил, возможно сделано из соображений безопасности. Времени потерял изрядно.
Пока получилась очень простая утилитка, но хочу её дописать как выпадет свободный вечер — сделать поддержку указания нескольких файлов и флага для режима присоединения. Размер радует, кстати, — всего 752 байта.
Дизассемблируй это ★
В моей жизни последовательно сменяли друг друга три ассемблера. Первый, для компьютера
В те времена я думал, что всю жизнь буду программировать на ассемблере, на деле же последнюю программу на нём написал в начале
В ассемблере есть такая команда — MOV (в некоторых ассемблерах — LD), записывает содержимое одного аргумента в другой. Сейчас набор комманд разросся, аргументом может быть почти что угодно — регистр, ячейка в памяти, сумма некоторого числа, одного регистра и другого, умноженного на число, но по сути это всегда присваивание.
И вот оказалось, что эта команда — полная по Тьюрингу. Звучит невероятно, но это так. Некие ребята заморочились и сделали компилятор, который компилирует любую программу на Си в последовательность команд MOV. Причём им даже ДУМ удалось скомпилировать, правда один кадр рисуется семь часов. Кстати, такая программа неуязвима для горюшка века — Мелтдауна и Спектра.
Есть небольшая (на 156 страниц и 90% воды) презентация, достаточно популярно объясняющая как этого удалось достичь, но для её чтения надо знать ассемблер, поэтому я позволю себе раскрыть детали трансляции двух инструкций, чтобы пояснить принцип для тех, кто ассемблера не знает или ленится причитать.
Например, сравнение двух чисел делается при помощи следующего псеводокода:
mov [X], 0
mov [Y], 1
mov R, [X]У нас есть два числа в аргументах «X» и «Y», результат сравнения которых попадает в «R» — там будет ноль, если числа не равны и единица в противном случае. Как же это работает?
Первой командой ноль записывается в ячейку по адресу «X». Это ассемблер, у нас тут всё — число, остальное — человеческие интерператации, поэтому записанное в «X» мы используем как адрес. Второй командой единица записывается в ячейку по адресу «Y». Третьей командой мы читаем значение по адресу «X» и если значения в «X» и «Y» совпадают, то ноль перетрётся единицей (и она попадёт в R), если нет, то в ячейке по адресу «X» ноль останется (который попадёт в R).
Несложно. Но из кода выше непонятно как получаются другие необходимые инструкции. Увы,
Возьмём, например, логическое «ИЛИ» («OR»), тут чуточку сложнее:
OR_ADDRS: dd OR_0, OR_1
OR_0: dd 0, 1
OR_1: dd 1, 1
; …
mov eax, X
mov edx, [OR_ADDRS + eax]
mov eax, Y
mov eax, [eax + edx]
mov R, eaxВо всех логических операциях ребята используют записанные заранее неизменяемые массивы значений.
Что тут происходит? В регистр (переменную, с которыми работает процессор) «eax» записывается значение «X» (возможные входные значения у нас тут — ноль или единица, численное представление булевых значений).
Далее в регистр «edx» записывается число из адреса, который является суммой адреса массива OR_ADDRS и содержимого регистра eax. Таким образом в eax попадёт OR_0 или OR_1, в зависимости от того былы записаны в eax ноль или единица. Эти значения — тоже числа и являются адресами двух других массивов из двух элементов.
Далее в eax мы записываем аргумент Y, его значение складывается с адресом полученным на предыдущем шаге и из получившегося адреса мы читаем записанное там значение. В переводе на ПХП получается следующее:
function mov_or(int $X, int $Y): int
{
define('OR_0', [0, 1]);
define('OR_1', [1, 1]);
define('OR_ADDRS', [OR_0, OR_1]);
$R = OR_ADDRS[$X][$Y];
return $R;
}Кстати, интересно, что у знаменитого дисассемблера «ИДА» от полученной таким образом программы крепко уносит крышу — при попытке отладки диссасемблер не видит никаких ветвлений и падает на анализе кода. Получился бы неплохой метод защиты от анализа, если бы не производительность.
Пацакку
Далёкие дни, Университет, вирус «Пацакку». С большим трудом припоминаю, что это был простой, написанный на Ассемблере вирус для ДОСа, который в день моего рождения выдвигал шторку сидирома (если он есть) и печатал на принтере (кажется; или в корень клал файл, или то и другое, не помню) фразу «в обществе где нет цветовой дифференциации штанов, нет и цели», цитату из любимого моего фильма «
Кстати, в Университетe самодельных вирусов хватало. Особенно мне запомнился вирус «TV» (был так назван моим тогдашним другом А.Г.
Тогда же я придумал свой первый антивирус, который назвал просто «Вакцина» (файл запуска — «vac.com» или «vac.exe», не помню точно размер), «вакциной» он назывался вполне заслуженно, так как именно вакцинировал компьютеры. Это была одна из двух утилит, которые сильно упрощали мою тогда лаборантскую жизнь. Принцип действия был следующий.
Большинство вирусов того времени ставили
Вирус, запущенный на таком компьютере, думал, что он тут уже есть и не заражал машину. Кроме того, моя вакцина предупреждала о попытке заражения вируса при вызове этих самых функций. У вакцины был ещё встроенный монитор перехвата вектора 13h, который использовали более серьёзные функции, принцип был подсказан тем же А.Г. — если обработчик 13h, при запуске моей программы, был не в БИОСе (а адреса его были известны), так же поднималась тревога.
Если честно, я смутно помню, что принцип был чуть хитрее, но в чём была тонкость, не помню. Если вспомню, расскажу.
Позднее я придумал и начал писать конфигуратор на Турбо Паскале, но так и не дописал, терпение кончилось на интерфейсе.
У моего антивируса было несколько достоинств: он ловил все тогдашние студенческие поделки, ходившие в нашем Университете, включая мои собственные, ну и просто популярные вирусы того времени, он был бесплатен и быстро обновлялся, был крошечным, не жрал ресурсы компьютера (что было особенно актуально в нашем компьютерном классе на IBM XT, там процессоры были 8086 и даже «Нортон» работал медленно) и предотвращал заражение, а не просто поднимал тревогу, когда вирус уже внедрился, т. е. лечить такой компьютер было не нужно, что было очень актуально в случае печально известного вируса «Чернобыль», эпидемию которого в своих классах я быстро пресёк на корню.
И раз уж у меня сегодня рубрика «воспоминания в утренней пробке», расскажу как лечили порчу вирусом «Чернобылем» БИОСа. Лечил всё тот же А.Г. Поражённый компьютер вскрывался, в него ставился такой же чип БИОСа с другого компьютера (напоминаю, дело в компьютерных классах было, когда я лаборантом работал в Университете, там было много однотипных машин), компьютер загружался, обычной программой для прошивки БИОСа с чипа списывалось его содержимое (или оно, как вариант, бралось из интернета), потом наживую чип доставался и вставлялся испорченный, после чего пациент перепрошивался правильной прошивкой.
Кстати, мне «повезло» учиться и лаборантить в классах где стоял весь зоопарк: 8086, 80286, 80386 и даже бездисковые станции, которые грузились по сети, настоящие (тормозные) тонкие клиенты с утилитами Новелла.
sgn(x) на ассемблере
Давненько я не брал в руки ассемблер. Лет так 8, наверное (речь идёт об ассемблере под 80x86, это, кстати,
cwd
neg ax
adc dx, dxВот объяснение как это работает:
Здесь начальный аргумент лежит в ax, значение помещается в dx. Работает следующим образом: сначала cwd расширяет знак ax в dx, после чего в dx лежит -1, если аргумент отрицательный, а иначе 0. neg ax меняет знак ax, но это на самом деле неважно, а важно то, что эта инструкция помещает в флаг carry единицу, только если аргумент был ненулевой.
Наконец adc dx, dx складывает dx с самим собой, добавляет carry от второй инструкции, и помещает результат в dx. В результате выходит:
— если ax<0, то dx вначале -1, carry равен 1, результат равен -1*2+1 = -1 (во как!) — если аx=0, то dx вначале 0, carry равен 0, результат 0*2+0=0 — если ax>0, то dx вначале 0, carry равен 1, результат 0*2+1=1
Зело тяжкий Ассемблер
Написание программъ на Ассемблере, рекомое канитель, паки и паки хитро будучи, душам праздным супротивно бысть, и ум девствен оное столь удручить может, что не только ночной покой смутит и дневное здравие, но и яствъ отвратит и сладости вина пиющих возлишить может, и похоти от жен человеческих. А тем мужам знатным, в коих имени ТАСМА и МАСМА трепету нет и бесии ти супротив них немощны и безсильны, от оного повреждения нет, а токмо благо есть для нихъ и рабов ихъ, и живота ихъ, и угодья ихъ всяческого, так сие полагая, что буде оные не имать в разсудке своем тяготу сию, Ассемблер рекомую, то чрез праздность ума ихъ и душам ихъ великий соблазнъ бысть, и господу хуление, и надругательство, и скверна всяческая, и государству поруха, и запустение в дворахъ ихъ, и животахъ ихъ, и твари всяческой, и бесям чрез то великое попущение. Ино и почитается Ассемблер, аки особая благодать.
Источник — ЖЖурнал den_rybakov.