Подтвердить что ты не робот

Является ли структурированная упаковка детерминированной?

Например, скажем, что у меня есть две эквивалентные структуры a и b в разных проектах:

typedef struct _a
{
    int a;
    double b;
    char c;
} a;

typedef struct _b
{
    int d;
    double e;
    char f;
} b;

Предполагая, что я не использовал никаких директив, таких как #pragma pack, и эти структуры компилируются в одном и том же компиляторе по одной и той же архитектуре, будут ли они иметь одинаковое дополнение между переменными?

4b9b3361

Ответ 1

Компилятор детерминирован; если бы это было не так, отдельная компиляция была бы невозможна. Две разные единицы перевода с тем же объявлением struct будут работать вместе; что гарантируется & sect; 6.2.7/1: Совместимые типы и составные типы.

Кроме того, два разных компилятора на одной платформе должны взаимодействовать, хотя это не гарантируется стандартом. (Это проблема качества выполнения.) Чтобы обеспечить возможность взаимодействия, компиляторы соглашаются на платформу ABI (Application Binary Interface), которая будет содержать точную спецификацию того, как представлены составные типы. Таким образом, программа, скомпилированная с одним компилятором, может использовать библиотечные модули, скомпилированные с другим компилятором.

Но вы не просто заинтересованы в детерминизме; вы также хотите, чтобы макет двух разных типов был одним и тем же.

В соответствии со стандартом два типа struct совместимы, если их члены (приняты по порядку) совместимы, и если их теги и имена участников одинаковы. Поскольку ваш пример structs имеет разные теги и имена, они несовместимы, даже если их типы-члены, поэтому вы не можете использовать тот, где требуется другое.

Может показаться странным, что стандарт позволяет тегам и именам участников влиять на совместимость. Стандарт требует, чтобы члены структуры были изложены в порядке объявления, поэтому имена не могут изменять порядок членов внутри структуры. Почему же они могут повлиять на заполнение? Я не знаю ни одного компилятора, где они есть, но стандартная гибкость основана на принципе, согласно которому требования должны быть минимальными, необходимыми для обеспечения правильного выполнения. Смещение по-разному помеченных структур не допускается в пределах единицы перевода, поэтому нет необходимости одобрять его между различными единицами перевода. И поэтому стандарт не позволяет этого. (Было бы законным, если бы реализация вносила информацию о типе в байтах заполнения struct, даже если ему нужно было детерминистически добавлять отступы, чтобы предоставить пространство для такой информации. Единственным ограничением является то, что заполнение не может быть помещено перед первым членом a struct.)

Платформа ABI, скорее всего, укажет макет составного типа без ссылки на его тег или имена участников. На конкретной платформе с платформой ABI, которая имеет такую ​​спецификацию и компилятор, документально подтвержденный платформой ABI, вы можете уйти с наложением, хотя это не будет технически корректным, и, очевидно, предпосылки делают его не переносным.

Ответ 2

Сам стандарт C ничего не говорит об этом, поэтому в принципе вы просто не можете быть уверены.

Но: скорее всего, ваш компилятор придерживается какого-то конкретного ABI, иначе общение с другими библиотеками и с операционной системой было бы кошмаром. В этом последнем случае ABI обычно предписывает точно, как работает упаковка.

Например:

  • на x86_64 Linux/BSD, ссылка SystemV AMD64 ABI является ссылкой. Здесь (§3.1) для каждого типа данных примитивного процессора подробно описано соответствие с типом С, его размером и требованием его выравнивания, и объясняется, как использовать эти данные для составления макета памяти битпостов, структур и объединений; все (помимо фактического содержимого заполнения) задан и детерминирован. То же самое относится ко многим другим архитектурам, см. эти ссылки.

  • ARM рекомендует EABI для своих процессоров, и в целом за ним следуют как Linux, так и Windows; выравнивание агрегатов указано в "Стандарт вызова процедуры для документации по архитектуре ARM", §4.3.

  • в Windows нет стандарта для разных поставщиков, но VС++ по существу диктует ABI, к которому практически привязан любой компилятор; здесь можно найти здесь для x86_64, здесь для ARM ( но для части интереса этого вопроса он просто относится к ARM EABI).

Ответ 3

Любой здравомыслящий компилятор будет создавать идентичный макет памяти для двух структур. Компиляторы обычно пишутся как совершенно детерминированные программы. Недетерминированность должна быть добавлена ​​явно и преднамеренно, и я для одного не вижу преимущества этого.

Однако это не позволяет вам использовать struct _a* для struct _b* и получать доступ к своим данным через оба. Afaik, это все равно будет нарушением строгих правил псевдонимов, даже если макет памяти идентичен, поскольку это позволит компилятору переупорядочить обращения через struct _a* с доступом через struct _b*, что приведет к непредсказуемому, undefined.

Ответ 4

будет ли у них одинаковое заполнение между переменными?

На практике они в основном любят иметь одинаковый макет памяти.

В теории, поскольку стандарт не говорит о том, как следует применять дополнения к объектам, вы не можете ничего принимать в дополнении между элементами.

Кроме того, я не вижу даже, почему вы хотите узнать/принять что-то о дополнении между членами структуры. просто напишите стандартный, совместимый код C, и все будет в порядке.

Ответ 5

Вы не можете детерминистически подойти к расположению структуры или объединения на языке C в разных системах.

В то время как много раз казалось, что макет, созданный разными компиляторами, одинаковый, вы должны учитывать случаи конвергенции, продиктованные практическим и функциональным удобством проектирования компилятора в рамках свободы выбора, предоставленной программисту стандартом, и, следовательно, неэффективны.

Стандарт C11 ISO/IEC 9899: 201x, практически не изменяющийся по сравнению с предыдущими стандартами, четко изложен в параграфе 6.7.2.1 Спецификации структуры и объединения:

Каждый член небитного поля объекта структуры или объединения выровнен в определенном порядке реализации, соответствующем его типу.

Даже в худшем случае битподы, где большая автономия остается программисту:

Реализация может выделять любой адресный блок хранения, достаточно большой для хранения битового поля. Если остается достаточно места, бит-поле, которое сразу следует за другим битовым полем в структуре, должно быть упаковано в соседние биты того же блока. Если недостаточно места, то будет ли бит-поле, которое не подходит, помещается в следующий блок или перекрывает смежные единицы, определяется реализацией. Порядок распределения бит-полей внутри единицы (от высокого порядка до младшего или низкого порядка) определяется реализацией. Выравнивание адресного блока хранения не указывается.

Просто подсчитайте, сколько раз термины "определяемые реализацией" и "неуказанные".

Согласился, что для проверки версии компилятора, машинной и целевой архитектуры, которые раньше выполнялись, чтобы использовать структуру или объединение, сгенерированные в другой системе, недоступен, вы должны получить достойный ответ на свой вопрос.

Теперь скажите, что да, есть способ.

Ясно, что это не определенно решение, но это общий подход, который можно найти во время обмена данными в разных системах: элементы структуры упаковки по значению 1 (стандартный char size).

Использование упаковки и точное определение структуры могут привести к достаточно надежному объявлению, которое может использоваться в разных системах. Упаковка заставляет компилятор удалять реализацию определенных выравниваний, уменьшая возможную несовместимость по стандарту. Кроме того, избегая использования битовых полей, вы можете удалить остаточные зависящие от реализации противоречия. Наконец, эффективность доступа из-за отсутствующего выравнивания может быть воссоздана путем ручного добавления некоторого фиктивного объявления между элементами, созданного таким образом, чтобы принудительно вернуть каждое поле в правильное выравнивание.

В качестве остаточного случая вам необходимо рассмотреть дополнение в конце структуры, которое некоторые компиляторы добавляют, но поскольку нет полезных данных, вы можете его игнорировать (если только для динамического распределения пространства, но опять-таки вы можете справиться с ним).

Ответ 6

ISO C говорит, что два типа struct в разных единицах перевода совместимы, если они имеют один и тот же тег и члены. Точнее, вот точный текст из стандарта C99:

6.2.7 Совместимый тип и составной тип

Два типа имеют совместимый тип, если их типы одинаковы. Дополнительные правила для определения совместимости двух типов описаны в 6.7.2 для спецификаторов типов, в 6.7.3 для классификаторов типов и в 6.7.5 для деклараторов. Более того, два типа структуры, объединения или перечисления, объявленные в отдельных единицах перевода, совместимы, если их теги и члены удовлетворяют следующим требованиям: если объявлено тегом, другое объявляется с тем же тегом. Если оба являются полными типами, то следующие применяются дополнительные требования: между их членами должно существовать взаимно-однозначное соответствие, чтобы каждая пара соответствующих членов была объявлена ​​с совместимыми типами и такая, что, если один член соответствующей пары объявлен с именем, другой член с тем же именем. Для двух структур соответствующие члены должны быть объявлены в том же порядке. Для двух структур или объединений соответствующие битовые поля должны иметь одинаковую ширину. Для двух перечислений соответствующие члены должны иметь одинаковые значения.

Кажется очень странным, если мы интерпретируем его с точки зрения "что, тег или имена участников могут повлиять на заполнение?" Но в принципе правила просто настолько строги, насколько они могут быть, если разрешить общий случай: несколько единиц перевода, разделяющих точный текст объявления структуры через файл заголовка. Если программы следуют более лёгким правилам, они не ошибаются; они просто не полагаются на требования к поведению из стандарта, но из других источников.

В вашем примере вы выполняете языковые правила, имея только структурную эквивалентность, но не эквивалентные имена тегов и членов. На практике это фактически не применяется; типы структуры с разными тегами и именами участников в разных единицах перевода фактически де-факто физически совместимы. От этого зависят всевозможные технологии, такие как привязки неязыковых языков к библиотекам C.

Если ваши проекты находятся на C (или С++), вероятно, стоит попытаться включить определение в общий заголовок.

Также неплохо было бы включить некоторую защиту от проблем с версиями, таких как поле размера:

// Widely shared definition between projects affecting interop!
// Do not change any of the members.
// Add new ones only at the end!
typedef struct a
{
    size_t size; // of whole structure
    int a;
    double b;
    char c;
} a;

Идея заключается в том, что любой, кто создает экземпляр a, должен инициализировать поле size до sizeof (a). Затем, когда объект передается другому программному компоненту (возможно, из другого проекта), он может проверять размер на sizeof (a). Если размер поля меньше, то он знает, что программное обеспечение, которое построило a, использует старую декларацию с меньшим количеством членов. Поэтому к несуществующим членам не следует обращаться.

Ответ 7

Любой конкретный компилятор должен быть детерминированным, но между любыми двумя компиляторов или даже одного и того же компилятора с различными параметрами компиляции, или даже между разными версиями одного и того же компилятора, все ставки отключены.

Вам намного лучше, если вы не будете зависеть от деталей структуры, или если вы это сделаете, вы должны вставлять код для проверки во время выполнения что структура на самом деле зависит от вас.

Хорошим примером этого является недавнее изменение от 32 до 64 бит архитектуры, где даже если вы не изменили размер целых чисел используемые в структуре, по умолчанию упакованы частичные целые числа; где ранее 3 32-битных целых числа в строке будут прекрасно упаковываться, теперь они упаковываются в два 64-битных слота.

Вы не можете предвидеть, какие изменения могут произойти в будущем; если вы зависите от деталей, которые не гарантированы языком, такие как структура упаковки, вы должны проверить свои предположения во время выполнения.

Ответ 8

Да. Вы всегда должны принимать детерминированное поведение от своего компилятора.

[EDIT] Из комментариев ниже видно, что многие программисты Java читают вопрос выше. Пусть будет ясно: C structs не генерируют никаких имен, хешей или подобных объектов в объектных файлах, библиотеках или dll. Подписи функций C также не относятся к ним. Это означает, что имена участников могут быть изменены по своему усмотрению - действительно! - при условии, что тип и порядок переменных-членов одинаковы. В C две структуры в примере эквивалентны, так как упаковка не меняется. что означает, что следующее злоупотребление совершенно справедливо в C, и, безусловно, гораздо худшее злоупотребление можно найти в некоторых наиболее широко используемых библиотеках.

[EDIT2] Никто не должен осмеливаться делать что-либо из следующего в С++

/* the 3 structures below are 100% binary compatible */
typedef struct _a { int a; double b; char c; }
typedef struct _b { int d; double e; char f; }
typedef struct SOME_STRUCT { int my_i; double my_f; char my_c[1]; }

struct _a a = { 1, 2.5, 'z' };
struct _b b;

/* the following is valid, copy b -> a  */
*(SOME_STRUCT*)&a = *(SOME_STRUCT*)b;
assert((SOME_STRUCT*)&a)->my_c[0] == b.f);
assert(a.c == b.f);

/* more generally these identities are always true. */
assert(sizeof(a) == sizeof(b));
assert(memcmp(&a, &b, sizeof(a)) == 0);
assert(pure_function_requiring_a(&a) == pure_function_requiring_a((_a*)&b));
assert(pure_function_requiring_b((b*)&a) == pure_function_requiring_b(&b));

function_requiring_a_SOME_STRUCT_pointer(&a);  /* may generate a warning, but not all compiler will */
/* etc... the name space abuse is limited to the programmer imagination */