Идентификация значения, возвращаемого функцией
…каждый язык - это своя философия, свой взгляд на деятельность программиста, отражение определенной технологии программирования.
Кауфман
Традиционно под "значением, возвращаемым функцией" понимается значение, возращенное оператором 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 |
||
свыше четырех байт |
через неявный аргумент по ссылке |
||
тип |
способ возврата |
|||
однобайтовый |
AL |
AX |
EAX |
|
двухбайтовый |
AX |
EAX |
||
четырехбайтовый |
EAX |
|||
восьми байтовый |
EDX:EAX |
|||
float |
стек сопроцессора |
EAX |
||
double |
стек сопроцессора |
EDX:EAX |
||
near pointer |
EAX |
|||
свыше восьми байт |
через неявный аргумент по ссылке |
|||
Оказывается, если возвращаемое значение не может быть втиснуто в регистры, компилятор скрыто от программиста передает функции неявный аргумент – ссылку на локальную переменную, в которую и записывается возвращенный результат. Таким образом, функции 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 |
В частности, через регистр 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