#1 11-05-2010 17:02

listener
From: Vice City
Registered: 09-11-2006
Posts: 616
Website

Организация и анализ структур в коде

Последнее время, я наблюдаю вопросы в стиле "Как согласуются объекты и ассемблер". Наверное, стоит остановиться на этом подробнее.

1. Несколько формальных определений
(я понимаю, что все это знают, но, точности ради стоит с этого начать)

Переменная - непрерывная область памяти, имеющая тип. Обычно, имеет имя.
Тип переменной - размер и формат данных, которые хранятся в этой переменной.
Массив - последовательный набор переменных одного типа. Размер массива - фиксирован. Используется одно общее имя на весь массив. Отдельные переменные внутри массива идентифицируются по комбинации имени массива и индекса переменной внутри массива.
Структура - последовательный набор переменных произвольного типа. Отдельная переменная внутри структуры называется полем или членом структуры. У каждого поля есть свое имя. Имя всей структуры является именем типа.

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

Пока все понятно? Хорошо, переходим к коду и объектам.
На уровне кода, разницы между объектом и структурой просто нет. В C++, вся разница между структурой и объектом сводится к тому, что поля структуры, по умолчанию, объявлены как public, а поля объекта - как private. На этапе кодогенерации, struct и class полностью равнозначны.

Необходимое пояснение. Говоря "объект", я подразумеваю экземпляр класса. Т.е. класс - это тип объекта (описание порядка и формата полей). Поскольку далее идет рассказ о данных в памяти, а не о их описании, я использую термин "объект".

2. Форматы даннных

Допустим, у нас есть объявления:

int a = 2;
float b = 1.0f;

struct A {
  char c[8];
  int d;
  float f;
};

A g = { "Struct", 6, 0.301f }

В сегменте данных, это будет выглядеть как:

; little-endian (x86)
a: 02 00 00 00 
b: 00 00 80 3F
g: 53 74 72 75 63 74 00 00 06 00 00 00 AC 1C 9A 3E

; big-endian (PPC)
a: 00 00 00 02 
b: 3F 80 00 00 
g: 53 74 72 75 63 74 00 00 00 00 00 06 3E 9A 1C AC

Обычно, компилятор располагает переменные в том же порядке, в котором они были объявлены (но, может и менять этот порядок, если это способствует лучшей оптимизации). Поля структуры всегда располагаются в памяти в том же порядке, в котором они были объявлены.

Архитектура современных процессоров такова, что доступ к переменным, находящимся по адресу кратному некой степени двойки, происходит быстрее. Например, обращение к четырехбайтовой переменной (int, float), расположенной по адресу, кратному 4, в общем случае, вдвое быстрее. А SSE-переменные всегда должны располагаться по адресу, кратному 16.
Процесс размещения переменных определенного типа по кратным адресам, называется выравниванием (alignment).

Применительно к структурам, это приводит к тому, что компилятор может добавлять в структуры не используемые байты, для того, чтобы поля структуры были выровнены на соответствующее количество байт.
В MSVC, выравнивание полей структуры по умолчанию, задается в свойствах проекта или при помощи #pragma pack.

Для упрощения жизни, при описании отреверсенных структур, рекомендуется перед структурой указывать #pragma pack(push, 1) и явно описывать байты выравнивания.

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

3. Доступ к переменным.

В большинстве случаев, в коде обращение к полю структуры хорошо отличается от обращениея к переменной.
Типично, компилятор заносит адрес структуры в один из регистров и обращается к полю, через [регистр+смещение].

Например:

.text:006B18AB 0E8                 call    ?getColModel@CEntity@@QAEPAVCColModel@@@Z ; CEntity::getColModel()
.text:006B18B0 0E8                 mov     eax, [eax+2Ch]

Известно, что CEntity::getColModel возвращает указатель на CColModel.
Т.е., в 006B18B0 мы получаем поле CColModel, по смещению 0x2C.
Упростим себе жизнь: ставим курсор на eax внутри скобок, нажимаем 'T', в открывшемся окне выбираем CColModel...

.text:006B18B0 0E8                 mov     eax, [eax+CColModel.m_pColData]

Аналогично, для PowerPC

; Было: 
.text:821A17A8 070                 sth     %r31, 0x34(%r30)
; Стало:
.text:821A17A8 070                 sth     %r31, grmGeometryQB.m_wVertexCount(%r30)

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

Как описывать неизвестные поля в неизвестных структурах, я расскажу чуть позже.

4. Инкапсуляция, полиморфизм, и как это использовать.

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

У "нормальных" методов (не статических), добавляется еще одно отличие: в них неявно передается еще один параметр - указатель на объект, для которого вызывается этот метод. Как правило, для оптимизации, он передается в регистре (на x86 - в ECX, на PPC - в R3). На этом различия исчерпываются.

Но есть еще одно маленькое "но". Метод можно объявить виртуальным. Опуская обоснования этого процесса (желающие подробностей, отсылаются к книжке Гради Буча или любой другой по основам ООП), определи это так: виртуальный метод, это метод, который вызывается для того, объекта, для которого он описан, а не для того, к которому мы привели тип.

Пример:

CEntity * pPed = new CPed ();	// поскольку CPed унаследован от CEntity, приведение делается без каких-либо проблем
CEntity * pVehicle = new CAutomobile (); // аналогично
pPed->processControl (); 
pVehicle->processControl ();

Если бы processControl не был объявлен виртуальным, для обоих переменных вызвался бы CEntity::processControl.
Но, поскольку это не так, в первом случае вызовется CPed::processControl, а во втором - CAutomobile::processControl.

Чтобы реализовать такое поведение вводится дополнительная сущность, называемая таблицей виртуальных методов (virtual methods table, VMT). Это массив указателей на виртуальные методы объекта.

В объект или структуру, имеющие виртуальные методы, компилятор добавляет первое, неявное поле, в которое заносится указатель на VMT.

Адрес VMT - уникален для класса. Т.е., если по адресу 0x871120 находится класса CAutomobile, то, если по какому-то адресу заносится 0x871120 - э этого адреса гарантированно будет начинаться экземпляр класса CAutomobile.
(нужно учитывать, что VMT может попасть на какой-нибудь "красивый" адрес типа 0x800000, и в этом случае, это может быть просто запись константы. Впрочем, инициализация VMT - достаточно характерный паттерн, и перепутать его с чем-то другим практически нереально)

.text:006B0AD2 0C4                 mov     dword ptr [esi], offset ??_7CAutomobile@@6B@ ; const CAutomobile::`vftable'

Имена классов, как и имена переменных, удаляются во время компиляции, поэтому классы, назначений которых определить "с ходу" не получается, я обычно называл по адресу VMT. (например, CEvent_8710D0).
Но все становится просто шоколадно, если в программе используется оператор dynamic_cast. В этом случае, компилятор оставляет так называемую RTTI (run-time type information). В ней содержится имя класса и диаграмма наследования.
Указатель на RTTI находится непосредственно перед VMT, для разбора RTTI в программах, скомпилированных MSVC,  есть великолепный скрипт ms_rtti, написанный Игорем Скочинским (Igor Skochinsky).

К сожалению, ms_rtti4 очень сильно ориентирован на x86; для PPC приходится использовать предыдущую версию.

5. Жизненный цикл объектов.
В жизни объекта есть три фазы: создание, использование и разрушение.
С использованием все понятно, поэтому остановимся на создании и разрушении.

5.1. Создание объектов
Для этих целей, для объекта создаются конструктор и деструктор. Первый выполняет начальную инициализацию объекта, второй - освобождает занятые объектом ресурсы. Конструкторов может быть несколько, для разных наборов параметров, деструктор всегда один. Более того, даже при множественном наследовании, у объекта один и только один деструктор (и по одному конструктору для каждого набора параметров).

Конструктор у объекта есть не только в том случае, если он явно указан, но и тогда, когда у объекта есть хотя бы один виртуальный метод (в этом случае, он ограничивается инициализацией VMT).

Объект может создаваться статически или динамически.

СTaskSequence pTaskSeq; // статически выделенный объект.
CTaskJetpack * pJPTask = new CTaskJetpack (); // динамически выделенный объект.

Для объектов, выделенных статически - все просто. Компилятор резервирует память на стеке или в сегменте данных, после чего, в нужный момент, вызывает конструктор объекта.
Для глобальных объектов, конструктор вызывается при инициализации программы (если залезать в дебри рантайма, то это делает функция _cinit, вызываемая до main), для объектов на стеке - конструктор вызывается при входе в зону видимости.

Про зону видимости, стоит пояснить подробнее.

bool doSomething () {
	bool bReady = areWeReady ();
	if (bReady)  {
		CAutoLock 	lock;	// в этой точке вызывается конструктор CAutoLock
		bool bDone = reallyDoSomething ();
		if (bDone) 
			return true;	// В этом точке мы не просто выходим из функции, но еще и предварительно вызываем деструктор CAutoLock, поскольку мы выходим из ее зоны видимости
		undoSomething ();
		// Здесь тоже будет вызов деструктора CAutoLock
	}
	return false;
}

Для динамически выделяемых объектов, оператор new разворачивается в достаточно сложную конструкцию.
В начале, для объекта выделяется память, а потом, если это удалось, для выделеннного фрагмента памяти вызывается конструктор.
Если явно определить оператор new для объекта (например, чтобы выделять память не в куче, а в каком-то пуле), то это позволяет заменить только функцию выделения памяти, но не меняет конструкцию в целом.

На примере все того же CAutomobile:

; для начала, вызываем переопределенный оператор new, который выделяет блок в vehicle pool
.text:0043A302 034                 push    988h	; это размер объекта
.text:0043A307 038                 call    ??2CVehicle@@YAXI@Z ; CVehicle::operator new(uint)
.text:0043A30C 038                 add     esp, 4
.text:0043A30F 034                 mov     [esp+34h+var_28], eax
; проверяем, удалось ли выделить память
.text:0043A313 034                 test    eax, eax	; 
.text:0043A315 034                 mov     [esp+34h+var_4], 8 ; это не относится напрямую к делу, это флаг для обработки исключений, если они возникнут в конструкторе
.text:0043A31D 034                 jz      short loc_43A32D ; не удалось? уходим отсюда
; вызываем конструктор.
.text:0043A31F 034                 push    1 ; bHaveEngine
.text:0043A321 038                 push    1 ; eVehicleType
.text:0043A323 03C                 push    edi ; wModelIndex
.text:0043A324 040                 mov     ecx, eax ; this
.text:0043A326 040                 call    ??0CAutomobile@@QAE@III@Z ; CAutomobile::CAutomobile(uint,uint,uint)
.text:0043A32B 034                 jmp     short loc_43A32F ; все. Объект корректно создан
.text:0043A32D
.text:0043A32D     loc_43A32D:                             ; CODE XREF: _spawnCarAtPlayerLocation+EDj
.text:0043A32D 034                 xor     eax, eax ; сюда мы попадаем, если выделить память не удалось. возвращаем NULL
.text:0043A32F
.text:0043A32F     loc_43A32F:                             ; CODE XREF: _spawnCarAtPlayerLocation+FDj

Пример для PPC (то, что оказалось под рукой - создание rage::grcTextureReference):

.text:821812B8 070                 li      %r3, 0x20	; размер объекта
.text:821812BC 070                 bl      _gta_alloc ; непереопределенный оператор new, выделяем память в куче
.text:821812C0 070                 cmplwi  cr6, %r3, 0 ; проверяем, что память удалось выделить
.text:821812C4 070                 beq     cr6, loc_821812D8 ; не удалось? уходим
.text:821812C8 070                 mr      %r4, %r31 ; pszTextureName
.text:821812CC 070                 lwz     %r5, 0(%r30) ; pTexture
; this у нас и так находится в r3, поэтому дополнительных телодвижений делать не надо
.text:821812D0 070                 bl      __0grcTextureReference_rage__QAE_PADPAVgrcTexture_rage___Z # rage::grcTextureReference::grcTextureReference(char *,rage::grcTexture *)
.text:821812D4 070                 b       loc_82181394 ; все, объект создан
.text:821812D8     # ---------------------------------------------------------------------------
.text:821812D8
.text:821812D8     loc_821812D8:                           # CODE XREF: sub_82181280+44j
.text:821812D8 070                 li      %r3, 0 ; выделить память не удалось, возвращаем NULL
.text:821812DC 070                 b       loc_82181394

Конец первой части. Дальше будет про деструкторы (простые, скалярные, векторные...) и про наследование.
Вопросы и уточнения приветствуются.

Offline

#2 11-05-2010 17:44

Alien
Registered: 12-10-2008
Posts: 564

Re: Организация и анализ структур в коде

listener wrote:

На уровне кода, разницы между объектом и структурой просто нет. В C++, вся разница между структурой и массивом сводится к тому, что поля структуры, по умолчанию, объявлены как public, а поля объекта - как private. На этапе кодогенерации, struct и class полностью равнозначны.

Тут наверное опечатка. Должно быть "В C++, вся разница между структурой и объектом сводится к тому..." Еще, насколько я понял, слово "объект" в этом контексте означает "класс"?

listener wrote:

.text:0043A307 038                 call    ??2CVehicle@@YAXI@Z ; CVehicle::operator new(uint)

Давно хотел спросить, как в иде давать имена в C++ style? А-то я все в C-style извращаюсь:

CVehicle__operator_new

Last edited by Alien (11-05-2010 17:45)

Offline

#3 11-05-2010 18:26

listener
From: Vice City
Registered: 09-11-2006
Posts: 616
Website

Re: Организация и анализ структур в коде

Alien wrote:

Тут наверное опечатка. Должно быть "В C++, вся разница между структурой и объектом сводится к тому..." Еще, насколько я понял, слово "объект" в этом контексте означает "класс"?

Да, опечатка. Писалось нон-стопом с редкими отвлечениями на техподдержку.
Вообще, глобально, объект == экземпляр класса. На этом нужно будет остановиться подробнее (заодно рассказать про static members).

Alien wrote:
listener wrote:

.text:0043A307 038                 call    ??2CVehicle@@YAXI@Z ; CVehicle::operator new(uint)

Давно хотел спросить, как в иде давать имена в C++ style? А-то я все в C-style извращаюсь:

CVehicle__operator_new

Вот здесь есть небольшое описание: http://mearie.org/documents/mscmangle/
IDA пробует вызвать UnDecorateName, если не получилось - пробует разобрать как gcc-шное, если и это не получилось - выводит как есть. (gcc-шный разборщик, кстати, гадкий - неймспэйсы не понимает. В IV/PS3 это несколько мешает жить).

Я собирался в конце пройтись по MSVC-шной структуре имен.
Я как-то незаметно насобачился их писать, в IV или MC:LA регулярно придумываются имена под сотню символов длиной.

Offline

#4 12-05-2010 06:06

Alien
Registered: 12-10-2008
Posts: 564

Re: Организация и анализ структур в коде

То есть автоматизирования в преобразовании ::operator new(uint) -> ??2@YAPAXI@Z нет? Приходится эти имена самому составлять?
Посмотрел  в википедии, там написано, что различные компиляторы C++ используют различные способы калечения имен. К примеру, тот же gcc 2.9x преобразует

void h(int) -> h__Fi

А gcc 3.x и 4.x

void h(int) -> _Z1hi

И как тогда осуществляется линковка с dll, скомпилированной чем-то другим? Внешние символы-то разные...
EDIT:
Уже не надо. Все так, как я и думал...

Because C++ symbols are routinely exported from DLL and shared object files, the name mangling scheme is not merely a compiler-internal matter. Different compilers (or different versions of the same compiler, in many cases) produce such binaries under different name decoration schemes, meaning that symbols are frequently unresolved if the compilers used to create the library and the program using it employed different schemes. For example, if a system with multiple C++ compilers installed (e.g. GNU GCC and the OS vendor's compiler) wished to install the Boost library, it would have to be compiled twice — once for the vendor compiler and once for GCC.

It is good for safety purposes that compilers producing incompatible object codes (codes based on different ABIs, regarding e.g. classes and exceptions) use different name mangling schemes. This guarantees that these incompatibilities are detected at the linking phase, not when executing the software.

For this reason name decoration is an important aspect of any C++-related ABI.

Last edited by Alien (12-05-2010 08:59)

Offline

#5 12-05-2010 13:29

listener
From: Vice City
Registered: 09-11-2006
Posts: 616
Website

Re: Организация и анализ структур в коде

5.2 Разрушение объекта.

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

Наличие виртуального деструктора означает, что, у нас есть два способа освобождения ресурсов объекта: либо сделать vmtcall виртуального деструктора (из внешней программы), либо вызвать деструктор по адресу (из сабклассов).
Довершает сумятицу то, что для динамически выделенных объектов нужно не только вызвать деструктор, но и освободить память. Создатели MSVC упростили себе жизнь (и ускорили код) тем, что поделили деструктор на две части.

Одна из этих частей - сам деструктор, в том виде, в котором он был описан в коде. Компилятор добавлет в него вызовы деструкторов статически выделенных членов класса и объекта-предка, и на этом его самодеятельность ограничивается.

Вторая часть, называется deleting destructor. Это полностью сгенерированный компилятором метод, который вызывает деструктор объекта и, если попросили, освобождает память. В VMT хранится указатель именно на deleting destructor

Все тот же CAutomobile

.text:006B1330     ; public: virtual void * __thiscall CAutomobile::`scalar deleting destructor'(unsigned int)
.text:006B1330     ??_GCAutomobile@@UAEPAXI@Z proc near    ; DATA XREF: .rdata:const CAutomobile::`vftable'o
.text:006B1330
.text:006B1330     __flags$        = byte ptr  8
.text:006B1330
.text:006B1330 000                 push    esi
.text:006B1331 004                 mov     esi, ecx
.text:006B1333 004                 call    ??1CAutomobile@@UAE@XZ ; CAutomobile::~CAutomobile(void)
.text:006B1338 004                 test    [esp+__flags$], 1
.text:006B133D 004                 jz      short loc_6B1348
.text:006B133F 004                 push    esi
.text:006B1340 008                 call    ??3CVehicle@@YAXPAX@Z ; CVehicle::operator delete(void *)
.text:006B1345 008                 add     esp, 4
.text:006B1348
.text:006B1348     loc_6B1348:                             ; CODE XREF: CAutomobile::`scalar deleting destructor'(uint)+Dj
.text:006B1348 004                 mov     eax, esi
.text:006B134A 004                 pop     esi
.text:006B134B 000                 retn    4
.text:006B134B     ??_GCAutomobile@@UAEPAXI@Z endp

Пример для PPC (опять же, что было под рукой)

.text:825EF2C8     # public: virtual void * __thiscall rage::swfFILE::`scalar deleting destructor'(unsigned int)
.text:825EF2C8     ___GswfFILE_rage__UAEPAXI_Z:            # DATA XREF: .rdata:const rage::swfFILE::`vftable'o
.text:825EF2C8
.text:825EF2C8     .set var_18, -0x18
.text:825EF2C8     .set var_10, -0x10
.text:825EF2C8     .set var_8, -8
.text:825EF2C8
.text:825EF2C8 000                 mflr    %r12
.text:825EF2CC 000                 stw     %r12, var_8(%sp)
.text:825EF2D0 000                 std     %r30, var_18(%sp)
.text:825EF2D4 000                 std     %r31, var_10(%sp)
.text:825EF2D8 000                 stwu    %sp, -0x70(%sp)
.text:825EF2DC 070                 mr      %r31, %r3       # this
.text:825EF2E0 070                 mr      %r30, %r4       # __flags$
.text:825EF2E4 070                 bl      __1swfFILE_rage__UAE_XZ # rage::swfFILE::~swfFILE(void)
.text:825EF2E8 070                 clrlwi  %r11, %r30, 31  # _r11 = __flags$ & 1;
.text:825EF2EC 070                 mr      %r3, %r31
.text:825EF2F0 070                 cmplwi  cr6, %r11, 0
.text:825EF2F4 070                 beq     cr6, loc_825EF300
.text:825EF2F8 070                 bl      _gta_free
.text:825EF2FC 070                 mr      %r3, %r31
.text:825EF300
.text:825EF300     loc_825EF300:                           # CODE XREF: rage::swfFILE::`scalar deleting destructor'(uint)+2Cj
.text:825EF300 070                 addi    %sp, %sp, 0x70
.text:825EF304 000                 lwz     %r12, var_8(%sp)
.text:825EF308 000                 mtlr    %r12
.text:825EF30C 000                 ld      %r30, var_18(%sp)
.text:825EF310 000                 ld      %r31, var_10(%sp)
.text:825EF314 000                 blr
.text:825EF314     # End of function rage::swfFILE::`scalar deleting destructor'(uint)

deleting destructor-ы бывают двух типов. скалярные и векторные. Первые, предназначены для удаления одиночного объекта. Вторые - для удаления массива объектов (т.е. оператор delete[]). Практически всегда, векторный деструктор, включает в себя и скалярный. Какой из них будет вызван - определяется параметром __flags$ (установлен бит 0 - освобождаем один объект , установлен бит 1 - освобождаем массив объектов, не установлено ни одного бита - просто вызываем деструктор и не освобождаем память).

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

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

5.3. Массивы объектов

Раз я упомянул векторные деструкторы, стоит остановиться подробнее на массивах объектов.
Компилятор, не знает ничего о распределении памяти. С его точки зрения, существуют операторы new и delete, которые обязаны выделить и освободить память. Как они это делают - компилятору совершенно не важно.

Все кардинально меняется, если нужно создать или освободить массив объектов. В этом случае, компилятору нужен размер массива (т.к., получить его из memory manager-а он не может, более того, компилятор даже не подозревает о наличии такового).
В таком случае, при выделении памяти, компилятор резервирует дополнительные 4 или 16 байтов (в зависимости от выравнивания). В них заносится размер массива. (В новых версиях MSVC, вычисление размера блока для new генерируется инлайном и хорошо заметно)

<здесь нужно добавить примеры>

6. Наследование

Инкапсуляцию потрогали, с полиморфизмом разобрались. Настала очередь наследования.

При наследовании структуры, все ее поля копируются в потомка, после чего добавляются поля, определенные в самом потомке. При монжественном наследовании, сначала в потомка вставляются все поля предков, а потом его собственные.

Допустим у нас есть некий класс, унаследованный от двух других:

class A {
public:
	virtual ~A ();
	virtual int ma ();
	virtual void mb ();
	int a;
	int b;
	char c[16];
};

class B {
public:
	virtual ~B ();
	virtual bool mc ();
	int d;
	int e;
};

class C : public A, B {
public: 
	virtual ~C ();
	virtual void ma ();
	virtual void mc ();
	virtual void md ();
	int f;
};

В памяти получается следующая структура:

-- поля первого предка
0000: C::'vftable'{ for 'C'} -- первая VMT объекта становится основной включает в себя виртуальные методы первого предка и потомка
0004: A::a
0008: A::b
000C: A::c
-- поля второго предка
001C: C::'vftable' {for 'B'} -- вторая VMT - включает в себя только методы второго предка
0020: B::d
0024: B::e
-- поля потомка
0038: C: f

Получившиеся VMT организуются так:

-- оригинальные VMT обязаны присутсовать в файле
A::'vftable'
0000: A::'scalar deleting destructor'
0004: A::ma
0008: A::mb

B::'vftable'
0000: B::'scalar deleting destructor'
0004: B::mc

-- а дальше начинается интересное: для С создаются две VMT (если бы предков был бы десяток - то все десять)
C::'vftable'{for 'C'}
0000: C::'scalar deleting destructor'
0004: C::ma -- это переопределенный метод. Он находится по тому же смещению в VMT, что и оригинальный.
0008: A::mb -- а этот метод не переопределен. В VMT вставляется ссылка на оригинальный метод
000C: C: md -- собственный метод объекта вставляется после методов предка

-- вторая VMT
C::'vftable'{for 'B'}
0000: C::`scalar deleting destructor'`adjustor{28}'
0004: C::mc - переопределенная функция.

Во второй VMT можно увидесть странную конструкцию: вместо деструктора используется какой-то непонятный adjustor.
Ранее упоминалось, что у объекта может быть один и только один деструктор, вне зависимости от того, от скольких предков он унаследован. Чтобы разобраться в этом механизме, нужно сначала коснуться приведения типов.

При отсутствии множественного наследования, указатель на объект можно безболезненно приводить к указателю на любого предка. Если мы приведем (обычным C-шным приведением типов) указатель на C к указателю на A, то к полям A можно обращаться безболезненно: A - поля A находятся в C по тем же смещениям. Но, если мы захотим привести указатель на C к указателю на B, нас ожидает большой сюрприз: вместо поля d, по смещению 4 мы получим поле a (а это совсем не то, что хотелось).

Что избежать такого конфуза, для приведения типов в таком случае, нужно использовать оператор dynamic_cast.
Компилятор разворачивает этот оператор в вызов стандартной функции __DynamicCast, которой передаются параметрами RTTI исходного и требуемого объекта. В результате вызова dynamic_cast, будет возвращен указатель не на начало объекта C, а на смещение +1C (что позволит свободно работать с получаенным указателем, как с указателем на объект B).

Что же произойдет при попытке удалить объект по указателю на B ? Нам нужно не только получить адрес деструктора из VMT и вызвать его, но и скорректировать this, чтобы он указывал на начало объекта. Эту операцию и выполняет adjustor.

.text:00A505D0     ; [thunk]:public: virtual void * __thiscall CTaskSimpleMove::`scalar deleting destructor'`adjustor{20}' (unsigned int)
.text:00A505D0     ??_GCTaskSimpleMove@@WBE@AEPAXI@Z proc near
.text:00A505D0    __flags$	= byte ptr  4
.text:00A505D0
.text:00A505D0 000                 sub     ecx, 14h ; корректируем this. 
.text:00A505D3 000                 jmp     ??_GCTaskSimpleMove@@UAEPAXI@Z ; CTaskSimpleMove::`scalar deleting destructor'(uint)
.text:00A505D3     ??_GCTaskSimpleMove@@WBE@AEPAXI@Z endp

Уф-ф-ф. Иссяк. Вроде по теории ничего не забыл. (Разве что, стоит поподробнее рассказать о Microsoft Name Manglig Scheme, если приведенной ссылки не достаточно)
Если будет желание и настроение, попробую рассказать о том, как восстанавливать объекты и структуры по коду, но это уже тема отдельного рассказа.

Offline

#6 29-09-2011 05:39

listener
From: Vice City
Registered: 09-11-2006
Posts: 616
Website

Re: Организация и анализ структур в коде

http://www.hexblog.com/wp-content/uploa … hinsky.pdf
Рекомендуется. Здесь есть еще несколько вещей, с которыми лично я не сталкивался.

Offline

#7 03-01-2012 18:40

BritishColonist
Registered: 30-09-2009
Posts: 72

Re: Организация и анализ структур в коде

Спасибо за труды.
От меня, как всегда, несколько вопросов (однотипных):
1. Как можно вычислить конструктор определённого объекта? У меня есть лишь начало его структуры. Хотелось бы узнать точный размер этой структуры. Я так понял из статьи, что мне поможет конструктор.
2. Функции-члены класса содержатся в структуре экземпляра класса или отдельно (в описании класса)?
3. Класс это тип данных. Значит у него есть определённый размер. Влияет ли размер функций класса на общий размер класса в памяти? Конечно, если вообще есть такое понятие, как размер функций %)

Offline

#8 03-01-2012 21:54

listener
From: Vice City
Registered: 09-11-2006
Posts: 616
Website

Re: Организация и анализ структур в коде

@BritishColonist
1. Здесь поможет не сам конструктор, а место, откуда он вызывается. Если объект выделяется динамически, перед вызовом конструктора вызывается оператор new. Ему передается параметром точный размер объекта. (примеры в 5.1). Если объект выделяется статически, пользы от конструктора ровно столько же, сколько и от любого другого метода - можно установить минимум, меньше которого объект быть не может (по максимально используемому смещению полей).

2. Функции члены класса никак не связаны с данными (и вообще не отличаются от функций, которым указатель на объект этого класса передается первым параметром). 

3. Не влияет, т.к. они никак не связаны с данными.

Offline

#9 03-01-2012 21:56

BritishColonist
Registered: 30-09-2009
Posts: 72

Re: Организация и анализ структур в коде

Тогда в дополнение к п.1: как найти место, откуда вызывается конструктор?
Вообще примерно. По каким ниточкам идти, пользуясь отладчиком?)

Last edited by BritishColonist (03-01-2012 21:57)

Offline

#10 03-01-2012 22:18

Den_spb
From: Ленинград
Registered: 23-11-2008
Posts: 941
Website

Re: Организация и анализ структур в коде

Если есть IDA и idb-база, то для "распутывания ниточек" можно использовать такие приёмы:

Как определить, откуда вызывается данная функция?
1.Открываем код функции в основной вкладке.
2.Далее выбираем в меню пункт View - Open Subviews - Function calls - список Caller.

Как определить, откуда происходит обращение к данному адресу?
1.Вставляем курсор между буквами названия адреса. Например здесь

.data:00B7CB84     _currentTime    dd ?                    ; DATA XREF: _sub_406E50r

вставляем курсор между любыми буквами _currentTime.
2.Жмём X. Открывается список ссылок на данный адрес.

Last edited by Den_spb (03-01-2012 22:24)

Offline

#11 21-01-2012 11:18

BritishColonist
Registered: 30-09-2009
Posts: 72

Re: Организация и анализ структур в коде

Увы, никаких баз у меня нет, и работаю я не с гта.
..и не в IDA : D
Я использую OllyDbg. Кстати, как можно приаттачить Иду к процессу игры, чтобы и процесс не был остановлен, и код был в Иде доступен для просмотра (вот прям как в Olly)?

[edit]
А можно __thiscall описывать как __cdecl и передавать this вручную?

Last edited by BritishColonist (21-01-2012 11:18)

Offline

#12 22-01-2012 18:49

listener
From: Vice City
Registered: 09-11-2006
Posts: 616
Website

Re: Организация и анализ структур в коде

@BritishColonist - Debugger -> Attach to process

__thiscall описывать как __cdecl нельзя. У __cdecl все параметры на стеке, у __thiscall -  this передается в ECX

Offline

#13 08-06-2015 22:04

Seemann
Registered: 07-08-2006
Posts: 2,155

Re: Организация и анализ структур в коде

Запомню ссылку:
Реверс-инжиниринг целочисленного деления на константу

хотелось бы собрать побольше примеров, потому что в листингах такое часто встречается

Last edited by Seemann (10-06-2015 19:08)

Offline

#14 10-06-2015 11:54

VcSaJen
Registered: 25-08-2006
Posts: 217

Re: Организация и анализ структур в коде

Seemann wrote:

Запомню ссылку:
Реверс-инжиниринг целочисленного деления на константу

хотелось бы собрать побольше примеров, потому что в листингах такое часто встречается

Ээ, ссылка битая.


[small][/small]

Offline

#15 10-06-2015 19:08

Seemann
Registered: 07-08-2006
Posts: 2,155

Re: Организация и анализ структур в коде

да, ерунда какая-то вышла, спасибо

Offline

Board footer

Powered by FluxBB