Динамическая память.Основы хакерства

   Принцип  работы  перераспределения и освобождения. Как работают обычные умные указатели в C ++? Как распознать операторов памяти с помощью дизассемблера, не понимающего его истинной природы? Чтобы понять все это, мы должны разобрать механизмы динамического распределения памяти приложения (то есть кучи) двух самых популярных компиляторов по байтам и выявить различия в их работе. Поэтому в статье нас ждут многочисленные листинги дизассемблеров и кода C ++.

 Идентификация указателя this

Указатель this — настоящий золотой ключик или, если хотите, спасательный круг, чтобы не утонуть в бурном океане ООП. Именно благодаря этому вы можете определить принадлежность вызываемой функции к тому или иному классу. Поскольку все невиртуальные функции объекта вызываются напрямую — по фактическому адресу, объект как бы разбивается на составляющие его функции на этапе компиляции. Без этих указателей восстановить иерархию функций было бы принципиально невозможно!

Поэтому очень важно правильно его идентифицировать. Единственная проблема в том, как это определить по указателям на массивы и структуры? В конце концов, экземпляр класса идентифицируется этим указателем (если он выделил память, это экземпляр класса), но то же самое по определению является указателем, относящимся к экземпляру класса. Замкнутый круг! К счастью, есть одна уязвимость … Код, управляющий этим указателем, довольно специфичен, что отличает его от всех других указателей.

Фактически, у каждого компилятора свой почерк, который настоятельно рекомендуется выучить, дизассемблируя свои собственные программы на C ++, но есть универсальные рекомендации, применимые к большинству реализаций. Поскольку это неявный аргумент каждой функции-члена класса, имеет смысл отложить обсуждение его идентификации до раздела «Идентификация аргументов функции». Здесь мы собираемся обсудить, как наиболее популярные компиляторы реализуют передачу этого указателя.

Мы, конечно, говорим здесь об архитектуре x64. На 32-битной платформе параметры, выровненные по 32-битному размеру, передаются через стек. С другой стороны, на 64-битной платформе все интереснее: первые четыре целочисленных аргумента передаются в регистрах RCX, RDX, R8, R9. Если целочисленных аргументов больше, остальные помещаются в стек. Аргументы со значениями с плавающей запятой передаются в регистры XMXMM0, XMM1, XMM2, M3. В этом случае 16-битные аргументы передаются по ссылке. Обратите внимание, что это соглашение о вызовах в операционных системах Microsoft (Microsoft ABI); В Unix-подобных системах все иначе. Но не будем заострять на них внимание.

Оба протестированных мной компилятора, Visual C ++ 2019 и C ++ Builder 10.3, независимо от соглашения о вызове функций (__cdecl, __clrcall, __stdcall, __fastcall, __thiscall) передают указатель this в регистре RCX, что соответствует его природе: это целочисленн ый аргумент.

Идентификация операторов new и delete

Операторы new и delete переводятся компилятором в вызовы библиотечных функций, которые могут распознаваться так же, как и обычные библиотечные функции. В частности, IDA Pro может автоматически распознавать библиотечные функции, снимая эту проблему с плеч исследователя. Однако не у всех есть IDA Pro и далеко не всегда в нужное время под рукой, к тому же она не знает всех функций библиотеки, а из тех, что знает, не всегда распознает new и delete … Словом есть основания идентифицировать их вручную.

Реализация new и delete может быть любой, но большинство компиляторов Windows редко реализуют сами функции кучи. Почему? Доступ к службам операционной системы намного проще. Однако было бы наивно ожидать, что HeapAlloc появится вместо new и HeapFree вместо delete. Нет, компилятор не такой простой! Может ли он отказать себе в удовольствии «вырезать матрешки»? Оператор new транслируется в новую функцию, которая вызывает malloc для выделения памяти, а malloc, в свою очередь, вызывает HeapAlloc (или его аналог, в зависимости от реализации библиотеки памяти), своего рода «оболочку» для одноименной процедуры Win32 API. Образ с общей памятью такой же.

Уг­лублять­ся в деб­ри вло­жен­ных вызовов слиш­ком уто­митель­но. Нель­зя ли new и delete иден­тифици­ровать как‑нибудь ина­че, с мень­шими тру­дозат­ратами и без лиш­ней голов­ной боли? Разуме­ется, мож­но! Давай вспом­ним все, что мы зна­ем о new:

  • new при­нима­ет единс­твен­ный аргу­мент — количес­тво бай­тов выделя­емой памяти, при­чем этот аргу­мент в подав­ляющем боль­шинс­тве слу­чаев вычис­ляет­ся еще на ста­дии ком­пиляции, то есть явля­ется кон­стан­той;
  • ес­ли объ­ект не содер­жит ни дан­ных, ни вир­туаль­ных фун­кций, его раз­мер равен еди­нице (минималь­ный блок памяти, выделя­емый толь­ко для того, что­бы было на что ука­зывать ука­зате­лю this); отсю­да будет очень мно­го вызовов типа
    mov ecx, 1 ; size
    call XXX

где XXX и есть адрес new! Вооб­ще же, типич­ный раз­мер объ­ектов сос­тавля­ет менее сот­ни бай­тов… ищи час­то вызыва­емую фун­кцию с аргу­мен­том‑кон­стан­той мень­ше ста бай­тов;

  • фун­кция new — одна из самых популяр­ных биб­лиотеч­ных фун­кций, ищи фун­кцию с «тол­пой» перек­рес­тных ссы­лок;

  • са­мое харак­терное: new воз­вра­щает ука­затель this, а this очень лег­ко иден­тифици­ровать даже при бег­лом прос­мотре кода (обыч­но он воз­вра­щает­ся в регис­тре RCX);

  • воз­вра­щен­ный new резуль­тат всег­да про­веря­ется на равенс­тво нулю (опе­рато­рами типа test RCX, RCX), и, если он дей­стви­тель­но равен нулю, конс­трук­тор (если он есть) не вызыва­ется.

New имеет более чем достаточно «родинок» для быстрой и надежной идентификации, нет необходимости тратить время на анализ кода для этой функции! Единственное, что следует помнить: new используется не только для создания новых экземпляров объектов, но также для выделения памяти для массивов (структур) и иногда для простых переменных (например, int * x = new int, что вообще безумие, но некоторые делают). К счастью, разницу между ними очень легко отличить — ни массивы, ни структуры, ни простые переменные не имеют указателя this!

Слож­нее иден­тифици­ровать delete. Каких‑либо харак­терных приз­наков эта фун­кция не име­ет. Да, она при­нима­ет единс­твен­ный аргу­мент — ука­затель на осво­бож­даемый реги­он памяти, при­чем в подав­ляющем боль­шинс­тве слу­чаев это ука­затель this. Но помимо нее, this при­нима­ют десят­ки, если не сот­ни дру­гих фун­кций! Рань­ше в эпо­ху 32-бит­ных кам­ней у иссле­дова­теля была удоб­ная зацеп­ка за то, что delete в боль­шинс­тве слу­чаев при­нимал ука­затель this через стек, а осталь­ные фун­кции — через регистр. В нас­тоящее же вре­мя, как мы уже неод­нократ­но убеж­дались, любые фун­кции при­нима­ют парамет­ры через регис­тры:

mov rcx, [rsp+58h+block] ; block
call operator delete(void *,unsigned __int64)

   В дан­ном слу­чае IDA без замеша­тель­ств рас­позна­ла delete.

Кроме того, удаление ничего не возвращает, но сколько функций делают то же самое? Единственная подсказка заключается в том, что вызов удаления следует за вызовом деструктора (если есть), но поскольку конструктор идентифицируется как функция перед удалением, он создает порочный круг!

Осталось только проанализировать содержимое функции: delete рано или поздно вызывает HeapFree (хотя здесь тоже есть варианты: например, Borland / Embarcadero содержит библиотеки, которые работают с кучей на низком уровне и освободите память, вызвав VirtualFree). К счастью, IDA Pro распознает удаление в большинстве случаев, и вам не нужно напрягаться.

А что про­изой­дет, если IDA не рас­позна­ет delete? Код будет выг­лядеть прщзи­мер­но так:

mov rcx, [rsp+58h+block] ; block
call XXX
cmp [rsp+58h+block], 0
jnz short loc_1400010B0
 
Поверхностный анализ показывает: в первой строке регистра RCX, очевидно, для передачи в качестве параметра, помещается блок памяти. Похоже на указатель на сущность. И после вызова XXX этот блок памяти сравнивается с нулем, и если блок не обнулен, адрес перескакивает. Таким простым способом мы можем легко идентифицировать удаление, даже если IDA не определяет его
 

Умные указатели

Visual C++

Сразу после введения интеллектуальных указателей в стандарт C ++ они приобрели популярность среди программистов. Это и понятно: их использование избавило программиста от головной боли при распределении памяти и работе с ней — теперь вам не нужно беспокоиться об ее освобождении.

Ском­пилиру­ем прос­той при­мер кода:

#include <iostream>

#include <memory> // Добавляет поддержку умных указателей
 
class MyClass {
public:
void print()
{
std::cout << "Hello Visual C++" << std::endl;
}
};
 
int main()
{
auto c = std::make_unique<MyClass>();
c->print();
std::cin.get();
}

Толь­ко не забудь отклю­чить опти­миза­цию, что­бы уви­деть деталь­ную работу ком­пилято­ра. Резуль­тат дизас­сем­бли­рова­ния будет такой:

main proc near
_Right = qword ptr -28h
result = std::unique_ptr<MyClass,std::default_delete<MyClass> > ptr -20h
var_18 = std::unique_ptr<MyClass,std::default_delete<MyClass> > ptr -18h
var_10 = qword ptr -10h
sub rsp, 48h
mov rax, cs:__security_cookie
xor rax, rsp
mov [rsp+48h+var_10], rax
mov edx, 8 ; unsigned __int64
lea rcx, [rsp+48h+var_18] ; this
call std::unique_ptr<MyClass,std::default_delete<MyClass>>::__autoclassinit2(unsigned __int64)
lea rcx, [rsp+48h+result] ; result
call std::make_unique<MyClass,,0>(void)
mov [rsp+48h+_Right], rax
mov rdx, [rsp+48h+_Right] ; _Right
lea rcx, [rsp+48h+var_18] ; this
call std::unique_ptr<MyClass,std::default_delete<MyClass>>::unique_ptr<MyClass,std::default_delete<MyClass>>(std::unique_ptr<MyClass,std::default_delete<MyClass>> &&)
nop
lea rcx, [rsp+48h+result] ; this
call std::unique_ptr<MyClass,std::default_delete<MyClass>>::~unique_ptr<MyClass,std::default_delete<MyClass>>(void)
lea rcx, [rsp+48h+var_18] ; this
call std::unique_ptr<MyClass,std::default_delete<MyClass>>::operator->(void)
mov rcx, rax ; this
call MyClass::print(void)
mov rcx, cs:std::basic_istream<char,std::char_traits<char>> std::cin
call cs:std::basic_istream<char,std::char_traits<char>>::get(void)
nop
lea rcx, [rsp+48h+var_18] ; this
call std::unique_ptr<MyClass,std::default_delete<MyClass>>::~unique_ptr<MyClass,std::default_delete<MyClass>>(void)
xor eax, eax
mov rcx, [rsp+48h+var_10]
xor rcx, rsp ; StackCookie
call __security_check_cookie
add rsp, 48h
retn
main endp

Ре­зуль­тат ком­пиляции будет таким:

main proc near

var_38 = qword ptr -38h

var_30 = qword ptr -30h

var_28 = qword ptr -28h

block = qword ptr -20h

var_18 = qword ptr -18h

sub rsp, 58h

mov ecx, 1 ; size

call operator new(unsigned __int64)

mov [rsp+58h+var_30], rax

cmp [rsp+58h+var_30], 0

jz short loc_140001067

mov rax, [rsp+58h+var_30]

mov [rsp+58h+var_28], rax

jmp short loc_140001070

loc_140001067:

mov [rsp+58h+var_28], 0

loc_140001070:

mov rax, [rsp+58h+var_28]

mov [rsp+58h+var_38], rax

mov rcx, [rsp+58h+var_38] ; this

call MyClass::print(void)

mov rax, [rsp+58h+var_38]

mov [rsp+58h+block], rax

mov edx, 1 ; __formal

mov rcx, [rsp+58h+block] ; block

call operator delete(void *,unsigned __int64)

cmp [rsp+58h+block], 0

jnz short loc_1400010B0

mov [rsp+58h+var_18], 0

jmp short loc_1400010C3

loc_1400010B0:

mov [rsp+58h+var_38], 8123h

mov rax, [rsp+58h+var_38]

mov [rsp+58h+var_18], rax

loc_1400010C3:

mov rcx, cs:std::basic_istream<char,std::char_traits<char>> std::cin

call cs:std::basic_istream<char,std::char_traits<char>>::get(void)

xor eax, eax

add rsp, 58h

retn

main endp 

Этот лис­тинг выг­лядит более при­выч­ным и наг­лядным. Меж­ду тем вер­немся к пре­дыду­щему лис­тингу, он для нас более инте­ресен. Из него вид­но, что объ­ект соз­дает­ся в фун­кции std::make_unique<MyClass,,0>(void), имен­но в ней вызыва­ется опе­ратор new:

class std::unique_ptr<class MyClass, struct std::default_delete<class MyClass>> std::make_unique<class MyClass, , 0>(void) proc near

...
mov [rsp+arg_0], rcx
sub rsp, 58h
mov [rsp+58h+var_38], 0
mov ecx, 1 ; size
call operator new(unsigned __int64)
...

Унич­тожа­ет объ­ект фун­кция

std::unique_ptr<MyClass,std::default_delete<MyClass>>::~unique_ptr<MyClass,std::default_delete<MyClass>>(void):

public: std::unique_ptr<class MyClass, struct std::default_delete<class MyClass>>::~unique_ptr<class MyClass, struct std::default_delete<class MyClass>>(void) proc near
arg_0 = qword ptr 8
mov [rsp+arg_0], rcx
sub rsp, 28h
mov rax, [rsp+28h+arg_0]
cmp qword ptr [rax], 0
jz short loc_140001131
mov rax, [rsp+28h+arg_0]
mov rcx, rax ; this
call std::forward<MyClass *>(MyClass * &)
mov rcx, [rsp+28h+arg_0]
mov rdx, [rcx] ; _Ptr
mov rcx, rax ; this
call std::default_delete<MyClass>::operator()(MyClass *)
loc_140001131:
add rsp, 28h
retn

В начале фун­кции выпол­няет­ся про­вер­ка: cmp qword ptr [rax], 0, то есть, если ука­затель на объ­ект нулевой, не име­ет смыс­ла его уда­лять. В ином слу­чае его уда­ляет биб­лиотеч­ная фун­кция std::default_delete, она работа­ет имен­но с уни­каль­ными ука­зате­лями (в стан­дарте, кро­ме них, при­сутс­тву­ют еще shared_ptr), внут­ри нее про­исхо­дит вызов опе­рато­ра delete:

public: void std::default_delete<class MyClass>::operator()(class MyClass *)const proc near
block = qword ptr -18h
var_10 = qword ptr -10h
arg_0 = qword ptr 8
arg_8 = qword ptr 10h
mov [rsp+arg_8], rdx
mov [rsp+arg_0], rcx
sub rsp, 38h
mov rax, [rsp+38h+arg_8]
mov [rsp+38h+block], rax
mov edx, 1 ; __formal
mov rcx, [rsp+38h+block] ; block
call operator delete(void *,unsigned __int64)
...

C++Builder

С Visual C++ разоб­рались, а как обсто­ят дела в ста­не Embarcadero? Смо­жет ли IDA разоб­рать­ся в его наг­ромож­дени­ях кода? Дизас­сем­бли­руем тот же при­мер. Одна­ко в C++Builder он будет иметь нем­ного дру­гой вид:

#include <stdio.h>
#include <iostream>
#include <memory>
 
class MyClass {
public:
void __fastcall print()
{
std::cout << "Hello C++ Builder" << std::endl;
}
};
 
int _tmain(int argc, _TCHAR* argv[])
{
auto c = std::unique_ptr<MyClass>();
c->print();
std::cin.get();
return 0;
}
Пе­ред ком­пиляци­ей про­екта добавь плат­форму Windows 64-bit в выпада­ющем спис­ке Target Platforms, а в спис­ке Build ConfigurationsRelease.
 
Доступные платформы для компиляции проекта
 
                                   Добавление платформы Windows x64
  Еще не забудь отклю­чить опти­миза­цию: Project → Opti
 

                                               Отключение оптимизации

В резуль­тате дизас­сем­блер­ный лис­тинг фун­кции _tmain будет выг­лядеть сле­дующим обра­зом:

main proc near

var_34 = dword ptr -34h
var_30 = qword ptr -30h
var_18 = byte ptr -18h
var_10 = qword ptr -10h
var_8 = dword ptr -8
var_4 = dword ptr -4
arg_2C = dword ptr 34h
arg_30 = qword ptr 38h
arg_38 = byte ptr 40h
sub rsp, 58h
mov [rsp+58h+var_4], 0
mov [rsp+58h+var_8], ecx
mov [rsp+58h+var_10], rdx
lea rdx, [rsp+58h+var_18]
mov rcx, rdx
mov [rsp+58h+var_30], rdx
call std::unique_ptr<MyClass,std::default_delete<MyClass>>::unique_ptr<std::default_delete<MyClass>,void>(void)
mov rcx, [rsp+58h+var_30]
call std::unique_ptr<MyClass,std::default_delete<MyClass>>::operator->(void)
mov rcx, rax
call MyClass::print(void)
jmp short $+2
loc_40137B:
lea rcx, qword_432698
call std::istream::get(void)
mov [rsp+58h+var_34], eax
jmp short $+2
loc_40138D:
lea rcx, [rsp+58h+var_18]
mov [rsp+58h+var_4], 0
call std::unique_ptr<MyClass,std::default_delete<MyClass>>::~unique_ptr()
jmp short loc_4013BA
lea rcx, [rsp+arg_38]
mov r8d, edx
mov [rsp+arg_30], rax
mov [rsp+arg_2C], r8d
call std::unique_ptr<MyClass,std::default_delete<MyClass>>::~unique_ptr()
jmp short loc_4013C3
loc_4013BA:
mov eax, [rsp+58h+var_4]
add rsp, 58h
retn
loc_4013C3:
mov rcx, [rsp+arg_30]
call _Unwind_Resume
ud2
off_4013CF dq offset unk_4290DC
main endp

К нашему удов­летво­рению, и в этом слу­чае IDA успешно рас­позна­ла конс­трук­ции обновлен­ного стан­дарта C++. Во мно­гом этот код похож на дизас­сем­бли­рован­ный лис­тинг, изго­тов­ленный ком­пилято­ром Visual C++. Меж­ду тем есть раз­личие при соз­дании объ­екта. В этом лис­тинге нет вызова make_unique. Поп­робу­ем разоб­рать­ся в тон­костях. Вот так выг­лядит вызов:

call std::unique_ptr<MyClass,std::default_delete<MyClass>>::unique_ptr<std::default_delete<MyClass>,void>(void)

 В резуль­тате мы попада­ем в сле­дующую фун­кцию:
public std::unique_ptr<MyClass, std::default_delete<MyClass>>::unique_ptr<std::default_delete<MyClass>, void>(void)
std::unique_ptr<MyClass, std::default_delete<MyClass>>::unique_ptr<std::default_delete<MyClass>, void>(void) proc near
var_C = dword ptr -0Ch
var_8 = qword ptr -8
sub rsp, 38h
mov [rsp+38h+var_8], rcx
mov rcx, [rsp+38h+var_8]
xor eax, eax
mov edx, eax
call std::_Unique_ptr_base<MyClass,std::default_delete<MyClass>>::_Unique_ptr_base<MyClass*>(MyClass*)
jmp short $+2
loc_401559:
add rsp, 38h
retn
mov ecx, edx
mov [rsp+38h+var_C], ecx
mov rcx, rax
call __clang_call_terminate
jmp short loc_401559
off_40156E dq offset unk_4290FC 

Здесь и соз­дает­ся базовый ука­затель. Обра­ти вни­мание на строч­ку, в которой вызыва­ется конс­трук­тор _Unique_ptr_base:

std::_Unique_ptr_base<MyClass,std::default_delete<MyClass>>::_Unique_ptr_base<MyClass*>(MyClass*)
 
Фух, какой запутан­ный код сге­нери­ровал имбарка­деров­ский ком­пилятор! С Visual C++ было гораз­до понят­нее и наг­ляднее.
  Ка­кие мож­но из это­го сде­лать выводы? Умные ука­зате­ли удоб­ны для прог­раммис­та, но они же вно­сят в дво­ичный код пол­ный хаос, хорошень­ко так парази­тируя на его лаконич­ности и устра­ивая тягучее болото для кодоко­пате­ля. Черт ногу сло­мит! Что будет, если исполь­зуемый дизас­сем­блер не рас­позна­ет умные ука­зате­ли? Мож­но ли собс­твен­ными силами это сде­лать? Очень зат­рудни­тель­но: здесь и клас­сы, и шаб­лоны, и ука­зате­ли, и все это впе­ремеш­ку. Поэто­му дизас­сем­бли­рован­ный лис­тинг будет гро­моз­дким. Обра­ти вни­мание: если в прог­рамме, ском­пилиро­ван­ной в VC++, IDA рас­позна­ла абсо­лют­но все фун­кции, то в про­ге от CB мно­гие фун­кции оста­лись нерас­познан­ными — с абс­трак­тны­ми име­нами sub_***.
 
 
                                             Окно со списком функций

Подходы к реализации кучи

В некото­рых, меж­ду про­чим дос­таточ­но популяр­ных, руководс­твах по прог­рамми­рова­нию на C++ встре­чают­ся при­зывы всег­да выделять память имен­но с исполь­зовани­ем new, а не malloc, пос­коль­ку new опи­рает­ся на эффектив­ные средс­тва управле­ния памятью самой опе­раци­онной сис­темы, а malloc реали­зует собс­твен­ный (и дос­таточ­но тор­мозной) менед­жер кучи. Все это гру­бые натяж­ки! Стан­дарт вооб­ще ничего не говорит о реали­зации кучи, и какая фун­кция ока­жет­ся эффектив­нее, наперед неиз­вес­тно. Все зависит от кон­крет­ных биб­лиотек кон­крет­ного ком­пилято­ра.

   Рас­смот­рим, как устро­ено управле­ние памятью в штат­ных биб­лиоте­ках двух популяр­ных ком­пилято­ров — Microsoft Visual C++ и Embarcadero C++ — на таком незамыс­ловатом при­мере:

#include <iostream>

 
class MyClass1 {
public:
void print()
{
std::cout << "I'm allocated by new" << std::endl;
}
};
 
class MyClass2 {
public:
void print()
{
std::cout << "I'm allocated by malloc" << std::endl;
}
};
 
int main()
{
auto c1 = new MyClass1();
c1->print();
delete c1;
 
MyClass2 *c2 = (MyClass2*)malloc(sizeof(MyClass2));
if (c2 == NULL)
std::cout << "error";
c2->print();
free(c2);
 
std::cin.get();
}

Собс­твен­но, в нем показа­ны оба слу­чая выделе­ния и очис­тки памяти: от язы­ков C и C++.

Microsoft C++

В Visual C++ и new, и malloc при­водят к вызову биб­лиотеч­ной фун­кции __imp_malloc. Раз­ница лишь в том, что malloc сра­зу заменя­ется __imp_malloc, а new выпол­няет допол­нитель­ные опе­рации, но в ито­ге все рав­но вызыва­ет __imp_malloc.

Сто­ит ли из‑за это­го отка­зывать­ся от new в поль­зу сиш­ного malloc? Рань­ше, лет двад­цать назад, воз­можно, это давало пре­иму­щес­тво, но сей­час делать что‑то подоб­ное катего­ричес­ки не надо. И хотя твой код не будет работать быс­трее, но и мед­леннее он не ста­нет. Тем более опе­ратор new дела­ет опе­рацию по выделе­нию памяти безопас­нее. Взгля­нем на его дизас­сем­блер­ный лис­тинг:

void * operator new(unsigned __int64) proc near

push rbx
sub rsp, 20h
mov rbx, rcx
jmp short loc_14000184A
loc_14000183B: ; CODE XREF: operator new(unsigned __int64)+22↓j
mov rcx, rbx
call _callnewh_0
test eax, eax
jz short loc_14000185A
mov rcx, rbx ; Size
loc_14000184A: ; CODE XREF: operator new(unsigned __int64)+9↑j

Сю­да (в тре­тий блок) выпол­няет­ся безус­ловный переход из пер­вого бло­ка кода. Здесь вызыва­ется malloc_0, за которым кро­ется _imp_malloc. Далее про­веря­ется выделен­ная память (напол­нение регис­тра RAX). Если память выделе­на, завер­шаем фун­кцию. В обратном слу­чае, ког­да выделить ничего не уда­лось, пры­гаем во вто­рой блок, где с помощью _callnewh_0, за которой так­же скры­вает­ся _imp_malloc, выделя­ем память в 32-бит­ных усло­виях, так как далее сле­дует про­вер­ка запол­нения регис­тра EAX.

call malloc_0
test rax, rax
jz short loc_14000183B
add rsp, 20h
pop rbx
retn
loc_14000185A: ; CODE XREF: operator new(unsigned __int64)+15↑j

Ес­ли обе попыт­ки выделить память про­вали­лись, управле­ние переда­ется в чет­вертый блок. А здесь все пло­хо: в зависи­мос­ти от сос­тояния регис­тра RBX могут быть выб­рошены раз­ные исклю­чения.

cmp rbx, 0FFFFFFFFFFFFFFFFh

jz short loc_140001866
call __scrt_throw_std_bad_alloc(void)
align 2
loc_140001866: ; CODE XREF: operator new(unsigned __int64)+2E↑j
call __scrt_throw_std_bad_array_new_length(void)
void * operator new(unsigned __int64) endp

Биб­лиотеч­ная фун­кция __imp_malloc язы­ка C/C++ для выделе­ния памяти вызыва­ет HeapAlloc. И на этом пор­тиру­емость закан­чива­ется, пос­коль­ку дан­ная фун­кция при­над­лежит Win32 API. Наконец, HeapAlloc вызыва­ет VirtualAlloc. И толь­ко пос­ле это­го мы получа­ем память для соз­дава­емо­го объ­екта — сколь­ко закулис­ной работы!

Что же у нас с очис­ткой памяти? Оба опе­рато­ра, delete и free, вызыва­ют биб­лиотеч­ную фун­кцию _imp_free. Так же как и в прош­лом слу­чае, опе­ратор free — нап­рямую, delete — кос­венно. Фун­кция _imp_free, в свою оче­редь, вызыва­ет HeapFree, а пос­ледняя — VirtuaFree из Win32 API для осво­бож­дения занятой памяти.

 Embarcadero C++

Сов­сем по‑дру­гому ведет себя Embarcadero C++! Этот зверь в нас­ледс­тво от Borland C++ получил собс­твен­ный менед­жер кучи, осно­ван­ный на «пря­мом» вызове сис­темных фун­кций VirtualAlloc/VirtualFree. В кавыч­ках, потому что вызовы ни фига не пря­мые, а идут в обход биб­лиотеч­ных фун­кций. Ком­пилятор запихи­вает в прог­рамму столь­ко сквоз­ного кода, который, ясное дело, уве­личи­вает и тор­мозит прог­рамму. Взгля­нем на дизас­сем­блер­ный лис­тинг фун­кции _malloc_init, которая сквоз­ным обра­зом вызыва­ется опе­рато­ром new и фун­кци­ей malloc:

_malloc_init proc near

push rsi
sub rsp, 20h
mov rsi, rcx
call __CRTL_MEM_CheckBorMem
test eax, eax
jz short loc_40DE8A
mov rax, cs:off_42C078
jmp short loc_40DEDA
loc_40DE8A: ; CODE XREF: _malloc_init+F↑j
lea rax, _internal_free
mov cs:off_42C070, rax
lea rax, _internal_malloc ; Здесь происходит вызов функции
mov cs:off_42C078, rax ; для выделения памяти
lea rcx, _internal_allocmem
mov cs:off_42C090, rcx
lea rcx, _internal_realloc
mov cs:off_42C080, rcx
lea rcx, _internal_free_heaps
mov cs:off_42C088, rcx
mov cs:dword_42C06C, 1
loc_40DEDA: ; CODE XREF: _malloc_init+18↑j
mov cs:dword_42C068, 1
mov rcx, rsi
add rsp, 20h
pop rsi
jmp rax
_malloc_init endp
 

   Да­лее, если все прош­ло успешно, выпол­нение про­дол­жает­ся в фун­кции _internal_malloc, отку­да вызыва­ется __dlmalloc:

_internal_malloc proc near

test rcx, rcx
jz short loc_41234A
jmp __dlmalloc
...

При этом __dlmalloc пред­став­ляет собой длин­нющую фун­кцию, сре­ди деб­рей которой мож­но отыс­кать строч­ку call VirtualAlloc.

В резуль­тате попада­ем в фун­кцию

VirtualAlloc proc near
jmp cs:__imp_VirtualAlloc
VirtualAlloc endp

   От­куда, как мы видим, выпол­няет­ся переход на биб­лиотеч­ную фун­кцию __imp_VirtualAlloc. Ну и зарос­ли неп­роходи­мого кода нагене­риро­вал C++Builder!

Думается, с уда­лени­ем будет поп­роще. Как бы не так! Опе­рато­ры delete и free в ито­ге при­водят к _free_init, которая по сво­ему обра­зу похожа на _malloc_init и для очис­тки памяти вызыва­ет _internal_free. Уже из нее вызыва­ется __dlfree, которая, как и в слу­чае с выделе­нием памяти, длин­нющая, слов­но «Вой­на и мир». В нед­рах этой фун­кции управле­ние переда­ется VirualFree, которая выг­лядит вот так:

 
VirtualFree proc near
jmp cs:__imp_VirtualFree
VirtualFree endp
 
То есть она вызыва­ет сис­темную фун­кцию очис­тки памяти (под­робнее о пос­ледова­тель­нос­ти работы фун­кций ини­циали­зации и очис­тки памяти можешь пос­мотреть в опи­сании работы Visual C++).

                Итоги

   Судя по тому, что мы видели, на поставленный выше вопрос, что быстрее — new или malloc, можно ответить: большой разницы нет, но новый определенно перевешивает безопасность программирования. И в современных реалиях, несмотря на то, что использование интеллектуальных указателей добавляет избыточный код, их использование в приложениях предпочтительнее с точки зрения безопасности и удобства программиста.
   Как мы выяснили, подходы к выделению и очистке памяти в двух популярных компиляторах сильно различаются. У каждого есть свои плюсы и минусы, поэтому однозначно утверждать, что один лучше другого, было бы неправильно. Однако следует признать, что C ++ Builder генерирует избыточный и цветистый код.
Click to rate this post!
[Total: 0 Average: 0]

Leave a reply:

Your email address will not be published.