При написании кода на ассемблере вы всегда должны знать, действительно ли данный фрагмент кода должен быть написан на ассемблере. Необходимо взвесить все за и против, современные компиляторы способны оптимизировать код и могут достичь сопоставимой производительности (в том числе и большей, если версия ассемблера, написанная программистом, не оптимальна с самого начала).
Главный недостаток ассемблера — непереносимость полученной программы в будущем для других платформ.
Надеюсь, вы знаете книгу Стивена Леви «Хакер: герои компьютерной революции». Если нет, обязательно прочтите! Теперь мы с вами будем ностальгировать по тем славным временам, которые описывает Леви. В частности, подумайте, что сделали пионеры хакерства в Клубе моделирования железных дорог Массачусетского технологического института и как они кодируют.
Тогдашние хакеры, которые думали о своих программах, пытались извлечь из инструкций на языке ассемблера все возможное, чтобы в итоге программа была как можно более компактной. Попытки отрезать некоторые инструкции из компьютерной программы, не повлияв на конечный результат, стали навязчивой идеей для хакеров.
Поскольку в Массачусетском технологическом институте существовало негласное правило: «Если вы написали процедуру для печати десятичных чисел собственными руками, то вы достаточно знаете о компьютере, чтобы считать себя своего рода программистом. Кроме того, если вы взяли около сотни инструкций по сборке для этой подпрограммы, то вы полный идиот, даже если вы программист. А если вы написали действительно хорошую и короткую процедуру меньшего размера, вы можете попробовать называть себя хакером. «
Хакеры шли на все, чтобы оптимизировать процедуры печати десятичных чисел. В течение нескольких месяцев они внесли множество изменений. Почему вдруг такой интерес именно к этой задаче?
Затем вопрос принял серьезный оборот. Однако, как ни старайся, барьер из пятидесяти команд преодолеть не удалось никому. И хотя почти все уже смирились с тем, что это невозможно, один из хакеров догадался, глядя на решение проблемы с другой точки зрения. В результате его версия подпрограммы умещается в 46 сборочных инструкциях.
Вплоть до этого знаменательного часа все считали, что оптимальным алгоритмом для процедуры десятичной печати является последовательное вычитание, использующее таблицы степеней 10. Однако, как выяснилось, проблема могла быть решена и без такой таблицы. Леви, к сожалению, не приводит ассемблерный код в своей книге, поэтому мы не сможем познакомиться с этим шедевром.
Но не еужно расстраиваться. Сейчас я покажу тебе иную версию такой подпрограммы. Она уместилась в 12 инструкций (и 23 байта).
Некоторым может показаться, что так сильно беспокоиться об уменьшении размера программы в наши дни больше не имеет смысла. Однако этот навык пригодится при написании какого-либо шелл-кода или редактировании скомпилированного двоичного файла. В любом случае, вам нужно использовать как можно меньше инструкций по сборке. А теперь я собираюсь показать вам несколько советов, которые помогут вам уменьшить размер ваших программ сборки.
Чтение данных из памяти по-новому
Во всех предыдущих уроках мы читали память, ссылаясь на нужную нам ячейку через регистр BX
. Примерно вот так.
Но то же самое можно сделать и вот так.
Инструкция lodsb
говорит процессору 8088: «Загрузи байт из адреса, на который указывает DS:
, и сохрани этот байт в регистр AL
. И затем увеличь SI
на единицу (если флаг направления сброшен в 0)».
Еще у 8088 есть инструкция lodsw
, которая работает почти как lodsb
, только загружает из DS:
слово (сдвоенный байт), сохраняет результат в регистр AX
и увеличивает SI
на 2.
Копирование данных, не используя циклы
Зная о существовании инструкций lodsb
/lodsw
и их противоположностей stosb
/stows
, мы можем написать подпрограмму для копирования области памяти.
Этот внутренний цикл занимает всего четыре байта. Но у процессора 8088 есть инструкции movsb
и movsw
, которые делают ту же самую операцию, но при этом не используют регистр AL
или AX
.
Теперь внутренний цикл занимает три байта. Но и это не предел! Мы можем сделать все то же самое без инструкции loop
.
Обрати внимание, что movsb
— это две инструкции в одной: lodsb
и stosb
. И аналогично в movsw
скомбинированы lodsw
и stosw
. При этом movsb
/movsw
не используют регистры AL
/AX
, что весьма приятно.
Сравнение строк, не используя циклы
У 8088 есть инструкции для сравнения строк (cmps
) и инструкция для сравнения регистра AX
или AL
с содержимым памяти (scas
).
Эти инструкции выставляют флаги в регистре флагов, так что после их выполнения можно использовать условные переходы.
Команда cmpsb сравнивает два байта — тот, на который указывает DS: SI, и тот, на который указывает ES: DI, — а затем увеличивает оба индексных регистра на единицу: SI, DI (или уменьшает на единицу, если флаг направления установлен на единицу).
Инструкция cmpsw
делает то же самое, но только не с байтами, а со словами (сдвоенными байтами) и уменьшает или увеличивает индексные регистры не на 1, а на 2.
Обрати внимание, что и та и другая инструкция не пользуется регистрами AL
и AX
, то есть наш самый ходовой регистр остается нетронутым. Это очень хорошо.
Инструкция scasb
сравнивает AL
с байтом, на который указывает ES:
, затем увеличивает DI
на единицу (или уменьшает, если флаг направления установлен в единицу).
Инструкция scasw
делает то же самое, но только с регистром AX
и уменьшает или увеличивает индексные регистры не на 1, а на 2.
Перед этими четырьмя инструкциями можно ставить префикс repe
/repne
, что значит «продолжать выполнять данную инструкцию до тех пор, пока не будет выполнено условие завершения» (E значит equal, равно, NE — not equal, не равно).
Замена местами значения двух регистров
Допустим, в регистре AX
записана четверка, а в DX
семерка. Как поменять местами значения регистров?
Вот первое, что приходит на ум.
Такой код занимает четыре байта. Неплохо, но, может быть, есть вариант покороче? Еще на ум приходит что‑то вроде такого, со вспомогательным регистром.
Но такой код занимает даже еще больше памяти — шесть байт. Размышляя дальше, мы можем задействовать хитрый трюк с исключающим ИЛИ, без вспомогательного регистра.
Но только этот вариант кода занимает столько же байтов, сколько предыдущий вариант. Так что выглядит он, конечно, изящно, но особых преимуществ не дает.
А теперь внимание! У процессора 8088 есть специальная инструкция, которая как раз и предназначена для обмена регистров. Обрати внимание, когда один из двух ее операндов — это регистр AX
, она занимает один байт, в противном случае — два байта.
Экономия на выполнении восьмибитных операций
Если вы выполняете несколько операций с 8-битными константами, лучше используйте регистр AL. Большинство арифметических и логических инструкций (в случае, когда один операнд является регистром, а другой — константой) будут короче, если вы используете регистр AL. Например, add al, 0x10 занимает два байта, а add bl, 0x10 занимает три байта. И, конечно же, чем больше инструкций в вашей цепочке преобразований, тем больше байтов вы сэкономите.
С 16-битными регистрами такая же история: с регистром AX
арифметические и логические инструкции получаются короче. Например: add
(три байта), add
(четыре байта).
Однако, когда в логической или арифметической инструкции один из операндов — это короткая константа в диапазоне –128..127, то инструкция оптимизируется до трех байт.
Двоично-десятичный код
Если вам срочно нужно работать с десятичными числами, а не с шестнадцатеричными, но вы не хотите выполнять сложные преобразования между двумя системами счисления, используйте BCD. Что это за код? Как в нем написаны числа? Смотрим. Предположим, у вас есть десятичное число 2389. В BCD оно выглядит как 0x2389. Понятен смысл?
Для работы с двоично‑десятичным кодом в процессоре 8088 предусмотрены инструкции daa
и das
. Инструкция daa
используется после add
, а инструкция das
— после sub
.
Например, если в регистре AL
записано 0x09
и ты добавишь 0x01
к этому значению, то там окажется 0x0a
. Но когда ты выполнишь инструкцию daa
, она скорректирует AL
до значения 0x10
.
Экономия при умножении и делении на 10
У процессора 8088 есть две любопытные инструкции: AAD
/AAM
. Изначально они задумывались для того, чтобы распаковывать двухциферные десятичные числа из AH
(0–9) и AL
(0–9). Обе инструкции занимают по два байта.
Инструкция AAD
выполняет вот такую операцию:
AL = AH*10+AL
AH = 0
А вот что выполняет инструкция AAM
:
AH = AL/10
AL = AL%10
Эти две инструкции позволяют сберечь драгоценные байты, когда тебе надо 8-битное число умножить или поделить на 10.
Еще несколько полезных трюков
Инициализируй числа при помощи XOR. Если тебе надо сбросить в 0 какой‑то 16-битный регистр, то короче всего это сделать так (на примере регистра DX
).
Инкрементируй AL
, а не AX
. Везде, где это возможно, пиши inc
вместо inc
. А где это возможно? Там, где ты уверен, что AL
не выйдет за пределы 255. То же самое с декрементом. Если ты уверен, что AL
никогда не будет меньше нуля, лучше пиши dec
, а не dec
. Так ты сэкономишь один байт.
Перемещай AX
через XCHG
. Если тебе надо скопировать AX
в какой‑то другой регистр, то пиши вот так: xchg
. Инструкция xchg
занимает всего один байт, тогда как mov
— два.
Вместо cmp
используй test
. Так ты сэкономишь один байт.
Возвращай результат через регистр флагов. Когда пишешь подпрограмму, которая должна возвращать только значения True
и False
, пользуйся регистром флагов. Для этого внутри подпрограммы применяй инструкции clc
и sec
, чтобы сбрасывать и устанавливать флаг переноса. И потом после выполнения подпрограммы используй jc
и jnc
— для обработки результата функции. Иначе придется потратить кучу байтов на инструкции присваивания вроде mov
и mov
и на инструкции сравнения вроде test
, and
, or
или cmp
.
Выводы
Что ж, теперь вы знаете несколько приемов, которые помогут сделать ваши программы сборки более компактными. Если вы изучали теорию и «щупали» программы на ассемблере своими руками, можете считать, что ваше знакомство с ассемблером прошло успешно. Но, конечно, чтобы освоить ассемблер, недостаточно прочитать всего несколько статей и перепечатать с них десяток чужих программ. Здесь, как и в любом другом деле, нужна постоянная практика и общение с единомышленниками.