Изучение листинга ассемблирования, генерируемого компилятором C++ - часть 1 - Деструкторы
ОГЛАВЛЕНИЕ
Деструкторы
Следующий пример показывает поведение деструкторов.
class SmartString
{
private:
char* m_sz;
public:
SmartString(char* sz)
{
m_sz = new char[strlen(sz) + 1];
strcpy(m_sz, sz);
}
char* ToStr()
{
return m_sz;
}
_declspec(noinline) ~SmartString()
{
delete[] m_sz;
}
};
int main(int argc, char* argv[])
{
SmartString sz1("Hello World");
printf(sz1.ToStr());
return 0;
}
Сгенерированный код выглядит так.
; 36 : {
push ecx
; 37 : SmartString sz1("Hello World");
push OFFSET FLAT:??_C@_0M@KPLPPDAC@Hello?5World?$AA@
lea ecx, DWORD PTR _sz1$[esp+8]
call ??0SmartString@@QAE@PAD@Z
; SmartString::SmartString
; 38 : printf(sz1.ToStr());
mov eax, DWORD PTR _sz1$[esp+4]
push eax
call _printf
add esp, 4
; 39 :
; 40 : вернуть 0;
lea ecx, DWORD PTR _sz1$[esp+4]
call ??1SmartString@@QAE@XZ
; SmartString::~SmartString
xor eax, eax
; 41 : }
pop ecx
ret 0
_main ENDP
Он весьма простой, и ясно виден вызов деструктора перед завершением функции. Интересно увидеть, как деструкторы работают для массива объектов. Приложение немного изменено.
int main(int argc, char* argv[])
{
SmartString arr[2];
arr[0] = ("Hello World");
arr[1] = ("Hola' World");
printf(arr[0].ToStr());
printf(arr[1].ToStr());
return 0;
}
Теперь последние несколько строк главной функции выглядят так.
push OFFSET FLAT:??1SmartString@@QAE@XZ
; SmartString::~SmartString
push 2
push 4
lea eax, DWORD PTR _arr$[ebp]
push eax
call ??_I@YGXPAXIHP6EX0@Z@Z
xor eax, eax
leave
ret 0
Этот код является вызовом функции ??_I@YGXPAXIHP6EX0@Z@Z, что означает "итератор деструктора вектора" (видно в листинге). Компилятор автоматически генерирует эту функцию. Перевод вышеприведенного ассемблерного кода на C++ выглядит так:
vector_destructor_iterator(arr, 2, 4,
&SmartString::SmartString);
Чем именно является "итератор деструктора вектора"? Если массив объектов покидает область видимости – вызывается деструктор для каждого из объектов в массиве. Как раз это делает итератор деструктора вектора. Ниже код итератора деструктора вектора L изучается и декомпилируется.
PUBLIC ??_I@YGXPAXIHP6EX0@Z@Z
; `итератор деструктора вектора'
; Флаги компиляции функции: /Ogsy
; COMDAT ??_I@YGXPAXIHP6EX0@Z@Z
_TEXT SEGMENT
___t$ = 8
___s$ = 12
___n$ = 16
___f$ = 20
??_I@YGXPAXIHP6EX0@Z@Z PROC NEAR
; `итератор деструктора вектора', COMDAT
push ebp
mov ebp, esp
mov eax, DWORD PTR ___n$[ebp]
mov ecx, DWORD PTR ___s$[ebp]
imul ecx, eax
push edi
mov edi, DWORD PTR ___t$[ebp]
add edi, ecx
dec eax
js SHORT $L912
push esi
lea esi, DWORD PTR [eax+1]
$L911:
sub edi, DWORD PTR ___s$[ebp]
mov ecx, edi
call DWORD PTR ___f$[ebp]
dec esi
jne SHORT $L911
pop esi
$L912:
pop edi
pop ebp
ret 16 ; 00000010H
??_I@YGXPAXIHP6EX0@Z@Z ENDP
; `итератор деструктора вектора'
Из вышесказанного следует, что __t$=8 и т.д. обозначают параметры функции. Уже известен способ, посредством которого функция была вызвана из ассемблерного кода функции _main. Из этого следует такая сигнатура функции:
typedef void (*DestructorPtr)(void* object);
void vector_destructor_iterator(void* _t,
int _n, int _s, DestructorPtr _f)
Реализацию функции можно декомпилировать до следующего
void vector_destructor_iterator(void* _t, int _n, int _s, DestructorPtr _f)
{
unsigned char* ptr = _t + _s*_n;
while(_n--)
{
ptr -= size;
_f(ptr);
}
}
Она вызывает деструктор для каждого объекта в массиве. Теперь можно выяснить, что означают отдельные параметры.
vector_destructor_iterator(arr, 2, 4,
&SmartString::SmartString);
• Первый параметр – указатель на массив.
• Второй параметр – количество элементов в массиве
• Третий параметр – размер отдельного элемента. В данном случае sizeof(SmartString) (= 4).
• Четвертый параметр – адрес функции деструктора.
В данном случае компилятор знал точное количество элементов в массиве. Так что он передает 2 как размер массива итератору деструктора вектора. Спрашивается, что происходит в случае динамических массивов, выделяемых с помощью new. В таком случае компилятор не может выяснить точный размер массива, так как массив выделяется во время выполнения. Для выяснения этого приложение снова меняется.
int main(int argc, char* argv[])
{
SmartString arr = new SmartString[2];
arr[0] = "Hello World";
arr[1] = "Hola' World";
printf(arr[0].ToStr());
printf(arr[1].ToStr());
delete [] arr;
return 0;
}
Сейчас листинг ассемблирования выглядит так:
; 30 :
; 31 : delete [] arr;
push 3
mov ecx, esi
call ??_ESmartString@@QAEPAXI@Z
pop edi
; 32 :
; 33 : return 0;
xor eax, eax
pop esi
Появилась новая функция - ??_ESmartString@@QAEPAXI@Z. Функция называется "деструктор удаления вектора". Ассемблерный код для деструктора удаления вектора:
PUBLIC ??_ESmartString@@QAEPAXI@Z
; SmartString::`деструктор удаления вектора'
; Флаги компиляции функции: /Ogsy
; COMDAT ??_ESmartString@@QAEPAXI@Z
_TEXT SEGMENT
___flags$ = 8
??_ESmartString@@QAEPAXI@Z PROC NEAR
; SmartString::`деструктор удаления вектора', COMDAT
; _this$ = ecx
push ebx
mov bl, BYTE PTR ___flags$[esp]
test bl, 2
push esi
mov esi, ecx
je SHORT $L896
push edi
push OFFSET FLAT:??1SmartString@@QAE@XZ
; SmartString::~SmartString
lea edi, DWORD PTR [esi-4]
push DWORD PTR [edi]
push 4
push esi
call ??_I@YGXPAXIHP6EX0@Z@Z
test bl, 1
je SHORT $L897
push edi
call ??3@YAXPAX@Z ; оператор удаления
pop ecx
$L897:
mov eax, edi
pop edi
jmp SHORT $L895
$L896:
mov ecx, esi
call ??1SmartString@@QAE@XZ
; SmartString::~SmartString
test bl, 1
je SHORT $L899
push esi
call ??3@YAXPAX@Z ; оператор удаления
pop ecx
$L899:
mov eax, esi
$L895:
pop esi
pop ebx
ret 4
??_ESmartString@@QAEPAXI@Z ENDP
; SmartString::`деструктор удаления вектора'
_TEXT ENDS
Эта функция имеет соглашение о вызовах __thiscall, означающее, что первый параметр this помещается в ECX. Это псевдосоглашение о вызовах используется для вызова функций-членов. Псевдокод C++ для этого таков:-
void SmartString::vector_deleting_destructor(int flags)
{
if (flags & 2)
{
int numElems = *((unsigned char*)this - 4);
vector_destructor_iterator(this, numElems,
4, &SmartString::SmartString);
}
else
{
this->~SmartString();
}
if (flags & 1)
delete ((unsigned char*)this - 4);
}
Видно, что количество элементов хранится непосредственно перед первым элементом массива. Это значит, что оператор new[] должен выделить лишние 4 байта. Изучение ассемблера, сгенерированного для вызова new[], подтверждает это.
; 23 : SmartString* arr = new SmartString[2];
push 12 ; 0000000cH
call ??2@YAPAXI@Z ; оператор new
Оператор new принимает размер выделяемого параметра. Видно, что число 12 помещается в стек. Размер SmartString - всего 4 байта, а суммарный размер двух элементов - 8 байтов. Оператор new действительно выделяет лишние 4 байта, чтобы запомнить количество элементов в массиве. Для дополнительного подтверждения этого перегружается оператор new[] в SmartString. Видно, что количество запрашиваемой памяти всегда на 4 байта больше фактической памяти, необходимой для хранения массива.