Невозможно отучить людей изучать самые ненужные предметы.
Введение в 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>
// Структура включает три переменные типа double
struct 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↓p
var_38= qword ptr -38h
var_30= qword ptr -30h
var_28= qword ptr -28h
arg_0= qword ptr 8
arg_8= qword ptr 10h
arg_10= qword ptr 18h
arg_18= qword ptr 20h
; Инициализация переменных-аргументов
movsd [rsp+arg_18], xmm3
movsd [rsp+arg_10], xmm2
movsd [rsp+arg_8], xmm1
mov [rsp+arg_0], rcx ; В RCX первый аргумент целочисленный
; Указатель на структуру
; Открываем кадр стека
push rsi
push rdi
sub rsp, 28h
; Заполнение полей структуры
movsd xmm0, [rsp+38h+arg_8]
movsd [rsp+38h+var_38], xmm0
movsd xmm0, [rsp+38h+arg_10]
movsd [rsp+38h+var_30], xmm0
movsd xmm0, [rsp+38h+arg_18]
movsd [rsp+38h+var_28], xmm0
lea rax, [rsp+38h+var_38]
mov rdi, [rsp+38h+arg_0]
mov rsi, rax
mov ecx, 18h
rep movsb ; Побайтно копируем структуру
mov rax, [rsp+38h+arg_0] ; В RAX возвращаем указатель на структуру
; Закрываем кадр стека
add rsp, 28h
pop rdi
pop rsi
retn
sub_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↓p
arg_0= qword ptr 8
arg_8= qword ptr 10h
arg_10= qword ptr 18h
arg_18= qword ptr 20h
; Инициализация переменных-аргументов
movsd [rsp+arg_18], xmm3
movsd [rsp+arg_10], xmm2
movsd [rsp+arg_8], xmm1
mov [rsp+arg_0], rcx
; Модификация полей структуры
mov rax, [rsp+arg_0]
movsd xmm0, [rsp+arg_8]
movsd qword ptr [rax], xmm0
mov rax, [rsp+arg_0]
movsd xmm0, [rsp+arg_10]
movsd qword ptr [rax+8], xmm0
mov rax, [rsp+arg_0]
movsd xmm0, [rsp+arg_18]
movsd qword ptr [rax+10h], xmm0
retn
sub_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 8
arg_8= byte ptr 10h
; Инициализация переменных значениями из регистров, переданных в параметрах
mov [rsp+arg_8], dl
mov [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
, иначе возможно переполнение.
retn
char char_func(char, char) endp
int int_func(int, int) proc near
; Два аргумента размером в 4 байта
arg_0= dword ptr 8
arg_8= dword ptr 10h
; Инициализация переменных значениями из регистров, переданных в параметрах
mov [rsp+arg_8], edx
mov [rsp+arg_0], ecx
; Копирование значений из памяти в 32-битные регистры
mov eax, [rsp+arg_8]
mov ecx, [rsp+arg_0]
; Сложение значений из регистров
add ecx, eax
Копирование в регистр EAX
суммы, которая будет возвращена. По всей вероятности, тип возвращаемого значения — int
.
mov eax, ecx
retn
int int_func(int, int) endp
__int64 int64_func(__int64, __int64) proc near
; Два аргумента размером в четыре слова (8 байт)
arg_0= qword ptr 8
arg_8= qword ptr 10h
; Загружаем в память значения из 64-битных регистров
mov [rsp+arg_8], rdx
mov [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) endp
int * near_func(int *, int *) proc near
; Две переменные размером в четыре слова (8 байт)
var_18= qword ptr -18h
var_10= qword ptr -10h
; Два аргумента размером в четыре слова (8 байт)
arg_0= qword ptr 8
arg_8= qword ptr 10h
; Загружаем в память значения из 64-битных регистров
mov [rsp+arg_8], rdx
mov [rsp+arg_0], rcx
; Открываем кадр стека
sub rsp, 38h
mov ecx, 4; size
; Выделяем 4 байта из кучи
call operator new(unsigned __int64)
; Заносим указатель на выделенную память в переменную var_10
mov [rsp+38h+var_10], rax
mov rax, [rsp+38h+var_10]
mov [rsp+38h+var_18], rax
Следующий витиеватый код используется, чтобы определить смещение внутри массива. Стоп! Но у нас же нет никакого массива. А как же строка c[
? Получается аж три массива, ведь компилятор обрабатывает указатели как массивы и наоборот.
mov eax, 4
imul rax, 0
mov ecx, 4
imul rcx, 0
; Готовим аргументы для сложения
mov rdx, [rsp+38h+arg_0]
mov eax, [rdx+rax]
mov rdx, [rsp+38h+arg_8]
; Суммируем
add eax, [rdx+rcx]
; Снова определяем смещение
mov ecx, 4
imul rcx, 0
mov rdx, [rsp+38h+var_18]
mov [rdx+rcx], eax
; Помещаем в RAX возвращаемое значение
mov rax, [rsp+38h+var_18]
; Закрываем кадр стека
add rsp, 38h
retn
int * near_func(int *, int *) endp
mainproc near
var_38= dword ptr -38h
var_30= qword ptr -30h
var_28= qword ptr -28h
b= dword ptr -20h
a= dword ptr -1Ch
var_18= qword ptr -18h
; __unwind { // __GSHandlerCheck
; Открываем кадр стека
sub rsp, 58h
mov rax, cs:__security_cookie
xor rax, rsp
; Инициализируем переменные
mov [rsp+58h+var_18], rax
; В переменную a типа int заносим значение 0x666
mov [rsp+58h+a], 666h
; В переменную b типа int заносим значение 0x777
mov [rsp+58h+b], 777h
; В 8-битные регистры помещаем параметры типа char перед вызовом функции
mov dl, 2; b
mov cl, 1; a
Вызываем функцию char_func(
. Как мы помним, у нас были сомнения в типе возвращаемого ею значения: либо int
, либо char
.
call char_func(char,char)
Расширяем возвращенное функцией значение до signed
, следовательно, она возвратила signed
.
movsx eax, al
; Сохраняем результат в переменной var_38
mov [rsp+58h+var_38], eax
; На этот раз помещаем параметры в 32-битные регистры, следовательно, их тип int
mov edx, 4; b
mov ecx, 3; a
; Вызываем функцию int_func(3,4), возвращающую значение типа int
call int_func(int,int)
mov ecx, [rsp+58h+var_38]
; Прибавляем результат к значению переменной var_38
add ecx, eax
mov eax, ecx
Преобразуем двойное слово, содержащееся в регистре EAX
, в четверное, помещаемое в регистр RAX
. Это говорит о том, что тип возвращенного функцией значения преобразуется из int
в int64
. Пока непонятно, для чего и зачем.
cdqe
; Копируем расширенное четверное слово в переменную var_30
mov [rsp+58h+var_30], rax
; Готовим 32-битные параметры
mov edx, 6; b
mov ecx, 5; a
Вызываем функцию int64_func(
, возвращающую тип int64
. Теперь становится понятно, чем вызвано расширение предыдущего результата.
call int64_func(__int64,__int64)
mov rcx, [rsp+58h+var_30]
; Прибавляем результат, возвращенный функцией int64_func, к четверному слову
add rcx, rax
mov rax, rcx
; Сохраняем результат предыдущего сложения в переменной var_28
mov [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 символов), так еще некоторые антивирусы считают такую программу малварью.
|
|