Представляю свою библиотеку для обнуления байт выравнивания (padding) в unmanaged структурах.
Зачем это нужно?
Обнуление байт паддинга (padding) обеспечивает детерминированное состояние памяти, что критически важно для двоичного сравнения или вычисления хэша. И не менее важно при бинарной сериализации.
Подробнее о том, что такое паддинг, можно прочитать здесь.
[StructLayout(LayoutKind.Sequential)]
struct ExampleStruct
{
public byte A; // 1 байт
// --- 3 байта выравнивания (padding) ---
public int B; // 4 байта
}
Структуры с неинициализированным паддингом могут быть получены из сторонних библиотек или своего кода.
Если вы когда-нибудь делали что-то подобное, то мои поздравления, у вас в паддинге лежат мусорные байты или возможно конфиденциальные данные.
[SkipLocalsInit]
ExampleStruct[] Method0()
{
Span<ExampleStruct> arr = stackalloc ExampleStruct[10];
for (var i = 0; i < arr.Length; i++)
{
ref var s = ref arr[i];
s.A = (byte) i;
s.B = i * 10;
}
return arr.ToArray();
}
[SkipLocalsInit]
ExampleStruct[] Method1()
{
var arr = GC.AllocateUninitializedArray<ExampleStruct>(10);
for (var i = 0; i < arr.Length; i++)
{
ref var s = ref arr[i];
s.A = (byte) i;
s.B = i * 10;
}
return arr;
}
ExampleStruct Method2()
{
Unsafe.SkipInit(out ExampleStruct s);
s.A = 5;
s.B = 10;
return s;
}
И в этом нет ничего страшного, пока вы не решите посчитать хеш, сделать двоичное сравнение или сохранить структуру в бинарном виде, например, в файл. Хеши и сравнения будут сломаны, потому что при, казалось бы, одинаковых значениях реальные байты структур будут отличаться. А сериализация будет потенциальным местом для утечки конфиденциальных данных, т. к. в паддинг попадёт то, что было ранее записано в этот участок памяти.
Что делать?
Решение в лоб. Для каждой структуры можно прописать оффсеты с паддингом и обнулять по этим оффсетам. Но это даже не обсуждается, такой способ подходит разве что при обучении программированию.
Наивное решение. Пройтись рефлексией по всем полям структуры, посчитать оффсеты паддингов и сохранить их в массив. Массив оффсетов кешировать для переиспользования. Когда нужно обнулить падиинги, делать это по оффсетам, полученным ранее.
И это вполне рабочее решение, если не важна производительность.
Не забываем, что структуры могут иметь десятки полей, которые могут быть другими структурами, а уровень вложенности ничем не ограничен.
Но можно пойти дальше. Собрать оффсеты. И создать в рантайме DynamicMethod, который будет равносилен тому, как если бы мы руками для каждой структуры прописали, какие байты нужно обнулить:
*(ptr + offset) = (byte) 0;
Т.е. это то, что было предложено в «решении в лоб», но не требует участия человека.
Пример кода из моей библиотеки StructPadding:
private static ZeroAction? CreateZeroer(Type type)
{
var regions = AnalyzePadding(type);
if (regions.Count == 0) return null;
var method = new DynamicMethod($"ZeroPadding_{type.Name}",
null,
[ typeof(byte*) ],
typeof(Zeroer).Module,
true);
var il = method.GetILGenerator();
foreach (var region in regions)
{
switch (region.Length)
{
case 1:
il.Emit(OpCodes.Ldarg_0); // push ptr
il.Emit(OpCodes.Ldc_I4, region.Offset); // push offset
il.Emit(OpCodes.Add); // ptr + offset
il.Emit(OpCodes.Ldc_I4_0); // 0
il.Emit(OpCodes.Stind_I1); // *(ptr+offset) = (byte) 0
break;
case 2:
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldc_I4, region.Offset);
il.Emit(OpCodes.Add);
il.Emit(OpCodes.Ldc_I4_0);
il.Emit(OpCodes.Stind_I2); // *(short*) = 0
break;
case 4:
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldc_I4, region.Offset);
il.Emit(OpCodes.Add);
il.Emit(OpCodes.Ldc_I4_0);
il.Emit(OpCodes.Stind_I4); // *(int*) = 0
break;
case 8:
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldc_I4, region.Offset);
il.Emit(OpCodes.Add);
il.Emit(OpCodes.Ldc_I8, 0L);
il.Emit(OpCodes.Stind_I8); // *(long*) = 0
break;
default:
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldc_I4, region.Offset);
il.Emit(OpCodes.Add); // Destination address
il.Emit(OpCodes.Ldc_I4_0); // Value (0)
il.Emit(OpCodes.Ldc_I4, region.Length); // Size
il.Emit(OpCodes.Initblk); // memset
break;
}
}
il.Emit(OpCodes.Ret);
return (ZeroAction) method.CreateDelegate(typeof(ZeroAction));
}
Такой динамический метод скомпилируется при первом вызове, а все последующие вызовы не будут отличаться от любого другого метода, который был написан руками в IDE.
А это означает, что нет рефлексии и итерации по списку полей в Hot Path. Поиск паддингов делается только один раз, поддерживаются структуры с произвольным количеством полей и любым уровнем вложенности.
Это я и сделал в StructPadding.
StructPadding
Скачать можно здесь:
Github: https://github.com/viruseg/StructPadding
Nuget: https://www.nuget.org/packages/StructPadding
Как использовать?
Обнуление паддинга в структуре:
using StructPadding;
[StructLayout(LayoutKind.Sequential)]
public struct MyData
{
public byte Id; // После этого поля будет 7 байт паддинга
public long Value;
}
void Example(MyData data)
{
Zeroer.Zero(ref data);
// После вызова: байты паддинга гарантированно равны 0
}
Обнуление паддинга в массиве:
public void Example0(Span<MyData> arr)
{
Zeroer.ZeroArray(arr);
// После вызова: байты паддинга гарантированно равны 0
}
public void Example1(MyData[] arr)
{
Zeroer.ZeroArray(arr);
// После вызова: байты паддинга гарантированно равны 0
}
public void Example2(MyData[] arr)
{
// Тоже самое что и в предыдущем примере, но только через метод-расширение.
arr.ZeroPadding();
}
Обнулить паддинги можно только в unmanaged типах:
Комментарии (20)

Nagg
21.11.2025 14:10Если вы не используете unsafe код, вы никогда не столкнетесь с никакими багами из-за мусора в паддинга (он там может быть даже без интеропа, просто со стека)
Абсолютно бессмысленнго его занулять для структур без StructLayout.Explicit, потому что JIT может легко структуру с зануллеными падингами временно куда-то скопировать на стек только полями и будет опять мусор со стека в полях, короче говоря, это UB.
Никогда не пишите код который считает хэш, сравнивает структуры через memcpy/memcmp идиомы, это отличный способ выстрелить себе в ногу с гранатомета. Единственное когда это возможно это для структур без падингов и блиттабл полями, но в дотнете нет АПИ через которое вы можете это проверить (+ может быть различие на разных архитектурах/платформах)
Рекомендую к прочтению статью.

viruseg Автор
21.11.2025 14:10Если структура лежит в массиве или где-то внутри класса, JIT ничего с ней не сделает. Хеш массива помогает мгновенно понять, были ли в нём изменения. И если не было, то пропустить огромное количество логики. Полезно, например, в играх, где нужно каждый кадр пересчитывать состояние мира. Или когда нужно часто писать в файл, и вместо насилия над диском проще сравнивать хеш и писать только когда реально изменились данные.

Nagg
21.11.2025 14:10Вашу задачу невозможно решить в общем случае без около-UB люто unsafe кода (который обязательно выстрелит в виде CVE) в .NET. Как я вам уже написал: Единственное когда это возможно это для структур без падингов и блиттабл полями, но в дотнете нет АПИ через которое вы можете это проверить

viruseg Автор
21.11.2025 14:10Я привёл пример с записью в файл. Проверка хеша, если не совпали, то идёт запись. Какая тут уязвимость может появиться? В худшем случае при несовпадении байт файл будет перезаписан, когда реальные данные не изменились. В примере с игрой в худшем случае будет выполнена тяжёлая логика лишний раз, но без проверки по хешу пришлось бы каждый кадр тратить большие ресурсы на эту же проверку. Естественно, что надо понимать, для чего используется подобный инструмент и какие опасности он несёт. Но это касается любых вещей. Очевидно, что подобный хеш не должен уйти в долговременное хранение, он предназначен для здесь и сейчас, для текущей сессии.

Nagg
21.11.2025 14:10Мой поинт в том что вы рекламируете это как библиотеку и у вас ни слова нет про то насколько это хрупкий/ненадежный около-UB/unsafe инструмент. Что бы вы понимали, бинарные сериализаторы, которые делают похожие хаки сериализуя объекты в байт массивы (и десереиализуя) без учета паддингов были не раз причиной CVE.

viruseg Автор
21.11.2025 14:10В моём коде нет никакого около-UB. Паддинги определяются через рефлексию здесь и сейчас. Генерация метода через IL - вполне официальный способ, а не хак. Остаётся лишь вопрос к тому, кто будет применять инструмент. Но это риторический вопрос. Я совершенно точно не в ответе за тех, кто пытается сделать подобные вещи без понимания их сути.

Kolonist
21.11.2025 14:10А вы точно уверены, что хэш массива структур (не байт) будет считаться как хэш области памяти, занимаемой массивом?
Если так, то у меня плохие новости для разработчиков (и пользователей) такой библиотеки вычисления хэша :)

viruseg Автор
21.11.2025 14:10Библиотека из статьи вообще не считает хеши. О чём речь вообще, для кого плохие новости?

Kolonist
21.11.2025 14:10Ну вы же несколько раз в ответах ссылались на то, что ваша библиотека нужна, например, чтобы можно было считать хэши массивов структур, не опасаясь мусора в паддингах.
Ну вот у меня и возник вопрос, точно ли библиотеки, которые считают хэши, поступают именно так, как вы пишите? Т.е. интерпретируют массив структур, как непрерывную область памяти, и считают хэш от получившегося байтового буфера?

viruseg Автор
21.11.2025 14:10Вот, можете ознакомится с исходниками: https://github.com/viruseg/GxHash.128bits.Overloads
Т.е. интерпретируют массив структур, как непрерывную область памяти, и считают хэш от получившегося байтового буфера?
Всё так.

Kolonist
21.11.2025 14:10Так это же ваша библиотека )
А у других вы такое видели? В Майкрософтовских из System.Security.Cryptography, например?

viruseg Автор
21.11.2025 14:10Другие я даже не искал. Потому что использую именно GxHash, т.к. он очень быстрый. Поэтому и написал для него обертку. Уверен, что если кто-то ставил цель получить хеш массива структур, то решил эту задачу точно так же. Span используется повсеместно.

Nagg
21.11.2025 14:10Забавно что вы написали небезопасный хэшер который считает с мусором в хеше, и потом героически решили это проблему через другую библиотеку, которая в комплект не входит :D кстати у вас там явно баг в коде https://github.com/viruseg/GxHash.128bits.Overloads/blob/master/GxHash.128bits.Overloads/Hash128Methods.cs#L190 - вместо buffer.Length должно было видимо бы asBytes.Length
withkittens
А зачем я буду решать это всё делать с неупакованными структурами?
viruseg Автор
Из плюсовой dll пришел массив структур, и у них паддинги набиты мусором. А нужно посчитать хеш, например, такой либой. И тут два варианта: или копировать их в новый массив, при чём нужно будет написать логику этого копирования, используя схожие техники, описанные мной в статье. Или обнулить паддинги. Это один пример из тысяч, зачем это нужно делать.
withkittens
Паддинг - это такая implementation-defined штука: сегодня там три байта между полями, завтра компилятор обновился и стало семь. Такие структуры в принципе нельзя шарить между разными платформами. Разве только если любите стрелять себе по ногам. Выкиньте эту плюсовую либу и замените на кросс-платформенную.
viruseg Автор
C# ли бы тоже выкинуть, где сделали что-то подобное? Или собственный код, где такой подход даёт ускорение, т.к. создаёт очень много таких структур и часто. А считать хеш нужно очень редко.
withkittens
Если вам важна разница в производительности между zero-initialized структурами и
SkipLocalsInit, то ваш кейс уже довольно специфичен, а не один из тысяч (вы ведь провели профилирование кода и уверены, что это необходимая оптимизация, верно?).Тогда хешу можно скормить данные и поле за полем, ручками.
viruseg Автор
А можно написать либу один раз и не писать ручками. Я не очень понимаю, о чём спор. Если вам не нужно, это не означает, что никому не нужно. Мы же тут программированием занимаемся. Если что-то можно автоматизировать, оно должно быть автоматизировано.
withkittens
О том, что если вы решаете задачу из статьи, вы почти наверняка что-то делаете не так.