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 г.