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

       

Идентификация значения, возвращаемого функцией


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

Кауфман

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

int xdiv(int a, int b, int *c=0)

{

if (!b) return –1;

if (c) c[0]=a % b;

return a / b;

}

Листинг 88 Демонстрация возвращения значения в аргументе, переданном по ссылке

Функция xdiv возвращает результат целочисленного деления аргумента a

на аргумент b, но помимо этого записывает в переменную c, переданную по ссылке, остаток. Так сколько же значений вернула функция? И чем возращение результата по ссылке хуже или "незаконнее" классического return?

Популярные издания склонны упрощать проблему идентификации значения, возращенного функций, рассматривая один лишь частный случай с оператором return. В частности, так поступает Мэтт Питтерек в своей книге "Секреты системного программирования в Windows 95", все же остальные способы остаются "за кадром". Мы же рассмотрим следующие механизмы:

-- возврат значения оператором return (через регистры или стек сопроцессора);

-- возврат значений через аргументы, переданные по ссылке;

-- возврат значений через динамическую память (кучу);



-- возврат значений через глобальные переменные;

– возврат значений через флаги процессора.

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


::возврат значения оператором return. По общепринятому соглашению значение, возвращаемое оператором return, помещается в регистр EAX (в AX у 16-разрядных компиляторов), а если его оказывается недостаточно, старшие 32 бита операнда помещаются в EDX (в 16-разрядном режиме старшее слово помещается в DX).

Вещественные типы в большинстве случаев возвращаются через стек сопроцессора, реже – через регистры EDX:EAX (DX:AX в 16-разрядном режиме).

А как возвращаются типы, занимающие более 8 байт? Скажем, некая функция возвращает структуру, состоящую из сотен байт или объект не меньшего размера. Ни то, ни другое в регистры не запихнешь, даже стека сопроцессора не хватит!

тип

способ возврата

однобайтовый

AL

AX

двухбайтовый

AX

четырехбайтовый

DX:AX

real

DX:BX:AX

float

DX:AX

стек сопроцессора

double

стек сопроцессора

near pointer

AX

far pointer

DX:AX

свыше четырех байт

через неявный аргумент по ссылке

Таблица 11 Механизм возращения значения оператором return в 16-разрядных компиляторах

тип

способ возврата

однобайтовый

AL

AX

EAX

двухбайтовый

AX

EAX

четырехбайтовый

EAX

восьми байтовый

EDX:EAX

float

стек сопроцессора

EAX

double

стек сопроцессора

EDX:EAX

near pointer

EAX

свыше восьми байт

через неявный аргумент по ссылке

Таблица 12 Механизм возращения значения оператором return в 32-разрядных компиляторах

Оказывается, если возвращаемое значение не может быть втиснуто в регистры, компилятор скрыто от программиста передает функции неявный аргумент – ссылку на локальную переменную, в которую и записывается возвращенный результат. Таким образом, функции struct mystuct MyFunc(int a, int b) и void MyFunc(struct mystryct *my, int a, int b) компилируются в идентичный

(или близкий к тому) код и "вытянуть" из машинного кода подлинный прототип невозможно!



Единственную зацепку дает компилятор Microsoft Visual C++, возвращающий в этом случае указатель на возвращаемую переменную, т.е. восстановленный прототип выглядит приблизительно так: struct mystruct* MyFunc(struct mystruct* my, int a, int b). Согласитесь, несколько странно, чтобы программист в здравом уме да при живой теще, возвращал указатель на аргумент, который своими руками только что и передал функции? Компилятор же Borland C++ в данной ситуации возвращает тип void, стирая различие между аргументом, возвращаемым по значению и аргументом, возвращаемым по ссылке. Впрочем, невозможность восстановления подлинного прототипа не должна огорчать. Скорее наоборот! "Истинный прототип" утверждает, что результат работы функции возвращается по значению, а в действительности он возвращается по ссылке! Так ради чего тогда называть кошку мышкой?

Пару слов об определении типа возвращаемого значения. Если функция при выходе явно присваивает регистру EAX или EDX некоторое значение (AX и DX в 16-разрядном режиме), то его тип можно начерно определить по таблицам 11 и 12. Если же оставляет эти регистры неопределенными – то, скорее всего, возвращается тип void, т.е. ничто. Уточнить информацию помогает анализ вызывающей функции, а точнее то, как она обращается с регистрами EAX [EDX] (AX [DX] в 16-разрядном режиме). Например, для типов char характерно либо обращение к младшей половинке регистра EAX (AX) – регистру AL, либо обнуление старших байт операцией логического AND. Логично предположить: если вызывающая функция не использует значения, отставленного вызываемой функцией в регистрах EAX [EDX], – ее тип void. Но это предположение неверно. Частенько программисты игнорируют возвращаемое значение, вводя тем самым исследователей в заблуждение.

Рассмотрим следующий пример, демонстрирующий механизм возвращения основных типов значений:

#include <stdio.h>

#include <malloc.h>

char char_func(char a, char b)

{

return a+b;

}

int int_func(int a, int b)



{

return a+b;

}

__int64 int64_func(__int64 a, __int64 b)

{

return a+b;

}

int* near_func(int* a, int* b)

{

int *c;

c=(int *)malloc(sizeof(int));

c[0]=a[0]+b[0];

return c;

}

main()

{

int a;

int b;

a=0x666;

b=0x777;

printf("%x\n",

char_func(0x1,0x2)+

int_func(0x3,0x4)+

int64_func(0x5,0x6)+

near_func(&a,&b)[0]);

}

Листинг 89 Пример, демонстрирующий механизм возвращения основных типов значений

Результат его компиляции Microsoft Visual C++ 6.0 с настойками по умолчанию будет выглядеть так:

char_func    proc near           ; CODE XREF: main+1Ap

arg_0         = byte ptr  8

arg_4        = byte ptr  0Ch

push   ebp

mov    ebp, esp

; Открываем кадр стека

movsx  eax, [ebp+arg_0]

; Загружаем в EAX arg_0 тип signed char, попутно расширяя его до int

movsx  ecx, [ebp+arg_4]

; Загружаем в EAX arg_0 тип signed char, попутно расширяя его до int

add    eax, ecx

; Складываем arg_0 и arg_4 расширенные до int, сохраняя их в регистре EAX -

; это есть значение, возвращаемое функцией.

; К сожалению, достоверно определить его тип невозможно. Он с равным успехом

; может представлять собой и int и char, причем, int даже более вероятен,

; т.к. сумма двух char по соображениям безопасности должна помещаться в int,

; иначе возможно переполнение.

pop    ebp

retn

char_func    endp

int_func     proc near           ; CODE XREF: main+29p

arg_0        = dword      ptr  8

arg_4        = dword      ptr  0Ch

push   ebp

mov    ebp, esp

; Открываем кадр стека

mov    eax, [ebp+arg_0]

; Загружаем в EAX значение аргумента arg_0 типа int

add    eax, [ebp+arg_4]

; Складываем arg_0 с arg_4 и оставляем результат в регистре EAX.

; Это и есть значение, возвращаемое функцией, вероятнее всего, типа int.

pop    ebp

retn

int_func     endp

int64_func   proc near           ; CODE XREF: main+40p



arg_0        = dword      ptr  8

arg_4        = dword      ptr  0Ch

arg_8        = dword      ptr  10h

arg_C        = dword      ptr  14h

push   ebp

mov    ebp, esp

; открываем кадр стека

mov    eax, [ebp+arg_0]

; Загружаем в EAX значение аргумента arg_0

add    eax, [ebp+arg_8]

; Складываем arg_0 с arg_8

mov    edx, [ebp+arg_4]

; Загружаем в EDX значение аргумента arg_4

adc    edx, [ebp+arg_C]

; Складываем arg_4 и arg_C

с учетом флага переноса, оставшегося от сложения

; arg_0 с arg_8.

; Выходит, arg_0 и arg_4, как и arg_8 и arg_C

это – половинки двух

; аргументов типа __int64, складываемые друг с другом.

; Стало быть, результат вычислений возвращается в регистрах EDX:EAX

pop    ebp

retn

int64_func   endp

near_func    proc near           ; CODE XREF: main+54p

var_4        = dword      ptr -4

arg_0        = dword      ptr  8

arg_4        = dword      ptr  0Ch

push   ebp

mov    ebp, esp

; Отрываем кадр стека

push   ecx

; Сохраняем ECX

push   4            ; size_t

call   _malloc

add    esp, 4

; Выделяем 4 байта из кучи

mov    [ebp+var_4], eax

; Заносим указатель на выделенную память в переменную var_4

mov    eax, [ebp+arg_0]

; Загружаем в EAX значение аргумента arg_0

mov    ecx, [eax]

; Загружаем в ECX значение ячейки памяти типа int на которую указывает EAX.

; Таким образом, тип аргумента arg_0 – int

*

mov    edx, [ebp+arg_4]

; Загружаем в EDX значение аргумента arg_4

add    ecx, [edx]

; Складываем с *arg_0 значение ячейки памяти типа int

на которое указывает EDX

; Следовательно, тип аргумента arg_4 – int

*

mov    eax, [ebp+var_4]

; Загружаем в EAX указатель на выделенный из кучи блок памяти

mov    [eax], ecx

; Копируем в кучу значение суммы *arg_0 и *arg_4

mov    eax, [ebp+var_4]

; Загружаем в EAX указатель на выделенный из кучи блок памяти

; Это и будет значением, возвращаемым функцией, т.е.


ее прототип выглядел так:

; int* MyFunc(int *a, int *b);

mov    esp, ebp

pop    ebp

retn

near_func    endp

main         proc near           ; CODE XREF: start+AFp

var_8        = dword      ptr -8

var_4        = dword      ptr -4

push   ebp

mov    ebp, esp

; Открываем кадр стека

sub    esp, 8

; Резервируем место для локальных переменных

push   esi

push   edi

; Сохраняем регистры в стеке

mov    [ebp+var_4], 666h

; Заносим в локальную переменную var_4 типа int

значение 0x666

mov    [ebp+var_8], 777h

; Заносим в локальную переменную var_8 типа int

значение 0x777

push   2

push   1

call   char_func

add    esp, 8

; Вызываем

функцию char_func(1,2). Как мы помним, у нас были сомнения в типе

; возвращаемого ею значения – либо int, либо char.

movsx  esi, al

; Расширяем возращенное функцией значение до signed int, следовательно, она

; возвратила signed char

push   4

push   3

call   int_func

add    esp, 8

; Вызываем функцию int_func(3,4), возвращающую значение типа int

add    eax, esi

; Прибавляем к значению, возвращенному функцией, содержимое ESI

cdq

; Преобразуем двойное слово, содержащееся в регистре EAX

в четверное,

; помещаемое в регистр EDX:EAX

mov    esi, eax

mov    edi, edx

; Копируем расширенное четверное слово в регистры EDI:ESI

push   0

push   6

push   0

push   5

call   int64_func

add    esp, 10h

; Вызываем функцию int64_func(5,6), возвращающую тип __int64

; Теперь становится понятно, чем вызвано расширение предыдущего результата

add    esi, eax

adc    edi, edx

; К четверному слову, содержащемуся в регистрах EDI:ESI добавляем результат

; возращенный функцией int64_func

lea    eax, [ebp+var_8]

; Загружаем в EAX указатель на переменную var_8

push   eax

; Передаем функции near_func

указатель на var_8 как аргумент

lea    ecx, [ebp+var_4]

; Загружаем в ECX указатель на переменную var_4



push   ecx

; Передаем функции near_func

указатель на var_4 как аргумент

call   near_func

add    esp, 8

; Вызываем near_func

mov    eax, [eax]

; Как мы помним, в регистре EAX функция возвратила указатель на переменную

; типа int, - загружаем значение этой переменной в регистр EAX

cdq

; Расширяем EAX до четверного слова

add    esi, eax

adc    edi, edx

; Складываем два четверных слова

push   edi

push   esi

; Результат сложения передаем функции printf

push   offset unk_406030

; Передаем указатель на строку спецификаторов

call   _printf

add    esp, 0Ch

pop    edi

pop    esi

mov    esp, ebp

pop    ebp

retn

main         endp

Листинг 90

Как мы видим: в идентификации типа значения, возращенного оператором return ничего хитрого нет, - все прозаично. Но не будем спешить. Рассмотрим следующий пример. Как вы думаете, что именно и в каких регистрах будет возвращаться?

#include <stdio.h>

#include <string.h>

struct XT

{

char s0[4];

int  x;

};

struct XT MyFunc(char *a, int b)

// функция возвращает значение типа структура "XT" по значению

{

struct XT xt;

strcpy(&xt.s0[0],a);

xt.x=b;

return xt;

}

main()

{

struct XT xt;

xt=MyFunc("Hello, Sailor!",0x666);

printf("%s %x\n",&xt.s0[0],xt.x);

}

Листинг 91 Пример демонстрирующий возвращения структуры по значению

Заглянем в откомпилированный результат:

MyFunc       proc near           ; CODE XREF: sub_401026+10p

var_8        = dword      ptr -8

var_4        = dword      ptr –4

; Эти локальные переменные на самом деле элементы "расщепленной" структуры XT

; Как уже говорилось в главе "Идентификация объектов, структур и массивов",

; компилятор всегда стремится обращаться к элементам структуры по их фактическим

; адресам, а не через базовый указатель.

; Поэтому, не так-то просто отличить структуру от несвязанных между собой переменных,



; а под час это и вовсе невозможно!

arg_0        = dword      ptr  8

arg_4        = dword      ptr  0Ch

; Функция принимает два аргумента

push   ebp

mov    ebp, esp

; Открываем кадр стека

sub    esp, 8

; Резервируем место для локальных переменных

mov    eax, [ebp+arg_0]

; Загружаем в регистр EAX содержимое аргумента arg_0

push   eax

; Передаем arg_0 функции strcpy, следовательно,

; arg_0 представляет собой указатель на строку.

lea    ecx, [ebp+var_8]

; Загружаем в ECX указатель на локальную переменную var_8 и…

push   ecx

;...передаем его функции strcpy

; Следовательно, var_8 – строковой буфер размером 4 байта

call   strcpy

add    esp, 8

; Копируем переданную через arg_0 строку в var_8

mov    edx, [ebp+arg_4]

; Загружаем в регистр EDX значение аргумента arg_4

mov    [ebp+var_4], edx

; Копируем arg_4 в локальную переменную var_4

mov    eax, [ebp+var_8]

; Загружаем в EAX содержимое (не указатель!) строкового буфера

mov    edx, [ebp+var_4]

; Загружаем в EDX значение переменной var_4

; Столь явная загрузка регистров EDX:EAX перед выходом из функции указывает

; на то, что это и есть значение, взращаемое функцией.

; Надо же какой неожиданный сюрприз! Функция возвращает в EDX

и EAX

; две переменные различного типа! А вовсе не __int64, как могло бы показаться

; при беглом анализе программы.

; Второй сюрприз – возврат типа char[4] не через указатель или ссылку, а через

; регистр!

; Нам еще повезло, если бы структура была объявлена как

; struct XT{short int a, char b, char c}, в регистре EAX возвратились бы

; целых три переменные двух типов!

mov    esp, ebp

pop    ebp

retn

MyFunc       endp

main   proc near           ; CODE XREF: start+AFp

var_8        = dword      ptr -8

var_4        = dword      ptr –4

; Две локальные переменные типа int

; Тип установлен путем вычисления размера каждой из них

push   ebp



mov    ebp, esp

; Открываем кадр стека

sub    esp, 8

; Резервируем восемь байт под локальные переменные

push   666h

; Передаем функции MyFunc аргумент типа int

; Следовательно, arg_4 имеет тип int (по коду вызываемой функции это не было

; очевидно, - arg_4 с не меньшим успехом мог оказаться и указателем).

; Значит, в регистре EDX функция возвращает тип int

push   offset aHelloSailor ; "Hello, Sailor!"

; Передаем функции MyFunc указатель на строку

; Внимание! Строка занимает более 4-х байт, поэтому, не рекомендуется

; запускать этот пример "вживую".

call   MyFunc

add    esp, 8

; Вызываем MyFunc. Она неким образом изменяет регистры EDX

и EAX

; Мы уже знаем типы возвращаемых в них значений и остается только

; удостоверится – "правильно" ли они используются вызывающей функцией.

mov    [ebp+var_8], eax

; Заносим в локальную переменную var_8 содержимое регистра EAX

mov    [ebp+var_4], edx

; Заносим в локальную переменную var_4 содержимое регистра EDX

; Согласитесь, – очень похоже на то, что функция возвращает __int64

mov    eax, [ebp+var_4]

; Загружаем в EAX содержимое var_4

; (т.е. регистра EDX, возвращенного функцией MyFunc) и…

push   eax

; …передаем его функции printf

; Согласно строки спецификаторов, это тип int

; Следовательно, в EDX функция возвратила int или, по крайней мере, его

; старшую часть

lea    ecx, [ebp+var_8]

; Загружаем в ECX указатель на переменную var_8, хранящую значение,

; возвращенное функцией через регистр EAX.

; Согласно строки спецификаторов, это указатель на строку

; Итак, мы подтвердили, что типы значений, возвращенных через регистры EDX:EAX

; различны!

; Немного поразмыслив, мы даже сможем восстановить подлинный прототип:

; struct X{char a[4]; int} MyFunc(char* b, int c);

push   ecx

push   offset aSX   ; "%s %x\n"

call   _printf

add    esp, 0Ch

mov    esp, ebp

pop    ebp

; Закрываем кадр стека



retn

main   endp

Листинг 92

А теперь слегка изменим структуру XT, заменив char s0[4]

на char9 s0[10], что гарантированно не влезает в регистры EDX:AX и посмотрим, как изменится от этого код:

main         proc near           ; CODE XREF: start+AFp

var_20       = byte ptr -20h

var_10       = dword      ptr -10h

var_C        = dword      ptr -0Ch

var_8        = dword      ptr -8

var_4        = dword      ptr -4

push   ebp

mov    ebp, esp

; Отрываем кадр стека

sub    esp, 20h

; Резервируем 0x20 байт под локальные переменные

push   666h

; Передаем функции MyFunc крайний правый аргумент – значение 0x666 типа int

push   offset aHelloSailor ; "Hello, Sailor!"

; Передаем функции MyFunc второй справа аргумент – указатель на строку

lea    eax, [ebp+var_20]

; Загружаем в EAX адрес локальной переменной var_20

push   eax

; Передаем функции MyFunc указатель на переменную var_20

; Стоп! Этого аргумента не было в прототипе функции! Откуда же он взялся?!

; Верно, не было. Его вставил компилятор для возвращения структуры по значению.

; Последнюю фразу вообще-то стоило заключить в кавычки для придания ей

; ироничного оттенка – структура, возвращаемая по значению, в действительности

; возвращается по ссылке.

call   MyFunc

add    esp, 0Ch

; Вызываем MyFunc

mov    ecx, [eax]

; Функция в ECX возвратила указатель на возвращенную ей по ссылке структуру

; Этот прием характерен лишь для Microsoft Visual C++, большинство компиляторов

; оставляют значение EAX на выходе неопределенным или равным нулю.

; Но, так или иначе, в ECX загружается первое двойное слово,

; на которое указывает указатель EAX. На первый взгляд, это элемент типа int

; Однако не будем бежать по перед косы и торопиться с выводами

mov    [ebp+var_10], ecx

; Сохранение ECX в локальной переменной var_10

mov    edx, [eax+4]

; В EDX загружаем второе двойное слово по указателю EDX



mov    [ebp+var_C], edx

; Копируем его в переменную var_C

; Выходит, что и второй элемент структуры – имеет тип int?

; Мы, знающие как выглядел исходный текст программы, уже начинам замечать

; подвох. Что-то здесь определенно не так...

mov    ecx, [eax+8]

; Загружаем третье двойное слово, от указателя EAX

и…

mov    [ebp+var_8], ecx

; …копируем его в var_8. Еще один тип int? Да откуда же они берутся в таком

; количестве, когда у нас он был только один! И где, собственно, строка?

mov    edx, [eax+0Ch]

mov    [ebp+var_4], edx

; И еще один тип int переносим из структуры в локальную переменную. Нет, это

; выше наших сил!

mov    eax, [ebp+var_4]

; Загружаем в EAX содержимое переменной var_4

push   eax

; Передаем значение var_4 функции printf.

; Судя по строке спецификаторов, var_4 действительно, имеет тип int

lea    ecx, [ebp+var_10]

; Получаем указатель на переменную var_10 и…

push   ecx

;...передаем его функции printf

; Судя по строке спецификаторов, тип ECX

– char

*, следовательно: var_10

; и есть искомая строка. Интуиция нам подсказывает, что var_C и var_8,

; расположенные ниже ее (т.е. в более старших адресах), так же содержат

; строку. Просто компилятор вместо того чтобы вызывать srtcpy

решил, что

; будет быстрее скопировать ее самостоятельно, чем и ввел нас в заблуждение.

; Поэтому, никогда не следует торопится с идентификацией типов элементов

; структур! Тщательно проверяйте каждый байт – как он инициализируется и как

; используется. Операции пересылки в локальные переменные еще ни о чем

; не

говорят!

push   offset aSX   ; "%s %x\n"

call   _printf

add    esp, 0Ch

mov    esp, ebp

pop    ebp

; Закрываем кадр стека

retn

main         endp

MyFunc       proc near           ; CODE XREF: main+14p

var_10       = dword      ptr -10h

var_C        = dword      ptr -0Ch

var_8        = dword      ptr –8

var_4        = dword      ptr –4



arg_0        = dword      ptr  8

arg_4        = dword      ptr  0Ch

arg_8        = dword      ptr  10h

; Обратите внимание, что функции передаются три аргумента, а не два, как было

; объявлено в прототипе

push   ebp

mov    ebp, esp

; Открываем кадр стека

sub    esp, 10h

; Резервируем память для локальных переменных

mov    eax, [ebp+arg_4]

; Загружаем а EAX указатель на второй справа аргумент

push   eax

; Передаем указатель на arg_4 функции strcpy

lea    ecx, [ebp+var_10]

; Загружаем в ECX указатель на локальную переменную var_10

push   ecx

; Передаем функции strcpy указатель на локальную переменную var_10

call   strcpy

add    esp, 8

; Копируем строку, переданную функции MyFunc, через аргумент arg_4

mov    edx, [ebp+arg_8]

; Загружаем в EDX значение самого правого аргумента, переданного MyFunc

mov    [ebp+var_4], edx

; Копируем arg_8 в локальную переменную var_4

mov    eax, [ebp+arg_0]

; Загружаем в EAX значение аргумента arg_0

; Как мы знаем, этот аргумент функции передает сам компилятор, и передает в нем

; указатель на локальную переменную, предназначенную для возращения структуры

mov    ecx, [ebp+var_10]

; Загружаем в ECX двойное слово с локальной переменной var_10

; Как мы помним, в локальную переменную var_10 ранее была скопирована строка,

; следовательно, сейчас мы вновь увидим ее "двухсловное" копирование!

mov    [eax], ecx

mov    edx, [ebp+var_C]

mov    [eax+4], edx

mov    ecx, [ebp+var_8]

mov    [eax+8], ecx

; И точно! Из локальной переменной var_10 в локальную переменную *arg_0

; копирование происходит "вручную", а не с помощью strcpy!

; В общей сложности сейчас было скопировано 12 байт, значит, первый элемент

; структуры выглядит так: char s0[12].

; Да, конечно, в исходном тесте было 'char s0[10]', но компилятор,

; выравнивая элементы структуры по адресам, кратным четырем, перенес второй

; элемент – int x, по адресу base+0x12, тем самым создав "дыру" между концом



; строки и началом второго элемента.

; Анализ дизассемблерного листинга не позволяет восстановить истинный вид

; структуры, единственное, что можно сказать – длина строки s0

; лежит в интервале [9 - 12]

;

mov    edx, [ebp+var_4]

mov    [eax+0Ch], edx

; Копируем переменную var_4 (содержащую аргумент arg_8) в [eax+0C]

; Действительно, второй элемент структуры -int x- расположен по смещению

; 12 байт от ее начала.

mov    eax, [ebp+arg_0]

; Возвращаем в EAX указатель на аргумент arg_0, содержащий указатель на

; возращенную структуру

mov    esp, ebp

pop    ebp

; Закрываем кадр стека

retn

; Итак, прототип функции выглядит так:

; struct X {char s0[12], int a} MyFunc(struct X *x, char *y, int z)

;

MyFunc       endp

Листинг 93

Возникает вопрос – а как возвращаются структуры, состоящие из сотен и тысяч байт? Ответ: они копируются в локальную переменную, неявно переданную компилятором по ссылке, инструкцией MOVS, в чем мы сейчас и убедимся, изменив в исходном тексте предыдущего примера "char s0[10]", на "char s0[0x666]". Результат перекомпиляции должен выглядеть так:

MyFunc       proc near           ; CODE XREF: main+1Cp

var_66C             = byte ptr -66Ch

var_4        = dword      ptr -4

arg_0        = dword      ptr  8

arg_4        = dword      ptr  0Ch

arg_8        = dword      ptr  10h

push   ebp

mov    ebp, esp

; Открываем кадр стека

sub    esp, 66Ch

; Резервируем память для локальных переменных

push   esi

push   edi

; Сохраняем регистры в стеке

mov    eax, [ebp+arg_4]

push   eax

lea    ecx, [ebp+var_66C]

push   ecx

call   strcpy

add    esp, 8

; Копируем переданную функции строку в локальную переменную var_66C

mov    edx, [ebp+arg_8]

mov    [ebp+var_4], edx

; Копируем аргумент arg_8 в локальную переменную var_4

mov    ecx, 19Bh

; Заносим в ECX значение 0x19B, пока еще не понимая, что оно выражает

lea    esi, [ebp+var_66C]



; Устанавливаем регистр ESI на локальную переменную var_66C

mov    edi, [ebp+arg_0]

; Устанавливаем регистр EDI на переменную на которую указывает

; указатель, переданный в аргументе arg_0

repe movsd

; Копируем ECX двойных слов с ESI в EDI

; Переводя это в байты, получаем: 0x19B*4 = 0x66C

; Таким образом, копируется и строка var_66C, и переменная var_4

mov    eax, [ebp+arg_0]

; Возвращаем в EAX указатель на возвращенную структуру

pop    edi

pop    esi

mov    esp, ebp

pop    ebp

; Закрываем кадр стека

retn

MyFunc       endp

Листинг 94

Следует учитывать, что многие компиляторы (например, WATCOM) передают функции указатель на буфер для возвращаемого значения не через стек, а через регистр, причем регистр по обыкновению берется не из очереди кандидатов в порядке предпочтения (см. таблицу 6), а используется особый регистр, специально предназначенный для этой цели. Например, у WATCOM-а это регистр ESI.

::возвращение вещественных значений.

Соглашения cdecl и stdcall предписывают возвращать вещественные значения (float, double, long double) через стек сопроцессора, значение же регистров EAX и EDX на выходе из такой функции может быть любым (другими словами, функции, возвращающие вещественные значения, оставляют регистры EAX и EDX в неопределенном состоянии).

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

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

#include <stdio.h>

float MyFunc(float a,  float b)

{

return a+b;

}

main()

{

printf("%f\n",MyFunc(6.66,7.77));

}

Листинг 95 Пример, демонстрирующий возвращение вещественных значений

Результат его компиляции Microsoft Visual C++ должен выглядеть приблизительно так:



main         proc near           ; CODE XREF: start+AFp

var_8        = qword      ptr -8

push   ebp

mov    ebp, esp

; Открываем кадр стека

push   40F8A3D7h

push   40D51EB8h

; Передаем функции MyFunc аргументы. Пока еще мы не можем установить их тип

call   MyFunc

fstp   [esp+8+var_8]

; Стягиваем со стека сопроцессора вещественное значение, занесенное туда

; функцией MyFunc

; Чтобы определить его тип смотрим опкод инструкции, – DD

1C 24

; По таблице 10 определяем – он принадлежит double

; Постой, постой, как double, ведь функция должна возвращать float?!

; Так-то оно так, но здесь имеет место неявное преобразование типов

; при передаче аргумента функции printf, ожидающей double.

; Обратите внимание на то, куда стягивается возращенное функцией значение:

; [esp+8-8] == [esp], т.е. оно помещается на вершину стека, что равносильно

; его заталкиваю командами PUSH.

push   offset aF    ; "%f\n"

; Передаем функции printf указатель на строку спецификаторов "%f\n"

call   _printf

add    esp, 0Ch

pop    ebp

retn

main         endp

MyFunc       proc near           ; CODE XREF: main+Dp

arg_0        = dword      ptr  8

arg_4        = dword      ptr  0Ch

push   ebp

mov    ebp, esp

; Открываем кадр стека

fld    [ebp+arg_0]

; Затягиваем на вершину стека сопроцессора аргумент arg_0

; Чтобы определять его тип, смотрим на опкод инструкции FLD

- D9 45 08

; Раз так, это – float

fadd   [ebp+arg_4]

; Складываем arg_0, только что затянутый на вершину стека сопроцессора, с arg_4

; помещая результат в тот же стек и…

pop    ebp

retn

; ...возвращаемся из функции, оставляя результат сложения двух float-ов

; на вершине стека сопроцессора

; Забавно, если объявить функцию как double

это даст идентичный код!

MyFunc       endp

Листинг 96

Замечание о механизме возращения значений в компиляторе WATCOM C: Компилятор WATCOM C предоставляет программисту возможность "вручную" выбирать: в каком именно регистре (регистрах) функция будет возвращать результат своей работы.


Это серьезно осложняет анализ, ведь (как уже было сказано выше) по общепринятым соглашениям функция не должна портить регистры EBX, ESI и EDI (BX, SI и DI в 16-разрядном коде). Увидев операцию чтения регистра ESI, идущую после вызова функции, в первую очередь мы решим, что он был инициализирован еще до ее вызова, - ведь так происходит в подавляющем большинстве случаев. Но только не с WATCOM! Этот товарищ может заставить функцию возвращать значение в любом регистре общего назначения за исключением EBP (BP), заставляя тем самым, исследовать и вызывающую и вызываемую функцию.

тип

допустимые регистры

однобайтовый

AL

BL

CL

DL

AH

BH

CH

DH

двухбайтный

AX

CX

BX

DX

SI

DI

четырехбайтный

EAX

EBX

ECX

EDX

ESI

EDI

восьмибайтовый

EDX:EAX

ECX:EBX

ECX:EAX

ECX:ESI

EDX:EBX

EBX:EAX

EDI:EAX

ECX:EDI

EDX:ESI

EDI:EBX

ESI:EAX

ECX:EDX

EDX:EDI

EDI:ESI

ESI:EBX

ближний указатель

EAX

EBX

ECX

EDX

ESI

EDI

дальний указатель

DX:EAX

CX:EBX

CX:EAX

CX:ESI

DX:EBX

DI:EAX

CX:EDI

DX:ESI

DI:EBX

SI:EAX

CX:EDX

DX:EDI

DI:ESI

SI:EBX

BX:EAX

FS:ECX

FS:EDX

FS:EDI

FS:ESI

FS:EBX

FS:EAX

GS:ECX

GS:EDX

GS:EDI

GS:ESI

GS:EBX

GS:EAX

DS:ECX

DS:EDX

DS:EDI

DS:ESI

DS:EBX

DS:EAX

ES:ECX

ES:EDX

ES:EDI

ES:ESI

ES:EBX

ES:EAX

float

8087

???

???

???

???

???

double

8087

EDX:EAX

ECX:EBX

ECX:EAX

ECX:ESI

EDX:EBX

EDI:EAX

ECX:EDI

EDX:ESI

EDI:EBX

ESI:EAX

ECX:EDX

EDX:EDI

EDI:ESI

ESI:EBX

EBX:EAX

Таблица 13 Допустимые регистры для возращения значения функции в компиляторе WATOM C. Жирным шрифтом выделен регистр (регистры) используемые по умолчанию. Обратите внимание, что по используемому регистру невозможно непосредственно узнать тип возвращаемого значения, а только его размер.


В частности, через регистр EAX может возвращаться и переменная типа int и структура из четырех переменных типа char (или двух char или одного short int)

Покажем, как это выглядит на практике. Рассмотрим следующий пример:

#include <stdio.h>

int MyFunc(int a, int b)

{

#pragma aux MyFunc value

[ESI]

// Прагма AUX вкупе с ключевым словом "value" позволяет вручную задавать регистр

// через который будет возращен результат вычислений.

// В данном случае его предписывается возвращать через ESI

return a+b;

}

main()

{

printf("%x\n",MyFunc(0x666,0x777));

}

Листинг 97 Пример, демонстрирующий возвращение значения в произвольном регистре

Результат компиляции этого примера должен выглядеть приблизительно так:

main_        proc near           ; CODE XREF: __CMain+40p

push   14h

call   __CHK

; Проверка стека на переполнение

push   edx

push   esi

; Сохраняем ESI и EDX

; Это говорит о том, что данный компилятор придерживается соглашения

; о сохранении ESI. Команды сохранения EDI не видно, однако, этот регистр

; не модифицируется данной функцией и, стало быть, сохранять его незачем

mov    edx, 777h

mov    eax, 666h

; Передаем функции MyFunc два аргумента типа int

call   MyFunc

; Вызываем MyFunc. По общепринятым соглашениям EAX, EDX

и под час ECX

; на выходе из функции содержат либо неопределенное,

; либо возращенное функцией значение

; Остальные регистры в общем случае должны быть сохранены

push   esi

; Передаем регистр ESI функции printf. Мы не можем с уверенностью сказать:

; содержит ли он значение, возращенное функцией, или был инициализирован еще

; до ее вызова

push   offset asc_420004 ; "%x\n"

call   printf_

add    esp, 8

pop    esi

pop    edx

retn

main_        endp

MyFunc       proc near           ; CODE XREF: main_+16p

push   4

call   __CHK

; Проверка стека на переполнение



lea    esi, [eax+edx]

; А вот уже знакомый нам хитрый трюк со сложением. На первый взгляд в ESI

; загружается указатель на EAX+EBX, - фактически так оно и происходит, но ведь

; указатель на EAX+EBX

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

; эквивалентна ADD EAX,EDX/MOV ESI,EAX.

; Это и есть возвращаемое функцией значение, - ведь ESI

был модифицирован, и

; не сохранен!

; Таким образом, вызывающая функция командой PUSH ESI

передает printf

; результат сложения 0x666 и 0x777, что и требовалось выяснить

retn

MyFunc       endp

Листинг 98

 

Возращение значений in-line assembler функциями. Создать ассемблерной функции волен возвращать значения в любых регистрах, каких ему будет угодно, однако, поскольку вызывающие функции языка высокого уровня ожидают увидеть результат вычислений в строго определенных регистрах, писаные соглашения приходится соблюдать. Другое дело, "внутренние" ассемблерные функции – они могут вообще не придерживаться никаких правил, что и демонстрирует следующий пример:

#include <stdio.h>

// naked-функция, не имеющая прототипа, - обо всем должен заботится сам программист!

__declspec( naked ) int MyFunc()

{

__asm{

lea ebp, [eax+ecx]  ; возвращаем в EBP сумму EAX и

ECX

; Такой трюк допустим лишь при условии, что эта

; функция будет вызываться из ассемблерной функции,

; знающей через какие регистры передаются аргументы

; и через какие – возвращается результат вычислений

ret

}

}

main()

{

int a=0x666;

int b=0x777;

int c;

__asm{

push ebp

push edi

mov eax,[a];

mov ecx,[b];

lea edi,c

// Вызываем функцию MyFunc из ассемблерной функции, передавая ей аргументы

// через те регистры, которые она "хочет"

call MyFunc;

// Принимаем возращенное в EBP значение и сохраняем его в локальной переменной

mov [edi],ebp

pop edi

pop ebp

}

printf("%x\n",c);

}

Листинг 99 Пример, демонстрирующий возвращение значения встроенными ассемблерными функциями



Результат компиляции Microsoft Visual C++ ( а другие компиляторами этот пример откомпилировать и вовсе не удастся, ибо они не поддерживают ключевое слово naked) должен выглядеть так:

MyFunc       proc near           ; CODE XREF: main+25p

lea    ebp, [eax+ecx]

; Принимаем аргументы через регистры EAX

и ECX, возвращая через регистр EBP

; их сумму

; Кончено, пример несколько надуман, зато нагляден!

retn

MyFunc       endp

main         proc near           ; CODE XREF: start+AFp

var_C        = dword      ptr -0Ch

var_8        = dword      ptr -8

var_4        = dword      ptr -4

push   ebp

mov    ebp, esp

; Открываем кадр стека

sub    esp, 0Ch

; Резервируем место для локальных переменных

push   ebx

push   esi

push   edi

; Сохраняем изменяемые регистры

mov    [ebp+var_4], 666h

mov    [ebp+var_8], 777h

; Инициализируем переменные var_4 и var_8

push   ebp

push   edi

; Сохраняем регистры или передаем их функции? Пока нельзя ответить

; однозначно

mov    eax, [ebp+var_4]

mov    ecx, [ebp+var_8]

; Загружаем в EAX значение переменной var_4, а в ECX – var_8

lea    edi, [ebp+var_C]

; Загружаем в EDI указатель на переменную var_C

call   MyFunc

; Вызываем MyFunc – из анализа вызывающей функции не очень понятно как

; ей передаются аргументы. Может через стек, а может и через регистры.

; Только исследование кода MyFunc позволяет установить, что верным оказывается

; последнее предположение. Да, - аргументы передаются через регистры!

mov    [edi], ebp

; Что бы это значило? Анализ одной лишь вызывающей функции не может дать

; исчерпывающего ответа и только анализ вызываемой подсказывает, что

; через EBP она возвращает результат вычислений.

pop    edi

pop    ebp

; Восстанавливаем измененные регистры

; Это говорит о том, что выше эти регистры действительно сохранялись в стеке

; а не передавались функции в качестве аргументов



mov    eax, [ebp+var_C]

; Загружаем в EAX содержимое переменной var_C

push   eax

push   offset unk_406030

call   _printf

add    esp, 8

; Вызываем printf

pop    edi

pop    esi

pop    ebx

; Восстанавливаем регистры

mov    esp, ebp

pop    ebp

; Закрываем кадр стека

retn

main         endp

Листинг 100

:: возврат значений через аргументы, переданные по ссылке. Идентификация значений, возращенных через аргументы, переданные по ссылке, тесно переплетается с идентификацией самих аргументов (см. главу "Идентификация аргументов функций"). Выделив среди аргументов, переданных функции, указатели – заносим их в список кандидатов на возвращаемые значения.

Теперь поищем: нет ли среди них указателей на неинициализированные переменные, – очевидно, их инициализирует сама вызываемая функция. Однако не стоит вычеркивать указатели на инициализированные переменные (особенно равные нулю) – они так же могут возвращать значения. Уточнить ситуацию позволит анализ вызываемой функции – нас будут интересовать все операции модификации переменных, переданных по ссылке. Только не спутайте это с модификацией переменных, переданных по значению. Последние автоматически умирают в момент завершения функции (точнее – вычистки аргументов из стека). Фактически – это локальные переменные функции и она безболезненно может изменять их как ей вздумается.

#include <stdio.h>

#include <string.h>

// Функция инвертирования строки src

с ее записью в строку dst

void Reverse(char *dst, const char *src)

{

strcpy(dst,src);

_strrev( dst);

}

// Функция инвертирования строки s

// (результат записывается в саму же строку s)

void Reverse(char *s)

{

_strrev( s );

}

// Функция возращает сумму двух аргументов

int sum(int a,int b)

{

// Мы можем безболезненно модифицировать аргументы, переданные по значению,

// обращаясь с ними как с обычными локальными переменными

a+=b;

return a;

}



main()

{

char s0[]="Hello,Sailor!";

char s1[100];

// Инвертируем строку s0, записывая ее в s1

Reverse(&s1[0],&s0[0]);

printf("%s\n",&s1[0]);

// Инвертируем строку s1, перезаписывая ее

Reverse(&s1[0]);

printf("%s\n",&s1[0]);

// Выводим сумму двух числел

printf("%x\n",sum(0x666,0x777));

}

Листинг 101 Пример, демонстрирующий возврат значений через переменные, переданные по ссылке

Результат компиляции этого примера должен выглядеть приблизительно так:

main         proc near           ; CODE XREF: start+AFp

var_74       = byte ptr -74h

var_10       = dword      ptr -10h

var_C        = dword      ptr -0Ch

var_8        = dword      ptr -8

var_4        = word ptr -4

push   ebp

mov    ebp, esp

; Открываем кадр стека

sub    esp, 74h

; Резервируем память для локальных переменных

mov    eax, dword ptr aHelloSailor ; "Hello,Sailor!"

; Заносим в регистр EAX четыре первых байта строки "Hello, Sailor!"

; Вероятно, компилятор копирует строку в локальную переменную таким

; хитро-тигриным способом

mov    [ebp+var_10], eax

mov    ecx, dword ptr aHelloSailor+4

mov    [ebp+var_C], ecx

mov    edx, dword ptr aHelloSailor+8

mov    [ebp+var_8], edx

mov    ax, word ptr aHelloSailor+0Ch

mov    [ebp+var_4], ax

; Точно, строка "Hello,Sailor!" копируется в локальную переменную var_10

; типа char s[0x10]

; Число 0x10 было получено подсчетом количества копируемых байт –

; четыре итерации по четыре байт в каждой – итого, шестнадцать!

lea    ecx, [ebp+var_10]

; Загрузка в ECX указателя на локальную переменную var_10,

; содержащую строку "Hello, World!"

push   ecx          ; int

; Передача функции Reverse_1 указателя на строку "Hello, World!"

; Смотрите, - IDA неверно определила тип, - ну какой же это int,

; когда это char *

; Однако, вспомнив, как копировалась строка, мы поймем, почему ошиблась IDA



lea    edx, [ebp+var_74]

; Загрузка в ECX указателя на неинициализированную локальную переменную var_74

push   edx          ; char *

; Передача функции Reverse_1 указателя на неинициализированную переменную

; типа char s1[100]

; Число 100 было получено вычитанием смещения переменной var_74 от смещения

; следующей за ней переменной, var_10, содержащей строку "Hello, World!"

; 0x74 – 0x10 = 0x64 или в десятичном представлении - 100

; Факт передачи указателя на неинициализированную переменную говорит о том,

; что, скорее всего, функция возвратит через нее некоторое значение –

; возьмите это себе на заметку.

call   Reverse_1

add    esp, 8

; Вызов функции Reverse_1

lea    eax, [ebp+var_74]

; Загрузка в EAX указателя на переменную var_74

push   eax

; Передача функции printf указателя на переменную var_74, - поскольку,

; вызывающая функция не инициализировала эту переменную, можно предположить,

; что вызываемая возвратила в через нее свое значение

; Возможно, функция Reverse_1 модифицировала и переменную var_10, однако,

; об этом нельзя сказать с определенностью до тех пор пока не будет

; изучен ее код

push   offset unk_406040

call   _printf

add    esp, 8

; Вызов функции printf для вывода строки

lea    ecx, [ebp+var_74]

; Загрузка в ECX указателя на переменную var_74, по-видимому,

; содержащую возращенное функцией Reverse_1 значение

push   ecx          ; char *

; Передача функции Reverse_2 указателя на переменную var_74

; Функция Reverse_2 так же может возвратить в переменной var_74

; свое значение, или некоторым образом, модифицировать ее

; Однако может ведь и не возвратить!

; Уточнит ситуацию позволяет анализ кода вызываемой функции.

call   Reverse_2

add    esp, 4

; Вызов функции Reverse_2

lea    edx, [ebp+var_74]

; Загрузка в EDX указателя на переменную var_74

push   edx

; Передача функции printf указателя на переменную var_74

; Поскольку, значение, возвращенное функцией через регистры EDX:EAX



; не используется, можно предположить, что она возвращает его не через

; регистры, а в переменной var_74. Но это не более чем предположение

push   offset unk_406044

call   _printf

add    esp, 8

; Вызов

функции printf

push   777h

; Передача функции Sum значения 0x777 типа int

push   666h

; Передача функции Sum значения 0x666 типа int

call   Sum

add    esp, 8

; Вызов

функции Sum

push   eax

; В регистре EAX содержится возращенное функцией Sum

значение

; Передаем его функции printf в качестве аргумента

push   offset unk_406048

call   _printf

add    esp, 8

; Вызов

функции printf

mov    esp, ebp

pop    ebp

; Закрытие кадра стека

retn

main         endp

; int __cdecl Reverse_1(char *,int)

; Обратите внимание, что прототип функции определен неправльно!

; На самом деле, как мы уже установили из анализа вызывающей функции, он выглядит так:

; Reverse(char *dst, char *src)

; Название аргументов дано на основании того, что левый аргумент – указатель

; на неинициализированный буфер и, скорее всего, он выступает в роли приемника,

; соответственно, правый аргумент в таком случае – источник.

Reverse_1    proc near           ; CODE XREF: main+32p

arg_0        = dword      ptr  8

arg_4        = dword      ptr  0Ch

push   ebp

mov    ebp, esp

; Открываем кадр стека

mov    eax, [ebp+arg_4]

; Загружаем в EAX значение аргумента arg_4

push   eax

; Передаем arg_4 функции strcpy

mov    ecx, [ebp+arg_0]

; Загружаем в ECX значение аргумента arg_0

push   ecx

; Передаем arg_0 функции strcpy

call   strcpy

add    esp, 8

; Копируем содержимое строки, на которую указывает arg_4, в буфер

; на который указывает arg_0

mov    edx, [ebp+arg_0]

; Загружаем в EDX содержимое аргумента arg_0, указывающего на буфер,

; содержащий только что скопированную строку

push   edx          ; char *

; Передаем функции __strrev arg_0



call   __strrev

add    esp, 4

; функция strrev инвертирует строку, на которую указывает arg_0

; следовательно, функция Reverse_1 действительно возвращает свое значение

; через аргумент arg_0, переданный по ссылке.

; Напротив, строка на которую указывает arg_4, остается неизменной, поэтому,

; прототип функции Reverse_1 выглядит так:

; void Reverse_1(char *dst, const char *src);

; Никогда не пренебрегайте квалификатором const, т.к. он ясно указывает на

; то, что переменная, на которую указывает данный указатель используется

; лишь на чтение. Эта информация значительно облегчит работу с

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

; некоторое время, основательно подзабыв алгоритм исследуемой программы

pop    ebp

; Закрываем кадр стека

retn

Reverse_1    endp

; int __cdecl Reverse_2(char *)

; А вот на этот раз прототип функции определен верно!

; (Ну, за исключением того, что возвращаемый тип void, а не int)

Reverse_2    proc near           ; CODE XREF: main+4Fp

arg_0        = dword      ptr  8

push   ebp

mov    ebp, esp

; Открываем кадр стека

mov    eax, [ebp+arg_0]

; Загружаем в EAX содержимое аргумента arg_0

push   eax          ; char *

; Передаем arg_0 функции strrev

call   __strrev

add    esp, 4

; Инвертируем строку, записывая результат на то же самое место

; Следовательно, функция Reverse_2 действительно возвращает значение

; через arg_0, и наше предварительное предположение оказалось правильным!

pop    ebp

; Закрываем кадр стека

retn

; Прототип функции Reverse_2 по данным последних исследований выглядит так:

; void Reverse_2(char *s)

Reverse_2    endp

Sum          proc near           ; CODE XREF: main+72p

arg_0        = dword      ptr  8

arg_4        = dword      ptr  0Ch

push   ebp

mov    ebp, esp

; Открываем кадр стека

mov    eax, [ebp+arg_0]

; Загружаем в EAX значение аргумента arg_0



add    eax, [ebp+arg_4]

; Складываем arg_0 с arg_4, записывая результат в EAX

mov    [ebp+arg_0], eax

; Копируем результат сложения arg_0 и arg_4 обратно в arg_0

; Неопытные хакеры могут принять это за возращение значения через аргумент,

; однако, это предположение неверно.

; Дело в том, что аргументы, переданные функции, после ее завершения

; выталкиваются из стека и тут же "погибают". Не забывайте:

; Аргументы, переданные по значению, ведут себя так же, как и локальные

; переменные.

mov    eax, [ebp+arg_0]

; А вот сейчас в регистр EAX действительно копируется возвращаемое значение

; Следовательно, прототип функции выглядит так:

; int Sum(int a, int b);

pop    ebp

; Закрываем кадр стека

retn

Sum          endp

Листинг 102

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

Вот если бы вызываемая функция самостоятельно выделяла для себя память, как раз по потребности, а потом возвращала на нее указатель. Сказано – сделано! Ошибка многих начинающих программистов как раз и заключается в попытке вернуть указать на локальные переменные, - увы, они "умирают" вместе с завершением функции и указатель указывает в "космос". Правильное решение заключается в выделении памяти из кучи (динамической памяти), скажем, вызовом malloc

или new, - эта память "живет" вплоть до ее принудительного освобождения функцией free или delete соответственно.



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

Разберем следующий пример:

#include <stdio.h>

#include <malloc.h>

#include <stdlib.h>

char* MyFunc(int a)

{

char *x;

x = (char *) malloc(100);

_ltoa(a,x,16);

return x;

}

main()

{

char *x;

x=MyFunc(0x666);

printf("0x%s\n",x);

free(x);

}

Листинг 103 Пример, демонстрирующий возвращения значения через кучу

main         proc near           ; CODE XREF: start+AFp

var_4        = dword      ptr -4

push   ebp

mov    ebp, esp

; Открываем кадр стека

push   ecx

; Выделяем память под локальную переменную размером 4 байта (см. var_4)

push   666h

; Передаем функции MyFunc значение 666 типа int

call   MyFunc

add    esp, 4

; Вызываем MyFunc – обратите внимание, что функции ни один аргумент

; не был передан по ссылке!

mov    [ebp+var_4], eax

; Копирование содержимого возращеного функцией значение в переменную var_4

mov    eax, [ebp+var_4]

; Супер! Загружаем в EAX возращенное функцией значение обратно!

push   eax

; Передаем возращенное функцией значение функции printf

; Судя по спецификатору, тип возвращенного значения – char

*

; Поскольку, функции MyFunc ни один из аргументов не передавался по ссылке,

; она явно выделила память самостоятельно и записала туда полученную строку.

; А если бы функции MyFunc передавались один или более аргументов по ссылке?

; Тогда – не было бы никакой уверенности, что она не возвратила один из таких

; аргументов обратно, предварительно его модифицировав.

; Впрочем, модификация необязательно, - скажем передаем функции указатели на

; две строки и она возвращает указатель на ту из них, которая, скажем, короче

; или содержит больше гласных букв.



; Поэтому, не всякое возращение указателя свидетельствует о модификации

push   offset a0xS  ; "0x%s\n"

call   _printf

add    esp, 8

; Вызов printf – вывод на экран строки, возращенной функцией MyFunc

mov    ecx, [ebp+var_4]

; В ECX загружаем значение указателя, возращенного функцией MyFunc

push   ecx          ; void *

; Передаем указатель, возращенный функцией MyFunc, функции free

; Значит, MyFunc действительно самостоятельно выделяла память вызовом malloc

call   _free

add    esp, 4

; Освобождаем память, выделенную MyFunc

для возращения значения

 

mov    esp, ebp

pop    ebp

; Закрываем кадр стека

retn

; Таким образом, протип MyFunc выглядит так:

; char* MyFunc(int a)

main         endp

MyFunc       proc near           ; CODE XREF: main+9p

var_4        = dword      ptr -4

arg_0        = dword      ptr  8

push   ebp

mov    ebp, esp

; Открываем кадр стека

push   ecx

; Резервируем память под локальные переменные

push   64h          ; size_t

call   _malloc

add    esp, 4

; Выделяем 0x64 байта памяти из кучи либо для собственных нужд функции, либо

; для возращения результата. Поскольку из анализа кода вызывающей функции нам

; уже известно, что MyFunc возвращает указатель, очень вероятно, что вызов

; malloc

выделяет память как раз для этой цели.

; Впрочем, вызовов malloc может быть и несколько, а указатель возвращается

; только на один из них

mov    [ebp+var_4], eax

; Запоминаем указатель в локальной переменной var_4

push   10h          ; int

; Передаем функции __ltoa аргумент 0x10 (крайний справа) – требуемая система

; исчисления для перевода числа

mov    eax, [ebp+var_4]

; Загружаем в EAX содержимое указателя на выделенную из кучи память

push   eax          ; char *

; Передаем функции ltoa указатель на буфер для возращения результата

mov    ecx, [ebp+arg_0]

; Загружаем в EAX значение аргумента arg_0



push   ecx          ; __int32

; Передаем функции ltoa аргумент arg_0 – значение типа int

call   __ltoa

add    esp, 0Ch

; Функция ltoa переводит число в строку и записывает ее в буфер по переданному

; указателю

mov    eax, [ebp+var_4]

; Возвращаем указатель на регион памяти, выделенный самой MyFunc

из кучи, и

; содержащий результат работы ltoa

mov    esp, ebp

pop    ebp

; Закрываем кадр стека

retn

MyFunc       endp

Листинг 104

::Возврат значений через глобальные переменные. "Мыльную оперу" перепевов с возращением указателей продолжает серия "Возращение значений через глобальные переменные (и/или указателя на глобальные переменные)". Вообще-то глобальные переменные – плохой тон и такой стиль программирования характерен в основном для программистов с мышлением, необратимо искалеченным идеологий Бацика с его недоразвитым механизмом вызова подпрограмм.

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

Фактически, все глобальные переменные можно рассматривать как неявные аргументы каждой вызываемой функции и в то же время – как возвращаемые значения. Любая функция может произвольным образом читать и модифицировать их, причем, ни "передача", ни "возращение" глобальных переменных не "видны" анализом кода вызывающей функции, - для этого необходимо тщательно исследовать вызываемую – манипулирует ли она с глобальными переменными и если да, то с какими. Можно зайти и с обратной стороны, - просмотром сегмента данных найти все глобальные переменные, определить их смещение и, пройдясь контекстным поиском по всему файлу, выявить функции, которые на них ссылаются (подробнее см. "Идентификация глобальных переменных :: перекрестные ссылки").

Помимо глобальных, еще существуют и статические переменные.


Они так же располагаются в сегменте данных, но непосредственно доступны только объявившей их функции. Точнее, ограничение наложено не на сами переменных, а на их имена. Чтобы предоставить другим функциям доступ к собственным статическим переменным достаточно передать указатель. К счастью, этот трюк не создает хакерам никаких проблем (хоть некоторые злопыхатели и объявляют его "прорехой в защите"), - отсутствие непосредственного доступа к "чужим" статическим переменным и необходимость взаимодействовать с функцией-владелицей через предсказуемый интерфейс (возращенный указатель), позволяет разбить программу на отдельные независимые модули, каждый из которых может быть проанализирован отдельно. Чтобы не быть голословным, продемонстрируем это на следующем примере:

#include <stdio.h>

char* MyFunc(int a)

{

static char x[7][16]={"Понедельник", "Вторник", "Среда", "Четверг", "Пятница",

"Суббота", "Воскресенье"};

return

&x[a-1][0];

}

main()

{

printf("%s\n",MyFunc(6));

}

Листинг 105 Пример, демонстрирующий возврат значения через глобальные статические переменные

Результат компиляции компилятором Microsoft Visual C++ 6.0 c настройками по умолчанию выглядит так:

MyFunc       proc near           ; CODE XREF: main+5p

arg_0        = dword      ptr  8

push   ebp

mov    ebp, esp

; Открываем кадр стека

mov    eax, [ebp+arg_0]

; Загружаем в EAX значение аргумента arg_0

sub    eax, 1

; Уменьшаем EAX на единицу. Это косвенно свидетельствует о том, что arg_0 –

; не указатель, хотя математические операции над указателями в Си разрешены

; и активно используются

shl    eax, 4

; Умножаем (arg_0 –1) на 16. Битовый сдвиг вправо на четыре равносилен 24 == 16

add    eax, offset aPonedelNik    ; "Понедельник"

; Складываем полученное значение с базовым указателем на таблицу строк,



; расположенных в сегменте данных. А в сегменте данных находятся либо

; статические, либо глобальные переменные.

; Поскольку, значение аргумента arg_0 умножаемся на некоторую величину

; (в данном случае на 16), можно предположить, что мы имеем дело с

; двухмерным массивом. В данном случае – массивом строк фиксированной длины.

; Таким образом, в EAX содержится указатель на строку с индексом arg_0 – 1

; Или, другими словами, – с индексом arg_0, считая с одного.

pop    ebp

; Закрываем кадр стека, возвращая в регистре EAX

указатель на соответствующий

; элемент массива.

; Как мы видим, нет никакой принципиальной разницы между возвращением указателя

; на регион памяти, выделенный из кучи, с возращением указателя на статические

; переменные, расположенные в сегменте данных.

retn

MyFunc       endp

main         proc near           ; CODE XREF: start+AFp

push   ebp

mov    ebp, esp

; Открываем кадр стека

push   6

; Передаем функции MyFunc значение типа int

; (шестой день – суббота)

call   MyFunc

add    esp, 4

; Вызываем MyFunc

push   eax

; Передаем возращенное MyFunc значение функции printf

; Судя по строке спецификаторов, это – указатель на строку

push   offset aS    ; "%s\n"

call   _printf

add    esp, 8

pop    ebp

; Закрываем кадр стека

retn

main         endp

aPonedelNik  db 'Понедельник',0,0,0,0,0 ; DATA XREF: MyFunc+Co

; Наличие перекрестной ссылки только на одну функцию, подсказывает, что тип

; этой переменной – static

aVtornik     db 'Вторник',0,0,0,0,0,0,0,0,0

aSreda       db 'Среда',0,0,0,0,0,0,0,0,0,0,0

aCetverg     db 'Четверг',0,0,0,0,0,0,0,0,0

aPqtnica     db 'Пятница',0,0,0,0,0,0,0,0,0

aSubbota     db 'Суббота',0,0,0,0,0,0,0,0,0

aVoskresenE  db 'Воскресенье',0,0,0,0,0

aS           db '%s',0Ah,0           ; DATA XREF: main+Eo

Листинг 106

А теперь сравним предыдущий пример с настоящими глобальными переменными:



#include <stdio.h>

int a;

int b;

int c;

MyFunc()

{

c=a+b;

}

main()

{

a=0x666;

b=0x777;

MyFunc();

printf("%x\n",c);

}

Листинг 107 Пример, демонстрирующий возврат значения через глобальные переменные

main         proc near           ; CODE XREF: start+AFp

push   ebp

mov    ebp, esp

; Открываем кадр стека

call   MyFunc

; Вызываем MyFunc. Обратите внимание – функции явно ничего не передается

; и ничего не возвращается. Потому, ее прототип выглядит

; (по предварительным заключением) так:

; void MyFunc()

call   Sum

; Вызываем функцию Sum, явно не принимающую и не возвращающую никаких значений

; Ее предварительный прототип выглядит так: void Sum()

mov    eax, c

; Загружаем в EAX значение глобальной переменной 'c'

; Смотрим в сегмент данных, - так-так, вот она переменная 'c', равная нулю

; Однако этому значению нельзя доверять – быть может, ее уже успели изменить

; ранее вызванные функции.

; Предположение о модификации подкрепляется парой перекрестных ссылок,

; одна из которых указывает на функцию Sum. Суффикс 'w', завершающий

; перекрестную ссылку, говорит о том, что Sum

записывает в переменную 'c'

; какое-то значение. Какое? Это можно узнать из анализа кода самой Sum.

push   eax

; Передаем значение, возращенное функцией Sum, через глобальную переменную 'c'

; функции printf.

; Судя по строке спецификаторов, аргумент имеет тип int

push   offset asc_406030 ; "%x\n"

call   _printf

add    esp, 8

; Выводим возвращенный Sum результат на терминал

pop    ebp

; Закрываем кадр стека

retn

main          endp

Sum          proc near           ; CODE XREF: main+8p

; Функция Sum не принимает через стек никаких аргументов!

push   ebp

mov    ebp, esp

; Открываем кадр стека

mov    eax, a

; Загружаем в EAX значение глобальной переменной 'a'

; Находим 'a' в сегменте данных, - ага, есть перекрестная ссылка на MyFunc,



; которая что-то записывает в переменную 'a'.

; Поскольку, вызов MyFunc предшествовал вызову Sum, можно сказать, что MyFunc

; возвратила в 'a' некоторое значение

add    eax, b

; Складываем EAX (хранящий значение глобальной переменной 'a') с содержимым

; глобальной переменной 'b'

; (все, сказанное выше относительно 'a', справедливо и для 'b')

mov    c, eax

; Помещаем результат сложения a+b в переменную 'c'

; Как мы уже знаем (из анализа функции main), функция Sum

в переменной 'c'

; возвращает результат своих вычислений. Теперь мы узнали – каких именно.

pop    ebp

; Закрываем кадр стека

retn

Sum          endp

MyFunc       proc near           ; CODE XREF: main+3p

push   ebp

mov    ebp, esp

; Открываем кадр стека

mov    a, 666h

; Присваиваем глобальной переменной 'a' значение 0x666

mov    b, 777h

; Присваиваем глобальной переменной 'b' значение 0x777

; Как мы выяснили из анализа двух предыдущих функций – функция MyFunc

; возвращает в переменных а и b

результат своих вычислений

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

; как три функции взаимодействуют друг с другом.

; main() вызывает MyFunc(), та инициализирует глобальные переменные 'a' и 'b',

; затем main() вызывает Sum(), помещающая сумму 'a' и 'b' в глобальную 'c',

; наконец, main() берет эту 'c' и передает ее через стек printf

; для вывода на экран.

; Уф! Как все запутано, а ведь это простейший пример из трех функций!

; Что же говорить о реальной программе, в которой этих функций тысячи, причем

; порядок вызова и поведение каждой из них далеко не так очевидны!

pop    ebp

retn

MyFunc       endp

a            dd 0                ; DATA XREF: MyFunc+3w    Sum+3r

b            dd 0                ; DATA XREF: MyFunc+Dw    Sum+8r

c            dd 0                ; DATA XREF: Sum+Ew main+Dr

; Судя по перекрестным ссылкам – все три переменные глобальные, т.к. к

; каждой из них имеет непосредственный доступ более одной функции.



Листинг 108

::возврат значений через флаги процессора. Для большинства ассемблерных функций характерно использование регистра флагов процессора для возвращения результата успешности выполнения функции. По общепринятому соглашению установленный флаг переноса (CF) свидетельствует об ошибке, второе место по популярности занимает флаг нуля (ZF), а остальные флаги практически вообще не используются.

Установка флага переноса осуществляется командой STC

или любой математической операцией, приводящей к образованию переноса (например, CMP a, b

где a < b), а сброс – командой CLC или соответствующей математической операцией.

Проверка флага переноса обычно осуществляется условными переходами JC xxx

и JNC xxx, соответственно исполняющихся при наличии и отсутствии переноса. Условные переходы JB xxx и JNB xxx – их синтаксические синонимы, дающие при ассемблировании идентичный код.

#include <stdio.h>

// Функция сообщения об ошибке деления

Err(){ printf("-ERR: DIV by Zero\n");}

// Вывод результата деления на экран

Ok(int a){printf("%x\n",a);}

// Ассемблерная функция деления.

// Делит EAX на EBX, возвращая частное в EAX, а остаток – в EDX

// При попытке деления на ноль устанавливает флаг переноса

__declspec(naked) MyFunc()

{

__asm{

xor edx,edx  ; Обнуляем EDX, т.е. команда div ожидает

делимого в EDX:EAX

test ebx,ebx ; Проверка делителя на равенство нулю

jz

_err             ; Если делитель равен нулю, перейти к ветке _err

div ebx             ; Делим EDX:EAX на EBX (EBX

заведомо не равен нулю)

ret          ; Выход в с возвратом частного в EAX

и остатка в EDX

_err:               ; // Эта ветка получает управление при попытке деления на ноль

stc          ; устанавливаем флаг переноса, сигнализируя об ошибке и...

ret          ; ...выходим

}

}

// Обертка для MyFunc

// Принимаем два аргумента через стек – делимое и делитель

// и выводим результат деления (или сообщение об ошибке) на экран

__declspec(naked) MyFunc_2(int a, int b)

{

__asm{

mov eax,[esp+4]     ; Загружаем в EAX содержимое

аргумента

'a'

mov ebx,[esp+8]     ; Загружаем в EDX содержимое аргумента 'b'

call MyFunc         ; Пытаемся делить a/b

jnc

_ok                    ; Если флаг переноса сброшен выводим результат, иначе…

call Err            ; …сообщение об ошибке

ret                 ; Возвращаемся

_ok:

push eax            ; Передаем результат деления и…

call Ok                    ; …выводим его на экран

add esp,4           ; Вычищаем за собой стек

ret                 ; Возвращаемся

}

}

main(){MyFunc_2(4,0);}

Листинг 109


Содержание раздела