web-dev-qa-db-ja.com

構造体のパッキングは決定的ですか?

たとえば、異なるプロジェクトに2つの同等の構造体abがあるとします。

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

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

#pragma packのようなディレクティブを使用しておらず、これらの構造体が同じアーキテクチャーの同じコンパイラーでコンパイルされていると仮定すると、変数間のパディングは同じになりますか?

42
Govind Parmar

コンパイラーは決定論的です。そうでない場合、個別のコンパイルは不可能です。同じstruct宣言を持つ2つの異なる翻訳単位が一緒に機能します。 §6.2.7/ 1:互換型と複合型 によって保証されています。

さらに、同じプラットフォーム上の2つの異なるコンパイラshouldは相互運用できますが、これは標準では保証されていません。 (これは実装品質の問題です。)相互運用性を可能にするために、コンパイラの作成者は、複合型の表現方法の正確な仕様を含むプラットフォームABI(Application Binary Interface)に同意します。このようにして、1つのコンパイラでコンパイルされたプログラムが、別のコンパイラでコンパイルされたライブラリモジュールを使用することができます。

しかし、あなたは決定論に興味があるだけではありません。また、2つの異なるタイプのレイアウトを同じにする必要があります。

標準によれば、2つのstruct型は、それらのメンバー(順序どおりに)に互換性があり、タグとメンバー名が同じ場合に互換性があります。サンプルstructsは異なるタグと名前を持っているため、メンバーの種類が異なる場合でも互換性がないため、一方が必要な場合は使用できません。

標準がタグとメンバー名が互換性に影響を与えることを許可しているのは奇妙に思えるかもしれません。標準では、構造体のメンバーを宣言順にレイアウトする必要があるため、名前は構造体内のメンバーの順序を変更できません。では、なぜパディングに影響を与えるのでしょうか?どこでコンパイラを使用するかは知りませんが、標準の柔軟性は、要件が正しい実行を保証するために最低限必要であるという原則に基づいています。異なるタグ付けされた構造体のエイリアスは翻訳ユニット内では許可されないため、異なる翻訳ユニット間でそれを容認する必要はありません。そのため、標準では許可されていません。 (実装がstructのパディングバイトに型に関する情報を挿入することは正当です。たとえそのような情報のためのスペースを提供するためにパディングを決定的に追加する必要がある場合でも。唯一の制限はパディングを前に配置できないことですstructの最初のメンバー。)

プラットフォームABIは、タグまたはメンバー名を参照せずに複合型のレイアウトを指定する可能性があります。特定のプラットフォームでは、そのような仕様とプラットフォームABIに準拠するように文書化されたコンパイラを備えたプラットフォームABIを使用すると、エイリアスは回避できますが、技術的には正しくありませんが、明らかに前提条件により移植性がなくなります。

55
rici

C標準自体はそれについて何も述べていないので、原則に沿ってあなたはただ確信が持てません。

But:ほとんどの場合、コンパイラは特定のABIに準拠しています。オペレーティングシステムは悪夢です。この最後のケースでは、ABIは通常、パッキングの仕組みを正確に規定します。

例えば:

  • x86_64 Linux/BSDでは、 SystemV AMD64 ABI がリファレンスです。ここ(3.1)では、すべてのプリミティブプロセッサデータ型について、C型との対応、サイズ、およびアライメント要件について詳しく説明し、このデータを使用してビットフィールド、構造体、および共用体のメモリレイアウトを構成する方法について説明します。 everything(パディングの実際の内容に加えて)が指定され、確定的である。他の多くのアーキテクチャでも同じことが言えます。 これらのリンク をご覧ください。

  • ARM そのEABIを推奨 プロセッサ用で、通常はLinuxとWindowsの両方が続きます。集約のアライメントは、「ARM Architecture Documentation」の手続き呼び出し標準」、§4.3で指定されています。

  • windowsにはクロスベンダー標準はありませんが、VC++は基本的にABIを指示します。これには実質的にすべてのコンパイラが準拠しています。それは見つけることができます here for x86_64、 here for ARM(しかし、この質問の興味のある部分では、= ARM EABI)。

15
Matteo Italia

正常なコンパイラは、2つの構造体に対して同一のメモリレイアウトを生成します。コンパイラは通常、完全に決定的なプログラムとして記述されています。非決定論を明示的かつ故意に追加する必要がありますが、私はそうすることの利点を理解できません。

ただし、notを使用すると、struct _a*struct _b*にキャストし、両方を介してそのデータにアクセスできます。 Afaik、これは、メモリレイアウトが同一であっても、厳密なエイリアスルールに違反します。コンパイラがstruct _a*を介したアクセスをstruct _b*を介したアクセスに並べ替えることができるため、予測不能、未定義の動作。

10
cmaster

変数間に同じパディングがありますか?

実際には、ほとんど同じメモリレイアウトが必要です。

理論的には、標準ではオブジェクトにパディングをどのように使用するかについてあまり言及していないため、要素間のパディングについては何も想定できません。

また、構造体のメンバー間のパディングについて何かを知りたい、または仮定したい理由がわからない。標準の準拠Cコードを書くだけで大丈夫です。

8
David Haim

異なるシステム上のC言語で構造体またはユニオンのレイアウトに決定論的にアプローチすることはできません。

多くの場合、異なるコンパイラによって生成されたレイアウトは同じに見えるかもしれませんが、標準によってプログラマに残された選択の自由の範囲で、コンパイラ設計の実用的および機能的な利便性によって決まる収束を考慮する必要があります。効果的。

C11規格ISO/IEC 9899:2011、以前の規格とほとんど変わらず、段落6.7.2.1構造体および共用体指定子

構造体または共用体オブジェクトの各非ビットフィールドメンバーは、そのタイプに適した実装定義の方法で整列されます。

最悪の場合、大きな自律性がプログラマに委ねられているビットフィールドの場合:

実装では、ビットフィールドを保持するのに十分な大きさのアドレス可能なストレージユニットを割り当てることができます。十分なスペースが残っている場合、構造内の別のビットフィールドの直後に続くビットフィールドは、同じユニットの隣接するビットにパックされます。十分なスペースが残っていない場合、適合しないビットフィールドが次のユニットに配置されるか、隣接するユニットと重複するかは、実装で定義されます。ユニット内のビットフィールドの割り当て順序(高位から低位へ、または低位から高位へ)は実装定義です。アドレス可能なストレージユニットのアライメントは指定されていません。

テキストに「実装定義」および「未指定」という用語が何回現れるかを数えるだけです。

別のシステムで生成された構造体またはユニオンを使用する前に、コンパイラーのバージョン、マシン、およびターゲットアーキテクチャーをそれぞれ実行することを確認することは手頃な価格であることに同意してください。

ええ、回避策があります。

明確な解決策ではないことを明確にしてくださいですが、データ構造交換が異なるシステム間で共有される場合に見られる一般的なアプローチです:値1(標準の文字サイズ)に構造要素をパックします。

パッキングと正確な構造定義を使用すると、さまざまなシステムで使用できる十分に信頼性の高い宣言を作成できます。パッキングにより、コンパイラーは実装定義のアライメントを強制的に削除し、標準による最終的な非互換性を削減します。さらに、ビットフィールドの使用を回避することで、実装に依存する不整合を解消できます。最後に、各フィールドを強制的に正しい位置に戻すように作成された要素間にダミーの宣言を手動で追加することにより、位置合わせの欠如によるアクセス効率を再現できます。

残余のケースとして、いくつかのコンパイラーが追加する構造体の終わりにパディングを考慮する必要がありますが、関連する有用なデータがないため、それを無視できます(動的なスペース割り当ての場合を除き、再度対応できます)。

5
Frankie_C

ISO Cは、異なる翻訳単位の2つのstruct型が同じタグとメンバーを持っている場合、互換性があると述べています。より正確には、C99標準の正確なテキストを次に示します。

6.2.7互換型と複合型

タイプが同じ場合、2つのタイプには互換性のあるタイプがあります。 2つの型に互換性があるかどうかを判断するための追加の規則については、型指定子については6.7.2、型修飾子については6.7.3、宣言子については6.7.5で説明しています。さらに、別々の翻訳単位で宣言された2つの構造体、ユニオン、または列挙型は、それらのタグとメンバーが次の要件を満たす場合、互換性があります。一方がタグで宣言されている場合、他方は同じタグで宣言されます。両方が完全な型である場合、次の追加要件が適用されます。対応するメンバーの各ペアが互換性のある型で宣言され、対応するペアの一方のメンバーが名前で宣言された場合、他のメンバーは同じ名前で宣言されます。 2つの構造の場合、対応するメンバーは同じ順序で宣言されます。 2つの構造体またはユニオンの場合、対応するビットフィールドの幅は同じでなければなりません。 2つの列挙の場合、対応するメンバーは同じ値を持ちます。

「タグ名やメンバー名がパディングに影響する可能性がある」という観点から解釈すると、非常に奇妙に思えます。しかし、基本的には、ルールは、一般的なケースを許可しながら、可能な限り厳格です:複数の翻訳単位が構造体宣言の正確なtextを共有するヘッダーファイル。プログラムがより緩やかな規則に従う場合、それらは間違っていません。それらは、標準からの動作の要件に依存しているのではなく、他の場所からの動作に依存しています。

この例では、構造的な同等性のみを持ち、同等のタグ名とメンバー名を持たないことにより、言語ルールに違反しています。実際には、これは実際には強制されません。異なる翻訳単位の異なるタグとメンバー名を持つ構造体型は、de factoとにかく物理的に互換性があります。非C言語からCライブラリへのバインディングなど、あらゆる種類のテクノロジーがこれに依存しています。

両方のプロジェクトが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)に初期化する必要があるという考え方です。次に、オブジェクトが別のソフトウェアコンポーネント(おそらく他のプロジェクトから)に渡されると、itssizeof (a)に対してサイズをチェックできます。 。サイズフィールドが小さい場合、aを構築したソフトウェアが、メンバーが少ない古い宣言を使用していることがわかります。したがって、存在しないメンバーにはアクセスしないでください。

4
Kaz

特定のコンパイラーは決定論的である必要がありますが、2つのコンパイラー間、異なるコンパイルオプションを持つ同じコンパイラー間、または同じコンパイラーの異なるバージョン間でさえ、すべての賭けは無効です。

構造の詳細に依存していない場合、または依存している場合は、実行時に構造が実際に依存していることを確認するコードを埋め込む必要があります。

この良い例は、32ビットアーキテクチャから64ビットアーキテクチャへの最近の変更です。構造で使用される整数のサイズを変更しなくても、部分整数のデフォルトのパッキングが変更されます。以前は3つの32ビット整数が連続して完全にパックされていましたが、現在は2つの64ビットスロットにパックされています。

将来どのような変更が発生するかを予測することはできません。構造パッキングなど、言語で保証されていない詳細に依存している場合は、実行時に仮定を検証する必要があります。

2
ddyer