Сокращение размера программ в ассемблере

Сокращение размера программ в ассемблере

При написании кода на ассемблере вы всегда должны знать, действительно ли данный фрагмент кода должен быть написан на ассемблере. Необходимо взвесить все за и против, современные компиляторы способны оптимизировать код и могут достичь сопоставимой производительности (в том числе и большей, если версия ассемблера, написанная программистом, не оптимальна с самого начала).
Главный недостаток ассемблера — непереносимость полученной программы в будущем для других платформ.

Надеюсь, вы знаете книгу Стивена Леви «Хакер: герои компьютерной революции». Если нет, обязательно прочтите! Теперь мы с вами будем ностальгировать по тем славным временам, которые описывает Леви. В частности, подумайте, что сделали пионеры хакерства в Клубе моделирования железных дорог Массачусетского технологического института и как они кодируют.

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

Поскольку в Массачусетском технологическом институте существовало негласное правило: «Если вы написали процедуру для печати десятичных чисел собственными руками, то вы достаточно знаете о компьютере, чтобы считать себя своего рода программистом. Кроме того, если вы взяли около сотни инструкций по сборке для этой подпрограммы, то вы полный идиот, даже если вы программист. А если вы написали действительно хорошую и короткую процедуру меньшего размера, вы можете попробовать называть себя хакером. «

Хакеры шли на все, чтобы оптимизировать процедуры печати десятичных чисел. В течение нескольких месяцев они внесли множество изменений. Почему вдруг такой интерес именно к этой задаче?

Затем вопрос принял серьезный оборот. Однако, как ни старайся, барьер из пятидесяти команд преодолеть не удалось никому. И хотя почти все уже смирились с тем, что это невозможно, один из хакеров догадался, глядя на решение проблемы с другой точки зрения. В результате его версия подпрограммы умещается в 46 сборочных инструкциях.

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

Но не еужно расс­тра­ивать­ся. Сей­час я покажу тебе иную вер­сию такой под­прог­раммы. Она умес­тилась в 12 инс­трук­ций (и 23 бай­та).

Сокращение размера программ в ассемблере

Некоторым может показаться, что так сильно беспокоиться об уменьшении размера программы в наши дни больше не имеет смысла. Однако этот навык пригодится при написании какого-либо шелл-кода или редактировании скомпилированного двоичного файла. В любом случае, вам нужно использовать как можно меньше инструкций по сборке. А теперь я собираюсь показать вам несколько советов, которые помогут вам уменьшить размер ваших программ сборки.

Чтение данных из памяти по-новому

Во всех пре­дыду­щих уро­ках мы читали память, ссы­лаясь на нуж­ную нам ячей­ку через регистр BX. При­мер­но вот так.

Сокращение размера программ в ассемблере

Но то же самое мож­но сде­лать и вот так.

Сокращение размера программ в ассемблере

Инс­трук­ция lodsb говорит про­цес­сору 8088: «Заг­рузи байт из адре­са, на который ука­зыва­ет DS:SI, и сох­рани этот байт в регистр AL. И затем уве­личь SI на еди­ницу (если флаг нап­равле­ния сбро­шен в 0)».

Еще у 8088 есть инс­трук­ция lodsw, которая работа­ет поч­ти как lodsb, толь­ко заг­ружа­ет из DS:SI сло­во (сдво­енный байт), сох­раня­ет резуль­тат в регистр 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, затем уве­личи­вает 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 ax, 0x1010 (три бай­та), add bx, 0x1010 (четыре бай­та).

Од­нако, ког­да в логичес­кой или ариф­метичес­кой инс­трук­ции один из опе­ран­дов — это корот­кая кон­стан­та в диапа­зоне –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 al вмес­то inc ax. А где это воз­можно? Там, где ты уве­рен, что AL не вый­дет за пре­делы 255. То же самое с дек­ремен­том. Если ты уве­рен, что AL никог­да не будет мень­ше нуля, луч­ше пиши dec al, а не dec ax. Так ты сэконо­мишь один байт.

Двоично-десятичный код

Пе­реме­щай AX через XCHG. Если тебе надо ско­пиро­вать AX в какой‑то дру­гой регистр, то пиши вот так: xchg ax, reg. Инс­трук­ция xchg занима­ет все­го один байт, тог­да как mov reg, ax — два.

Вмес­то cmp ax, 0 исполь­зуй test ax, ax. Так ты сэконо­мишь один байт.

Двоично-десятичный код

Воз­вра­щай резуль­тат через регистр фла­гов. Ког­да пишешь под­прог­рамму, которая дол­жна воз­вра­щать толь­ко зна­чения True и False, поль­зуйся регис­тром фла­гов. Для это­го внут­ри под­прог­раммы при­меняй инс­трук­ции clc и sec, что­бы сбра­сывать и уста­нав­ливать флаг перено­са. И потом пос­ле выпол­нения под­прог­раммы исполь­зуй jc и jnc — для обра­бот­ки резуль­тата фун­кции. Ина­че при­дет­ся пот­ратить кучу бай­тов на инс­трук­ции прис­ваива­ния вро­де mov al, 0 и mov al, 1 и на инс­трук­ции срав­нения вро­де test al, al, and al, al, or al, al или cmp al, al.

Выводы

Что ж, теперь вы знаете несколько приемов, которые помогут сделать ваши программы сборки более компактными. Если вы изучали теорию и «щупали» программы на ассемблере своими руками, можете считать, что ваше знакомство с ассемблером прошло успешно. Но, конечно, чтобы освоить ассемблер, недостаточно прочитать всего несколько статей и перепечатать с них десяток чужих программ. Здесь, как и в любом другом деле, нужна постоянная практика и общение с единомышленниками.

 

 
 
 
Click to rate this post!
[Total: 1 Average: 5]

Leave a reply:

Your email address will not be published.