Assembler для Windows


Глава 5. Основы анализа кода программ

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

I

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

Для примера я взял простую консольную программу, написанную на Borland C++. В текстовом варианте программа занимает полтора десятка строк, тогда как ЕХЕ-файл имеет размер более 50 Кб. Впрочем, размер исполняемых файлов давно уже никого не удивляет. Интересно другое: корректно справился с задачей, т.е. корректно выявил точку входа - метку _main, только один дизассемблер - IDA PRO. Т.е., конечно, реально работающий участок программы дизассемблировали все, но выявить, как происходит переход на участок, смог только упомянутый мной дизассемблер. Приятно также и то, что аккуратно была распознана функция _printf. На Рис. 4.5.1 показан фрагмент дизассемблированной программы, соответствующей основной процедуре main. С другой стороны, в данном случае нет никаких видимых возможностей быстрого поиска данного фрагмента в отладчике. Осюда наглядно можно понять полезность совместного использования отладчика и дизассемблера.

CODE 00401108 _main proc near ; DATA XREF: DATA:0040B044
CODE 00401108
CODE 00401108 argc = dword ptr 8
CODE 00401108 argv = dword ptr 0Ch
CODE 00401108 envp = dword ptr 10h
CODE 00401108
CODE 00401108 push ebp
CODE 00401109 mov ebp, esp
CODE 0040110B push ebx
CODE 0040110C mov edx, offset unk_40D42C
CODE 00401111 xor eax, eax
CODE 00401113
CODE 00401113 loc_401113: ; CODE XREF: _main+22
CODE 00401113 mov ecx, 1Fh
CODE 00401118 sub ecx, eax
CODE 0040111A mov ebx, ds:off_40B074
CODE 00401120 mov cl, [ebx+ecx]
CODE 00401123 mov [edx+eax], cl
CODE 00401126 inc eax
CODE 00401127 cmp eax, 21h
CODE 0040112A jl short loc_401113
CODE 0040112C mov byte ptr [edx+20h], 0
CODE 00401130 push edx ; char
CODE 00401131 push offset aS ;_va_args
CODE 00401136 call _printf
CODE 0040113B add esp, 8
CODE 0040113E pop ebx
CODE 0040113F pop ebp
CODE 00401140 retn
CODE 00401140 _main endp

Рис. 4.5.1. Функция main консольного приложения. Дизассемблер с программы на Си, IDA PRO.

Попытаемся теперь понять, какая программа на Си была исходным источником данного фрагмента. Начнем с рассмотрения стандартных структур. Собственно, налицо только одна структура - цикл. Команда

CODE:0040112A jl short loc_401113
и является ключевой в организации цикла. Команда же inc eax, очевидно, инкрементирует параметр цикла. Т.о. в EAX хранится некая переменная, играющая роль параметра цикла. Назовем эту переменную i. Вышесказанное подтверждается и наличием команды xor eax,eax перед началом цикла. Команда эта, разумеется, эквивалентна просто i=0. Команда inc eax означает просто i++. Попытаемся выявить еще переменные. Обратим внимание на команду
CODE:0040110C mov edx, offset unk_40D42C
Команда весьма примечательна, т.к. в регистр EDX помещается некий адрес, адрес чего-то. Проследим далее, как используется регистр EDX. Команда
CODE:00401123 mov [edx+eax], cl
а также отсутствие в цикле других команд с регистром EDX убеждает нас, что EDX играет роль указателя на строку, массив или запись. Это нам предстоит сейчас выяснить. Тут примечательны две команды:
CODE:0040112C mov byte ptr [edx+20h],0
и
CODE:00401130 push edx ; char

Первая команда убеждает нас, что EDX указывает на строку, т.к. именно строка должна содержать в конце символ 0. Вторая команда является посылкой второго для функции printf. Исходя из этого, а также комментария IDA PRO (она раньше нас поняла, что это такое), заключаем, что EDX являет собой указатель на некую строку. Заметьте, что мы не обратились к просмотру блока данных, что, несомненно, ускорило бы заключение. Обозначим этот указатель как s1. В этой связи выражение [edx+eax] можно трактовать как s1[i] или как *(s1+i). Рассмотрим теперь команду

CODE:0040111A mov ebx, ds:off_40B074

несомненно, означающую, что и регистр EBX также представляет собой указатель на строку (будем обозначать ее s2), подтверждением этому также служат последующие строки, означающие переброску символов из строки s2 в строку s1. Вот об этом следует поговорить подробнее. Что означает последовательность команд

CODE:00401113 mov ecx, 1Fh
CODE:00401118 sub ecx, eax

Значить она может только одно: на каждом шаге цикла в ECX будут находиться числа от 1FH (31) до 0 (последним значением регистра EAX, то бишь переменной i, участвующим в команде sub ecx,eax будет 1FH). Поскольку в формировании содержимого регистра ECX участвует еще команда mov cx,1FH, логично предположить, что в регистре ECX перед операцией перекачки символов из одной строки в другую всегда будет находиться число 1FH-i или 31-i. Выражение [ebx+ecx] тогда будет эквивалентно s2[31-i] или *(s2+31-i). В результате имеем, что строки

CODE:00401120 mov cl, [ebx+ecx]
CODE:00401123 mov [edx+eax], cl
по сути, можно заменить на s1[i]=s2[31-i]. Думаю, что теперь мы готовы рассмотреть целый фрагмент:
CODE 00401111 xor eax, eax
CODE 00401113
CODE 00401113 loc_401113: ; CODE XREF: _main+22
CODE 00401113 mov ecx, 1Fh
CODE 00401118 sub ecx, eax
CODE 0040111A mov ebx, ds:off_40B074
CODE 00401120 mov cl, [ebx+ecx]
CODE 00401123 mov [edx+eax], cl
CODE 00401126 inc eax
CODE 00401127 cmp eax, 21h
CODE 0040112A jl short loc_401113

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

i=0;
do {
s1[i]=s2[31-i] ;
i++;
} while(i>0x20)

Все бы хорошо, но не понятным остается наличие в цикле строки

CODE:0040111A mov ebx, ds:off_40B074

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

Имеется и еще один вопрос: где хранятся строки s1 и s2? Здесь можно разобраться достаточно быстро.

main является самой обычной процедурой, и если бы строки являлись локальными переменными, то для них была бы зарезервирована область в стеке процедуры путем вставки команды SUB ESP,N (или ADD ESP,-N).

Таким образом, строковые переменные являются глобальными.

Интересно, что другие переменные, хотя они тоже локальные, хранятся в регистрах, в полном соответствии с принципом, что по мере возможности переменные следует хранить в регистрах. 56

Итак, окончательный результат наших изысканий может быть представлен на Рис. 4.5.2.

char s1[32];
char * s2="абвгдежзийклмнопрстуфхцчшщыьъэюя";
void main()
{
int i;
i=0;
do {
s1[i]=s2[31-i];
i++;
} while(i>32);
s1[32]=0;
printf("%s\n",s1);
}

Рис. 4.5.2. Окончательный вариант программы на Си.

Ну, уж чтобы совсем покончить с рассматриваемым примером, отмечу, что переменная s1 является неинициализированной, а переменная s2 - инициализированной. Инициализированные переменные хранятся в секции DATA, неинициализированные - в BSS (компилятор Borland C++).

56 В старом Си для того, чтобы для хранения переменных компилятор использовал регистры, переменные следует объявлять как AUTO.

II

В данном разделе мы рассмотрим некоторые классические структуры языка Си и их отображение в язык ассемблера.

1. Условные конструкции.

Неполная условная конструкция.

if (простое условие)
{
...
...
...
}
если условие простое, то оно, разумеется, заменяется следующей возможной последовательностью
CMP EAX,1
JNZ L1
...
...
...
L1:

Полная условная конструкция.

if (простое условие)
{
...
...
...
} else
{
...
...
...
}
CMP EAX,1
JNZ L1
...
...
...
JMP L2
L1:
...
...
...
L2:

Вложенные условные конструкции.

Здесь все достаточно очевидно.

CMP EAX,1
JNZ L1
CMP EBX,2
JNZ L1
...
...
...
L1:

Что, конечно, равносильно одному составному условию, связанному союзом "И". Союз "ИЛИ", как известно, заменяется проверкой условий в блоке "ELSE".

2. Оператор switch или оператор выбора.

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

switch(i)
{
case 1:
...
...
...
break;
case 3:
...
...
...
break;
case 5:
...
...
...
break;
}

А вот соответствующий данной структуре ассемблерный код.

DEC EAX
JZ L1
SUB EAX,2
JZ L2
SUB EAX,2
JZ L3
JMP L4
L1:
...
...
...
JMP L4
L2:
...
...
...
JMP L4
L3:
...
...
...
L4:

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

CMP EAX,10
JE L1
CMP EAX,5
JE L2
CMP EAX,11
JE L3
...

3. Циклы.

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

1-я структура.

L1:
...
...
...
CMP EAX,10
JL L1

2-я структура.

L2:
CMP EAX,10
JA L1
...
...
...
JMP L2
L1:

Первая структура может быть несколько видоизменена и принимать следующий вид.

JMP L2
L1:
...
...
...
CMP EAX,10
JL L1
L2:

Как по мановению волшебной палочки, в последней структуре цикл "ДО" превращается в цикл "ПОКА".

В своем рассмотрении я не упомянул цикл "FOR" - этого "монстра" языка Си, но, в принципе, это все тот же цикл "ПОКА".

4. Локальные переменные.

Чаще всего нам приходится работать с локальными переменными. С глобальными переменными мы уже встречались, они хранятся во вполне определенных секциях. Хороший дизассемблер их легко локализует. С локальными переменными часто разбираться гораздо сложнее. Особенно это касается записей или массивов. Хороший дизассемблер значительно упростит задачу. Рассмотрим, например, следующий фрагмент, взятый из IDA PRO.

CODE 00401108 _main proc near ; DATA XREF: DATA:0040B044
CODE 00401108
CODE 00401108 var_54 = dword ptr -54h
CODE 00401108 var_28 = byte ptr -28h
CODE 00401108 argc = dword ptr 8
CODE 00401108 argv = dword ptr 0Ch
CODE 00401108 envp = dword ptr 10h
CODE 00401108
CODE 00401108 push ebp
CODE 00401109 mov ebp, esp
CODE 0040110B add esp, 0FFFFFFACh
CODE 0040110E push ebx
CODE 0040110F xor ebx, ebx

Рис. 4.5.3. Пример задания двух локальных массивов. Взят из отладчика IDA PRO.

Взгляните внимательно на Рис. 4.5.3. Как видите, отладчик нам все прописал. Две переменные var_54 и var_28 являются, несомненно, массивами типа DWORD. Причем если на первый отводится 28h байт, т.е. 40 байт или 10 элементов массива, то на второй 54h-28h=2CH=44 или 11 элементов массива. И всего, следовательно, под локальные переменные зарезервировано 84 байта. А что означает команда add esp,0FFFFFFACH ? Но нас не обманешь! 0-0FFFFFFACH = 54h, что в десятичном исчислении и есть 84. В связи с массивами хотелось бы упомянуть, что в Си изначально практиковалось два способа доступа к элементам массива: посредством индексов и посредством указателей. Другими словами, можно было написать: a[i]=10 и *(a+i)=10. Причем вторая запись оказывалась более эффективной (см. по этому поводу книгу [1], гл. 15.). Разумеется, делать это можно и сейчас, но обе записи теперь в Borland C++ и Microsoft C++ 6.0 приводят к совершенно одинаковым ассемблерным эквивалентам. Что весьма отрадно - значит, компиляторы действительно развиваются. Кстати, весьма поучительным было бы сравнение ассемблерного кода, производимого разными компиляторами. Мы не будем этим заниматься, замечу только, что мое весьма поверхностное сравнение компиляторов Borland C++ 5.0 и Visual C++ 6.0 привело к выводу, что средства, разработанные фирмой Microsoft, несколько более эффективно оптимизируют транслируемый код.

Но вернемся опять к фрагменту на Рис. 4.5.3. Посмотрим, как выглядит начало функции main в дизассемблере W32Dasm.

:00401108 55 push ebp
:00401109 8ВЕС mov ebp, esp
:0040110B 83C4AC add esp, FFFFFFAC
:0040110E 53 push ebx
:0040110F 33DB xor ebx, ebx

Рис. 4.5.4. Как выглядит фрагмент программы, представленный на Рис. 4.5.3 в окне дизассемблера W32Dasm.

Как видите, дизассемблер W32Dasm менее информативен, и из данного фрагмента можно заключить только, что для локальных переменных отведено 84 байта. Саму же структуру локальных переменных можно понять только путем анализа кода, который следует ниже.

5. Функции и процедуры.

Функции и процедуры идентифицируются достаточно просто. Структуру вызова и внутреннюю часть процедур Вы уже хорошо знаете. Остается только напомнить некоторые положения. Вызов процедуры:

PUSH par1
PUSH par2
PUSH par3
CALL 232343

Здесь все достаточно просто. Главное - распознать параметры и понять порядок помещения их в стек. Надо также иметь в виду, что существует протокол передачи параметров через регистры (см. главу 3.7). После вызова процедуры может стоять команда очистки стека ADD ESP,N.

Внутренняя часть процедуры также нами неоднократно разбиралась (см. Гл. 1.2, 3.7). Думаю, что она достаточно Вами узнаваема, и мы не будем здесь на этом останавливаться.

Не забывайте, что функции возвращают результат через регистр EAX. Это может помочь Вам быстро разобраться в назначении функции.

6. Оптимизация кода.

Выше мы пытались восстановить исходный текст программы по ассемблерному коду. Насколько это удалось, можно выяснить, сравнив получившийся текст с исходным. Следует заметить, что мы ошиблись: в исходном тексте стоит цикл while, а в нашем do. Однако, если откомпилировать получившуюся программу, она будет работать, так же как и исходная. Причина отличия двух текстов программ (весьма простых, кстати) заключается в том, что транслятор, кроме всего прочего, производит еще оптимизацию кода. Результат оптимизации - принципиальная невозможность восстановить исходный текст по исполняемому коду. Можно, однако, получить программный текст, правильно описывающий алгоритм исполнения. Можно просто понять то, что делает данный код, чем мы в данной главе и занимаемся.

Рассмотрим небольшую и весьма тривиальную программу на Си.

void main ()
{
int i;
char a[10];
char b[11];
for(i=0; i<10; i++)
{
a[i]='a';
*(b+i)='a';
printf("%c %c\n",a[i],b[i]);
}
ExitProcess(0);
}

Рис. 4.5.5.

Вот как выглядит ассемблерный код, полученный с помощью транслятора Borland C++ 5.0.

.text 00401108 _main proc near ; DATA XREF: .data:0040A0B8
.text 00401108
.text 00401108 var_18 = byte ptr -18h
.text 00401108 var_C = byte ptr -0Ch
.text 00401108 argc = dword ptr 8
.text 00401108 argv = dword ptr 0Ch
.text 00401108 envp = dword ptr 10h
.text 00401108
.text 00401108 push ebp
.text 00401109 mov ebp, esp
.text 0040110B add esp, 0FFFFFFE8h
.text 0040110E push ebx
.text 0040110F xor ebx, ebx
.text 00401111
.text 00401111 loc_401111: ; CODE XREF: _main+30
.text 00401111 mov [ebp+ebx+var_C], 61h
.text 00401116 mov [ebp+ebx+var_18], 61h
.text 0040111B movsx eax, [ebp+ebx+var_18]
.text 00401120 push eax
.text 00401121 movsx edx, [ebp+ebx+var_C]
.text 00401126 push edx ; char
.text 00401127 push offset aCC ; _va_args
.text 0040112C call _printf
.text 00401131 add esp, 0Ch
.text 00401134 inc ebx
.text 00401135 cmp ebx, 0Ah
.text 00401138 jl short loc_401111
.text 0040113A push 0 ; uExitCode
.text 0040113C call ExitProcess
.text 00401141 pop ebx
.text 00401142 mov esp, ebp
.text 00401144 pop ebp
.text 00401145 retn
.text 00401145 _main endp

Рис. 4.5.6. Дизассемблированный текст программы (Рис. 4.5.5). Транслятор Borland C++ 5.0.

Кстати, функция ExitProcess(0) введена в текст программы для быстрого поиска нужного фрагмента в отладчике или дизассемблированном коде. Сразу видно, что оптимизацией здесь и не пахло. По такому тексту достаточно просто восстановить исходную Си-программу. А вот код, оптимизированный транслятором Visual C++ 6.0 (Рис. 4.5.7).

.text 00401000 sub_401000 proc near ; CODE XREF: start+AF
.text 00401000 push esi
.text 00401001 mov esi, 0Ah
.text 00401006
.text 00401006 loc_401006: ; CODE XREF: sub_401000+18
.text 00401006 push 61h
.text 00401008 push 61h
.text 0040100A push offset aCC ; "%c %c\n"
.text 0040100F call sub_401030 ;printf
.text 00401014 add esp, 0Ch
.text 00401017 dec esi
.text 00401018 jnz short loc_401006
.text 0040101A push 0 ; uExitCode
.text 0040101C call ds:ExitProcess
.text 00401022 pop esi
.text 00401023 retn
.text 00401023 sub_401000 endp

Рис. 4.5.7. Дизассемблированный текст программы (Рис. 4.5.5). Транслятор Visual C++ 6.0.

Я думаю, что текст на Рис. 4.5.7 удивит Вас. Однако, что же здесь удивительного? Взгляните на текст Си-программы. Заданные нами два символьных массива - абсолютно ни к чему. Транслятор Visual C++ очень точно это подметил и изменил код так, что, как ни старайся, текст исходной программы восстановить не удасться. Конечно, такая оптимизация оказалась возможной только потому, что наши массивы используются в ограниченной области.

Рассмотрим далее некоторые способы оптимизации, которые могут пригодиться нам и при написании программ на ассемблере.

Скорость или объем. Это весьма важный вопрос, когда дело касается микропроцессора.

Например, команда MOV EAX,EBX выполняется быстрее команды XCHG EAX,EBX, но длиннее ее. Зная такую особенность, можно либо сокращать объем, либо увеличивать скорость программы. Особенно часто используется замена такой операции, как MUL, другими командами, в частности SHL. Это может значительно увеличить скорость выполнения программы и увеличить ее объем. Интересно, что арифметические действия можно производить и с помощью команды LEA (см. Приложение 2), и современные трансляторы уже взяли это на вооружение. Так что команда MUL не так часто может встретиться в оттранслированном коде, как об этом можно подумать, исходя из текста программы. Вообще, свойство команд надо исследовать, и иногда узнаешь довольно интересные вещи. Например, нахождение остатка от деления числа без знака на 4 производится следующим образом: AND EAX,0003h - не правда ли, оригинально?

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

...
CMP EAX, 100
JB L1
Фрагмент 1
JMP L2
L1:
Фрагмент 2
L2:

Мы знаем, что содержимое EAX чаще всего оказывается меньше 100. Следовательно, фрагмент можно заменить на следующий.

...
CMP EAX,100
JNB L1
Фрагмент 2
JMP L2
L1:
Фрагмент 1
L2:

Оптимизация вызовов процедур. Рассмотрим следующий фрагмент.

P1 PROC
...
...
...
CALL P2
RET
P1 ENDP
...
...
...
P2 PROC
...
...
...
RET
P2 ENDP

Данный фрагмент можно заменить более эффективным. Подобный подход можно часто встретить, просматривая отладчиком код программы.

Р1 PROC
...
...
...
JMP P2
Р1 ENDP
...
...
...
P2 PROC
...
...
...
RET
P2 ENDP

Код становится и быстрее, и короче, вот только разобраться в нем становится сложнее. На этом мы оставляем вопрос оптимизации. Всех интересующихся могу отослать к книге [14].

6. Объектное программирование. Объектное программирование в значительной степени усложняет получаемый исполняемый код. Мы рассмотрим один простой пример. На Рис. 4.5.7 представлена программа на C++.

#include <windows.h>
#include <stdio.h>
class stroka {
public:
char с[200];
char g[100];
stroka() { strcpy(c,"Privet"); strcpy(g,"Privet");}
int strcp();
};
stroka::stri()
{
strcpy(c,g);
return 0;
}
main()
{
stroka * s = new stroka;
s->strcp();
printf("%s\n",s->c);
delete s;
}

Рис. 4.5.7. Программа на Си++, использующая объекты.

Ниже на Рис. 4.5.8. представлен дизассемблированный код процедуры main.

CODE 00401122 _main proc near ; DATA XREF: DATA:0040C044
CODE 00401122
CODE 00401122 var_28 = dword ptr -28h
CODE 00401122 var_18 = word ptr -18h
CODE 00401122 dest = dword ptr -4
CODE 00401122 argc = dword ptr 8
CODE 00401122 argv = dword ptr 0Ch
CODE 00401122 envp = dword ptr 10h
CODE 00401122
CODE 00401122 push ebp
CODE 00401123 mov ebp, esp
CODE 00401125 add esp, 0FFFFFFD8h
CODE 00401128 push ebx
CODE 00401129 mov eax, offset stru_40C084
CODE 0040112E call @_InitExceptBlockLDTC
CODE 00401133 push 12Ch
CODE 00401138 call unknown_libname_8
CODE 0040113D pop ecx
CODE 0040113E mov [ebp+dest], eax
CODE 00401141 test eax, eax
CODE 00401143 jz short loc_40117D
CODE 00401145 mov [ebp+var_18 ], 14h
CODE 0040114B push offset aPrivet ; src
CODE 00401150 push [ebp+dest] ; dest
CODE 00401153 call _strcpy
CODE 00401158 add esp, 8
CODE 0040115B push offset aPrivet_0 ; src
CODE 00401160 mov edx, [ebp+dest]
CODE 00401163 add edx, 0C8h
CODE 00401169 push edx ; dest
CODE 0040116A call _strcpy
CODE 0040116F add esp, 8
CODE 00401172 mov [ebp+var_18], 8
CODE 00401178 mov ebx, [ebp+dest]
CODE 0040117B jmp short loc_401180
CODE 0040117D ; ------------------------------------
CODE 0040117D
CODE 0040117D loc_40117D: ; CODE XREF: _main+21
CODE 0040117D mov ebx, [ebp+dest]
CODE 00401180
CODE 00401180 loc_401180: ; CODE XREF: _main+59
CODE 00401180 push ebx ; dest
CODE 00401181 call sub_401108
CODE 00401186 pop ecx
CODE 00401187 push ebx ; char
CODE 00401188 push offset aS ; _va_args
CODE 0040118D call _printf
CODE 00401192 add esp, 8
CODE 00401195 push ebx ; block
CODE 00401196 call @$bdele$qpv ; operator delete(void *)
CODE 0040119B pop ecx
CODE 0040119C mov eax, [ebp+var_28]
CODE 0040119F mov large fs:0, eax
CODE 004011A5 xor eax, eax
CODE 004011A7 pop ebx
CODE 004011A8 mov esp, ebp
CODE 004011AA pop ebp
CODE 004011AB retn
CODE 004011AB _main endp

Рис. 4.5.8. Дизассемблированный код функции main с Рис. 4.5.7. Дизассемблер IDA PRO.

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

  1. Оператор new сводится к выполнению библиотечной процедуры: unknown_libname_8. Последняя процедура выделяет память для свойств экземпляра объекта (300 байт).
  2. Конструктор хранится и выполняется непосредственно в теле программы. Это связано с тем, что и сам конструктор определен в тексте класса. Для эксперимента попробуйте вынести текст конструктора в отдельную функцию, и Вы увидите, что конструктор будет вызываться из main, как обычная функция.
  3. Процедуру @_InitExceptBlockLDTC вставляет транслятор для поддержки так называемых исключений (Exception). Вы можете удалить информацию для использования исключений, несколько сократив получившийся код, но тогда Вы не сможете использовать операторы исключений, такие как try или catch.
  4. При вызове какого-либо метода в стек всегда помещается, по крайней мере, один параметр - указатель на экземпляр объекта.

Приведу теперь фрагмент той же дизассемблированной программы, но с использованием опции -x, что для транслятора Borland C++ означает - не использовать исключения. Как видите, текст программы оказался значительно проще.

.text 00401122 _main proc near ; DATA XREF: .data:0040B0C8
.text 00401122
.text 00401122 argc = dword ptr 8
.text 00401122 argv = dword ptr 0Ch
.text 00401122 envp = dword ptr 10h
.text 00401122
.text 00401122 push ebp
.text 00401123 mov ebp, esp
.text 00401125 push ebx
.text 00401126 push 12Ch
.text 0040112B call unknown_libname_8
.text 00401130 pop ecx
.text 00401131 mov ebx, eax
.text 00401133 test eax, eax
.text 00401135 jz short loc_40115D
.text 00401137 push offset aPrivet ; src
.text 0040113C push ebx ; dest
.text 0040113D call _strcpy
.text 00401142 add esp, 8
.text 00401145 push offset aPrivet_0 ; src
.text 0040114A lea edx, [ebx+0C8h]
.text 00401150 push edx ; dest
.text 00401151 call _strcpy
.text 00401156 add esp, 8
.text 00401159 mov ecx, ebx
.text 0040115B jmp short loc_40115F
.text 0040115D ;-----------------———-—----—-—---———
.text 0040115D
.text 0040115D loc_40115D: ; CODE XREF: _main+13
.text 0040115D mov ecx, ebx
.text 0040115F
.text 0040115F loc_40115F: ; CODE XREF: _main+39
.text 0040115F mov ebx, ecx
.text 00401161 push ebx ; dest
.text 00401162 call sub_401108
.text 00401167 pop ecx
.text 00401168 push ebx ; char
.text 00401169 push offset aS ; _va_args
.text 0040116E call _printf
.text 00401173 add esp, 8
.text 00401176 push ebx ; handle
.text 00401177 call _rtl_close
.text 0040117C pop ecx
.text 0040117D xor eax, eax
.text 0040117F pop ebx
.text 00401180 pop ebp
.text 00401181 retn
.text 00401181 _main endp

Рис. 4.5.8. Дизассемблированный код функции main с Рис. 4.5.7, при трансляции использована опция -x (исключить исключения, Borland C++). Дизассемблер IDA PRO.








банк российский кредит новости, полный текст.