Воробьёвы

(-:

№8. Непобедимый герой-читер, разборки со стеком, циклами, оптимизацией

ПРОДОЛЖАЕМ ПРИНИМАТЬ ПОЗДРАВЛЕНИЯ! (УВЫ)

10, 20, 30... после третьего десятка - и это с утра-то! - мы поняли, что наш проект перешел на качественно новый этап своего существования. На наш сайт повалил народ! Впервые за 4 месяца одиночества мы поняли, что у нас есть друзья и единомышленники...

Сорри, но более высокопарной речи я придумать не смог. Как-то тошно было. Наверное, передZенствовал вчера. День все же - знаменательный был. Во всяком случае для нашего "авторского коллектива"...

150 уникальных посетителей и 1800 новых подписчиков - это круто!

Может пообещать вам, "дорогие друзья", что мы постараемся оправдать оказанное нам доверие? А еще сказать, что вы никогда не пожалеете о том, что стали "нашими читателями"?

Ну дык совру ж ведь! Еще как пожалеете! Горько пожалеете!! Прямо начиная с этого выпуска - пожалеете!!

DUKE NUKEM ДОЛЖЕН УМЕРЕТЬ

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

Вчера, изрядно насытив свои программерские организмы программерским напитком, мы решили поразвлечься и сели играть в Дюка по сетке.

В большинстве сетевых игр, надо сказать, есть одна досадная недоработка - невозможно использовать "халявные" коды, которые дают, например, неограниченное количество патронов или вообще делают игрока бессмертным. В некоторых играх об использовании кода тут же узнают все твои противники и начинают ругаться нехорошими словами. В некоторых играх коды вообще не предусмотрены...

Но нам, дZенствующим программерам, поднаторевшим в святой борьбе с Матрицей, это, естественно, было фиолетово...

Как следствие - наша битва друг с другом началась весьма необычно. Мы ВСЕ ринулись убивать друга голыми руками :). И долго не могли убить. Да чего уж там - так и не смогли на протяжении часа. А потом... потом игра, в который каждый неубиваем и обладает неограниченным боекомплектом, потеряла всякий смыл...

Мы не использовали коды. Мы исходили из гипотетического предположения, что многочисленные циферки "в Дюке" (как-то: здоровье, количество патронов) являются целочисленными переменными и находятся в памяти компьютера, где постоянно обновляются.

Таким образом задача "стать бессмертным" сводилась к поиску МЕСТА, где эта переменная находится. А все что нужно сделать потом - это "заморозить" значение этой переменной.

Все более чем просто... Тем более, что для этого и существуют утилиты с громким названием "взломщики игр". Вроде где-то ;) на нашем сайте несколько подобных утилит как раз и валяются... ;)

Вы сами разберетесь, как ими пользоваться, потому что некоторые весьма и весьма навороченые... Я же вам просто расскажу как самой простенькой из них заморозить "здоровье" в Дюке...

Называтся эта простенткая приблуда Cheat 'O Matic и версия у нее 0.99a.

После того, как вы ее скачали, нужно сделать вот что (по пунктам):

1. Запустите "игру-жертву" и начните играть. Постарайтесь, чтобы кровожадные монстры не убили вас в первую же секунду. Успейте нажать на паузу или выйдите в меню!!

Успели? Это хорошо :). Игра приостановлена и можно расслабиться... Сколько там "здоровья" было? Все 100? Замечательно!

2. Запускаем программу Cheat 'O Matic. В самом ее верху есть ниспадающий списочек программ, которые в данный момент запущены. Наш Дюк тоже там должен быть. Вот его и выбираем ;).

Здоровья у нас сколько? 100? Вот и вводим это значение в маленькое беленькое окошечко (есть там такое). А затем жмем на кнопку "Search".

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

Сканить может долго. Слышите, как шебуршит ваш винт? Это из-за своп-файла, который тоже как бы оперативная память... (за подробностями к Micro$oft'у обращайтесь).

Поиск закончен. Программа выдает сообщение "Change the value in the program, then enter the new value... ". Это означает, что она нашла несколько переменных, содержащих значение 100. Например, патронов у вас тоже 100 - чем не переменная?

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

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

Ранили! Здоровья стало, например, 90. Пауза!! Новое значение, которое нужно внести - 90.

4. Вводим и жмем на "поиск". Из списка, сформированного в прошлый раз (при поиске "100") программа ищет те переменные, значение которых изменилось из 100 на 90. Опять то же самое! Нашлись несколько переменных, которые изменились со 100 на 90... А нужна только одна!

5. Переключаемся в Дюка. Даем себя ранить. Жмем на паузу. Запоминаем, сколько у нас стало "здоровья"... 78, например, стало...

6. Вводим новое значение, жмем на "поиск". Черт подери, опять не нашла!! То есть нашла, но несколько, а не одну!!

Сужаем круг поиска!! Повторяем пункт 5 инструкции.

7. Здоровья стало совсем мало. У меня - 35 только. Последний шанс!

Дрожащей рукой вводим 35 (или сколько там у вас). Поиск... УРА!!

НАШЛОСЬ!! Программа сообщает: "Cha-ching! Got it. Enter the value you'd like to change it to, and hit 'Set' (Code: x83E9DE22:3E0 x83E09356:78B x83DDAD36:724) ".

8. В то же самое поле, куда мы вносили значение переменной "здоровье", напишем то, что на английском языке нам и предлагается написать: ЗНАЧЕНИЕ, НА КОТОРОМ МЫ ХОТИМ ЗАМОРОЗИТЬ нашу переменную.

Пишем: 100. Жмем: "Set". Отмечаем: флажок Lock.

Готово...

9. Идем и мочим мутантов голыми руками, некоторое врямя получая от этого весьма и весьма сомнительное удовольствие.

Поздравляю :((. Вы стали АБСОЛЮТНЫМ ИГРОКОМ!!

Прощай, здоровье...

ПРЕДЫДУЩИЕ ВЫПУСКИ РАССЫЛКИ

Ну не то, чтобы повторение - мать учения... просто не проштудировав материал прыдыдущих номеров, вам будет весьма затруднительно "въехать" в номер текущий...

Выпуск 2 - про систему счисления.

Выпуск 3 - про порядок загрузки компьютера.

Выпуск 4 - про регистры.

Выпуск 5 - про программу в памяти.

Выпуск 6 - про прерывания.

Выпуск 7 - программируем и отлаживаем.

Кстати, если у кого проблемы с инетом - обращайтесь :). Нам не западло выслать предыдущие номера мылом...

ИЗВИНЕНИЯ

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

Для вас же, непонятливых, старались! А вы этого не цените... "Где восьмой выпуск?!" - орете...

Да вот он, перед вами! Радуйтесь!! А девятый в воскресенье будет, если мне на голову не свалится кирпич и/или очередная разновидность простудифилиса...

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

Кстати, сегодня у нас в деревне пошел снег!!

РАЗБОРКИ СО СТЕКОМ

[1] В средней школе, где я когда-то учился, учителем физкультуры был настоящий зверь. Помимо того, что он заставлял нас бегать-прыгать-подтягиваться, у него еще любимое наказание было - проходило оно в так называемом "зале тяжелой атлетики".

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

Штанга - это такая "палка", по бокам которой навешиваются так называемые "блины" - круглые-плоские с дыркой посередине диски невероятной тяжести.

Хранятся же эти диски на штырях, которые представляют собой те же "палки", но вторкнутые вертикально в пол. На них и хранились диски - один на другой положенные...

"Наказание" заключалось вот в чем: этот придурок заставлял нерадивого ученика комплектовать штангу!! Это нефиг делать, если диски просто валяются по залу... Но когда они аккуратно сложены на штырь - это намного сложнее :(.

Садист!! Чтобы достать со штыря диск заданной тяжести (который по-приколу обычно находился внизу) необходимо было снять со штыря все "вышележащие" диски. Достать самый нижний, а остальные снова надеть на штырь. А потом - точно так же достать диск другой "тяжести" со второго штыря... А он как-бы случайно тоже в самом низу... И так далее - до полной победы идиотизма над здравым рассудком... Не правда ли, изощренная пытка?

По окончании школы мы с этим отморозком разобрались :) Он осознал свою ошибку и больше так не издевается...

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

М-да... на "кипение возмущенного разума" уходит чертовски много энергии...

[2] Так вот, про стек: "штырь" для блинов находится в оперативной памяти (а где ж еще?). А роль блинов выполняют хорошо знакомые нам всем регистры, вернее - их "значения".

Правила работы с ним те же: вы можете снять только верхний блин. Чтобы самый нижний блин - вам нужно прежде снять все те, которые НАД ним.

Очевидно, что из десяти "блинов", которые вы на "штырь" надели, первым будет сниматься последний из надетых (верхний), а последним - первый, то есть самый нижний...

Все очень просто: "первый пришел - последним уйдешь" и наоборот "пришел последним - уйдешь первым".

Это вам не очередь времен социализма... Это очередь "загрузки-разгрузки" стека! Привыкайте...

[3] Для работы со стеком вам пока что необходимо знать только две команды: push и pop. Так как в качестве "блинов" у нас регистры - то соответственно необходимо после этих команд указывать и "имена собственные" помещаемых в стек значений регистров...

Соответственно:

push AX - ПОМЕЩАЕТ В СТЕК значение регистра AX;

pop AX - ИЗВЛЕКАЕТ ИЗ СТЕКА значение регистра AX.

Ну а как делать то же самое с остальными регистрами вы, наверняка уже и сами догадались...

Очень важно помнить каким нездоровым образом "в стеке" реализована ОЧЕРЕДЬ поместить/извлечь. Помните, мы вас предупреждали, что нам нельзя верить на слово? Не верьте! А по сему - обязательно убедитесь в истинности/неистинности нашего голословного утверждения:

Это легко можно проверить следующей программулькой:

     :0100 MOV AX,0001          ;AX = 1
     :0103 PUSH AX              ;В стек записана 1-ца
     :0104 MOV AX,0002 
     :0107 PUSH AX              ;В стек записана 2-ка
     :0108 MOV AX,0003 
     :010B PUSH AX              ;В стек записана 3-ка
     :010C MOV AX,0004 
     :010F PUSH AX              ;В стек записана 4-ка
     :0110 MOV AX,0005 
     :0113 PUSH AX              ;В стек записана 5-ка
     :0114 POP AX                
     :0115 POP AX                
     :0116 POP AX 
     :0117 POP AX 
     :0118 POP AX 
     :0119 INT 20 

Вот... с очередностью заполнения стека оно, наверное, и ежу понятно :). Я много про абстрактные "блины" загружал. А вот с адреса 114 начинается извлечение из стека. В какой последовательности это делается мы можете увидеть сами, произведя трассировку этой небольшой проги...

     -r 
     AX=0000 BX=0000 CX=001B DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 
     DS=14DC ES=14DC SS=14DC CS=14DC IP=0100 NV UP EI PL NZ NA PO NC 
     14DC:0100 B80100           MOV AX,0001
     - 

Анализируем-с: прога еще не начала работать. Готовится выполниться команда по адресу 100. Делаем ШАГ!

     -t 
     AX=0001 BX=0000 CX=001B DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 
     DS=14DC ES=14DC SS=14DC CS=14DC IP=0103 NV UP EI PL NZ NA PO NC 
    14DC:0103 50                  PUSH AX
     - 

Анализируем: AX=0001 - значит команда выполнилась правильно :). Следующая команда, по идее, должна поместить 1 в стек...

     -t 
     AX=0001 BX=0000 CX=001B DX=0000 SP=FFFC BP=0000 SI=0000 DI=0000 
     DS=14DC ES=14DC SS=14DC CS=14DC IP=0104 NV UP EI PL NZ NA PO NC 
     14DC:0104 B80200           MOV AX,0002 
     -

И что? Команда выполнилась... где мы можем увидеть, что в стек действительно "ушла" единица? Увы, но здесь это не отображается :). Проверим потом. Ведь логично, что если эти значения действительно сохранились в стеке, то мы их потом без проблем оттуда извлечем? Ну то есть если найдем "там" наши 1 2 3 4 5 - значит все ок...

А по сему - дадим проге работать дальше до адреса 114 (не включительно), не вдаваясь в подробный анализ. Че тут анализировать? Если значение регистра AX последовательно меняется от 1 до 5 - значит команда mov работает. А стек (команда push) проверим потом, как и договорились...

Проехали до адреса 114...

     - g 114
     AX=0005 BX=0000 CX=001B DX=0000 SP=FFF4 BP=0000 SI=0000 DI=0000 
     DS=14DC ES=14DC SS=14DC CS=14DC IP=0114 NV UP EI PL NZ NA PO NC 
     14DC:0114 58                  POP AX 
     - 

А вот теперь снова анализируем :). При следующем шаге выполнится команда, извлекающая некогда "запомненное" значение AX из стека...

Обратите нимание, регистр IP указывает на адрес (114) выполняемой команды. Мы с вами это уже проходили, не так ли?

Поехали дальше!!

     -t 
     AX=0005 BX=0000 CX=001B DX=0000 SP=FFF6 BP=0000 SI=0000 DI=0000 
     DS=14DC ES=14DC SS=14DC CS=14DC IP=0115 NV UP EI PL NZ NA PO NC 
     14DC:0115 58                  POP AX 
     - 

Выполнился первый POP. Готовиться выполниться второй. AX=5. То есть по сравнению с предыдущим шагом вроде ничего не изменилось... Но на самом деле это не так. AX=5 - эта пятерка "загрузилась" из стека :)). В этом вы легко убедитесь, сделав следующий шаг трассировки...

     -t
     AX=0004 BX=0000 CX=001B DX=0000 SP=FFF8 BP=0000 SI=0000 DI=0000 
     DS=14DC ES=14DC SS=14DC CS=14DC IP=0116 NV UP EI PL NZ NA PO NC 
     14DC:0116 58                  POP AX 
     - 

Ууупс... AX=4 :). А команда вроде та же :) - POP AX :)

     -t
     AX=0003 BX=0000 CX=001B DX=0000 SP=FFFA BP=0000 SI=0000 DI=0000 
     DS=14DC ES=14DC SS=14DC CS=14DC IP=0117 NV UP EI PL NZ NA PO NC 
     14DC:0117 58                  POP AX 
     - 

AX=3 :)

     -t 
     AX=0002 BX=0000 CX=001B DX=0000 SP=FFFC BP=0000 SI=0000 DI=0000 
     DS=14DC ES=14DC SS=14DC CS=14DC IP=0118 NV UP EI PL NZ NA PO NC 
     14DC:0118 58                  POP AX 
     - 

AX=2 :)

     -t    
     AX=0001 BX=0000 CX=001B DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 
     DS=14DC ES=14DC SS=14DC CS=14DC IP=0119 NV UP EI PL NZ NA PO NC 
     14DC:0119 CD20              INT 20 
     -

AX=1 :) То есть нашлись-таки наши 1 2 3 4 5 :). Восстановились из стека...

Теперь поверили?? А то!!

Еще раз обращаю ваше внимание на то, что: последовательность записи (четыре PUSH'а) была 1 2 3 4 5, а вот последовательность извлечения (четыре POP'а) 5 4 3 2 1, то есть "последний пришел - первый ушел". Зарубите это себе на носу! (Как сделал это на своем перебитом носе наш школьный учитель физкультуры...).

медитируйте над этой темой до полного просветления!!

иначе потом - придется туго!!

РАЗБОРКИ С ЦИКЛАМИ

[1] Наша программа для работы со стеком линейна. А линейное программирование - это плохо.

Но не всегда :)

Итак давайте еще раз посмотрим на нашу программу для работы со стеком. С 100-го до 113-го адреса - у нас имеется пять почти идентичных блоков. Изменяется только значение AX, но на одно и то же число - на единицу, в большую сторону. То есть AX = предыдущее значение + 1. Это очевидно.

Еще более очевидно, что простая команда POP AX (с 114 по 119) повторяется у нас тоже 5 раз...

Мне почему-то сразу вспомнился анекдот о том, как два мента едут в машине и один спрашивает у другого: "Глянь, работает ли у нас мигалка на крыше". Тот высунул в голову в форточку и говорит: "Работает-неработает-работает-неработает-работает-неработает..."

Так вот, не будем уподобляться этим нехорошим людям и сделаем нашу прогу более нормальной...

Добьемся мы этого с помощью так называемого "цикла"...

[2] Цикл - это... хм... не буду давать общепринятые определения... кто хочет - поищите в книжках, благо их навалом...

Скажу только: "сесть-встать, сесть-встать, сесть-встать" - это не цикл, а вот "сесть-встать и так три раза" - уже можно считать циклом.

Реализуется же он (цикл) при помощи регистра CX и команды LOOP следующим образом:

Число циклов заносится в регистр CX. После этого следует простыня из команд, которые вы хотите "зациклить", т. е. выполнить энное количество раз. Заканчиваться все это должно LOOP'ом с указанием адреса "строки", с которой необходимо начать цикл (обычно это "строка", следующая сразу же после mov СХ...

Давайте мы сначала "набьем" нелинейный вариант нашей проги, а потом разберемся, что там к чему...

Набиваем:

      :0100 ХOR AX,AX           ; AX=0 
      :0102 MOV CX,0005         ; нижеследующий до команды LOOP кусок повторить CX раз
      :0105 ADD AX,0001         ; AX=AX+1 (у нас же значение AX на 1 увеличивается...)
      :0108 PUSH AX             ; помещаем в стек
      :0109 LOOP 0105           ; конец цикла; инициируем повторение; CX = CX-1
      :010B MOV CX,0005         ; второй цикл повторить тоже 5 раз
      :010E POP AX              ; достаем из стека
      :010F LOOP 010E           ; конец цикла; повторить! ; CX=CX-1
      :0111 INT 20              ; выход из нашей "правильной" проги...

Наверное, вы уже поняли, что цикл повторяется до тех пор, пока CX не станет равен 0. Несмотря на то, что CX - он как бы регистр общего назначения, для "зацикливания" используется именно он :). С остальными такой фокус не проходит. Хотя, зачем это я говорю? "Специализацию" регистров мы уже затрагивали при первом знакомстве с ними...

Протрассируйте эту программу! Искренне надеюсь, что вы поняли, о чем это я вас тут загружал...

РАЗБОРКИ С ОПТИМИЗАЦИЕЙ

Как я уже говорил, линейное программирование - это плохо, но не всегда...

Сравните размеры ваших линейной и нелинейной программ. Не знаю, как у вас, но у меня линейная "весит" 27, а нелинейная - 19 байт :). Как по вашему, какая быстрее работать будет?

"Ну естественно нелинейная, потому что она меньше!" - скажете вы и будете неправы...

Попытайтесь оттрассировать "зацикленную". Не правда ли, она трассируется намного дольше своего линейного аналога?

Угу... все поняли? Сам знаю, что не все :(

Объясняю: в "зацикленной" программе "компутеру" приходится выпонять БОЛЬШЕ команд, нежели в "незацикленной"...

Аргументирую это голословное утверждение следующей таблицей (построенной на основе трассировки):

Что делает линейная Что делает нелинейная
AX=1 AX=0
помещаем в стек 1 CX=5
AX=2 AX=AX+1=1
помещаем в стек 2 помещаем в стек 1
AX=3 конец цикла - переход
помещаем в стек 3 AX=AX+1=2
AX=4 помещаем в стек 2
помещаем в стек 4 конец цикла - переход
AX=5 AX=AX+1=3
помещаем в стек 5 помещаем в стек 3
достаем из стека 5 конец цикла - переход
достаем из стека 4 AX=AX+1=4
достаем из стека 3 помещаем в стек 4
достаем из стека 2 AX=AX+1=5
достаем из стека 1 помещаем в стек 5
выход CX=5
  достаем из стека 5
  конец цикла - переход
  достаем из стека 4
  конец цикла - переход
  достаем из стека 3
  конец цикла - переход
  конец цикла - переход
  достаем из стека 1
  выход

Ну и как по-вашему, какую из двух простыней проц быстрее обработает?? Сказать вам по секрету?? Ан нихрена я вам не скажу!! Сами думайте!! :]

Гы... как щас пор помню... был в моем турбо-си в преференсах к компилятору такой радиобуттон: оптимайзить по размеру или по скорости выполнения... Угадайте, на чем основан принцип этой оптимизации? Хм...

Только не вздумайте писать линейные проги! Пишите "нелинейные"! Нелинейную в линейную "переоптимайзить" - как два пальца намочить! А вот наоборот - :((

АНОНС!

Что будет в следующий раз - не знаю!! Скорее всего - мне на голову упадет-таки кирпич!

Не надо меня дергать по пустякам, а то вот сами видите, каким нервным из-за этого выпуск получился...

Белым же по черному сказано: "В связи с затянувшимся "расширением канала" у нашего провайдера возможны задержки с выпуском рассылки"!

Для чего я это писал?? Думаете, чтобы место пустое забить??

Так вот, если да, то нет!!