Изучение листинга ассемблирования, генерируемого компилятором 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 байта больше фактической памяти, необходимой для хранения массива.