[c] “구조 해킹”은 기술적으로 정의되지 않은 동작입니까?

내가 묻는 것은 잘 알려진 “구조체의 마지막 멤버가 가변 길이를 가짐”트릭입니다. 다음과 같이 진행됩니다.

struct T {
    int len;
    char s[1];
};

struct T *p = malloc(sizeof(struct T) + 100);
p->len = 100;
strcpy(p->s, "hello world");

구조체가 메모리에 배치되는 방식으로 인해 필요한 것보다 큰 블록 위에 구조체를 오버레이하고 마지막 멤버를 1 char지정된 것보다 큰 것처럼 처리 할 수 있습니다.

그래서 질문은 : 이 기술이 기술적으로 정의되지 않은 동작입니까? . 나는 그것이 될 것이라고 예상했지만 표준이 이것에 대해 말하는 것이 궁금했습니다.

추신 : 나는 이것에 대한 C99 접근 방식을 알고 있으며, 위에 나열된 트릭 버전에 대한 답변을 구체적으로 고수하고 싶습니다.



답변

현상태대로 C 자주 묻는 질문 말합니다 :

합법적인지 이식 가능한지는 확실하지 않지만 오히려 인기가 있습니다.

과:

… 공식 해석은 C 표준을 엄격하게 준수하지 않는다고 간주했지만 모든 알려진 구현에서 작동하는 것 같습니다. (배열 경계를주의 깊게 확인하는 컴파일러는 경고를 발행 할 수 있습니다.)

‘엄격하게 준수하는’비트의 근거 는 정의되지 않은 동작 목록에 포함 된 J.2 Undefined behavior 스펙 섹션에 있습니다.

  • 주어진 첨자로 객체에 분명히 접근 할 수있는 경우에도 배열 첨자가 범위를 벗어납니다 ( a[1][7]선언이 주어진 lvalue 표현식에서와 같이 int a[4][5]) (6.5.6).

섹션 6.5.6 의 단락 8에는 정의 된 배열 경계를 넘어서는 액세스가 정의되지 않는다는 또 다른 언급이 있습니다.

포인터 피연산자와 결과가 동일한 배열 개체의 요소를 가리 키거나 배열 개체의 마지막 요소를 지나는 요소를 가리키는 경우 평가는 오버플로를 생성하지 않습니다. 그렇지 않으면 동작이 정의되지 않습니다.


답변

기술적으로 정의되지 않은 행동이라고 생각합니다. 표준 (논란의 여지가 있음)은이를 직접적으로 다루지 않으므로 “행동에 대한 명시 적 정의를 생략하거나”에 해당합니다. 정의되지 않은 동작이라고 말하는 조항 (§4 / 2 of C99, §3.16 / 2 of C89).

위의 “논란의 여지가있는”것은 배열 첨자 연산자의 정의에 따라 다릅니다. 특히, “접미사 식 다음에 대괄호 []로 묶인 식은 배열 개체의 첨자 지정입니다.” (C89, §6.3.2.1 / 2).

여기서 “배열 객체의”가 위반되고 있다고 주장 할 수 있습니다 (배열 객체의 정의 된 범위를 벗어난 첨자이므로),이 경우 동작은 정의되지 않은 것이 아니라 (조금 더) 명시 적으로 정의되지 않은 것입니다. 그것을 정의하는 것은 아무것도 없기 때문입니다.

이론 상으로는 배열 경계 검사를 수행하고 범위를 벗어난 첨자를 사용하려고 할 때 (예를 들어) 프로그램을 중단하는 컴파일러를 상상할 수 있습니다. 사실 저는 그런 것이 존재하는지 모릅니다. 그리고 이런 스타일의 코드의 인기를 감안할 때, 컴파일러가 어떤 상황에서 첨자를 강제하려고하더라도, 누군가가 그렇게하는 것을 참을 것이라고 상상하기 어렵습니다. 이 상황.


답변

예, 정의되지 않은 동작입니다.

C 언어 결함 보고서 # 051은이 질문에 대한 확실한 답을 제공합니다.

관용구는 일반적이지만 엄격하게 준수하지는 않습니다.

http://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_051.html

C99 근거 문서에서 C위원회는 다음을 추가합니다.

이 구조의 유효성은 항상 의심 스러웠습니다. 하나의 결함 보고서에 대한 응답에서위원회는 공간의 존재 여부에 관계없이 배열 p-> items에 하나의 항목 만 포함되어 있기 때문에 정의되지 않은 동작이라고 결정했습니다.


답변

이를 수행하는 특정 방법은 C 표준에 명시 적으로 정의되어 있지 않지만 C99는 언어의 일부로 “struct hack”을 포함합니다. C99에서 구조체의 마지막 멤버는 char foo[](대신 원하는 형식 으로) 선언 된 “유연한 배열 멤버”일 수 있습니다 char.


답변

누구든지 공식적으로 말하든 그렇지 않든 간에 표준에 의해 정의 되었기 때문에 정의되지 않은 행동이 아닙니다 . p->s, lvalue로 사용되는 경우를 제외하고는와 동일한 포인터로 평가됩니다 (char *)p + offsetof(struct T, s). 특히 이것은 charmalloc 객체 내부의 유효한 포인터이며 char할당 된 객체 내부의 객체 로도 유효한 100 개 (또는 그 이상, 정렬 고려 사항에 따라 다름) 연속 주소가 바로 뒤에 있습니다 . 에서 ->반환 된 포인터에 오프셋을 명시 적으로 추가하는 대신를 사용하여 포인터가 파생되었다는 사실은 malloc캐스트 char *와 관련이 없습니다.

기술적으로 p->s[0]charstruct 내부 배열 의 단일 요소이며 다음 몇 개의 요소 (예 : p->s[1]~ p->s[3])는 struct 내부의 패딩 바이트 일 가능성이 높습니다. 이는 struct 전체에 할당을 수행하면 손상 될 수 있지만 개별적으로 액세스하는 경우에는 그렇지 않습니다. 멤버 및 나머지 요소는 할당 된 개체의 추가 공간이며 정렬 요구 사항을 준수하고 정렬 요구 사항 char이없는 한 원하는대로 자유롭게 사용할 수 있습니다 .

구조체의 패딩 바이트와 겹칠 가능성이 어떻게 든 비강 악마를 호출 할 수 있다고 걱정되면 1in [1]을 구조체 끝에 패딩이없는 값 으로 대체하여이를 피할 수 있습니다. 간단하지만 낭비적인 방법은 끝에 배열이없는 것을 제외하고는 동일한 멤버로 구조체를 만들고 배열에 사용 s[sizeof struct that_other_struct];하는 것입니다. 그런 다음 p->s[i]for 구조체의 배열 요소로 명확하게 정의되고 for 구조체 i<sizeof struct that_other_struct의 끝 뒤에 오는 주소의 char 객체로 정의됩니다 i>=sizeof struct that_other_struct.

편집 : 실제로, 올바른 크기를 얻기위한 위의 트릭에서, 배열 자체가 다른 요소의 패딩 중간이 아닌 최대 정렬로 시작되도록 배열 앞에 모든 단순 유형을 포함하는 공용체를 넣어야 할 수도 있습니다. . 다시 말하지만, 나는 이것이 필요하다고 생각하지 않지만, 거기에있는 가장 편집증적인 언어 변호사들을 위해 그것을 제공하고 있습니다.

편집 2 : 패딩 바이트와의 겹침은 표준의 다른 부분으로 인해 문제가되지 않습니다. C에서는 두 구조체가 요소의 초기 하위 시퀀스에서 일치하는 경우 두 유형에 대한 포인터를 통해 공통 초기 요소에 액세스 할 수 있어야합니다. 동일한 구조체 경우 결과적으로, struct T그러나 더 큰 최종 배열이 선언 된, 요소가 s[0]요소와 일치해야합니다 s[0]에서 struct T, 이러한 추가 요소의 존재에 영향을 줄 수 없거나 더 큰 구조체의 공통 요소에 액세스하여 영향을받을 수 에 대한 포인터 사용 struct T.


답변

예, 기술적으로 정의되지 않은 동작입니다.

“struct hack”을 구현하는 방법에는 최소한 세 가지가 있습니다.

(1) 크기가 0 인 후행 배열 선언 (레거시 코드에서 가장 “인기있는”방법). 크기가 0 인 배열 선언은 항상 C에서 불법이기 때문에 이것은 분명히 UB입니다. 컴파일을해도 언어는 제약을 위반하는 코드의 동작에 대해 보장하지 않습니다.

(2) 최소한의 법적 크기로 배열 선언-1 (귀하의 경우). 이 경우 포인터를 가져 와서 p->s[0]포인터 산술에 사용 하려는 시도 p->s[1]는 정의되지 않은 동작입니다. 예를 들어, 디버깅 구현은 범위 정보가 포함 된 특수 포인터를 생성 할 수 있으며,이 포인터는 p->s[1].

(3) 예를 들어 10000과 같은 “매우 큰”크기로 배열을 선언합니다 . 아이디어는 선언 된 크기가 실제 실제 필요한 것보다 더 커야한다는 것입니다. 이 방법은 어레이 액세스 범위와 관련하여 UB가 없습니다. 그러나 실제로 실제로는 항상 더 적은 양의 메모리를 할당합니다 (실제로 필요한만큼만). 나는 이것의 합법성에 대해 확신하지 못합니다. 즉, 선언 된 객체의 크기보다 더 적은 메모리를 객체에 할당하는 것이 얼마나 합법적인지 궁금합니다 ( “비 할당 된”멤버에 액세스하지 않는다고 가정).


답변

표준은 배열의 끝에있는 항목에 액세스 할 수 없다는 것이 매우 분명합니다. (그리고 포인터를 통해 이동하는 것은 도움이되지 않습니다. 배열 끝 이후에 포인터를 증가시키는 것도 허용되지 않기 때문입니다).

그리고 “실무에서 일하기”를 위해. 표준의이 부분을 사용하는 gcc / g ++ 최적화 프로그램을 보았습니다. 따라서이 잘못된 C를 충족 할 때 잘못된 코드를 생성합니다.