Общество

StructPadding

Представляю свою библиотеку для обнуления байт выравнивания (padding) в unmanaged структурах. Зачем это нужно? Обнуление байт паддинга (padding) обеспечивает детерминированное состояние памяти, что критически важно для двоичного сравнения или вычисления хэша. И не менее важно при бинарной сериализации. Подробнее о том, что такое паддинг, можно прочитать здесь. [StructLayout(LayoutKind.Sequential)] struct ExampleStruct { public byte A; // 1 байт // --- 3 байта выравнивания (padding) --- public int B; // 4 байта } Структуры с неинициализированным паддингом могут быть получены из сторонних библиотек или своего кода. Если вы когда-нибудь делали что-то подобное, то мои поздравления, у вас в паддинге лежат мусорные байты или возможно конфиденциальные данные. [SkipLocalsInit] ExampleStruct[] Method0() { Span 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(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 arr) { Zeroer.ZeroArray(arr); // После вызова: байты паддинга гарантированно равны 0 } public void Example1(MyData[] arr) { Zeroer.ZeroArray(arr); // После вызова: байты паддинга гарантированно равны 0 } public void Example2(MyData[] arr) { // Тоже самое что и в предыдущем примере, но только через метод-расширение. arr.ZeroPadding(); } Обнулить паддинги можно только в unmanaged типах:

Фильтры и сортировка