tl;dr по умолчанию, MinGW на win32 ведет себя так, как будто игнорирует __attribute__((packed)), вам нужно добавить -mno-ms-bitfields strong>, чтобы заставить его работать как положено.
Несколько дней назад мне поручили портировать простое сетевое приложение на Windows 7. Однако есть небольшая проблема: для сериализации сообщений код заполнил упакованную структуру, которая затем memcpy-ed в буфер, а затем бросается в стек TCP.
Нам это может сойти с рук (каким-то образом это работает!). Большая проблема здесь в том, что все разваливается, если макет структуры не такой, как мы ожидаем! В лучшем случае приложение segfaults, потому что оно пыталось получить доступ к некоторой памяти, к которой мы не должны не обращаться, или некоторые внутренние проверки срабатывают, потому что значения настолько неверны, что просто выходят за пределы допустимого диапазона.
Допустим, я передаю эту упакованную структуру...
typedef struct _mypackedstruct { uint32_t magic_number; uint8_t version; uint32_t interesting_property; uint64_t creation_time_sec; uint64_t creation_time_usec; uint32_t first_info_no; uint32_t second_info_no; } __attribute__((packed)) MyPackedStructure;
… и код получает и анализирует сериализованную структуру, как в следующем примере псевдо-C++:
// a very simplified sample of the code that highlighted the problem MyPackedStructure destination; char* buffer = new char[sizeof(destination)]; // read some data from a previously created socket // and push them into a buffer socket.recv_message(buffer); // push the unparsed data into destination // to perform an automatic "deserialization". // Don't try this at home. Please, use serialization libraries. memcpy(&destination, buffer, sizeof(destination)); // perform validity check on the structure's content. assert(is_packet_ok(destination));
В разных дистрибутивах Linux все работает нормально. Как только я собрал проект в Windows, сработало утверждение is_packet_ok. Почему?
После просмотра кода, пытаясь понять проблему, у меня возникла идея проверить размер структуры. В Windows (с использованием MinGW) sizeof(MyPackedStructure) возвращает 40, а в Linux (с использованием трех разных компиляторов: gcc 4.6.3, gcc 4.9.2, clang/llvm 3.8) то же выражение возвращает 33 , Семь бит, безусловно, имеют большое значение! Я хочу, чтобы вы сосредоточили свое внимание на этих 40. Легко распознать, что 40 mod 8 = 0: структура была выровнена по 8 байтам. Почему атрибут упаковки был проигнорирован?
Простой поиск в Wired показал мне, что это странное поведение известно с апреля 2012 года, но тикет по-прежнему помечен как новый. В комментариях вы найдете обходной путь: установите -mno-ms-bitfields.
Что это за параметр -mno-ms-bitfields? В разделе руководства 6.36.5 Переменные атрибуты i386 говорится:
«Если в структуре используется пакет или битовые поля, возможно, Microsoft ABI размещает структуру иначе, чем обычно это делает GCC. В частности, при перемещении упакованных данных между функциями, скомпилированными с помощью GCC, и собственным компилятором Microsoft (через вызов функции или в виде данных в файле) может потребоваться доступ к любому формату».
[фрагмент]
“2. Каждый объект данных имеет требование выравнивания. Требование выравнивания для всех данных, кроме структур, объединений и массивов, — это либо размер объекта, либо текущий размер упаковки (указанный либо атрибутомalign, либо прагмой pack), в зависимости от того, что меньше. Для структур, объединений и массивов требование выравнивания является самым большим требованием выравнивания их членов. Каждому объекту назначается смещение, так что: offset %alignment_requirement == 0”
Что это означает? Это означает, что по умолчанию MinGW использует алгоритм Microsoft для расчета требований к упакованным структурам, и он работает не так, как хотелось бы.
Ожидаемое поведение (как описано в той же ссылке!):
упаковано
Атрибут "packed" указывает, что поле переменной или структуры должно иметь наименьшее возможное выравнивание: один байт для переменной и один бит для поля, если только вы не укажете большее значение с помощью атрибута "align".
Чтобы объяснить это, я напишу простую программу, которая создает упакованную структуру и инициализирует ее известными значениями. Позже программа будет проверена с помощью GDB, чтобы проверить фактическое размещение структуры в памяти.
// file: main.cpp // MyPackedStructure is the same structure we defined at the start of the blog post int main(){ // print the size of the structure std::cout << "sizeof(MyPackedStructure) = " << sizeof(MyPackedStructure) << std::endl; // prepare a structure variable... MyPackedStructure obj; obj.magic_number = 0xa5a61ff5; obj.version = 2; obj.interesting_property = 0x33; obj.creation_time_sec = 1462224873; obj.creation_time_usec = 4141457; obj.first_info_no = 5; obj.second_info_no = 2; // ... and put a breakpoint here to inspect its memory! return 0; }
Я собрал и запустил код на ArchLinux (x86, GCC 5.3.0, GDB 7.11) и…
[~/projects/experimental/sizeof_packed_struct]> g++ -o test main.cpp -g # let's build it with the latest GCC on ArchLinux, x86 [winter@timeofeve] [/dev/pts/3] [master] [~/projects/experimental/sizeof_packed_struct]> gdb ./test Reading symbols from ./test...done. (gdb) break main.cpp:29 Breakpoint 1 at 0x804875c: file main.cpp, line 29. (gdb) r Starting program: /home/winter/projects/experimental/sizeof_packed_struct/test sizeof(MyPackedStructure) = 33 Breakpoint 1, main () at main.cpp:29 29 return 0; (gdb) x /33x &obj # let's dump 33 bytes of memory from the address of `obj` 0xbffff5af: 0xf5 0x1f 0xa6 0xa5 0x02 0x33 0x00 0x00 0xbffff5b7: 0x00 0xe9 0xc7 0x27 0x57 0x00 0x00 0x00 0xbffff5bf: 0x00 0x91 0x31 0x3f 0x00 0x00 0x00 0x00 0xbffff5c7: 0x00 0x05 0x00 0x00 0x00 0x02 0x00 0x00 0xbffff5cf: 0x00
… память устроена так, как мы и ожидали: мы видим магическое число в первых 4 байтах, версия занимает только пятый байт. интересное_свойство не выровнено: поле имеет длину 4 байта, но первые 3 байта лежат в первой строке (0xbffff5af), а последний лежит во второй строке (0xbffff5b7). Обратите внимание, что MyPackedStructure в ArchLinux имеет длину 33 байта.
Если мы запустим его в Windows 7 (x86_64, mingw 4.9.3, gdb 7.6.1, PowerShell x86), мы получим…
PS C:\Users\WinterHarrison\Desktop> g++ main.cpp -o test -g PS C:\Users\WinterHarrison\Desktop> gdb .\test Reading symbols from C:\Users\WinterHarrison\Desktop\test.exe...done. (gdb) break main.cpp:29 Breakpoint 1 at 0x401498: file main.cpp, line 29. (gdb) r Starting program: C:\Users\WinterHarrison\Desktop/.\test.exe [New Thread 164.0x5c8] sizeof(MyPackedStructure) = 40 Breakpoint 1, _fu0___ZSt4cout () at main.cpp:29 29 return 0; (gdb) x /40x &obj 0x28ff08: 0xf5 0x1f 0xa6 0xa5 0x02 (0xff 0x28 0x00) 0x28ff10: 0x33 0x00 0x00 0x00 (0x53 0x40 0x0e 0x64) 0x28ff18: 0xe9 0xc7 0x27 0x57 0x00 0x00 0x00 0x00 0x28ff20: 0x91 0x31 0x3f 0x00 0x00 0x00 0x00 0x00 0x28ff28: 0x05 0x00 0x00 0x00 0x02 0x00 0x00 0x00
… что MyPackedStructure теперь имеет длину 40 байт. Тот факт, что мой Arch 32-битный, ничего не значит, так как я использую целые числа фиксированного размера.
Также обратите внимание, что невыровненных полей нет: интересное_свойство теперь начинается с первого байта второй строки (0x28ff10).
Этот факт является реальной причиной проблемы, которую я описал в начале этого поста: memcpy записывает важные (и правильно упакованные!) данные внутрь байтов заполнения, которые затем не считаются и не доступны. Когда вы попытаетесь прочитать структуру, вы получите какой-то мусор, не имеющий никакого смысла. Простой пример: после memcpy в псевдокоде в начале статьи мы ожидаем, что obj.interesting_property будет 0x33, но в Windows это 0. 0x33 будет там, где мы сейчас можем видеть 0xff (первая строка, шестой байт), но этот конкретный адрес недоступен клиентскому коду. Что ж, я могу получить к нему доступ, выполняя некоторые арифметические операции с указателями, но разве смысл использования упакованной структуры не в том, чтобы вообще избежать арифметических операций с указателями?
Теперь давайте применим предложенный обходной путь: скажите GCC «неважно, меня не волнует этот алгоритм битовых полей MS, пожалуйста, делайте то, что вы делаете в Linux», используя флаг компилятора -mno-ms-bitfields.
PS C:\Users\WinterHarrison\Desktop> g++ main.cpp -o test -g -mno-ms-bitfields PS C:\Users\WinterHarrison\Desktop> gdb .\test Reading symbols from C:\Users\WinterHarrison\Desktop\test.exe...done. (gdb) break main.cpp:29 Breakpoint 1 at 0x401498: file main.cpp, line 29. (gdb) r Starting program: C:\Users\WinterHarrison\Desktop/.\test.exe [New Thread 832.0xaf8] sizeof(MyPackedStructure) = 33 Breakpoint 1, _fu0___ZSt4cout () at main.cpp:29 29 return 0; (gdb) x /33x &obj 0x28ff0f: 0xf5 0x1f 0xa6 0xa5 0x02 0x33 0x00 0x00 0x28ff17: 0x00 0xe9 0xc7 0x27 0x57 0x00 0x00 0x00 0x28ff1f: 0x00 0x91 0x31 0x3f 0x00 0x00 0x00 0x00 0x28ff27: 0x00 0x05 0x00 0x00 0x00 0x02 0x00 0x00 0x28ff2f: 0x00
И размер, и дамп памяти выглядят как размер и дамп Linux. Это именно то, что мы хотели! Теперь все работает! Миссия выполнена, давайте покончим с этим!
Итак, чему я научился на этом опыте?
- Используйте -mno-ms-bitfields, если вы хотите использовать упакованные структуры в Windows (используя MinGW).
- Библиотеки сериализации (cereal, messagepack, protocolbuffers и т. д.) существуют по определенной причине: чтобы избежать написания одинаковых сообщений в блогах и убедиться, что сообщения сериализуются и десериализуются одинаковым образом на каждой платформе.
- Не думайте, что ваш код будет работать одинаково везде: всегда имейте набор тестов (и не усложняйте сборку и запуск на новой целевой платформе!)
- Прочитайте документацию вашего компилятора, особенно если вы используете языковые расширения.
Это все на сегодня. Дайте мне знать, если вы нашли эти заметки полезными — если они вам нравятся, рассмотрите возможность предложить мне Ко-фи, чтобы я мог продолжать писать!
Первоначально опубликовано на https://wintermade.it 6 мая 2016 г.