Хакер - Погружение в ассемблер. Сокращаем размер программы

Хакер - Погружение в ассемблер. Сокращаем размер программы

hacker_frei

https://t.me/hacker_frei

Антон Карев

Содержание статьи

  • Резьба по ассемблерному коду
  • Читаем данные из памяти по-новому
  • Копируем данные, не используя циклы
  • Сравниваем строки, не используя циклы
  • Меняем местами значения двух регистров
  • Выполняем восьмибитные операции экономно
  • Знакомимся с двоично-десятичным кодом
  • Умножаем и делим на 10 экономно
  • Еще несколько полезных трюков
  • Выводы

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

РЕЗЬБА ПО АССЕМБЛЕРНОМУ КОДУ

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

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

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

Осо­бое рве­ние хакеры про­явля­ли к опти­миза­ции под­прог­раммы для печати десятич­ных чисел. За нес­коль­ко месяцев они изго­тови­ли целую кучу вари­аций. С чего вдруг такой инте­рес имен­но к этой задаче?

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

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

Даль­ше дело при­няло серь­езный обо­рот. Поиск луч­шего решения прев­ратил­ся в неч­то боль­шее, чем прос­то сос­тязание, — в поиск свя­того Гра­аля. Одна­ко, сколь­ко бы сил ни было пот­рачено, никому не уда­валось пре­одо­леть барь­ер из пятиде­сяти команд. И ког­да прак­тичес­ки все уже сми­рились с тем, что это невоз­можно, один из хакеров догадал­ся пос­мотреть на решение задачи под дру­гим углом. В ито­ге его вер­сия под­прог­раммы умес­тилась в 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, — и пос­ле это­го уве­личи­вает оба индек­сных регис­тра на еди­ницу: SIDI (или умень­шает на еди­ницу, если флаг нап­равле­ния уста­нов­лен в еди­ницу).

Инс­трук­ция 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, она занима­ет один байт, в про­тив­ном слу­чае — два бай­та.

ВЫПОЛНЯЕМ ВОСЬМИБИТНЫЕ ОПЕРАЦИИ ЭКОНОМНО

Ес­ли выпол­няешь нес­коль­ко вось­мибит­ных опе­раций с кон­стан­тами, луч­ше исполь­зуй регистр AL. Боль­шинс­тво ариф­метичес­ких и логичес­ких инс­трук­ций (в том вари­анте, ког­да один опе­ранд — это регистр, а дру­гой — это кон­стан­та) получа­ются короче, если ты исполь­зуешь регистр AL. Нап­ример, add al, 0x10 занима­ет два бай­та, тог­да как add bl, 0x10 занима­ет три бай­та. И само собой, чем боль­ше инс­трук­ций в тво­ей цепоч­ке пре­обра­зова­ний, тем боль­ше бай­тов ты сэконо­мишь.

С 16-бит­ными регис­тра­ми такая же исто­рия: с регис­тром AX ариф­метичес­кие и логичес­кие инс­трук­ции получа­ются короче. Нап­ример: add ax, 0x1010 (три бай­та), add bx, 0x1010 (четыре бай­та).

Од­нако, ког­да в логичес­кой или ариф­метичес­кой инс­трук­ции один из опе­ран­дов — это корот­кая кон­стан­та в диапа­зоне –128..127, то инс­трук­ция опти­мизи­рует­ся до трех байт.

ЗНАКОМИМСЯ С ДВОИЧНО-ДЕСЯТИЧНЫМ КОДОМ

Ког­да тебе позарез надо работать имен­но с десятич­ными чис­лами, а не с шес­тнад­цатерич­ными, но при этом не хочет­ся делать слож­ные пре­обра­зова­ния меж­ду дву­мя сис­темами счис­ления, исполь­зуй дво­ично‑десятич­ный код. Что это за код? Как в нем записы­вают­ся чис­ла? Смот­ри. Допус­тим, у тебя есть десятич­ное чис­ло 2389. В дво­ично‑десятич­ном коде оно выг­лядит как 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, aland al, alor al, al или cmp al, al.

ВЫВОДЫ

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

Ес­ли же ты хочешь еще нем­ного матери­ала, что­бы углу­бить и зак­репить зна­ния, можешь вер­нуть­ся к моим более ран­ним стать­ям — «Floppy Bird», где мы пишем мини­атюр­ную игру, и «Мик­роБ», где я показы­ваю, как соз­дать свой интер­пре­татор бей­сика на ассем­бле­ре. Поэк­спе­римен­тируй с эти­ми прог­рамма­ми, и ты сде­лаешь еще один боль­шой шаг в сто­рону осво­ения ассем­бле­ра.

Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei

Report Page