Невозможно отучить людей изучать самые ненужные предметы.
Введение в CSS
Преимущества стилей
Добавления стилей
Типы носителей
Базовый синтаксис
Значения стилевых свойств
Селекторы тегов
Классы
CSS3
Надо знать обо всем понемножку, но все о немногом.
Идентификаторы
Контекстные селекторы
Соседние селекторы
Дочерние селекторы
Селекторы атрибутов
Универсальный селектор
Псевдоклассы
Псевдоэлементы
Кто умеет, тот делает. Кто не умеет, тот учит. Кто не умеет учить - становится деканом. (Т. Мартин)
Группирование
Наследование
Каскадирование
Валидация
Идентификаторы и классы
Написание эффективного кода
Вёрстка
Изображения
Текст
Цвет
Линии и рамки
Углы
Списки
Ссылки
Дизайны сайтов
Формы
Таблицы
CSS3
HTML5
Блог для вебмастеров
Новости мира Интернет
Сайтостроение
Ремонт и советы
Все новости
Справочник от А до Я
HTML, CSS, JavaScript
Афоризмы о учёбе
Статьи об афоризмах
Все Афоризмы
| Помогли мы вам |
Традиционно под возвращаемым функцией значением понимается то, что передает оператор return. Однако это лишь надводная часть айсберга, не раскрывающая всей картины взаимодействия функций друг с другом. В качестве наглядной демонстрации рассмотрим типичный пример, в котором происходит возвращение значения в аргументе, переданном по ссылке:
int xdiv(int a, int b, int *c=0) { if (!b) return -1; if (c) c[0] = a % b; return a / b;}Функция xdiv возвращает результат целочисленного деления аргумента а на аргумент b, но, помимо этого, она записывает остаток в переменную с, переданную по ссылке. Так сколько же значений вернула функция? И чем возвращение результата по ссылке хуже или «незаконнее» классического return?
Популярные издания склонны упрощать проблему идентификации возвращенного функцией значения, рассматривая один лишь частный случай с оператором return.
Мы же рассмотрим следующие механизмы:
return;Вообще‑то к этому списку не помешало бы добавить возврат значений через дисковые и проецируемые в память файлы, но это выходит за рамки обсуждаемой темы (хотя, рассматривая функцию как «черный ящик» с входом и выходом, нельзя не признать, что вывод функцией результатов своей работы в файл фактически и есть возвращаемое ею значение).
По общепринятому соглашению на платформе x86-64 значение, возвращаемое оператором return, помещается в регистр RAX (в EAX в 32-разрядном режиме). Вещественные типы (float, double) — в регистр XMM0.
А как возвращаются типы, занимающие более 8 байт? Скажем, некая функция возвращает структуру, состоящую из сотен байтов, или объект сопоставимого размера. Ни то ни другое в регистры не запихнешь!
Оказывается, если возвращаемое значение не может быть втиснуто в регистры, компилятор скрыто от программиста передает функции неявный аргумент — ссылку на локальную переменную, в которую и записывается возвращенный результат. Таким образом, функции mystuct и void компилируются в идентичный (или близкий к тому) код, из‑за чего «вытянуть» из машинного кода подлинный прототип невозможно!
Давай проверим наши предположения. Откомпилируй следующий код с отключенной оптимизацией в Visual C++:
#include <iostream>// Структура включает три переменные типа doublestruct mystruct { double d_var1; double d_var2; double d_var3;};mystruct MyFunc1(double a, double b, double c) { mystruct my; my.d_var1 = a; my.d_var2 = b; my.d_var3 = c; return my;}void MyFunc2(struct mystruct* my, double a, double b, double c) { my->d_var1 = a; my->d_var2 = b; my->d_var3 = c;}int main() { mystruct my; my = MyFunc1(1.001, 2.002, 3.003); std::cout << my.d_var1 << " " << my.d_var2 << " " << my.d_var3 << std::endl; MyFunc2(&my, 3.004, 5.005, 6.006); std::cout << my.d_var1 << " " << my.d_var2 << " " << my.d_var3 << std::endl;}Теперь открой экзешник в IDA и откажись от загрузки отладочной информации, ведь при отладке в боевых условиях никакой дебажной инфы не будет.
На следующей иллюстрации я привел обе функции для сравнения в одном окне VS Code.
Хотя по объему функции различаются, выполняемые ими действия схожи. И без отладочной информации понять их различия непросто. Между тем, если приглядеться, можно обнаружить явные намеки, в какой функции происходит работа над ссылочным типом, а где выполняется возврат по значению. Смотри:
sub_140001000 proc near; CODE XREF: main+33↓pvar_38= qword ptr -38hvar_30= qword ptr -30hvar_28= qword ptr -28harg_0= qword ptr 8arg_8= qword ptr 10harg_10= qword ptr 18harg_18= qword ptr 20h; Инициализация переменных-аргументовmovsd [rsp+arg_18], xmm3movsd [rsp+arg_10], xmm2movsd [rsp+arg_8], xmm1mov [rsp+arg_0], rcx ; В RCX первый аргумент целочисленный; Указатель на структуру; Открываем кадр стекаpush rsipush rdisub rsp, 28h; Заполнение полей структурыmovsd xmm0, [rsp+38h+arg_8]movsd [rsp+38h+var_38], xmm0movsd xmm0, [rsp+38h+arg_10]movsd [rsp+38h+var_30], xmm0movsd xmm0, [rsp+38h+arg_18]movsd [rsp+38h+var_28], xmm0lea rax, [rsp+38h+var_38]mov rdi, [rsp+38h+arg_0]mov rsi, raxmov ecx, 18hrep movsb ; Побайтно копируем структуруmov rax, [rsp+38h+arg_0] ; В RAX возвращаем указатель на структуру; Закрываем кадр стекаadd rsp, 28hpop rdipop rsiretnsub_140001000 endpВ начале функции значения копируются из регистров в память. Видно, что первый аргумент целочисленный. Можно предположить, что это указатель на структуру. Остальные аргументы передаются в регистрах XMM*, следовательно, представляют числа с плавающей запятой. После открытия кадра стека значения размещаются в смежных областях памяти, это может указывать, что они принадлежат общему контейнеру (структуре, массиву).
Далее копируется 24 байта (0x18) или 192 бита (0xC0). Если это число разделить на три, получим 64 бита (0x40). Что и требовалось доказать: один элемент double — это 64 бита, а в структуре их три. Предпоследним действием помещаем указатель на структуру в RAX, в который будет возвращен результат. Последним действием закрываем кадр стека. В итоге напрашивается вывод, что здесь происходит возврат значения. Следовательно, прототип функции выглядит так:
mystruct Func(double a, double b, double c)Разберем дизассемблерный листинг второй функции.
sub_140001060 proc near; CODE XREF: main+DD↓parg_0= qword ptr 8arg_8= qword ptr 10harg_10= qword ptr 18harg_18= qword ptr 20h; Инициализация переменных-аргументовmovsd [rsp+arg_18], xmm3movsd [rsp+arg_10], xmm2movsd [rsp+arg_8], xmm1mov [rsp+arg_0], rcx; Модификация полей структурыmov rax, [rsp+arg_0]movsd xmm0, [rsp+arg_8]movsd qword ptr [rax], xmm0mov rax, [rsp+arg_0]movsd xmm0, [rsp+arg_10]movsd qword ptr [rax+8], xmm0mov rax, [rsp+arg_0]movsd xmm0, [rsp+arg_18]movsd qword ptr [rax+10h], xmm0retnsub_140001060 endpНачало такое же, как в прошлый раз. Но в этой функции не открывается кадр стека, что может указывать на работу с существующей структурой. Далее одно за другим выполняется замещение значений полей структуры. В конце функция ничего не возвращает. Отсюда можно вывести такой прототип:
void Func(struct mystruct* my, double a, double b, double c)Нам удалось определить различия функций только после их глубокого анализа. Теперь взглянем на самодеятельность компилятора C++Builder.
В этом случае при беглом взгляде разница совсем незаметна. В обеих функциях присутствует кадр стека. Между тем и здесь есть зацепка: после открытия кадра стека только в MyFunc1 содержимое регистра RCX копируется в RAX. А в первом по логике вещей находится целочисленное значение или указатель. Также в MyFunc1 перед закрытием кадра стека указатель на область памяти RBP+var_20 копируется в регистр RAX явно для возврата результата, следовательно, эта функция имеет такой прототип:
mystruct MyFunc1(double a, double b, double c) Тип возвращаемого значения можно приблизительно определить по размеру регистра, в котором он возвращается. Если возвращаемое значение помещается в EAX, можно предположить, что возвращается int, float или другой четырехбайтовый тип. Не исключен вариант возврата меньшего типа, например char. Также для возврата однобайтовых типов может быть использован регистр AL или AX. В последнем может быть возвращено двухбайтовое значение. Восьмибайтовые типы возвращаются в регистре RAX, при этом никто не запрещает компилятору вернуть в нем значение более мелкого типа, тут уж как повезет. На 64-битной платформе это основное средство возврата значения из функции.
Если функция при выходе явно присваивает одному из перечисленных выше регистров некоторое значение, значит, оно возвращается в вызывающую функцию. Если же эти регистры остаются неопределенными, то, скорее всего, возвращается тип void, то есть ничто. Уточнить информацию помогает анализ вызывающей функции, а точнее, то, как она обращается с регистрами RAX [. Логично предположить: если вызывающая функция не использует значения, оставленного вызываемой функцией в регистрах RAX [, ее тип — void. Но это предположение не всегда верно. Частенько программисты игнорируют возвращаемое значение, вводя исследователей в заблуждение.
Для закрепления изученного рассмотрим следующий пример, демонстрирующий механизм возвращения основных типов значений:
// Демонстрация возвращения переменной типа char оператором returnchar char_func(char a, char b) { return a + b;}// Демонстрация возвращения переменной типа int оператором returnint int_func(int a, int b) { return a + b;}// Демонстрация возвращения переменной типа int64 оператором return__int64 int64_func(__int64 a, __int64 b) { return a + b;}// Демонстрация возвращения указателя на int оператором return// Демонстрация возвращения значения через аргументы, переданные по ссылкеint* near_func(int* a, int* b) { int* c = new int; c[0] = a[0] + b[0]; return c;}int main() { int a; int b; a = 0x666; b = 0x777; printf("%I64xn",
char_func(0x1, 0x2) +
int_func(0x3, 0x4) +
int64_func(0x5, 0x6) +
near_func(&a, &b)[0]);}Результат его компиляции в Microsoft Visual C++ 2019 с отключенной оптимизацией будет выглядеть так:
char char_func(char, char) proc near; Два аргумента размером в байтarg_0= byte ptr 8arg_8= byte ptr 10h; Инициализация переменных значениями из регистров, переданных в параметрахmov [rsp+arg_8], dlmov [rsp+arg_0], clИз памяти оба значения копируются в 32-битные регистры. Вместе с тем они преобразуются из типа char в int.
movsx eax, [rsp+arg_0]movsx ecx, [rsp+arg_8]Сложение значений, находящихся в регистрах. Сумма накапливается в регистре EAX. В нем также выполняется возврат результата в вызвавшую функцию.
add eax, ecxК сожалению, достоверно определить тип возвращаемого значения невозможно. Он с равным успехом может представлять собой int и char, причем int даже более вероятен, так как сумма двух char по соображениям безопасности должна помещаться в int, иначе возможно переполнение.
retnchar char_func(char, char) endpint int_func(int, int) proc near; Два аргумента размером в 4 байтаarg_0= dword ptr 8arg_8= dword ptr 10h; Инициализация переменных значениями из регистров, переданных в параметрахmov [rsp+arg_8], edxmov [rsp+arg_0], ecx; Копирование значений из памяти в 32-битные регистрыmov eax, [rsp+arg_8]mov ecx, [rsp+arg_0]; Сложение значений из регистровadd ecx, eaxКопирование в регистр EAX суммы, которая будет возвращена. По всей вероятности, тип возвращаемого значения — int.
mov eax, ecxretnint int_func(int, int) endp__int64 int64_func(__int64, __int64) proc near; Два аргумента размером в четыре слова (8 байт)arg_0= qword ptr 8arg_8= qword ptr 10h; Загружаем в память значения из 64-битных регистровmov [rsp+arg_8], rdxmov [rsp+arg_0], rcx; Копируем значения из памяти в более удобные регистрыmov rax, [rsp+arg_8]mov rcx, [rsp+arg_0]; Выполняем суммированиеadd rcx, rax; Переносим результат в регистр RAX, который и будет возвращенmov rax, rcxСледовательно, возвращаемый тип имеет размер 64 бита, то есть int64, что и требовалось доказать.
retn__int64 int64_func(__int64, __int64) endpint * near_func(int *, int *) proc near; Две переменные размером в четыре слова (8 байт)var_18= qword ptr -18hvar_10= qword ptr -10h; Два аргумента размером в четыре слова (8 байт)arg_0= qword ptr 8arg_8= qword ptr 10h; Загружаем в память значения из 64-битных регистровmov [rsp+arg_8], rdxmov [rsp+arg_0], rcx; Открываем кадр стекаsub rsp, 38hmov ecx, 4; size; Выделяем 4 байта из кучиcall operator new(unsigned __int64); Заносим указатель на выделенную память в переменную var_10mov [rsp+38h+var_10], raxmov rax, [rsp+38h+var_10]mov [rsp+38h+var_18], raxСледующий витиеватый код используется, чтобы определить смещение внутри массива. Стоп! Но у нас же нет никакого массива. А как же строка c[? Получается аж три массива, ведь компилятор обрабатывает указатели как массивы и наоборот.
mov eax, 4imul rax, 0mov ecx, 4imul rcx, 0; Готовим аргументы для сложенияmov rdx, [rsp+38h+arg_0]mov eax, [rdx+rax]mov rdx, [rsp+38h+arg_8]; Суммируемadd eax, [rdx+rcx]; Снова определяем смещениеmov ecx, 4imul rcx, 0mov rdx, [rsp+38h+var_18]mov [rdx+rcx], eax; Помещаем в RAX возвращаемое значениеmov rax, [rsp+38h+var_18]; Закрываем кадр стекаadd rsp, 38hretn int * near_func(int *, int *) endpmainproc nearvar_38= dword ptr -38hvar_30= qword ptr -30hvar_28= qword ptr -28hb= dword ptr -20ha= dword ptr -1Chvar_18= qword ptr -18h; __unwind { // __GSHandlerCheck; Открываем кадр стекаsub rsp, 58hmov rax, cs:__security_cookiexor rax, rsp; Инициализируем переменныеmov [rsp+58h+var_18], rax; В переменную a типа int заносим значение 0x666mov [rsp+58h+a], 666h; В переменную b типа int заносим значение 0x777mov [rsp+58h+b], 777h; В 8-битные регистры помещаем параметры типа char перед вызовом функцииmov dl, 2; bmov cl, 1; aВызываем функцию char_func(. Как мы помним, у нас были сомнения в типе возвращаемого ею значения: либо int, либо char.
call char_func(char,char)Расширяем возвращенное функцией значение до signed , следовательно, она возвратила signed .
movsx eax, al; Сохраняем результат в переменной var_38mov [rsp+58h+var_38], eax; На этот раз помещаем параметры в 32-битные регистры, следовательно, их тип intmov edx, 4; bmov ecx, 3; a; Вызываем функцию int_func(3,4), возвращающую значение типа intcall int_func(int,int)mov ecx, [rsp+58h+var_38]; Прибавляем результат к значению переменной var_38add ecx, eaxmov eax, ecxПреобразуем двойное слово, содержащееся в регистре EAX, в четверное, помещаемое в регистр RAX. Это говорит о том, что тип возвращенного функцией значения преобразуется из int в int64. Пока непонятно, для чего и зачем.
cdqe ; Копируем расширенное четверное слово в переменную var_30mov [rsp+58h+var_30], rax ; Готовим 32-битные параметрыmov edx, 6; bmov ecx, 5; aВызываем функцию int64_func(, возвращающую тип int64. Теперь становится понятно, чем вызвано расширение предыдущего результата.
call int64_func(__int64,__int64)mov rcx, [rsp+58h+var_30]; Прибавляем результат, возвращенный функцией int64_func, к четверному словуadd rcx, raxmov rax, rcx; Сохраняем результат предыдущего сложения в переменной var_28mov [rsp+58h+var_28], raxДля передачи в качестве параметров загружаем в регистры RDX и RCX указатели на переменные b и a, значения которых определены в начале функции.
lea
rcx, [rsp+58h+a] ; a; Вызываем near_funccall
near_func(int *,int *); Определяем смещениеmov
ecx, 4imul
rcx, 0; Расширяем двойное слово, на которое указывают два регистра [rax+rcx], в четверное словоmovsxd rax, dword ptr [rax+rcx]mov
rcx, [rsp+58h+var_28]; Складываем два четверных словаadd
rcx, raxmov
rax, rcxmov
rdx, rax; Передаем формат строки выводаlea
rcx, _Format
; "%I64xn"; И, наконец, выводим результатcall
printfxor
eax, eaxmov
rcx, [rsp+58h+var_18]xor
rcx, rsp; StackCookiecall
__security_check_cookieadd
rsp, 58hretnmainendpКак мы видим, в идентификации типа значения, возвращенного оператором return, ничего хитрого нет, все прозаично. Но не будем спешить. Рассмотрим следующий пример, демонстрирующий возвращение структуры по значению. Как ты думаешь, что именно и в каких регистрах будет возвращаться?
#include <string.h>struct XT { char s0[4]; int x;};// Функция возвращает значение типа "структура XT" по значениюstruct XT MyFunc(const char* a, int b) { struct XT xt; strcpy_s(&xt.s0[0], 4, a); xt.x = b; return xt;}int main() { struct XT xt; xt = MyFunc("Hello, Sailor!", 0x666); printf("%s %xn", &xt.s0[0], xt.x);}Внимание! Не запускай откомпилированную программу. Мало того что она содержит ошибку (в буфер вместимостью четыре символа помещается строка размером в 14 символов), так еще некоторые антивирусы считают такую программу малварью.
|
|
|

