C-2つのポインタ間の変換動作

Dec 11 2020

更新2020-12-11:コメントの提案に対して@ "一部のプログラマーの男"に感謝します。私の根本的な問題は、私たちのチームが動的タイプのストレージエンジンを実装していることです。動的タイプのデータを格納するために、16整列の複数のchar配列[PAGE_SIZE]バッファーを割り当てます(固定構造体はありません)。効率上の理由から、バイトエンコーディングを実行したり、を使用するために追加のスペースを割り当てたりすることはできませんmemcpy

アラインメントが決定されているので(つまり、16)、残りはポインタのキャストを使用して、指定されたタイプのオブジェクトにアクセスします。次に例を示します。

int main() {
    // simulate our 16-aligned malloc
    _Alignas(16) char buf[4096];

    // store some dynamic data:
    *((unsigned long *) buf) = 0xff07;
    *(((double *) buf) + 2) = 1.618;
}

しかし、私たちのチームは、この操作が未定義の動作であるかどうかについて異議を唱えています。


私は次のような多くの同様の質問を読みました

  • x86で-Wcast-alignがchar *からint *へのキャストについて警告しないのはなぜですか?
  • 非整列位置でchar配列をintにキャストする方法は?
  • Cの未定義の振る舞い。厳密なエイリアシングルール、または不適切な配置?
  • SEI CERT C CS EXP36-C

しかし、これらは私のC標準の解釈とは異なり、それが私の誤解であるかどうかを知りたいと思います。

主な混乱は、C11のセクション6.3.2.3#7に関するものです。

オブジェクトタイプへのポインタは、別のオブジェクトタイプへのポインタに変換される場合があります。結果のポインタが参照されたタイプに対して正しく整列されていない場合68)、動作は定義されていません。

68)一般に、「正しく整列された」という概念は推移的です。タイプAへのポインターがタイプBへのポインターに対して正しく整列され、次にタイプCへのポインターに対して正しく整列された場合、タイプへのポインターはAは、タイプCへのポインターに対して正しく整列されています。

たポインタは、こちらをご参照ポインタオブジェクトまたはポインタ値

私の意見では、答えはポインタオブジェクトだと思いますが、より多くの答えがポインタ値を示しているようです。


解釈A:ポインタオブジェクト

私の考えは次のとおりです。ポインタ自体はオブジェクトです。6.2.5#28によると、ポインタが異なれば、表現と配置の要件も異なる場合があります。したがって、6.3.2.3#7によると、2つのポインターが同じ配置である限り、未定義の動作なしで安全に変換できますが、逆参照できる保証はありません。プログラムでこのアイデアを表現します。

#include <stdio.h>

int main() {
    char buf[4096];

    char *pc = buf;
    if (_Alignof(char *) == _Alignof(int *)) {
        // cast safely, because they have the same alignment requirement?
        int *pi = (int *) pc; 
        printf("pi: %p\n", pi);
    } else {
        printf("char * and int * don't have the same alignment.\n");
    }
}

解釈B:ポインタ値

ただし、C11標準が、ポインターオブジェクトではなく、参照される型のポインター値について話している場合。上記のコードのアライメントチェックは無意味です。プログラムでこのアイデアを表現します。

#include <stdio.h>

int main() {
    char buf[4096];

    char *pc = buf;
    
    /*
     * undefined behavior, because:
     * align of char is 1
     * align of int is 4
     * 
     * and we don't know whether the `value` of pc is 4-aligned.
     */
    int *pi = (int *) pc;
    printf("pi: %p\n", pi);
}

どの解釈が正しいですか?

回答

6 dbush Dec 11 2020 at 01:36

解釈Bは正しいです。標準は、オブジェクト自体ではなく、オブジェクトへのポインタについて話します。「結果ポインタ」はキャストの結果を指し、キャストは左辺値を生成しないため、キャスト後のポインタ値を指します。

あなたの例では、コードを取る、と仮定intのアドレス場合は4バイト境界で整列されなければならない、すなわち、それのアドレスは4の倍数にする必要がありますbufされ0x1001、その後にそのアドレスを変換するにはint *、ポインタ値が適切に整列されていないため無効です。のアドレスbuf0x1000に変換されてint *いる場合は有効です。

更新:

追加したコードは配置の問題に対処しているので、その点では問題ありません。ただし、別の問題があります。厳密なエイリアシングに違反します。

定義した配列には、タイプのオブジェクトが含まれていますchar。アドレスを別のタイプにキャストし、続いて変換されたタイプタイプを逆参照することにより、あるタイプのオブジェクトに別のタイプのオブジェクトとしてアクセスします。これはC規格では許可されていません。

「厳密なエイリアシング」という用語は標準では使用されていませんが、この概念はセクション6.5のパラグラフ6および7で説明されています。

6格納された値にアクセスするためのオブジェクトの有効なタイプは、オブジェクトの宣言されたタイプです(存在する場合)。87)文字型ではない型を持つ左辺値を介して、宣言された型を持たないオブジェクトに値が格納されている場合、左辺値の型は、そのアクセスおよび後続のアクセスではオブジェクトの有効な型になります。保存された値を変更します。memcpyまたはを使用して宣言された型のないオブジェクトに値がコピーされる場合memmove、または文字型の配列としてコピーされる場合、そのアクセスおよび値を変更しない後続のアクセスの変更されたオブジェクトの有効な型が有効な型になります。値のコピー元のオブジェクト(ある場合)。宣言された型を持たないオブジェクトへの他のすべてのアクセスの場合、オブジェクトの有効な型は、単にアクセスに使用される左辺値の型です。

7オブジェクトの格納値には、次のいずれかのタイプの左辺値式によってのみアクセスする必要があります。88)

  • オブジェクトの有効なタイプと互換性のあるタイプ、
  • オブジェクトの有効なタイプと互換性のあるタイプの修飾バージョン、
  • オブジェクトの有効な型に対応する符号付きまたは符号なしの型である型、
  • オブジェクトの有効な型の修飾バージョンに対応する符号付きまたは符号なしの型である型。
  • メンバー内に前述のタイプの1つを含むアグリゲートまたはユニオンタイプ(再帰的に、サブアグリゲートまたは含まれているユニオンのメンバーを含む)、または
  • 文字タイプ。

..。

87)割り当てられたオブジェクトには宣言されたタイプがありません。

88)このリストの目的は、オブジェクトがエイリアスされる場合とされない場合がある状況を指定することです。

あなたの例では、オブジェクトの上にunsigned longとを書いています。これらのタイプはいずれも、パラグラフ7の条件を満たしていません。doublechar

それに加えて、ここでのポインタ演算は無効です。

 *(((double *) buf) + 2) = 1.618;

あなたがそうでないときのbuf配列として扱っているようにdouble。少なくとも、必要な演算をbuf直接実行し、最後に結果をキャストする必要があります。

では、なぜこれがchar配列の問題であり、によって返されるバッファではないのmallocですか?から返されるメモリには、何かを格納するまで有効なタイプmallocないためです。これは、段落6と脚注87で説明されています。

したがって、標準の厳密な観点から、あなたがしているのは未定義の振る舞いです。ただし、コンパイラによっては、厳密なエイリアシングを無効にして、これが機能するようにすることができる場合があります。gccを使用している場合は、-fno-strict-aliasingフラグを渡します。

1 supercat Dec 11 2020 at 05:09

標準では、コードがT*タイプTにアラインされていないaの値を監視する可能性を実装で考慮する必要はありません。たとえば、clangでは、「より大きな」ロード/ストア命令がアラインされていないアクセスをサポートしないプラットフォームをターゲットにする場合、ポインターをアライメントが満たされない型に変換してから使用memcpyすると、コンパイラーがコードを生成し、ポインターがmemcpyアライメントされていない場合、それ自体がアライメント要件を課さない場合でも失敗する可能性があります。

たとえば、ARM Cortex-M0またはCortex-M3をターゲットにする場合、次のようになります。

void test1(long long *dest, long long *src)
{
    memcpy(dest, src, sizeof (long long));
}
void test2(char *dest, char *src)
{
    memcpy(dest, src, sizeof (long long));
}
void test3(long long *dest, long long *src)
{
    *dest = *src;
}

打ち鳴らすはTEST1とならば失敗するTEST3コードの両方のために生成されsrc、またはdest整列されなかった、しかしのためにtest2、それは大きく、遅いコードを生成しますが、ソースとデスティネーションオペランドの任意の位置合わせを支援なります。

確かに、clangでさえ、整列されていないポインターをに変換する行為は、long long*通常、それ自体で奇妙なことを引き起こすことはありませんが、そのような変換は、コンパイラーが処理する責任を免除するUBを生成するという事実です。 unaligned-ポインタの場合test1