web-dev-qa-db-ja.com

十分なスペースが割り当てられた状態で、メンバーアドレスを介して構造のサイズを超えてアクセスしても問題ありませんか?

具体的には、次のコード、マーカーの下の行はOKですか?

struct S{
    int a;
};

#include <stdlib.h>

int main(){
    struct S *p;
    p = malloc(sizeof(struct S) + 1000);
    // This line:
    *(&(p->a) + 1) = 0;
}

人々は ここ と主張しましたが、説得力のある説明や参照を与えた人は誰もいません。

彼らの議論はわずかに異なる根拠に基づいていますが、本質的に同じです

typedef struct _pack{
    int64_t c;
} pack;

int main(){
    pack *p;
    char str[9] = "aaaaaaaa"; // Input
    size_t len = offsetof(pack, c) + (strlen(str) + 1);
    p = malloc(len);
    // This line, with similar intention:
    strcpy((char*)&(p->c), str);
//                ^^^^^^^
21
iBug

少なくとも1989年のCの標準化以来の意図は、実装が配列の境界をチェックできるようにする配列アクセスのためであることでした。

メンバー_p->a_は、タイプintのオブジェクトです。 C11 6.5.6p7

7 [additive operator]の目的のために、配列の要素ではないオブジェクトへのポインターは、と同じように動作します。タイプが1の長さの配列の最初の要素へのポインターその要素タイプとしてのオブジェクトの

したがって、

_&(p->a)
_

intへのポインタです。ただし、オブジェクトタイプとしてintを使用して、長さ1の配列の最初の要素へのポインタであるかのようにもなります。

6.5.6p8 を使用すると、配列の終わりを過ぎたところへのポインタである&(p->a) + 1を計算できるため、未定義の動作はありません。ただし、そのようなポインタの逆参照は無効です。 付録J.2 の説明から、次の場合の動作は定義されていません。

配列オブジェクトおよび整数型への、またはそのすぐ先へのポインターの加算または減算は、配列オブジェクトのすぐ先を指す結果を生成し、評価される単項_*_演算子のオペランドとして使用されます(6.5。 6)。

上記の式では、配列は1つだけで、1つは(あたかも)正確に1つの要素を持っています。 &(p->a) + 1が逆参照されると、長さ1の配列が範囲外でアクセスされ、 未定義の振る舞い が発生します。

動作[...]、[C11]標準は要件を課していません

それを言っていることに注意してください

考えられる未定義の動作は、状況を完全に無視する予測できない結果を伴う、環境に特徴的な文書化された方法での翻訳またはプログラム実行中の動作(診断メッセージの発行の有無にかかわらず)、翻訳または実行の終了にまで及びます。 (診断メッセージの発行を伴う)。

最も一般的な動作は状況を完全に無視する、つまり、ポインタが直後のメモリ位置を参照しているように動作するということは、他の種類の動作が標準の観点から受け入れられないという意味ではありません。ビュー-標準は、想像できるすべての、想像できない結果を許可します。


C11標準テキストは漠然と書かれているとの主張があり、委員会の意図はこれが実際に許可されることであり、以前は問題がなかったでしょう。それは本当ではない。 [1992年12月10日付けの欠陥レポート#017からC89]に対する委員会の回答の一部を読んでください。

質問16

[...]

応答

配列の配列の場合、6.3.6ページの47ページの12〜40行で許可されているポインタ演算は、Word objectの使用を特定のオブジェクトが決定されたことを示すものとして解釈することによって理解されます。ポインタのタイプと値によって直接、隣接によってそれに関連する他のオブジェクトではありません。したがって、式がこれらのアクセス許可を超える場合、動作は定義されていません。たとえば、次のコードの動作は未定義です。

_ int a[4][5];

 a[1][7] = 0; /* undefined */ 
_

準拠する実装の中には、配列境界違反の診断を選択するものもあれば、そのような試行されたアクセスを明らかな拡張セマンティクスで正常に解釈することを選択するものもあります。

(太字の強調鉱山)

同じwould n'tが構造体のスカラーメンバーに転送される理由はありません。特に、6.5.6p7が、それらへのポインタがポインタと同じように動作すると見なされるべきであると述べている場合はそうです。オブジェクトのタイプを要素タイプとして持つ長さ1の配列の最初の要素に

連続するstructsをアドレス指定する場合は、いつでも最初のメンバーへのポインターを取得し、それをstructへのポインターとしてキャストして、代わりにそれを進めることができます。

_*(int *)((S *)&(p->a) + 1) = 0;
_
24
Antti Haapala

配列ではないものにアクセスしているため、これは未定義の動作です(int a 以内に struct S)配列として、そしてその範囲外です。

目的を達成する正しい方法は、最後のstructメンバーとしてサイズのない配列を使用することです。

#include <stdlib.h>

typedef struct S {
    int foo;    //avoid flexible array being the only member
    int a[];
} S;

int main(){
    S *p = malloc(sizeof(*p) + 2*sizeof(int));
    p->a[0] = 0;
    p->a[1] = 42;    //Perfectly legal.
}
8
cmaster

C標準はそれを保証します
§6.7.2.1/ 15:

[...]適切に変換された構造体オブジェクトへのポインタは、その最初のメンバー(または、そのメンバーがビットフィールドの場合は、それが存在するユニット)を指し、その逆も同様です。構造オブジェクト内に名前のないパディングがある場合がありますが、最初はありません。

&(p->a)は_(int *)p_と同等です。 &(p->a) + 1は、2番目の構造体の要素のアドレスになります。この場合、要素は1つだけで、構造体にパディングがないため、これは機能しますが、パディングがある場合、このコードは壊れて、未定義の動作につながります。

1
haccks