programing

구조에서는 하나의 어레이 필드를 사용하여 다른 어레이 필드에 액세스하는 것이 합법입니까?

projobs 2022. 8. 11. 00:03
반응형

구조에서는 하나의 어레이 필드를 사용하여 다른 어레이 필드에 액세스하는 것이 합법입니까?

예를 들어 다음 구조에 대해 생각해 보겠습니다.

struct S {
  int a[4];
  int b[4];
} s;

s.a[6] ' 하다'라고 될 것 같아요.s.b[2]개인적으로는 C++의 UB가 틀림없다고 생각합니다만, C는 잘 모르겠습니다.그러나 C와 C++ 언어의 기준에 관련된 것을 찾지 못했습니다.


갱신하다

코드가 안정적으로 동작하기 위해 필드 사이에 패딩이 없는지 확인하는 방법을 제안하는 답변이 몇 개 있습니다.이 코드가 UB라면 패딩이 없는 것만으로는 부족하다는 것을 강조하고 싶습니다.는 UB에 할 수 .S.a[i] ★★★★★★★★★★★★★★★★★」S.b[j]컴파일러는 메모리 액세스 순서를 자유롭게 변경할 수 있습니다.를 들어 '예'라고 하면,

    int x = s.b[2];
    s.a[6] = 2;
    return x;

으로 변환될 수 있다

    s.a[6] = 2;
    int x = s.b[2];
    return x;

반환됩니다.2.

s.a[6]라고 쓰고 s.b[2]와 동일하다고 생각하는 것이 합법입니까?

아니요. 어레이에 액세스하면 C 및 C++에서 정의되지 않은 동작이 호출되기 때문입니다.

C11 J.2 정의되지 않은 동작

  • 및로 하는 가 생성되어 됩니다.* (6.5.6)로 .

  • 특정 표현식 「」와 같이)로할 수 있는 한 경우라도, .a[1][7] int " " int " 가 되었습니다.a[4][5]) (6.5.6)

C++ 표준 초안 섹션 5.7 가법 연산자 문단 5는 다음과 같이 말한다.

적분형식을 포인터에 추가하거나 포인터에서 빼면 포인터 피연산자 유형이 결과에 포함됩니다.포인터 피연산자가 배열 객체의 요소를 가리키고 배열이 충분히 클 경우, 결과는 원래 요소에서 오프셋된 요소를 가리키며, 결과 및 원래 배열 요소의 첨자 차이가 정수 표현식과 같도록 합니다.[...] 포인터 피연산자와 결과가 모두 배열 개체의 요소를 가리킬 경우 동일한 배열 객체 또는 배열 객체의 마지막 요소 뒤에 있는 배열 객체의 평가에서 오버플로가 발생하지 않아야 합니다. 그렇지 않으면 동작이 정의되지 않습니다.

@rsp )Undefined behavior for an array subscript that is out of range.b★★★★★★★★★★★★★★★★★를 통해aC 언어에서는 a에 할당되어 있는 영역의 끝과 b의 시작 사이에 어느 정도의 패딩 공간을 지정할 수 있는지 알 수 없기 때문에 특정 구현에서 실행할 수 있어도 pading space는 사용할 수 없습니다.

instance of struct:
+-----------+----------------+-----------+---------------+
|  array a  |  maybe padding |  array b  | maybe padding |
+-----------+----------------+-----------+---------------+

이 놓칠 수 패딩의 수 있습니다.struct object의 정렬입니다.a 정렬을 할 때 을 할 수 있습니다.b그러나 C 언어도 거기에 없는 두 번째 패딩을 강요하지는 않습니다.

a ★★★★★★★★★★★★★★★★★」b과 2개의 배열이 .a는, 을 한 것으로 됩니다.4 이렇게 해서,a[6]는 배열을 경계 밖으로 액세스하기 때문에 정의되지 않은 동작입니다. 첨자 「」에 해 주세요.a[6]되어 있습니다.*(a+6)따라서 UB의 증명은 실제로 포인터와 함께 "추가 연산자" 섹션에 의해 제공됩니다.이 측면에 대해서는, C11 표준의 다음의 항(를 들면, 이 온라인 초안 버전)을 참조해 주세요.

6.5.6 가법 연산자

정수 유형을 가진 식을 포인터에 추가하거나 포인터에서 빼면 결과에는 포인터 피연산자 유형이 포함됩니다.포인터 오퍼랜드가 배열 객체의 요소를 가리키고 배열이 충분히 클 경우, 결과는 결과 및 원본 배열 요소의 첨자 차이가 정수식과 같도록 원래 요소로부터의 오프셋을 가리킵니다.즉, 식 P가 배열 객체의 i번째 요소를 가리킬 경우, 식 P+N(등가 N+P) 및 (P)-N(여기서 N은 값 n을 가진다)이 각각 설치된 배열 객체의 i+n번째 및 i-n번째 요소를 가리킬 수 있다.또한 식 P가 배열 객체의 마지막 요소를 가리키면 식 (P)+1은 배열 객체의 마지막 요소를 가리키고 식 Q가 배열 객체의 마지막 요소를 가리키면 식 (Q)-1은 배열 객체의 마지막 요소를 가리키고 있다.포인터 오퍼랜드와 결과 모두 동일한 배열 객체의 요소를 가리킬 경우 또는 배열 객체의 마지막 요소를 가리킬 경우 평가에서 오버플로가 발생하지 않습니다.그렇지 않을 경우 동작은 정의되지 않습니다.결과가 배열 객체의 마지막 요소를 1개 지나갔을 경우, 평가되는 단항 * 연산자의 피연산자로 사용할 수 없습니다.

같은 인수가 C++에도 적용됩니다(단, 여기서는 따옴표로 묶지 않습니다).

또,에 의해서 않은 이지만, 의 경계치를 넘는 것이 입니다.a가 멤버 하는 경우가 a그리고.b그러한 포인터 산술이 허용되더라도 -a+6반드시 같은 주소를 얻을 필요는 없다b+2.

단답: 아니요.당신은 정의되지 않은 행동의 세계에 있습니다.

장황한 답변: 아니요.그렇다고 해서 다른 스케치 방식으로 데이터에 액세스할 수 없는 것은 아닙니다.GCC를 사용하는 경우 다음과 같은 작업을 수행할 수 있습니다(dwillis의 답변 생략).

struct __attribute__((packed,aligned(4))) Bad_Access {
    int arr1[3];
    int arr2[3];
};

(Godbolt source+asm)을 통해 액세스할 수 있습니다.

int x = ((int*)ba_pointer)[4];

하지만 그 출연자들은 엄격한 에일리어스를 위반해서g++ -fno-strict-aliasing구조 포인터를 첫 번째 멤버에게 포인터로 던질 수 있지만 첫 번째 멤버 외부에서 접근하기 때문에 UB 보트로 돌아갑니다.

아니면, 그냥 그렇게 하지 마세요.미래의 프로그래머(아마도 당신 자신)가 그 난장판으로 인한 마음의 아픔을 구하라.

이왕이면 std::vector를 사용하면 어떨까요?그것은 바보 같은 짓은 아니지만, 뒷면에는 그런 나쁜 행동을 막기 위한 보호 장치가 있다.

부록:

퍼포먼스를 정말 중시하는 경우:

같은 타입의 포인터가 2개 있어 액세스 하고 있다고 합시다.컴파일러는 두 포인터 모두 간섭할 가능성이 있다고 가정하고, 당신을 바보 같은 짓으로부터 보호하기 위해 추가적인 논리를 인스턴스화할 것입니다.

에일리어스를 붙이지 않겠다고 컴파일러에게 엄숙히 맹세하면 컴파일러는 당신에게 후한 보상을 할 것입니다.restrict 키워드는 gcc/g++에서 큰 이점을 제공합니까?

결론:사악하게 굴지 마라. 미래의 네 모습, 그러면 컴파일러가 너에게 고마워할 것이다.

그것은 합법인가요?아니요. 다른 사람들이 언급했듯이 정의되지 않은 동작을 호출합니다.

효과가 있을까요?컴파일러에 따라 다릅니다.그것이 정의되지 않은 행동에 대한 것입니다. 정의되지 않은 것입니다.

많은 C 및 C++ 컴파일러에서는 b가 메모리에서 a 뒤에 바로 나타나도록 구조가 배치되어 경계 검사가 없습니다.따라서 a[6]에 액세스하는 것은 b[2]와 실질적으로 동일하며 어떠한 예외도 발생하지 않습니다.

정해진

struct S {
  int a[4];
  int b[4];
} s

추가 패딩이 없다고 가정할 때, 이 구조는 8개의 정수를 포함하는 메모리 블록을 보는 방법일 뿐입니다.당신이 그것을 던질 수 있다.(int*)그리고.((int*)s)[6]같은 기억을 가리키다s.b[2].

당신은 이런 종류의 행동에 의존해야 합니까?절대로 그렇지 않아요.정의되지 않음은 컴파일러가 이를 지원할 필요가 없음을 의미합니다.컴파일러는 &(s.b[2]) == &(s.a[6])의 가정을 부정확하게 만들 수 있는 구조를 자유롭게 패딩할 수 있습니다.컴파일러는 어레이 액세스에 대한 경계 검사도 추가할 수 있습니다(단, 컴파일러 최적화를 활성화하면 이러한 검사가 비활성화될 수 있습니다).

나는 과거에 이것의 영향을 경험한 적이 있다.이런 구조를 갖는 것은 꽤 흔한 일이다.

struct Bob {
    char name[16];
    char whatever[64];
} bob;
strcpy(bob.name, "some name longer than 16 characters");

자, 밥."16자 미만"이 될 수 있습니다.(따라서 항상 strncpy, BTW를 사용해야 합니다)

@MartinJames가 댓글에서 언급했듯이, 만약 당신이 그것을 보장할 필요가 있다면a그리고.b(아키텍처/패드에서 비정상적인 메모리블록 크기/패딩 및 패딩 추가를 필요로 하는 강제 얼라인먼트를 사용하지 않는 한 적어도 (편집)으로 취급할 수 있습니다)를 사용해야 합니다.union.

union overlap {
    char all[8]; /* all the bytes in sequence */
    struct { /* (anonymous struct so its members can be accessed directly) */
        char a[4]; /* padding may be added after this if the alignment is not a sub-factor of 4 */
        char b[4];
    };
};

직접 접속할 수 없습니다.b부터a(예:a[6](요청하신 대로) 단, 두 가지 요소에 모두 접근할 수 있습니다.a그리고.b을 사용하여all(예:all[6]와 같은 메모리 위치를 나타냅니다.b[2]).

(편집: 교환 가능)8그리고.4상기의 암호로2*sizeof(int)그리고.sizeof(int)각각 아키텍처의 얼라인먼트와 일치할 가능성이 높아집니다.특히 코드가 더 이식성이 필요한 경우입니다.다만, 이 경우, 바이트 수에 대한 어떠한 가정도 하지 않도록 주의해 주세요.a,b, 또는all다만, 이것은 가장 일반적인 메모리 얼라인먼트(1바이트, 2바이트, 4바이트)에서는 동작합니다.

다음으로 간단한 예를 제시하겠습니다.

#include <stdio.h>

union overlap {
    char all[2*sizeof(int)]; /* all the bytes in sequence */
    struct { /* anonymous struct so its members can be accessed directly */
        char a[sizeof(int)]; /* low word */
        char b[sizeof(int)]; /* high word */
    };
};

int main()
{
    union overlap testing;
    testing.a[0] = 'a';
    testing.a[1] = 'b';
    testing.a[2] = 'c';
    testing.a[3] = '\0'; /* null terminator */
    testing.b[0] = 'e';
    testing.b[1] = 'f';
    testing.b[2] = 'g';
    testing.b[3] = '\0'; /* null terminator */
    printf("a=%s\n",testing.a); /* output: a=abc */
    printf("b=%s\n",testing.b); /* output: b=efg */
    printf("all=%s\n",testing.all); /* output: all=abc */

    testing.a[3] = 'd'; /* makes printf keep reading past the end of a */
    printf("a=%s\n",testing.a); /* output: a=abcdefg */
    printf("b=%s\n",testing.b); /* output: b=efg */
    printf("all=%s\n",testing.all); /* output: all=abcdefg */

    return 0;
}

아니요. 배열을 액세스하면 C와 C++ 모두에서 정의되지 않은 동작이 호출되기 때문입니다.

Jed Schaff의 대답은 옳지만, 정확하지는 않다.컴파일러가 사이에 패딩을 삽입하는 경우a그리고.b, 그의 해결책은 여전히 실패할 것이다.단, 다음과 같이 선언할 경우:

typedef struct {
  int a[4];
  int b[4];
} s_t;

typedef union {
  char bytes[sizeof(s_t)];
  s_t s;
} u_t;

이제 에 접속할 수 있습니다.(int*)(bytes + offsetof(s_t, b))주소를 알아내다s.b컴파일러가 구조를 어떻게 배치하든 상관없습니다.offsetof()매크로가 선언되어 있다<stddef.h>.

표현sizeof(s_t)는 상수 표현으로 C와 C++ 양쪽 어레이 선언에서 유효합니다.가변 길이 배열은 제공되지 않습니다. (이전 C 표준을 잘못 읽은 것에 대한 사과입니다.그게 잘못된 것 같았어.)

그러나 실제 세계에서는 두 개의 연속된 어레이가int원하는 방식으로 배치될 것입니다.(다음의 범위를 설정함으로써 매우 교묘한 반례를 작성할 수 있습니다.a4가 아닌 3 또는 5로 변환하여 컴파일러가 양쪽을 정렬하도록 합니다.a그리고.b16 바이트 경계에 있습니다).표준의 엄격한 표현을 넘어서는 어떠한 가정도 하지 않는 프로그램을 얻기 위한 복잡한 방법보다, 다음과 같은 방어적인 코딩을 필요로 합니다.static assert(&both_arrays[4] == &s.b[0], "");. 이것들은 런타임 오버헤드를 추가하지 않으며, 어설션 자체에서 UB를 트리거하지 않는 한 컴파일러가 프로그램을 망가뜨릴 수 있는 무언가를 하고 있을 경우 실패합니다.

양쪽 서브어레이가 연속된 메모리 범위로 패킹되거나 메모리 블록을 다른 방법으로 분할할 수 있는 포터블한 방법이 필요한 경우 다음과 같이 복사할 수 있습니다.memcpy().

이 표준은 프로그램이 다른 구조 필드의 구성원에 액세스하기 위해 한 구조 필드의 범위 밖의 배열 첨자를 사용하려고 할 때 구현해야 하는 작업에 어떠한 제한도 가하지 않습니다.따라서 엄격히 호환되는 프로그램에서는 Out-of-Bounds 액세스가 "불법"이며, 이러한 액세스를 사용하는 프로그램은 100% 이식 가능하고 오류가 없는 프로그램이 될 수 없습니다.한편, 많은 실장에서는 이러한 코드의 동작을 정의하고 있으며, 이러한 실장만을 대상으로 하는 프로그램에서는 그러한 동작을 이용할 수 있습니다.

이러한 코드에는, 다음의 3개의 문제가 있습니다.

  1. 많은 구현이 예측 가능한 방식으로 구조를 배치하는 반면, 이 기준서는 구현이 첫 번째 구성원을 제외한 모든 구성원에 대해 임의적인 내용을 추가할 수 있도록 허용한다.코드는 사용할 수 있습니다.sizeof또는offsetof구조 부재가 예상대로 배치되도록 하지만 나머지 두 가지 문제는 남아 있습니다.

  2. 예를 들어 다음과 같습니다.

    if (structPtr->array1[x])
     structPtr->array2[y]++;
    return structPtr->array1[x];
    

    컴파일러는 보통 컴파일러의 사용을 가정하는 것이 유용할 것입니다.structPtr->array1[x]는, 2개의 어레이간의 에일리어스에 의존하는 코드의 동작을 변경해도, 「if」상태에서의 앞의 사용치와 같은 값을 산출합니다.

  3. 한다면array1[]에는 4가지 요소가 있습니다.컴파일러에는 다음과 같은 것이 있습니다.

    if (x < 4) foo(x);
    structPtr->array1[x]=1;
    

정의된 케이스가 없기 때문에x4보다 작으면 안 돼요.foo(x)무조건.

아쉽게도 프로그램에서는sizeof또는offsetof구조 레이아웃에 놀라지 않도록 하기 위해 컴파일러가 #2 또는 #3의 최적화를 자제할 것을 약속하는지 테스트할 수 있는 방법은 없습니다.또한 이 기준서는 다음과 같은 경우에 무엇을 의미하는지 약간 모호하다.

struct foo {char array1[4],array2[4]; };

int test(struct foo *p, int i, int x, int y, int z)
{
  if (p->array2[x])
  {
    ((char*)p)[x]++;
    ((char*)(p->array1))[y]++;
    p->array1[z]++;
  }
  return p->array2[x];
}

표준에서는 z가 0.3 범위에 있는 경우에만 동작이 정의된다는 것은 매우 명확하지만, 이 식에서의 p-> 배열의 유형은 char*이기 때문에 (붕괴로 인해) 다음과 같은 방법으로 액세스의 캐스트를 클리어할 수 없습니다.y효과가 있을 거예요.한편, 포인터를 구조체의 첫 번째 요소로 변환하기 때문에char*구조 포인터를 로 변환하는 것과 같은 결과를 얻을 수 있습니다.char*변환된 구조 포인터는 모든 바이트에 액세스할 수 있어야 합니다.x(최소) x=0..7에 대해 정의해야 한다.array24보다 크면 다음 값에 영향을 줍니다.x멤버를 때릴 필요가 있다array2단, 일부 가치는x정의되어 있는 동작으로 할 수 있습니다.

IMHO는 포인터 붕괴를 수반하지 않는 방법으로 어레이 유형에 서브스크립트 연산자를 정의하는 것이 좋습니다.이 경우 표현은p->array[x]그리고.&(p->array1[x])컴파일러가 다음과 같이 가정할 수 있다.x0.3이지만p->array+x그리고.*(p->array+x)다른 값을 사용할 수 있도록 컴파일러가 필요합니다.그렇게 하는 컴파일러가 있는지는 모르겠지만, 표준에서는 그렇게 요구하지 않습니다.

언급URL : https://stackoverflow.com/questions/47094166/in-a-structure-is-it-legal-to-use-one-array-field-to-access-another-one

반응형