Невозможно отучить людей изучать самые ненужные предметы.
Введение в CSS
Преимущества стилей
Добавления стилей
Типы носителей
Базовый синтаксис
Значения стилевых свойств
Селекторы тегов
Классы
CSS3
Надо знать обо всем понемножку, но все о немногом.
Идентификаторы
Контекстные селекторы
Соседние селекторы
Дочерние селекторы
Селекторы атрибутов
Универсальный селектор
Псевдоклассы
Псевдоэлементы
Кто умеет, тот делает. Кто не умеет, тот учит. Кто не умеет учить - становится деканом. (Т. Мартин)
Группирование
Наследование
Каскадирование
Валидация
Идентификаторы и классы
Написание эффективного кода
Вёрстка
Изображения
Текст
Цвет
Линии и рамки
Углы
Списки
Ссылки
Дизайны сайтов
Формы
Таблицы
CSS3
HTML5
Блог для вебмастеров
Новости мира Интернет
Сайтостроение
Ремонт и советы
Все новости
Справочник от А до Я
HTML, CSS, JavaScript
Афоризмы о учёбе
Статьи об афоризмах
Все Афоризмы
Помогли мы вам |
Идентификация аргументов функций — ключевое звено в исследовании дизассемблированных листингов. Поэтому приготовь чай и печеньки, разговор будет долгим. В сегодняшней статье мы рассмотрим список соглашений о передаче параметров, используемых в разных языках программирования и компиляторах. В довесок мы рассмотрим приложение, в котором можно отследить передачу параметров, а также определить их количество и тип. Это может быть весьма нетривиальной задачей, особенно если один из параметров — структура.
Существует три способа передать аргументы функции: через стек, регистры и комбинированный — через стек и регистры одновременно. К этому списку вплотную примыкает и неявная передача аргументов через глобальные переменные.
Сами же аргументы могут передаваться либо по значению, либо по ссылке. В первом случае функции передается копия соответствующей переменной, а во втором — указатель на саму переменную.
Для успешной совместной работы вызывающая функция должна не только знать прототип вызываемой, но и «договориться» с ней о способе передачи аргументов: по ссылке или по значению, через регистры или через стек. Если через регистры — оговорить, какой аргумент в какой регистр помещен, а если через стек — определить порядок занесения аргументов и выбрать «ответственного» за очистку стека от аргументов после завершения вызываемой функции.
Неоднозначность механизма передачи аргументов — одна из причин несовместимости различных компиляторов. Кажется, почему бы не заставить всех производителей компиляторов придерживаться какой‑то одной схемы? Увы, это принесет больше проблем, чем решит.
Каждый механизм имеет свои достоинства и недостатки и, что еще хуже, тесно связан с самим языком. В частности, «сишные» вольности с соблюдением прототипов функций возможны именно потому, что аргументы из стека выталкивает не вызываемая, а вызывающая функция, которая наверняка помнит, что она передавала. Например, функции main
передаются два аргумента — количество ключей командной строки и указатель на содержащий их массив. Однако, если программа не работает с командной строкой (или получает ключ каким‑то иным путем), прототип main
может быть объявлен и так: main(
.
На паскале подобная выходка привела бы либо к ошибке компиляции, либо к краху программы, так как в нем стек очищает непосредственно вызываемая функция. Если она этого не сделает (или сделает неправильно, вытолкнув не то же самое количество машинных слов, которое ей было передано), стек окажется несбалансированным и все рухнет. Точнее, у материнской функции «слетит» вся адресация локальных переменных, а вместо адреса возврата в стеке окажется, что глюк на душу положит.
Минусом «сишного» решения является незначительное увеличение размера генерируемого кода, ведь после каждого вызова функции приходится вставлять машинную команду (и порой не одну) для выталкивания аргументов из стека, а у паскаля эта команда внесена непосредственно в саму функцию и потому встречается в программе один‑единственный раз.
Не найдя золотой середины, разработчики компиляторов решили использовать все доступные механизмы передачи данных, а чтобы справиться с проблемой совместимости, стандартизировали каждый из механизмов, введя ряд соглашений.
this
(в программах, написанных на C++) передается через стек последним по счету аргументом.this
передается через стек последним по счету аргументом.fastcall
, но интерпретируют его по‑разному. Имена функций, следующих соглашению fastcall
, предваряются символом @
, автоматически вставляемым компилятором.this
, большинство компиляторов при вызове по умолчанию передают его через регистр. У Microsoft это RCX
, у Embarcadero — RAX
. Остальные аргументы также могут передаться через регистры, если оптимизатор посчитает, что так будет лучше. Механизм передачи и логика выборки аргументов у всех разная и наперед непредсказуемая, разбирайся по ситуации.Вместе с появлением архитектуры x64 для нее было изобретено только одно новое соглашение вызова, заменившее собой все остальные:
RCX
, RDX
, R8
, R9
;XMM0
— XMM3
;RAX
. Значение с плавающей запятой возвращается в регистре XMM0
.Однако благодаря обратной совместимости с x86 современные процессоры на базе x86_64 также поддерживают все перечисленные способы передачи параметров.
Стоит отметить, что регистры RAX
, RCX
, RDX
, а также R8...
— изменяемые, тогда как RBX
, RBP
, RDI
, RSI
, R12...
— неизменяемые. Что это значит? Это свойство было добавлено в архитектуру x64, оно означает, что значения первых могут быть изменены непосредственно в вызываемой функции, тогда как значения вторых должны быть сохранены в памяти в начале вызываемой функции, а в ее конце, перед возвращением, — восстановлены.
При исследовании функции перед нами стоят следующие задачи: определить, какое соглашение используется для вызова, подсчитать количество аргументов, передаваемых функции (и/или используемых функцией), и, наконец, выяснить тип и назначение самих аргументов. Начнем?
Тип соглашения грубо идентифицируется по способу вычистки стека. Если его очищает вызываемая функция, мы имеем дело c cdecl, в противном случае это либо stdcall, либо PASCAL. Такая неопределенность в отождествлении вызвана тем, что подлинный прототип функции неизвестен и, стало быть, порядок занесения аргументов в стек определить невозможно. Единственная зацепка: зная компилятор и предполагая, что программист использовал тип вызовов по умолчанию, можно уточнить тип вызова функции. Однако в программах под Windows широко используются оба типа вызовов: и PASCAL (он же WINAPI), и stdcall, поэтому неопределенность по‑прежнему остается.
Впрочем, порядок передачи аргументов ничего не меняет: имея в наличии и вызывающую, и вызываемую функции, между передаваемыми и принимаемыми аргументами всегда можно установить взаимную однозначность. Или, проще говоря, если действительный порядок передачи аргументов известен (а он и будет известен, см. вызывающую функцию), то знать очередность расположения аргументов в прототипе функции уже ни к чему.
Другое дело — библиотечные функции, прототип которых известен. Зная порядок занесения аргументов в стек, по прототипу можно автоматически восстановить тип и назначение аргументов!
Как уже было сказано выше, аргументы могут передаваться либо через стек, либо через регистры, либо и через стек, и через регистры сразу, а также неявно через глобальные переменные.
Если бы стек был задействован только для передачи аргументов, подсчитать их количество было бы относительно легко. Увы, стек активно используется и для временного хранения регистров с данными. Поэтому, встретив инструкцию «заталкивания» PUSH
, не торопись идентифицировать ее как аргумент. Узнать количество байтов, переданных функции в качестве аргументов, невозможно, но достаточно легко определить количество байтов, выталкиваемых из стека после завершения функции!
Если функция следует соглашению stdcall (или PASCAL), она наверняка очищает стек командой RET
, где n
и есть искомое значение в байтах. Хуже с cdecl-функциями. В общем случае за их вызовом следует инструкция ADD
, где n
— искомое значение в байтах, но возможны и вариации: отложенная очистка стека или выталкивание аргументов в какой‑нибудь свободный регистр. Впрочем, отложим головоломки оптимизации на потом, а пока ограничимся лишь кругом неоптимизирующих компиляторов.
Логично предположить, что количество занесенных в стек байтов равно количеству выталкиваемых, иначе после завершения функции стек окажется несбалансированным и программа рухнет (о том, что оптимизирующие компиляторы допускают дисбаланс стека на некотором участке, мы помним, но поговорим об этом потом). Отсюда следует: количество аргументов равно количеству переданных байтов, деленному на размер машинного слова. Под машинным словом понимается не только два байта, но и размер операндов по умолчанию, в 32-разрядном режиме машинное слово равно четырем байтам (двойное слово), в 64-разрядном режиме машинное слово — это учетверенное слово (восемь байтов).
Верно ли это? Нет! Далеко не всякий аргумент занимает ровно один элемент стека. Взять тот же тип int, отъедающий только половину. Или символьную строку, переданную не по ссылке, а по непосредственному значению: она «скушает» столько байтов, сколько захочет. К тому же строка может засылаться в стек (как и структура данных, массив, объект) не командой PUSH
, а с помощью MOVS
! Кстати, наличие MOVS
— явное свидетельство передачи аргумента по значению.
Если я успел окончательно тебя запутать, то попробуем разложить по полочкам тот кавардак, что образовался в твоей голове. Итак, анализом кода вызывающей функции установить количество переданных через стек аргументов невозможно. Даже количество переданных байтов определяется весьма неуверенно. С типом передачи полный мрак. Позже мы к этому еще вернемся, а пока вот пример. PUSH
— элемент 0x404040
— что это: аргумент, передаваемый по значению (то есть константа 0x404040
), или указатель на нечто, расположенное по смещению 0x404040
, и тогда, стало быть, передача происходит по ссылке? С ходу определить это невозможно, не правда ли?
Но не волнуйся, нам не пришли кранты — мы еще повоюем! Большую часть проблем решает анализ вызываемой функции. Выяснив, как она манипулирует переданными ей аргументами, мы установим и их тип, и количество! Для этого нам придется познакомиться с адресацией аргументов в стеке, но, прежде чем приступить к работе, рассмотрим в качестве небольшой разминки следующий пример:
#include <string.h>struct XT {
char s0[20];
int x;};void MyFunc(double a, struct XT xt) {
printf("%f,%x,%sn", a, xt.x, &xt.s0[0]);}int main() {
XT xt;
strcpy_s(&xt.s0[0], 13, "Hello,World!");
xt.x = 0x777;
MyFunc(6.66, xt);}
Вывод нашего приложенияРезультат его компиляции компилятором Microsoft Visual C++ с включенной поддержкой платформы x64 и в релизном режиме, но с выключенной оптимизацией (/Od) выглядит так:
main proc near
var_58 = byte ptr -58h
Dst= byte ptr -38h
var_24 = dword ptr -24h
var_20 = qword ptr -20h
; Инициализируем стек
push rsi
push rdi
|
|