07. Механизмы управления виртуальной и динамически распределяемой памятью

Тема 7. Механизмы управления виртуальной и динамически распределяемой памятью

Введение и цели лекции

📌 Слайд 1: Механизмы управления виртуальной и динамически распределяемой памятью

  • Курс: Системное программирование
  • Тема 7 (4 академических часа)
  • Лектор: каф. Системного программирования

Управление памятью — один из фундаментальных компонентов современных операционных систем общего назначения. Концепция виртуальной памяти решает три критические задачи вычислительных систем:

  1. Изоляция и безопасность: каждый процесс функционирует в собственном изолированном адресном пространстве, не имея физической возможности случайно или преднамеренно прочитать или модифицировать данные других процессов или привилегированного ядра ОС.
  2. Абстракция физической адресации: прикладной программист оперирует непрерывным плоским адресным пространством (до 128 ТБ / 256 ТБ в 64-разрядных системах), абстрагируясь от фрагментации, объема и физического расположения микросхем ОЗУ.
  3. Эффективное разделение данных и кода: механизм виртуальных страниц позволяет разделять между процессами исполняемый код разделяемых библиотек (DLL / shared objects) и организовывать высокоскоростное межпроцессное взаимодействие с нулевым копированием (zero-copy IPC) через проецирование файлов и разделяемые сегменты.

📌 Слайд 2: Учебные вопросы лекции

  1. Архитектура виртуальной памяти: страничная трансляция x86-64, MMU, TLB, регистр CR3.
  2. Жизненный цикл виртуальных страниц: состояния Free, Reserved, Committed.
  3. Аппаратный сбой страницы (Page Fault): типы сбоев и взаимодействие VMM с диском.
  4. Системный API виртуальной памяти: VirtualAlloc, VirtualProtect, VirtualFree.
  5. Иерархия распределения памяти: CRT-рантайм, системные кучи (HeapAlloc), LFH.
  6. Проецирование файлов на память (Memory-Mapped Files, MMF) и разделяемая память.
  7. Разделяемые сегменты данных в DLL (#pragma data_seg).
  8. Архитектура DLL: формат PE, DllMain, таблицы EAT и IAT, механизмы связывания.

1. Архитектура виртуальной памяти и аппаратная трансляция адресов

📌 Слайд 3: Архитектура виртуальной памяти и адресация

  • Виртуальное адресное пространство процесса (Virtual Address Space, VAS).
  • Линейная адресация: пользовательский диапазон (User Space) и системный (System/Kernel Space).
  • Страничная организация: страницы виртуальной памяти и физические страницы (Page Frames / PFN).
  • Четырехуровневая трансляция в x86-64: PML4 $\to$ PDPT $\to$ PD $\to$ PT $\to$ Offset.
  • Регистр процессора CR3 и буфер ассоциативной трансляции (TLB).

В современных защищенных операционных системах процессор всегда выполняет команды в режиме виртуальной адресации. Центральный процессор никогда не передает сгенерированный программой указатель напрямую на шину системной памяти; каждый адрес предварительно транслируется аппаратным блоком управления памятью (MMU — Memory Management Unit).

Канонический формат виртуального адреса x86-64

В классической 64-битной архитектуре x86-64 используется 48-битная каноническая виртуальная адресация (4-level paging), предоставляющая адресное пространство размером $2^{48} = 256$ ТБ:

  • Пользовательское адресное пространство (User Space): нижняя половина диапазона (0x0000'0000'0000'0000 – 0x0000'7FFF'FFFF'FFFF, 128 ТБ). Доступно коду процесса в Ring 3.
  • Адресное пространство ядра (Kernel/System Space): верхняя половина диапазона (0xFFFF'8000'0000'0000 – 0xFFFF'FFFF'FFFF'FFFF, 128 ТБ). Доступно только в привилегированном режиме Ring 0.
  • Неканоническая дыра (Non-canonical hole): диапазон между 128 ТБ и $2^{64}-128$ ТБ, попытка обращения к которому вызывает исключение общей защиты (#GP — General Protection Fault).

Трансляция виртуального адреса в физический выполняется аппаратно через древовидную иерархию 4 уровней таблиц страниц:

  1. PML4 (Page Map Level 4): биты адреса [47:39] (9 бит, 512 записей по 8 байт). Базовый физический адрес таблицы PML4 текущего потока хранится в системном регистре процессора CR3. При переключении контекста потока другого процесса операционная система загружает в CR3 адрес PML4 нового процесса, мгновенно меняя все отображение памяти.
  2. PDPT (Page Directory Pointer Table): биты [38:30] (9 бит, указывает на область размером 1 ГБ).
  3. PD (Page Directory): биты [29:21] (9 бит, указывает на область размером 2 МБ).
  4. PT (Page Table): биты [20:12] (9 бит, указывает на 4-килобайтную страницу).
  5. Offset (Смещение в странице): биты [11:0] (12 бит, $2^{12} = 4096$ байт = 4 КБ).

Финальный физический адрес формируется конкатенацией базового физического номера страницы (PFN — Page Frame Number), извлеченного из записи таблицы страниц (PTE), и смещения: $$\text{Physical Address} = (\text{PFN} \ll 12) \mid \text{Offset}$$

📌 Слайд 4: Архитектура виртуальной памяти и трансляция адресов MMU

  • Битовая декомпозиция адреса x86-64.
  • Аппаратная трансляция MMU через регистр CR3 и кэш TLB.
  • Жизненный цикл состояний страниц: Free, Reserved, Committed.

center height:480px

Буфер ассоциативной трансляции (TLB)

Поскольку 4-уровневая трансляция требует 4 последовательных обращений к памяти для чтения каждого указателя, процессор оснащен сверхбыстрым кэшем трансляций — TLB (Translation Lookaside Buffer):

  • TLB Hit: адрес найден в кэше $\to$ трансляция занимает 1 такт процессора.
  • TLB Miss: аппаратный блок Page Walker обходит иерархию таблиц в ОЗУ $\to$ задержка составляет от десятков до сотен тактов.
  • Сброс TLB: при смене значения регистра CR3 (переключение процесса) процессор автоматически инвалидирует записи TLB (за исключением записей с установленным флагом Global Page, используемых для разделяемого ядра ОС).

2. Состояния страниц виртуальной памяти и обработка Page Fault

📌 Слайд 5: Состояния страниц памяти: Free, Reserved, Committed

  • Free (Свободная): не выделена в виртуальном адресном пространстве, обращение вызывает Access Violation.
  • Reserved (Зарезервированная): непрерывный блок адресов забронирован в VMM, но физическая память не назначена.
  • Committed (Выделенная): за страницей закреплена физическая память в RAM или дисковом файле подкачки (pagefile).
  • Механизм двухфазного выделения памяти для разреженных структур данных (Sparse Arrays).

Диспетчер виртуальной памяти (Virtual Memory Manager, VMM) Windows оперирует страницами, находящимися в одном из трех взаимоисключающих состояний:

  1. Free (Свободная): страница не привязана ни к какому объекту. Любая попытка чтения или записи по адресу из свободной страницы приводит к аппаратному исключению нарушения прав доступа (Access Violation, код исключения 0xC0000005 в Windows / сигнал SIGSEGV в Linux).
  2. Reserved (Зарезервированная): диапазон виртуальных адресов резервируется в адресном пространстве процесса. Никакая физическая оперативная память и место в файле подкачки при этом не расходуются; VMM лишь гарантирует, что данный непрерывный диапазон адресов не будет занят другими вызовами VirtualAlloc или загрузчиком DLL. Чтение и запись по зарезервированным страницам запрещены.
  3. Committed (Выделенная / Зафиксированная): за страницей закрепляется физическое хранилище. Операционная система гарантирует, что при обращении к этой странице данные будут находиться либо в оперативной памяти (RAM), либо в файле подкачки (pagefile.sys).

Паттерн работы с разреженными массивами

Двухфазное выделение позволяет эффективно размещать гигантские разреженные структуры данных (например, хэш-таблицы или матрицы на 10 ГБ):

1// 1. Фаза резервирования: бронируем 1 ГБ адресов (0 байт физической RAM потрачено)
2void* pHugeArray = VirtualAlloc(NULL, 1024 * 1024 * 1024, MEM_RESERVE, PAGE_READWRITE);
3
4// 2. Фаза фиксации: по мере поступления данных выделяем только нужные страницы (например, 64 КБ)
5void* pChunk = VirtualAlloc(pHugeArray, 64 * 1024, MEM_COMMIT, PAGE_READWRITE);
6
7// 3. Работа с выделенным участком
8((int*)pChunk)[0] = 42;

📌 Слайд 6: Аппаратный сбой страницы (Page Fault)

  • Аппаратное прерывание #PF (Interrupt 14) процессора.
  • Проверка бита присутствия (Present = 0) в записи PTE.
  • Hard Fault: страница отсутствует в RAM, требует блокирующего чтения с диска (высокая латентность $\sim 5$–$10$ мс).
  • Soft Fault: страница находится в системном кэше (Standby / Modified list), восстановление PTE за доли микросекунды.
  • Структура VAD (Virtual Address Descriptor) процесса в ядре ОС.

Когда поток выполняет инструкцию, обращающуюся к виртуальному адресу, MMU проверяет бит Present в соответствующей записи таблицы страниц PTE:

  • Если Present == 1: физическая страница находится в RAM, трансляция завершается успешно.
  • Если Present == 0: процессор прерывает выполнение инструкции и генерирует исключение Page Fault (#PF).

Диспетчер памяти ядра перехватывает #PF и выполняет проверку по дереву дескрипторов виртуальных адресов процесса (VAD — Virtual Address Descriptor):

  1. Недопустимое обращение: адрес попадает в свободную страницу или нарушает права (например, запись в страницу с PAGE_READONLY). Ядро транслирует прерывание в структурированное исключение STATUS_ACCESS_VIOLATION (или отправляет сигнал SIGSEGV).
  2. Soft Page Fault: страница была вытеснена из рабочего набора процесса (Working Set), но ее содержимое еще не затерто и хранится в оперативной памяти в списке ожидания (Standby List) или списке измененных страниц (Modified List). Ядро просто восстанавливает бит Present = 1 в PTE без обращения к диску.
  3. Hard Page Fault: страница находится на дисковом накопителе (в файле подкачки или проецируемом файле программы). Поток переводится в состояние ожидания I/O, ядро считывает страницу с диска в свободный фрейм физической памяти, обновляет PTE и возобновляет инструкцию потока.

3. Системный API низкоуровневого управления виртуальной памятью

📌 Слайд 7: Системный API виртуальной памяти

  • Функция VirtualAlloc: параметры flAllocationType (MEM_RESERVE, MEM_COMMIT, MEM_RESET).
  • Гранулярность распределения: выделение кратно размеру страницы (4 КБ), резервирование кратно гранулярности распределения системы (64 КБ).
  • Функция VirtualProtect: динамическое изменение прав доступа страниц (PAGE_READWRITE, PAGE_EXECUTE_READ).
  • Функция VirtualFree: освобождение памяти (MEM_DECOMMIT против MEM_RELEASE).
  • Функция VirtualQuery: опрос атрибутов и состояний региона памяти (MEMORY_BASIC_INFORMATION).

В Windows NT прямой доступ к диспетчеру виртуальной памяти предоставляется семейством функций Virtual*:

1LPVOID VirtualAlloc(
2    LPVOID lpAddress,        // Желаемый адрес (или NULL для выбора системой)
3    SIZE_T dwSize,           // Размер региона в байтах
4    DWORD  flAllocationType, // Тип: MEM_RESERVE, MEM_COMMIT, MEM_RESET
5    DWORD  flProtect         // Атрибуты защиты: PAGE_READWRITE, PAGE_EXECUTE_READ
6);

Гранулярность распределения (Allocation Granularity)

Функция VirtualAlloc подчиняется двум жестким системным ограничениям архитектуры:

  • Размер страницы (Page Size): минимальный квант фиксации физической памяти (MEM_COMMIT) составляет ровно 4 КБ (4096 байт). Если запросить 10 байт, система выделит целую страницу в 4096 байт.
  • Гранулярность распределения (Allocation Granularity): базовый адрес зарезервированного региона (MEM_RESERVE) всегда выравнивается на границу 64 КБ (0x10000). Это историческое проектное решение процессоров с аппаратными кэшами позволяет минимизировать фрагментацию таблиц страниц.

Освобождение памяти: VirtualFree

1BOOL VirtualFree(
2    LPVOID lpAddress,
3    SIZE_T dwSize,
4    DWORD  dwFreeType // MEM_DECOMMIT или MEM_RELEASE
5);

Различие типов освобождения критично:

  • MEM_DECOMMIT: переводит страницы из состояния Committed обратно в Reserved. Физическая память освобождается, но виртуальные адреса остаются закрепленными за процессом. Параметр dwSize может указывать произвольное число страниц.
  • MEM_RELEASE: полностью освобождает зарезервированный регион, переводя страницы в состояние Free. При этом lpAddress обязан указывать на базовый адрес региона, возвращенный при первичном резервировании, а dwSize обязан быть строго равен 0.

4. Иерархия уровней распределения памяти: кучи процесса и CRT

📌 Слайд 8: Иерархия уровней управления памятью

  • Проблема использования VirtualAlloc для мелких объектов: внутреннее переполнение (Internal Fragmentation) и накладные расходы на системные вызовы.
  • Четырехуровневая модель:
    1. Языковой уровень / CRT: malloc, free, new, delete.
    2. Системные кучи Win32: HeapCreate, HeapAlloc, HeapFree.
    3. Виртуальная память: VirtualAlloc, VirtualProtect, VirtualFree.
    4. Ядро ОС и физическая RAM.
  • Понятие дефолтной кучи процесса: GetProcessHeap().

📌 Слайд 9: Иерархия уровней управления памятью в ОС

  • Стек подсистем распределения: от CRT до контроллера памяти.
  • Критерии выбора между VirtualAlloc, HeapAlloc и malloc.

center height:480px

Если процессу требуется выделить блок памяти размером 32 байта, вызов VirtualAlloc был бы катастрофически неэффективен:

  1. Система вынуждена выделить целую страницу в 4096 байт, что дает потерю более 99% памяти (внутренняя фрагментация).
  2. Вызов VirtualAlloc требует переключения контекста процессора в режим ядра (Ring 0), что занимает сотни тактов.

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

📌 Слайд 10: Управление кучами процесса (Heap API)

  • Системные функции: HeapCreate, HeapAlloc, HeapFree, HeapDestroy.
  • Флаг синхронизации: HEAP_NO_SERIALIZE для однопоточных структур без мьютексов.
  • Изоляция данных: создание приватных куч для исключения фрагментации дефолтной кучи.
  • Механизм Low Fragmentation Heap (LFH): кэширование предопределенных корзин (bins) мелких размеров блоков.
  • Мгновенная очистка: вызов HeapDestroy уничтожает все выделенные объекты разом.

Куча представляет собой менеджер памяти пользовательского режима, который запрашивает у диспетчера виртуальной памяти большие непрерывные блоки (сегменты по несколько мегабайт через VirtualAlloc), а затем нарезает их на блоки произвольного размера по запросам приложения.

 1// Создание приватной изолированной кучи с начальным размером 1 МБ и безлимитным ростом (0)
 2HANDLE hCustomHeap = HeapCreate(0, 1024 * 1024, 0);
 3
 4// Выделение 256 байт внутри созданной кучи
 5void* pData = HeapAlloc(hCustomHeap, HEAP_ZERO_MEMORY, 256);
 6
 7// Освобождение блока
 8HeapFree(hCustomHeap, 0, pData);
 9
10// Полное уничтожение всей кучи и всех оставшихся блоков за один вызов
11HeapDestroy(hCustomHeap);

Преимущества создания собственных куч:

  1. Защита от фрагментации: интенсивное выделение и удаление временных мелких объектов в отдельной куче не фрагментирует основную кучу процесса (GetProcessHeap()), в которой живут критические структуры CRT и системных библиотек.
  2. Многопоточная производительность: передача флага HEAP_NO_SERIALIZE при работе из одного потока исключает накладные расходы на блокировки межпоточной синхронизации.
  3. Бесплатная пакетная деаллокация: при завершении сложной задачи вызов HeapDestroy мгновенно возвращает всю задействованную память операционной системе без необходимости поштучного вызова free для каждого узла графа или дерева.

5. Проецирование файлов на память (Memory-Mapped Files)

📌 Слайд 11: Проецирование файлов на память (MMF)

  • Концепция Memory-Mapped Files: отображение содержимого файла непосредственно в виртуальные адреса процесса.
  • Устранение двойной буферизации: доступ к файлу через обычные указатели в обход промежуточных системных буферов ReadFile / WriteFile.
  • Ленивая подгрузка страниц: с диска считываются только те страницы, к которым реально обратился процесс.
  • Когерентность дискового кэша: изменения, внесенные в память, автоматически сбрасываются ядром на диск (FlushViewOfFile).

Проецирование файлов на виртуальное адресное пространство (Memory-Mapped Files, MMF) — один из наиболее мощных механизмов системного программирования. Он стирает границу между работой с оперативной памятью и дисковым файловым вводом-выводом.

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

📌 Слайд 12: Создание и отображение проекций

  • Трехэтапный конвейер WinAPI:
    1. Открытие/создание файла: CreateFile.
    2. Создание объекта ядра «проекция»: CreateFileMapping.
    3. Отображение проекции в адресное пространство: MapViewOfFile.
  • Закрытие ресурсов: UnmapViewOfFile $\to$ CloseHandle(hMap) $\to$ CloseHandle(hFile).
  • Ограничения 32/64 бит: отображение файлов гигабайтного размера через скользящее окно (View Offset).
 1// 1. Открытие физического файла на диске
 2HANDLE hFile = CreateFileA("large_database.dat", GENERIC_READ | GENERIC_WRITE,
 3                           0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
 4
 5// 2. Создание объекта ядра «Проекция файла»
 6HANDLE hMapping = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 0, NULL);
 7
 8// 3. Отображение всего файла в виртуальное адресное пространство процесса
 9char* pFileView = (char*)MapViewOfFile(hMapping, FILE_MAP_ALL_ACCESS, 0, 0, 0);
10
11// Прямой доступ к данным файла через указатель в памяти:
12pFileView[0] = 'H';
13pFileView[1] = 'E';
14pFileView[2] = 'A';
15pFileView[3] = 'D';
16
17// 4. Принудительный сброс модифицированных страниц на накопитель
18FlushViewOfFile(pFileView, 4);
19
20// 5. Очистка ресурсов
21UnmapViewOfFile(pFileView);
22CloseHandle(hMapping);
23CloseHandle(hFile);

📌 Слайд 13: Межпроцессное взаимодействие (IPC) через MMF

  • Использование файла подкачки вместо дискового файла: передача дескриптора INVALID_HANDLE_VALUE.
  • Именованные объекты ядра: публикация имени в пространстве Global\ или Local\.
  • Нулевое копирование (Zero-Copy IPC): два независимых процесса получают разные виртуальные адреса, указывающие на одни и те же физические фреймы RAM.
  • Необходимость внешней синхронизации: мьютексы и семафоры ядра.

Если при вызове CreateFileMapping вместо реального дескриптора файла передать INVALID_HANDLE_VALUE, операционная система создает блок разделяемой оперативной памяти (Shared Memory), резервируемый в системном файле подкачки:

1// Процесс A: Создает именованный блок памяти на 1 МБ
2HANDLE hMap = CreateFileMappingA(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 
3                                0, 1024 * 1024, "Global\\SharedBuffer");
4void* pSharedMemA = MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, 0);
5
6// Процесс B: Открывает существующую проекцию по общему имени
7HANDLE hMapB = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, "Global\\SharedBuffer");
8void* pSharedMemB = MapViewOfFile(hMapB, FILE_MAP_ALL_ACCESS, 0, 0, 0);

Виртуальные адреса pSharedMemA и pSharedMemB в процессах A и B могут иметь совершенно разные числовые значения, но благодаря записям в PTE их адреса указывают на одни и те же физические фреймы оперативной памяти (PFN). Запись данных процессом A становится мгновенно доступна процессу B на аппаратной скорости оперативной памяти.


6. Разделяемые сегменты памяти в DLL

📌 Слайд 14: Разделяемые сегменты данных в DLL

  • Поведение по умолчанию: механизм Copy-on-Write изолирует глобальные переменные DLL для каждого процесса.
  • Директива создания именованной секции: #pragma data_seg(".shared").
  • Обязательное условие: все переменные в секции должны быть явно инициализированы!
  • Директива компоновщику: атрибуты секции RWS (Read, Write, Shared).
  • Применение: подсчет запущенных экземпляров приложения, межпроцессные флаги.

По умолчанию, когда динамическая библиотека загружается в несколько процессов, операционная система использует для ее страниц данных механизм Copy-on-Write (копирование при записи): страницы являются общими до первой модификации, после чего ядро создает для пишущего процесса приватную физическую копию страницы.

Однако WinAPI позволяет разработчику явно объявить секцию данных разделяемой (Shared Section):

1// mylib.c в составе DLL
2#pragma data_seg(".shared")
3// Обязательная явная инициализация! Неинициализированные переменные попадут в .bss
4int  g_InstanceCounter = 0;
5char g_SharedStateString[256] = "DefaultState";
6#pragma data_seg()
7
8// Указание компоновщику сделать секцию разделяемой (RWS: Read, Write, Shared)
9#pragma comment(linker, "/SECTION:.shared,RWS")

При загрузке такой DLL любым количеством процессов все они будут совместно использовать единственную физическую страницу памяти с переменными g_InstanceCounter и g_SharedStateString.

📌 Слайд 15: Проецирование файлов и разделяемая память IPC

  • Архитектура MMF с физическим хранилищем (файлы / pagefile).
  • Разделение физических страниц между адресными пространствами двух процессов.
  • Механизм разделяемых сегментов DLL (.shared).

center height:480px


7. Архитектура динамически подключаемых библиотек (DLL)

📌 Слайд 16: Динамически подключаемые библиотеки: концепция

  • Назначение DLL (Dynamic-Link Library): модульность, разделение кода между процессами, независимое обновление компонентов.
  • Экономия памяти: секция кода .text DLL отображается как PAGE_EXECUTE_READ и разделяется всеми процессами системы.
  • Структура формата PE (Portable Executable): заголовки DOS/PE, секции .text, .data, .rdata, .rsrc, .reloc.
  • Базовый адрес загрузки (Image Base) и механизм релокации (ASLR / секция .reloc).

Динамически подключаемые библиотеки (DLL в Windows, Shared Objects .so в Linux) представляют собой исполняемые PE-модули, содержащие код и ресурсы, которые могут использоваться одновременно несколькими независимыми процессами.

Главные преимущества DLL:

  1. Экономия физической оперативной памяти: код библиотеки (.text), будучи неизменяемым, загружается в физическую память в единственном экземпляре и отображается в адресные пространства всех использующих ее процессов.
  2. Модульность и плагинность: функциональность программы может расширяться добавлением новых модулей без перекомпиляции основного .exe.
  3. Обновляемость: исправление ошибок или оптимизация функций внутри DLL не требует пересборки зависимых клиентских приложений.

📌 Слайд 17: Внутренняя структура DLL: точка входа DllMain

  • Сигнатура BOOL WINAPI DllMain(HINSTANCE, DWORD, LPVOID).
  • Причины вызова (fdwReason): DLL_PROCESS_ATTACH, DLL_PROCESS_DETACH, DLL_THREAD_ATTACH, DLL_THREAD_DETACH.
  • Блокировка загрузчика (Loader Lock): критическая секция внутри вызова DllMain.
  • Чего категорически нельзя делать в DllMain: вызывать LoadLibrary, создавать потоки, захватывать прикладные мьютексы (риск взаимной блокировки — Deadlock).
 1BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) {
 2    switch (fdwReason) {
 3    case DLL_PROCESS_ATTACH:
 4        // Вызывается при первичной загрузке DLL в адресное пространство процесса.
 5        // Возврат FALSE прерывает загрузку всего процесса!
 6        DisableThreadLibraryCalls(hinstDLL); // Оптимизация: отключает THREAD_ATTACH/DETACH
 7        break;
 8
 9    case DLL_PROCESS_DETACH:
10        // Вызывается при выгрузке DLL (FreeLibrary или завершение процесса)
11        // lpReserved != NULL указывает, что процесс аварийно завершается
12        break;
13
14    case DLL_THREAD_ATTACH:
15        // Вызывается при создании каждого нового потока в процессе
16        break;
17
18    case DLL_THREAD_DETACH:
19        // Вызывается при корректном завершении потока (ExitThread)
20        break;
21    }
22    return TRUE;
23}
🛑 Caution
Точка входа DllMain выполняется операционной системой под системной блокировкой Loader Lock. Внутри DllMain категорически запрещено: вызывать LoadLibrary/FreeLibrary, запускать новые потоки и ожидать их завершения (WaitForSingleObject), инициализировать подсистемы COM или графики. Нарушение этого правила приводит к гарантированному тупику потоков (Deadlock).

📌 Слайд 18: Таблицы экспорта (EAT) и импорта (IAT)

  • Таблица экспорта (Export Address Table, EAT): список имен функций, ординалов и их относительных адресов (RVA).
  • Директива экспорта: __declspec(dllexport) или сценарии описания модуля (.def-файлы).
  • Таблица импорта (Import Address Table, IAT): массив указателей на внешние функции, заполняемый загрузчиком при старте.
  • Директива импорта: __declspec(dllimport) генерирует прямой косвенный вызов call [__imp_Function].
1// mathlib.h
2#ifdef MATHLIB_EXPORTS
3    #define MATHLIB_API __declspec(dllexport)
4#else
5    #define MATHLIB_API __declspec(dllimport)
6#endif
7
8extern "C" MATHLIB_API double CalculateIntegral(double a, double b, int steps);

Директива __declspec(dllimport) критически важна для оптимизации:

  • Без dllimport компилятор генерирует вызов статической заглушки (thunk), которая делает дополнительный jmp по таблице указателей.
  • С директивой __declspec(dllimport) компилятор сразу генерирует прямую инструкцию косвенного вызова call qword ptr [__imp__CalculateIntegral], исключая накладные расходы на лишний переход.

📌 Слайд 19: Способы связывания: неявное vs явное

  • Неявное (Load-Time / Implicit Linking):
    • Требует заголовочный файл .h и статическую библиотеку импорта .lib.
    • Загрузчик ОС автоматически подгружает DLL при старте программы.
    • Ошибка: если DLL отсутствует, процесс завершается с ошибкой инициализации.
  • Явное (Run-Time / Explicit Linking):
    • Функции LoadLibrary (LoadLibraryEx), GetProcAddress, FreeLibrary.
    • Библиотека импорта .lib не требуется, ручная проверка ошибок и плагинность.
    • В Linux: функции dlopen, dlsym, dlclose (библиотека libdl).
 1// Пример явной динамической загрузки модуля плагина
 2typedef double (*CALC_FN)(double, double, int);
 3
 4HMODULE hModule = LoadLibraryA("mathlib.dll");
 5if (hModule != NULL) {
 6    CALC_FN pfnCalculate = (CALC_FN)GetProcAddress(hModule, "CalculateIntegral");
 7    if (pfnCalculate != NULL) {
 8        double result = pfnCalculate(0.0, 3.14159, 1000);
 9    }
10    FreeLibrary(hModule); // Выгрузка DLL и декремент счетчика ссылок
11} else {
12    // Грамотная обработка отсутствия библиотеки в рантайме
13    ReportMissingPlugin("mathlib.dll");
14}

📌 Слайд 20: Внутренняя структура DLL и механизмы связывания

  • Схема жизненного цикла DllMain.
  • Таблицы EAT и IAT.
  • Сравнение конвейера компиляции и связывания Load-Time против Run-Time.

center height:480px


8. Итоги и системные принципы управления памятью

📌 Слайд 21: Итоги и системные принципы управления памятью

  • 4-уровневые таблицы x86-64 и регистр CR3 изолируют адресные пространства процессов.
  • Двухфазное выделение (VirtualAlloc): резервирование адресов без затрат физической ОЗУ.
  • Аппаратный сбой Page Fault реализует прозрачную ленивую подгрузку страниц с накопителя.
  • Кучи процесса (Heap API) и LFH защищают систему от внутренней фрагментации памяти.
  • Проекция файлов (MMF) обеспечивает высокоскоростной Zero-Copy IPC в разделяемой памяти.
  • Динамические библиотеки (DLL) разделяют физический код и секции данных между процессами.

Контрольные вопросы и литература

📌 Слайд 22: Вопросы для самопроверки

  1. Как формируется физический адрес при 4-уровневой трансляции x86-64 и какова роль регистра CR3?
  2. Чем отличается страница в состоянии Reserved от страницы в состоянии Committed?
  3. В чем различие между аппаратными сбоями Hard Page Fault и Soft Page Fault?
  4. Каковы правила выравнивания и гранулярности при вызовах VirtualAlloc?
  5. В чем разница между вызовами VirtualFree с флагами MEM_DECOMMIT и MEM_RELEASE?
  6. Почему не рекомендуется использовать VirtualAlloc для мелких объектов вместо HeapAlloc?
  7. Как организовать межпроцессный обмен данными через CreateFileMapping с INVALID_HANDLE_VALUE?
  8. Что необходимо указать в коде DLL для создания разделяемой секции памяти (#pragma data_seg)?
  9. Какие операции запрещено выполнять внутри функции DllMain из-за блокировки Loader Lock?
  10. Чем отличается неявное связывание через .lib от явной загрузки через LoadLibrary/GetProcAddress?

📌 Слайд 23: Литература и рекомендуемые ресурсы

  1. Руссинович М., Соломон Д., Ионеску А. Внутреннее устройство Microsoft Windows. 7-е изд. — СПб.: Питер, 2019. (Главы 5: «Управление памятью», 3: «Механизмы управления»).
  2. Рихтер Дж. Windows для профессионалов: создание эффективных Win32-приложений с учетом специфики 64-разрядной версии Windows. — СПб.: Питер, 2008. (Главы 13–18: Архитектура виртуальной памяти, кучи, MMF и DLL).
  3. Таненбаум Э., Бос Х. Современные операционные системы. 4-е изд. — СПб.: Питер, 2015. (Глава 3: «Управление памятью»).
  4. Microsoft Learn. Memory Management & Dynamic-Link Libraries Documentation. URL: https://learn.microsoft.com/en-us/windows/win32/memory/memory-management

📌 Слайд 21: Вопросы для самоконтроля

  1. Как формируется физический адрес при 4-уровневой трансляции x86-64 и какова роль регистра CR3?
  2. Чем отличается страница в состоянии Reserved от страницы в состоянии Committed?
  3. В чем различие между аппаратными сбоями Hard Page Fault и Soft Page Fault?
  4. Каковы правила выравнивания и гранулярности при вызовах VirtualAlloc?
  5. В чем разница между вызовами VirtualFree с флагами MEM_DECOMMIT и MEM_RELEASE?
  6. Почему не рекомендуется использовать VirtualAlloc для мелких объектов вместо HeapAlloc?
  7. Как организовать межпроцессный обмен данными через CreateFileMapping с INVALID_HANDLE_VALUE?
  8. Что необходимо указать в коде DLL для создания разделяемой секции памяти (#pragma data_seg)?
  9. Какие операции запрещено выполнять внутри функции DllMain из-за блокировки Loader Lock?
  10. Чем отличается неявное связывание через .lib от явной загрузки через LoadLibrary/GetProcAddress?

📌 Слайд 22: Рекомендуемая литература

  1. Руссинович М., Соломон Д., Ионеску А. Внутреннее устройство Microsoft Windows. 7-е изд. — СПб.: Питер, 2019. (Главы 5: «Управление памятью», 3: «Механизмы управления»).
  2. Рихтер Дж. Windows для профессионалов: создание эффективных Win32-приложений с учетом специфики 64-разрядной версии Windows. — СПб.: Питер, 2008. (Главы 13–18: Архитектура виртуальной памяти, кучи, MMF и DLL).
  3. Таненбаум Э., Бос Х. Современные операционные системы. 4-е изд. — СПб.: Питер, 2015. (Глава 3: «Управление памятью»).
  4. Microsoft Learn. Memory Management & Dynamic-Link Libraries Documentation. URL: https://learn.microsoft.com/en-us/windows/win32/memory/memory-management
← 06. Организация графического … Управление памятью в Windows →