Се­год­ня мы прис­тупим к под­робно­му изу­чению условно­го хода выпол­нения прог­рамм. Без него невоз­можно пред­ста­вить прог­рамми­рова­ние как таковое. Мы нач­нем наше пог­ружение с условных опе­рато­ров if then else и раз­берем­ся, как ком­пилятор интер­пре­тиру­ет подоб­ные конс­трук­ции.

Фундаментальные основы хакерства

Пят­надцать лет назад эпи­чес­кий труд Кри­са Кас­пер­ски «Фун­дамен­таль­ные осно­вы хакерс­тва» был нас­толь­ной кни­гой каж­дого начина­юще­го иссле­дова­теля в области компь­ютер­ной безопас­ности. Одна­ко вре­мя идет, и зна­ния, опуб­ликован­ные Кри­сом, теря­ют акту­аль­ность. Редак­торы «Хакера» попыта­лись обно­вить этот объ­емный труд и перенес­ти его из вре­мен Windows 2000 и Visual Studio 6.0 во вре­мена Windows 10 и Visual Studio 2019.

Ссыл­ки на дру­гие статьи из это­го цик­ла ищи на стра­нице авто­ра.

Рань­ше мы уже встре­чались с условным ходом выпол­нения прог­рамм, одна­ко огра­ничи­вались корот­кими опи­сани­ями команд и повер­хностным раз­бором выпол­няемых ими опе­раций. Управля­емый ход выпол­нения, без сом­нения, самая важ­ная веха раз­вития прог­рамми­рова­ния сво­его вре­мени и зат­мева­ет мно­гие пос­леду­ющие, даже такие, как струк­турное прог­рамми­рова­ние или ООП. Толь­ко с высоты высоко­уров­невого язы­ка кажет­ся, что в усло­вии if then else нет ничего любопыт­ного и оно лишено какого‑либо раз­нооб­разия. Но для ком­пилято­ра это прос­тор для самоде­ятель­нос­ти! И в близ­ких по духу ситу­ациях он может пос­тро­ить кар­диналь­но раз­лича­ющий­ся код.

В про­цес­сорной архи­тек­туре x86 пре­дус­мотрен весь­ма «ори­гиналь­ный» набор команд для перенап­равле­ния хода выпол­нения прог­рам­мно­го кода. «Ори­гиналь­ный» не зна­чит хороший или пло­хой, это зна­чит, что он отли­чает­ся от набора мик­ропро­цес­соров дру­гих архи­тек­тур и про­изво­дите­лей: ARM, MIPS, SPARC и так далее. Как ты зна­ешь, пер­вый про­цес­сор этой серии соз­давал­ся впо­пыхах для вре­мен­ной замены еще не готово­го iAPX-432, пос­ледний, в свою оче­редь, дол­жен был уметь в мно­гоза­дач­ность и управле­ние памятью на аппа­рат­ном уров­не. Чего в ито­ге не слу­чилось. А x86 про­дол­жил свое раз­витие. Поэто­му сей­час, ког­да за годы эво­люции про­цес­соров серии x86 ско­пил­ся набор инс­трук­ций, хакерам при­ходит­ся раз­гре­бать весь этот хлам, дабы рас­кру­тить порядок выпол­нения машин­ных инс­трук­ций.

 

Идентификация условных операторов

Су­щес­тву­ет два вида алго­рит­мов — безус­ловные и условные. Порядок дей­ствий безус­ловно­го алго­рит­ма всег­да пос­тоянен и не зависит от вход­ных дан­ных. Нап­ример, a = b + c. Порядок дей­ствий условных алго­рит­мов, нап­ротив, зависит от дан­ных, пос­тупа­ющих «на вход». Нап­ример:

ес­ли c не рав­но нулю,
то: a = b/c;
ина­че: вывес­ти сооб­щение об ошиб­ке

Об­рати вни­мание на выделен­ные жир­ным шриф­том клю­чевые сло­ва «если», «то» и «ина­че», называ­емые опе­рато­рами усло­вия или условны­ми опе­рато­рами. Без них не обхо­дит­ся ни одна прог­рамма (вырож­денные при­меры наподо­бие «Hello, World!» не в счет). Условные опе­рато­ры — сер­дце любого язы­ка прог­рамми­рова­ния. Поэто­му чрез­вычай­но важ­но уметь их пра­виль­но опре­делять.

В общем виде (не углубля­ясь в син­такси­чес­кие под­робнос­ти отдель­ных язы­ков) опе­ратор усло­вия схе­матич­но изоб­ража­ется так:

IF (усло­вие) THEN { опе­ратор1; опе­ратор2; } ELSE { опе­раторa; опе­раторb; }

За­дача ком­пилято­ра — пре­обра­зовать эту конс­трук­цию в пос­ледова­тель­ность машин­ных команд, выпол­няющих опе­ратор1, опе­ратор2, если усло­вие истинно, и опе­раторa, опе­раторb — если оно лож­но. Одна­ко мик­ропро­цес­соры серии 80x86 под­держи­вают весь­ма скром­ный набор условных команд, огра­ничен­ный фак­тичес­ки одни­ми условны­ми перехо­дами. Прог­раммис­там, зна­комым лишь с IBM PC, такое огра­ниче­ние не покажет­ся чем‑то неес­тес­твен­ным, меж­ду тем сущес­тву­ет мас­са про­цес­соров, под­держи­вающих пре­фикс условно­го выпол­нения инс­трук­ции. То есть вмес­то того, что­бы писать:

TEST ECX,ECX
JNZ xxx
MOV EAX,0x666

там пос­тупа­ют так:

TEST ECX,ECX
IFZ MOV EAX,0x666

IFZ и есть пре­фикс условно­го выпол­нения, раз­реша­ющий выпол­нение сле­дующей коман­ды толь­ко в том слу­чае, если уста­нов­лен флаг нуля. В этом смыс­ле мик­ропро­цес­соры 80x86 мож­но срав­нить с ран­ними диалек­тами язы­ка Basic, не раз­реша­ющи­ми исполь­зовать в условных выраже­ниях никакой дру­гой опе­ратор, кро­ме GOTO. Срав­ни:

Ста­рый диалект Basic:
10 IF A=B THEN GOTO 30
20 GOTO 40
30 PRINT "A=B"
40 ... // Прочий код программы
Но­вый диалект Basic:
IF A=B THEN PRINT "A=B"

Ес­ли ты ког­да‑нибудь прог­рамми­ровал на ста­рых диалек­тах Basic, то, веро­ятно, пом­нишь, что гораз­до выгод­нее выпол­нять GOTO, если усло­вие лож­но, а в про­тив­ном слу­чае про­дол­жать нор­маль­ное выпол­нение прог­раммы. Как видишь, воп­реки рас­хожему мне­нию, навыки прог­рамми­рова­ния на Basic отнюдь не бес­полез­ны, осо­бен­но в дизас­сем­бли­рова­нии прог­рамм.

Боль­шинс­тво ком­пилято­ров (даже не опти­мизи­рующих) инверти­руют истинность усло­вия, тран­сли­руя конс­трук­цию

IF (усло­вие) THEN { опе­ратор1; опе­ратор2; }

в сле­дующий псев­докод:

IF (NOT усло­вие) THEN continue
опе­ратор1;
опе­ратор2;
continue:

Сле­дова­тель­но, для вос­ста­нов­ления исходно­го тек­ста прог­раммы нам при­дет­ся вновь инверти­ровать усло­вие и «под­цепить» блок опе­рато­ров { опе­ратор1; опе­ратор2; } к клю­чево­му сло­ву THEN. Ина­че говоря, если откомпи­лиро­ван­ный код выг­лядит так:

10 IF A<>B THEN 30
20 PRINT "A=B"
30 ...// Прочий код программы

мож­но с уве­рен­ностью утвер­ждать, что в исходном тек­сте при­сутс­тво­вали стро­ки IF A=B THEN PRINT "A=B". А если прог­раммист, наобо­рот, про­верял перемен­ные A и B на неравенс­тво, то есть в коде при­сутс­тво­вала конс­трук­ция IF A<>B THEN PRINT "A<>B"? Все рав­но ком­пилятор инверти­рует истинность усло­вия и сге­нери­рует сле­дующий код:

10 IF A=B THEN 30
20 PRINT "A<>B"
30 ... // Прочий код программы

Ко­неч­но, встре­чают­ся и ком­пилято­ры, стра­дающие мно­гос­лови­ем. Их лег­ко рас­познать по безус­ловно­му перехо­ду, сле­дующе­му сра­зу же пос­ле условно­го опе­рато­ра:

IF (усло­вие) THEN do
GOTO continue
do:
опе­ратор1;
опе­ратор2;
continue:

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

Рас­смот­рим теперь, как тран­сли­рует­ся пол­ная конс­трук­ция

IF (усло­вие) THEN { опе­ратор1; опе­ратор2; } ELSE { опе­раторa; опе­раторb; }.

Од­ни ком­пилято­ры пос­тупа­ют так:

IF (усло­вие) THEN do_it
// Вет­ка ELSE
опе­раторa;
опе­раторb;
GOTO continue
do_it:
//Вет­ка IF
опе­ратор1;
опе­ратор2;
continue:

А дру­гие — так:

IF (NOT усло­вие) THEN else
// Вет­ка IF
опе­ратор1;
опе­ратор2;
GOTO continue
else:
// Вет­ка ELSE
опе­раторa;
опе­раторb
continue:

Раз­ница меж­ду ними в том, что вто­рые инверти­руют истинность усло­вия, а пер­вые — нет. Поэто­му, не зная «нра­ва» ком­пилято­ра, опре­делить, как выг­лядел под­линный исходный текст прог­раммы, невоз­можно! Одна­ко это не соз­дает проб­лем, ибо усло­вие всег­да мож­но записать так, как это удоб­но. Допус­тим, не нра­вит­ся тебе вот такая конс­трук­ция:

IF (c <> 0) THEN a = b / c ELSE PRINT "Ошиб­ка!"

Пи­ши ее так:

IF (c == 0) THEN PRINT "Ошиб­ка!" ELSE a = b / c

И никаких гвоз­дей!

Продолжение доступно только участникам

Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».

Присоединяйся к сообществу «Xakep.ru»!

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

Подписаться
Уведомить о
0 комментариев
Межтекстовые Отзывы
Посмотреть все комментарии