Откуда берётся память программы и как код одной программы попадает в другую
VirtualAlloc и VirtualFreenew/delete, не допуская утечекLoadLibraryA и указатель на функциюnewdelete.cpp — она переносимая.Адреса, которые программа видит (раздел 9), — виртуальные. У каждого процесса своё адресное пространство: в 64-разрядной Windows программе доступны 128 ТБ адресов — во много раз больше, чем оперативной памяти в компьютере. Процессор с помощью таблиц, которые ведёт ОС, переводит каждый виртуальный адрес в физический — адрес ячейки в микросхеме памяти.
Переводится не каждый байт отдельно, а страницами — кусками по 4 КБ. Страница виртуальной памяти может быть связана со страницей оперативной памяти, лежать в файле подкачки на диске (тогда при обращении система загрузит её обратно — это медленно) или не быть связанной ни с чем.
// Фрагмент meminfo.cpp. Полностью — в примерах раздела
SYSTEM_INFO system;
GetSystemInfo(&system);
std::cout << "Page size: " << system.dwPageSize << " bytes\n";
std::cout << "Allocation granularity: " << system.dwAllocationGranularity << " bytes\n";
std::cout << "Application addresses: " << system.lpMinimumApplicationAddress
<< " .. " << system.lpMaximumApplicationAddress << '\n';
MEMORYSTATUSEX memory;
memory.dwLength = sizeof(memory); // размер структуры — до вызова, как в разделе 14
if (GlobalMemoryStatusEx(&memory)) {
std::cout << "Physical memory: " << memory.ullTotalPhys / MB << " MB, free "
<< memory.ullAvailPhys / MB << " MB (" << memory.dwMemoryLoad << "% used)\n";
}
Page size: 4096 bytes
Allocation granularity: 65536 bytes
Application addresses: 0x10000 .. 0x7ffffffeffff
Processors: 8
Physical memory: 16254 MB, free 7891 MB (51% used)
Virtual address space: 134217727 MB, free 134213210 MB
Что это даёт: изоляцию — процесс не может обратиться к чужой памяти, у него просто нет таких адресов; больше памяти, чем есть — редко используемые страницы уходят на диск; защиту — у каждой страницы свои права (пункт 02). Числа памяти у вас будут свои. «Гранулярность выделения» 64 КБ — области адресов резервируются кратно ей.
| Состояние | Смысл | Обращение |
|---|---|---|
свободна MEM_FREE | адреса никем не заняты | нарушение доступа |
зарезервирована MEM_RESERVE | диапазон адресов занят за программой, но памяти за ним нет | нарушение доступа |
передана MEM_COMMIT | за страницей закреплена память (в ОЗУ или в файле подкачки) | по правам защиты |
| Защита | Что можно | Где встречается |
|---|---|---|
PAGE_READWRITE | читать и писать | данные, стек, кучи |
PAGE_READONLY | только читать | константы программы, строковые литералы |
PAGE_EXECUTE_READ | выполнять и читать | код программы и DLL |
PAGE_NOACCESS | ничего | зарезервированная область |
Нарушение прав — запись в страницу только для чтения, выполнение данных, обращение к свободной странице — процессор останавливает, а Windows завершает программу с ошибкой Access violation (код 0xC0000005). Это та самая аварийная остановка из разделов 8 и 9 при выходе за границы и разыменовании nullptr: адрес 0 и вокруг него всегда свободны именно для того, чтобы ошибка с nullptr проявилась сразу. Защита от исполнения данных (DEP) мешает атакам через переполнение буфера: даже если злоумышленник записал код в массив, выполнить его нельзя.
VirtualAlloc и VirtualFreeВыделение памяти в два шага: зарезервировать диапазон адресов и передать память тем страницам, которые действительно нужны. Так можно занять непрерывную область под большую таблицу, а память брать по мере заполнения:
// Фрагмент vmem.cpp. Полностью — в примерах раздела
char* base = static_cast<char*>(VirtualAlloc(nullptr, RESERVE_SIZE, MEM_RESERVE, PAGE_NOACCESS));
if (base == nullptr) {
std::cout << "VirtualAlloc(reserve) failed, error " << GetLastError() << '\n';
return 2;
}
printRegion("reserved", base);
// передать память двум страницам в середине области — только они получают физическую память
char* pages = base + 512 * page;
if (VirtualAlloc(pages, 2 * page, MEM_COMMIT, PAGE_READWRITE) == nullptr) {
std::cout << "VirtualAlloc(commit) failed, error " << GetLastError() << '\n';
return 2;
}
printRegion("committed", pages);
pages[0] = 'A'; // писать можно: страница передана и доступна для записи
pages[page + 10] = 'B'; // вторая страница
DWORD oldProtect = 0;
VirtualProtect(pages, page, PAGE_READONLY, &oldProtect); // первая страница — только чтение
printRegion("protected", pages);
VirtualFree(pages, 2 * page, MEM_DECOMMIT); // вернуть физическую память, адреса остаются занятыми
printRegion("decommitted", pages);
VirtualFree(base, 0, MEM_RELEASE); // освободить всю область; размер — 0
printRegion("released", base);
reserved: 0x1e6e2f60000 reserved, -, region 1048576 KB
committed: 0x1e6e3160000 committed, read-write, region 8 KB
written: AB, new page reads as 0
protected: 0x1e6e3160000 committed, read-only, region 4 KB
next page: 0x1e6e3161000 committed, read-write, region 4 KB
decommitted: 0x1e6e3160000 reserved, -, region 1046528 KB
released: 0x1e6e2f60000 free, -, region …
VirtualQuery(адрес, &info, sizeof(info)) сообщает о регионе — подряд идущих страницах с одинаковым состоянием и защитой: State, Protect, RegionSize. Резервирование 1 ГБ не стоит памяти — только адресов.MEM_DECOMMIT возвращает память, адреса остаются зарезервированными. MEM_RELEASE освобождает всю область — адрес должен быть тем, что вернул резервирующий VirtualAlloc, а размер — 0.VirtualAlloc работает страницами: на 10 байт он отдаст целую страницу 4 КБ. Для мелких объектов он расточителен — для них кучи.Куча — менеджер мелких блоков: она берёт у VirtualAlloc страницы и нарезает из них блоки любого размера, помня, какие заняты. У каждого процесса есть куча по умолчанию; можно создать и свою:
// Кучи: куча процесса и своя куча; блоки произвольного размера поверх страниц виртуальной памяти
#include <windows.h>
#include <iostream>
int main()
{
HANDLE processHeap = GetProcessHeap(); // есть у каждого процесса; закрывать не нужно
int* numbers = static_cast<int*>(HeapAlloc(processHeap, HEAP_ZERO_MEMORY, 10 * sizeof(int)));
if (numbers == nullptr) {
std::cout << "HeapAlloc failed\n";
return 2;
}
numbers[9] = 99;
std::cout << "process heap block: " << HeapSize(processHeap, 0, numbers) << " bytes, numbers[0] = "
<< numbers[0] << ", numbers[9] = " << numbers[9] << '\n';
HeapFree(processHeap, 0, numbers);
HANDLE heap = HeapCreate(0, 0, 0); // своя куча, растёт по мере надобности
if (heap == nullptr) {
std::cout << "HeapCreate failed, error " << GetLastError() << '\n';
return 2;
}
constexpr int BLOCKS = 1000;
char* blocks[BLOCKS];
for (int i = 0; i < BLOCKS; ++i) {
blocks[i] = static_cast<char*>(HeapAlloc(heap, 0, 100));
}
std::cout << "1000 blocks of 100 bytes: first " << static_cast<void*>(blocks[0])
<< ", second " << static_cast<void*>(blocks[1]) << '\n';
HeapDestroy(heap); // все блоки кучи освобождаются разом
return 0;
}
process heap block: 40 bytes, numbers[0] = 0, numbers[9] = 99
1000 blocks of 100 bytes: first 0x2a3b1f40860, second 0x2a3b1f408d0
Соседние блоки отстоят на 112 байт, а не на 100: у кучи есть служебные данные для каждого блока. Своя куча удобна, когда много объектов живут и умирают вместе: HeapDestroy освобождает всё сразу, и утечек отдельных блоков не бывает. (void*) при выводе нужен, чтобы std::cout показал адрес, а не строку (раздел 10, пункт 05).
new и deleteВ C++ память в куче выделяет оператор new: библиотека C++ берёт её у кучи процесса. Это решает давнюю проблему раздела 8 — массив, размер которого известен только во время работы:
// new и delete: массив, размер которого известен только при выполнении
#include <iostream>
int main()
{
int n;
std::cout << "How many numbers: ";
std::cin >> n;
if (std::cin.fail() || n < 1 || n > 100000000) {
std::cout << "Error: n must be 1..100000000\n";
return 1;
}
long long* squares = new long long[n]; // n · 8 байт в куче — не в стеке
for (int i = 0; i < n; ++i) {
squares[i] = static_cast<long long>(i) * i;
}
long long sum = 0;
for (int i = 0; i < n; ++i) {
sum += squares[i];
}
std::cout << "sum of squares 0.." << n - 1 << " = " << sum << '\n';
delete[] squares; // массив — delete[], одиночный объект — delete
squares = nullptr; // чтобы по старому адресу больше не обратиться
int* one = new int(42);
std::cout << "one = " << *one << '\n';
delete one;
return 0;
}
How many numbers: 5
sum of squares 0..4 = 30
one = 42
| Ошибка | Что происходит |
|---|---|
утечка: нет delete | блок занят до конца процесса, адрес потерян; в долго работающей программе память растёт |
висячий указатель: обращение после delete | неопределённое поведение: блок уже отдан другим |
двойной delete | порча кучи, аварийное завершение, иногда позже и в другом месте |
delete вместо delete[] | неопределённое поведение; g++ предупреждает -Wmismatched-new-delete |
В стеке (раздел 7) память освобождается сама — при выходе из блока; в куче — только явно. Поэтому в современном C++ голые new/delete пишут редко: динамический массив — std::vector<long long> squares(n);, он освобождает память в своём деструкторе (раздел 12, пункт 02). В курсе new нужен, чтобы понимать, что происходит под std::vector, и для данных потока, которые переживают функцию, их создавшую (практическая работа №18, задание 3). Виртуальный деструктор (раздел 12, пункт 09) нужен именно тогда, когда объект удаляют delete через указатель на базовый класс.
До сих пор программа была одним файлом .cpp. Функции, которые нужны нескольким программам, выносят в отдельный файл .cpp, а их прототипы — в заголовочный файл .h. Программа подключает заголовок директивой #include "имя.h" (кавычки — искать рядом с исходником, угловые скобки — в каталогах компилятора) и получает прототипы — как те, что вы писали перед main.
| Файл | Что в нём |
|---|---|
mathlib.h | #pragma once (подключать один раз, даже если #include встретился дважды), константы, структуры, прототипы |
mathlib.cpp | #include "mathlib.h" и определения функций |
main.cpp | #include "mathlib.h" и main, который вызывает функции |
Сборка: g++ -std=c++17 -Wall -Wextra main.cpp mathlib.cpp -o app.exe — компилятор переводит каждый .cpp отдельно, а компоновщик соединяет их. Если функция объявлена, но нигде не определена, ошибку выдаст компоновщик: undefined reference to 'gcd' (раздел 7). В заголовке — только объявления: определение функции в .h, подключённом в два .cpp, даст multiple definition.
Динамическая библиотека (DLL, Dynamic-Link Library) — файл с кодом, который загружается в процесс во время работы. Все функции Windows API живут в DLL: CreateProcessA — в kernel32.dll, WriteConsoleOutputA — там же, функции сокетов — в ws2_32.dll. Зачем DLL: один код для многих программ (одна копия на диске и в памяти), обновление без пересборки программ, модули, которые программа загружает по желанию (плагины).
// Функции библиотеки mathlib.dll: объявления для программ, которые её используют
#pragma once
#ifdef MATHLIB_BUILD
#define MATHLIB_API extern "C" __declspec(dllexport) // при сборке DLL — экспорт
#else
#define MATHLIB_API extern "C" __declspec(dllimport) // при сборке программы — импорт
#endif
MATHLIB_API int gcd(int a, int b);
MATHLIB_API double average(const int a[], int n);
MATHLIB_API int callCount();
__declspec(dllexport) — функция попадает в таблицу экспорта DLL: её видят другие модули. Остальные функции DLL — внутренние.extern "C" — имя в таблице экспорта будет простым: gcd. Без него компилятор C++ искажает имя, записывая в него типы параметров: _Z3gcdii — так он различает перегруженные функции (раздел 7). Искажение у разных компиляторов разное, и найти функцию по имени gcd будет нельзя.-DMATHLIB_BUILD, и MATHLIB_API превращается в экспорт; в программе — в импорт.g++ -std=c++17 -Wall -Wextra -DMATHLIB_BUILD -shared mathlib.cpp -o mathlib.dll -Wl,--out-implib,libmathlib.a
-shared — собрать DLL, а не программу. --out-implib — заодно создать библиотеку импорта libmathlib.a: маленький файл со списком экспортируемых функций, нужный для неявного связывания. Файл mathlib.cpp — в примерах раздела; данные DLL (счётчик вызовов) у каждого процесса, загрузившего её, свои: код DLL общий, а данные копируются.
DllMain, поиск DLL// Неявное связывание: DLL загружается при запуске программы. Сборка: g++ implicit.cpp -L. -lmathlib -o implicit.exe
#include <iostream>
#include "mathlib.h"
int main()
{
std::cout << "main started\n";
const int a[] = {3, 8, 4};
std::cout << "gcd(84, 36) = " << gcd(84, 36) << '\n';
std::cout << "average = " << average(a, 3) << '\n';
std::cout << "calls: " << callCount() << '\n';
return 0;
}
[mathlib loaded]
main started
gcd(84, 36) = 12
average = 5
calls: 2
[mathlib unloaded]
При неявном связывании программа вызывает функции DLL, как свои; -lmathlib добавляет в неё список нужных DLL. Система загружает эти DLL до main — поэтому «loaded» выводится раньше «main started». Нет DLL — программа не запускается вовсе, с сообщением системы.
DllMain — необязательная функция DLL, которую система вызывает с причиной: DLL_PROCESS_ATTACH — загрузка в процесс, DLL_PROCESS_DETACH — выгрузка, DLL_THREAD_ATTACH/DETACH — создание и завершение потоков. В ней делают только самое простое: система в это время держит блокировку загрузчика, и загрузка других DLL, ожидание потоков, даже std::cout могут привести к зависанию. Поэтому mathlib пишет сообщение напрямую функцией WriteFile.
Где система ищет DLL, заданную только именем (безопасный порядок по умолчанию):
C:\Windows\System32;PATH.Program Files), а DLL, которые загружают явно, указывают полным путём. Для программ MinGW-w64 есть и практическая ловушка: им нужны DLL самого компилятора (libstdc++-6.dll и другие) — вне консоли MSYS2 программа может не найти их. Ключ -static включает библиотеки компилятора в сам .exe.Программа может загрузить DLL сама, когда нужно, и выбрать её по имени во время работы — так устроены плагины. Функции тогда вызываются по адресу:
// Явная загрузка: LoadLibraryA, GetProcAddress, FreeLibrary; функция вызывается через указатель.
// Сборка без библиотеки импорта: g++ dynamic.cpp -o dynamic.exe
#include <windows.h>
#include <iostream>
using GcdFunction = int (*)(int, int); // тип «указатель на функцию (int, int) → int»
int main(int argc, char* argv[])
{
const char* name = argc > 1 ? argv[1] : "mathlib.dll";
std::cout << "loading " << name << '\n';
HMODULE library = LoadLibraryA(name);
if (library == nullptr) {
std::cout << "LoadLibrary failed, error " << GetLastError() << '\n'; // 126 — DLL не найдена
return 2;
}
FARPROC address = GetProcAddress(library, "gcd"); // имя — как экспортировано
if (address == nullptr) {
std::cout << "GetProcAddress failed, error " << GetLastError() << '\n'; // 127 — функции нет
FreeLibrary(library);
return 2;
}
// FARPROC — «адрес любой функции»; приведение к нужному типу — через void*
const GcdFunction gcd = reinterpret_cast<GcdFunction>(reinterpret_cast<void*>(address));
std::cout << "gcd(84, 36) = " << gcd(84, 36) << '\n';
FreeLibrary(library); // после этого вызывать gcd нельзя: кода по адресу больше нет
std::cout << "done\n";
return 0;
}
> dynamic
loading mathlib.dll
[mathlib loaded]
gcd(84, 36) = 12
[mathlib unloaded]
done
> dynamic nosuch.dll
loading nosuch.dll
LoadLibrary failed, error 126
int (*)(int, int); using даёт типу короткое имя. Вызов — как обычной функции: gcd(84, 36). Имя функции без скобок — тоже её адрес: так в разделе 16 функцию потока передавали в CreateThread.GetProcAddress ничего не знает о параметрах — она возвращает адрес как FARPROC. Правильность типа — на совести программиста: указатель не того типа — вызов с мусором в параметрах. Прямое приведение g++ отмечает предупреждением cast between incompatible function types; приведение через void* говорит «я знаю, что делаю».FreeLibrary DLL выгружается (если её не держит кто-то ещё) — указатели на её функции становятся висячими.| Ошибка | Признак | Сообщение g++ / ошибка Windows | Как избежать |
|---|---|---|---|
| запись в зарезервированную, но не переданную память | аварийное завершение | Access violation | MEM_COMMIT перед использованием |
MEM_RELEASE не с тем адресом или ненулевым размером | память не освобождается | 87 — неверный параметр | адрес от резервирования, размер 0 |
нет delete / HeapFree | память процесса растёт | — | пара для каждого выделения; std::vector |
delete для массива | неопределённое поведение | -Wmismatched-new-delete | delete[] для new[] |
обращение после delete, двойной delete | порча кучи, падение позже | — | после delete — p = nullptr |
нет extern "C" у экспорта | GetProcAddress не находит функцию | 127 | extern "C" __declspec(dllexport) |
| DLL не рядом с программой | программа не запускается / LoadLibrary = NULL | 126 | DLL в каталоге программы |
прямое приведение FARPROC | предупреждение | cast between incompatible function types | через void*, тип строго по заголовку |
вызов после FreeLibrary | аварийное завершение | Access violation | выгружать, когда функции больше не нужны |
тяжёлая работа в DllMain | зависание при загрузке | — | в DllMain — только простое; остальное — в функции инициализации |
VirtualAlloc: сначала адреса (MEM_RESERVE), потом память (MEM_COMMIT); VirtualFree — наоборотnew/delete работают через кучу; каждому выделению — освобождение.h, определения — в .cpp; сборка нескольких файлов одной командойextern "C" __declspec(dllexport), -shared; неявно — -l и загрузка до main; явно — LoadLibraryA, GetProcAddress, указатель на функцию, FreeLibraryDllMain — только простоеws2_32.dll, программа связывается с ней -lws2_32; итогового проекта — модули в DLL.