Ориентация главы

Глава 3 установила язык «владения и заимствования»; эта глава даёт физический выход для «владения»: вся динамическая память XRT проходит через пять точек входа — xrtMalloc / xrtCalloc / xrtRealloc / xrtMemDup / xrtFree — плюс xrtSecureZero для чувствительных случаев. Ценность стока не в синтаксическом сахаре: он делает возможными три иначе невозможные вещи — замену нижележащего аллокатора целиком (для подключения собственного пула или рантайма хоста), позылементное наблюдение (статистика и отладка главы 6) и точечное внедрение сбоев (для проверки путей отказа). После этой главы каждый ваш блок памяти стоит на едином выходе.

5.1引入

Реальная дилемма: вашу программу нужно встроить в чужую большую систему, и там требуют, чтобы вся память шла через их аллокатор (они ведут учёт, квоты и возврат по модулям); а ваш код уже содержит десятки вызовов malloc/free. Заменять по одному — катастрофа: вы определяете макросы, массово заменяете и молитесь, чтобы ничего не ускользнуло. Другая дилемма: в тестах вы хотите знать «если 1000-е выделение не удастся, корректно ли откатится программа» — у разрозненного malloc просто нет точки внедрения.

Обе дилеммы указывают на один и тот же проектный ответ: точки входа памяти должны быть стянуты в одно горлышко, причём на уровне библиотеки, а не дисциплины кодирования. XRT сводит всю динамическую память в пять функций, поэтому замена, наблюдение и внедрение — операции уровня одного глобального переключателя.

Сток приносит и третий дивиденд: единые гарантии выравнивания и раскладки. Указатели, возвращаемые пятью точками входа, выделяются с максимальным базовым выравниванием — в них можно напрямую размещать любые скаляры, структуры и даже SIMD-векторы, и вам не нужно обсуждать «этот буфер под массив float — не нужен ли aligned_alloc»; все контейнеры XRT (том 3) внутренне идут через те же точки входа — выравнивание элементов в контейнере гарантирует библиотека, независимо от того, что случайно делает переданный вами аллокатор. Выход, построенный в этой главе, — общая физическая база контейнеров, парсеров и сетевых буферов всех последующих томов.

5.2概念

Семантика пяти точек входа

Точка входаСемантикаКлючевые детали
xrtMallocВыделить (владеющее)Семантика как у стандартного malloc
xrtCallocВыделить count × size и обнулитьПроверки переполнения как в стандарте
xrtReallocРост/сжатиеПри неудаче возвращает NULL, а исходный блок остаётся валидным
xrtMemDupСкопировать диапазон памяти в новое владеющее выделениеВыделение + memcpy за один шаг
xrtFreeОсвободитьNULL допустим — проверки на пустоту не нужны
xrtSecureZeroБезопасная очисткаНе будет выброшено компилятором как мёртвая запись

Первые пять следуют владеющему соглашению: кто получил возвращаемое значение, тот и освобождает, ровно один раз. Законность xrtFree(NULL) выглядит мелочью на письме, а на чтение стоит дорого — коду очистки больше никогда не нужно кутаться в слои проверок на пустоту.

Замена аллокатора: одно решение, глобальный эффект

1Бизнес-кодпять точек входа вроде xrtMalloc
2Глобальный стокединственный канал, несущий заменяемую таблицу аллокатора
3Таблица аллокатораxrtSetAllocator заменяет её до первого выделения
4Системная кучабэкенд по умолчанию; заменяема на аллокатор хоста / собственный пул

Структура xallocator несёт три колбэка — выделение, рост, освобождение — плюс указатель контекста; xrtSetAllocator меняет всю таблицу, xrtGetAllocator копирует текущую. Замена разрешена только до первого выделения — после этого таблица навсегда блокируется, и повторная замена возвращает неудачу. Это соглашение «ранней блокировки» покупает драгоценное свойство: во время работы аллокатор постоянен, поэтому никакому пути выделения не нужно рассматривать гонку «аллокатор сменился посреди полёта», и горячим путям не нужны блокировки.

Семейство At: выделения с местом рождения

xrtMallocAt / xCallocAt / xReallocAt / xFreeAt / xMemDupAt — отладочные точки входа, несущие __FILE__ / __LINE__: обычный макрос xrtMalloc при включённом отладочном модуле разворачивается в семейство At, и каждое выделение получает позицию. Это краеугольный камень поиска утечек в главе 6; здесь достаточно знать, что «информация о позиции прилагается к стоку бесплатно».

Когда действительно нужен другой аллокатор

Три типичных сценария стоит узнать заранее. Встраивание в хост: ваш модуль работает в чужом процессе (игровой движок, рантайм скриптов), и хост требует всю память через свой учёт — оберните функции хоста в три колбэка и замените в самый ранний момент; второй пример этой главы — полный шаблон. Квоты и лимиты: мультитенантный сервис ограничивает по модулям; собственный аллокатор накапливает байты в контексте и возвращает NULL сверх лимита — в сочетании с моделью ошибок главы 4 превышение ведёт себя как обычный OOM, и бизнес-коду не нужен особый случай. Измеримое сравнение: при настройке производительности вы хотите A/B-сравнить разные бэкенды (пул памяти главы 22 — готовый кандидат) — одна строка замены переключает. Общее предусловие одно: из-за «блокировки до первого выделения» замена обязана случиться в самом начале main — поставить её после чтения конфигурации может быть уже поздно.

5.3示例

Полная программа: пять точек входа за один проход

Из примера репозитория examples/core/memory/main.c — полный жизненный цикл выделения, роста, копирования, безопасной очистки и освобождения:

examples/core/memory/main.c源码 ↗



















 */

#include <stdio.h>
#include <string.h>

#include <xrt.h>



int main(void)
{
	/* 被复制源的只读字节(static const:不占用栈也不可写)。 */
	static const unsigned char Source[] = { 1, 2, 3, 4 };
	unsigned char* pValues;
	unsigned char* pCopy;

	/* 分配 4 字节并清零:Calloc 语义与标准一致,返回拥有式指针。 */
	pValues = (unsigned char*)xrtCalloc(4, sizeof(unsigned char));
	if ( pValues == NULL ) {
		return 1;
	}

	/* 写入一个标记值,稍后验证 Realloc 保留了旧内容。 */
	pValues[0] = 42;

	



	 */
	pValues = (unsigned char*)xrtRealloc(pValues, 16);
	if ( pValues == NULL ) {
		return 2;
	}

	/* 把 4 字节源复制为独立的拥有式缓冲区(分配 + memcpy 一步完成)。 */
	pCopy = (unsigned char*)xrtMemDup(Source, sizeof(Source));
	if ( pCopy == NULL ) {
		xrtFree(pValues);   /* 失败路径也要释放已持有的资源 */
		return 3;
	}

	/* 验证:扩容后旧值仍在;复制内容与源逐字节一致。 */
	printf(
		"value=%u copied=%s\n",
		(unsigned int)pValues[0],
		memcmp(pCopy, Source, sizeof(Source)) == 0 ? "yes" : "no"
	);

	



	 */
	xrtSecureZero(pValues, 16);
	xrtSecureZero(pCopy, sizeof(Source));

	/* 释放顺序与持有顺序相反;xrtFree 允许 NULL,无需判空。 */
	xrtFree(pCopy);
	xrtFree(pValues);
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/core/memory/main.c -lws2_32 -liphlpapivalue=42 copied=yes

Что только что произошло. (1) xrtCalloc(4, 1) выделяет и обнуляет; записывается маркер 42. (2) xrtRealloc растит до 16 байт — семантический нюанс на стороне неудачи: при возврате NULL исходный блок остаётся валидным; для краткости пример выходит напрямую — надёжная форма в ловушке 1. (3) xrtMemDup копирует статический источник в независимый владеющий буфер, memcmp сверяет побайтово; заметьте, путь отказа сначала освобождает уже удерживаемый pValues и лишь затем выходит — «на пути отказа нет полуфабрикатов» практикуется с первой главы, и это стандартная форма сотрудничества модели ошибок главы 4 с соглашением владения этой главы. (4) xrtSecureZero очищает оба буфера: он направлен против оптимизации компилятора «удаление мёртвой записи» — обычный memset прямо перед free может быть признан бессмысленным и удалён целиком, и чувствительные данные (ключи, токены) утекут в переиспользуемую память кучи; xrtSecureZero использует volatile-записи или барьеры, гарантируя, что очистка действительно происходит. (5) Порядок освобождения обратен порядку получения, а xrtFree не требует проверки на пустоту.

Полная программа: замена аллокатора и наблюдение блокировки

Из examples/core/allocator_tour/main.c — реализуем счётный аллокатор, заменяем до первого выделения и проверяем соглашение о ранней блокировке:

examples/core/allocator_tour/main.c源码 ↗



















 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <xrt.h>

/* 计数分配器:仍路由系统堆,便于验证替换生效。 */
typedef struct examplecount {
	size_t iAllocs;
	size_t iFrees;
} examplecount;

static ptr exampleAlloc(ptr pContext, size_t iSize)
{
	++((examplecount*)pContext)->iAllocs;
	return (ptr)malloc(iSize);
}

static ptr exampleRealloc(ptr pContext, ptr pMemory, size_t iSize)
{
	(void)pContext;
	return (ptr)realloc(pMemory, iSize);
}

static void exampleFree(ptr pContext, ptr pMemory)
{
	++((examplecount*)pContext)->iFrees;
	free(pMemory);
}

int main(void)
{
	xallocator Default;
	xallocator Custom;
	examplecount Count;
	uint8* pBlock;
	uint8* pDup;
	int iResult = 1;

	/* ---- 必须在任何 XRT 分配前完成替换。 ---- */
	xrtGetAllocator(&Default);
	memset(&Count, 0, sizeof(Count));
	Custom.Context = (ptr)&Count;
	Custom.Alloc = exampleAlloc;
	Custom.Realloc = exampleRealloc;
	Custom.Free = exampleFree;
	if ( !xrtSetAllocator(&Custom) ) {
		goto Cleanup;
	}

	/* ---- At 族五接口:位置参数不影响堆行为。 ---- */
	pBlock = (uint8*)xrtMallocAt(4u, __FILE__, __LINE__);
	if ( (pBlock == NULL) ||
		((pDup = (uint8*)xrtMemDupAt("hi", 2u, __FILE__,
			__LINE__)) == NULL) ||
		(memcmp(pDup, "hi", 2u) != 0) ||
		((pBlock = (uint8*)xrtReallocAt(pBlock, 16u, __FILE__,
			__LINE__)) == NULL) ||
		(xrtCallocAt(2u, 4u, __FILE__, __LINE__) ==
			NULL) ) {
		goto Cleanup;
	}
	/* Calloc 清零验证单独做——保留指针用于释放。 */
	{
		uint8* pZero = (uint8*)xrtCallocAt(2u, 4u, __FILE__,
			__LINE__);

		if ( (pZero == NULL) || (pZero[0] != 0u) ||
			(pZero[7] != 0u) ) {
			goto Cleanup;
		}
		xrtFreeAt(pZero, __FILE__, __LINE__);
	}
	xrtFreeAt(pDup, __FILE__, __LINE__);
	xrtFreeAt(pBlock, __FILE__, __LINE__);
	printf("allocator: swap -> at-family alloc/calloc/dup/realloc ok\n");

	
	 * 且 GetAllocator 仍报告已安装的计数分配器。 ---- */
	
	 * 分配才经过自定义 Alloc;计数只需 >= 1 即证明替换生效。 */
	if ( xrtSetAllocator(&Default) ||
		(Count.iAllocs < 1u) ) {
		goto Cleanup;
	}
	{
		xallocator Current;

		xrtGetAllocator(&Current);
		if ( (Current.Alloc != Custom.Alloc) ||
			(Current.Free != Custom.Free) ) {
			goto Cleanup;
		}
	}
	printf("allocator: locked after first alloc -> swap rejected ok\n");
	iResult = 0;

Cleanup:
	return iResult;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/core/allocator_tour/main.c -lws2_32 -liphlpapiallocator: swap -> at-family alloc/calloc/dup/realloc okallocator: locked after first alloc -> swap rejected ok

Что только что произошло. (1) Счётный аллокатор по-прежнему маршрутизирует в системную кучу, лишь ведя учёт: три колбэка exampleAlloc и остальные заполняют таблицу xallocator, и main сразу при входе вызывает xrtSetAllocator. (2) После замены раунд выделений через семейство At идёт целиком через новую таблицу. (3) Вторая замена отвергнута: после первого выделения таблица заблокирована, и вызов возвращает неудачу, а не молчаливое игнорирование — сигнал ошибки явный, и инвариант «аллокатор постоянен» выполняется. Этот шаблон — стандартное подключение при встраивании в рантайм хоста: оберните функции хоста в три колбэка и завершите замену в самый ранний момент.

Прочтите и детали реализации примера — три вещи стоит украсть. Сигнатуры колбэков несут указатель контекста (exampleAlloc(ptr pContext, size_t iSize)) — собственное состояние аллокатора не живёт в глобальных переменных, и несколько экземпляров аллокатора не мешают друг другу. Счётное состояние живёт в структуре, переносимой контекстом — полностью изоморфно паттерну «пользовательские данные колбэка» из главы 3, который вы будете встречать снова и снова. И наконец, замена происходит до любого вызова XRT: первое действие main — xrtSetAllocator, даже printf стоит после — «самое начало» не риторика, а жёсткая последовательность.

5.4契约

  • Владеющее: указатели из пяти точек входа освобождаются ровно один раз; xrtFree(NULL) законен; коду очистки не нужны слои проверок на пустоту.
  • Сохранность при неудаче Realloc: неудача возвращает NULL, содержимое и валидность исходного блока неизменны; после успеха старый указатель мёртв и больше не используется.
  • SecureZero не опционален: очистка чувствительной памяти только через него; memset может быть оптимизирован прочь, и этот риск проявляется только в release-сборках.
  • Блокировка аллокатора: xrtSetAllocator действует только до первого выделения; во время работы аллокатор постоянен, горячие пути без блокировок; «самое начало» — первое действие main.
  • Выравнивание: точки входа выделяют с максимальным базовым выравниванием; возвращённые указатели годятся для любых скаляров, структур и векторов; выравнивание элементов контейнеров гарантирует библиотека.
  • Форма колбэков: три колбэка аллокатора следуют общебиблиотечной конвенции — ведущий указатель контекста несёт состояние, никаких глобальных переменных.

Типичные ошибки

Ловушка 1: перезапись исходного указателя результатом Realloc:Ловушка 1: перезапись исходного указателя результатом Realloc

Симптомы: случайные утечки памяти (ветви отказа), усиливающиеся под нагрузкой или при внедрении OOM.

Причина: xrtRealloc при неудаче возвращает NULL, а исходный блок остаётся валидным — перезаписав исходный указатель возвращаемым значением, вы теряете его в момент неудачи, и тот блок уже никто не освободит.

错误写法
pValues = (unsigned char*)xrtRealloc(pValues, 4096);
if ( pValues == NULL ) {
	return 1;   /* исходный указатель перезаписан — блок до 4096 утекает навсегда */
}
正确写法
unsigned char* pNew = (unsigned char*)xrtRealloc(pValues, 4096);
if ( pNew == NULL ) {
	xrtFree(pValues);   /* исходный блок валиден: чистим по пути отказа как обычно */
	return 1;
}
pValues = pNew;         /* принимаем новый указатель только при успехе */
Ловушка 2: очистка ключей через memset:Ловушка 2: очистка ключей через memset

Симптомы: аудит безопасности находит фрагменты ключей в дампах кучи или повторно используемой памяти; в release-сборках проявляется чаще, чем в debug.

Причина: компилятор видит memset(key, 0, n) сразу за которым следует free(key), признаёт очистку «мёртвой записью» и удаляет целиком — разрешённая стандартом оптимизация, оставляющая чувствительные данные в куче.

错误写法
memset(pSecretKey, 0, iKeySize);
xrtFree(pSecretKey);   /* memset почти наверняка удалён оптимизацией — ключ остаётся в памяти кучи */
正确写法
xrtSecureZero(pSecretKey, iKeySize);
xrtFree(pSecretKey);   /* очистка гарантированно происходит на самом деле */

Практические задания

Базовое:Чек-лист пяти точек входа

Следуя examples/core/memory/main.c, напишите программу: Calloc диапазон, Realloc больше и проверьте сохранность старого содержимого, MemDup со сверкой memcmp и освобождение в обратном порядке. Напечатайте все результаты проверок.

Продвинутое: счётный аллокатор

Реализуйте «считающий байты» аллокатор (отслеживающий текущее число живых байт); после замены прогоните отрезок смешанного кода выделения/освобождения и напечатайте в конце число живых байт и вердикт об утечке. Подсказка: три колбэка несут состояние счёта в указателе контекста; выделение прибавляет, освобождение вычитает; когда колбэк realloc получает старый указатель, сначала вычтите старый размер — как его взять? Проще всего выделять один дополнительный заголовок с размером и читать его в колбэке. Закончив, вы лучше почувствуете, что значит «аллокатор — это полный бэкенд».

Вызов: проверка времени блокировки

Напишите программу, проверяющую границу блокировки xrtSetAllocator: первая замена проходит, происходит одно выделение, вторая замена обязана провалиться. Затем ответьте: если бы XRT разрешал замену во время работы, какую защиту потребовал бы ваш счётный аллокатор? (Подсказка: подумайте, нужны ли блокировки для чтения и записи самой таблицы аллокатора, когда два потока выделяют одновременно.) Критерий приёмки: возвращаемые значения двух замен — одно истинное и одно ложное, вывод совпадает с комментариями.

Итоги

ТемаКраткая справка
Пять точек входаMalloc / Calloc / Realloc / MemDup / Free (NULL допустим; очистка без проверок)
ВладеющееКто получил, тот освобождает, ровно один раз; пути отказа тоже убирают
ReallocПри неудаче NULL, исходный блок валиден — сначала принять новый указатель, потом перезаписывать
Чувствительная очисткаxrtSecureZero; никогда не полагаться на memset
АллокаторxrtSetAllocator заменяет до первого выделения; дальше блокирован навсегда; замена — первое действие main
Семейство AtxrtMallocAt и др. несут __FILE__/__LINE__ — краеугольный камень отладочного модуля
Сценарии смены бэкендаВстраивание в хост (учёт), квоты (превышение как OOM), A/B-сравнение бэкендов

📖 Справочники API в этой главе:memory (19 записей)core (25 записей)

Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки