"구조 해킹"은 기술적으로 정의되지 않은 동작입니까?


111

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

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 접근 방식을 알고 있으며, 위에 나열된 트릭 버전에 대한 답변을 구체적으로 고수하고 싶습니다.


33
이것은 매우 명확하고 합리적이며 무엇보다도 대답 할 수있는 질문 처럼 보입니다 . 가까운 투표의 이유가 보이지 않습니다.
cHao

2
struct hack을 지원하지 않는 "ansi c"컴파일러를 도입했다면 내가 아는 대부분의 C 프로그래머는 당신의 컴파일러가 "올바르게 작동했다"는 것을 받아들이지 않을 것입니다. 그들이 표준을 엄격하게 읽는다는 사실에도 불구하고. 위원회는 그것에 대해 하나를 놓쳤습니다.
dmckee --- 전 중재자 새끼 고양이

4
@james 해킹은 최소 배열을 선언 했음에도 불구하고 의미하는 배열에 대해 충분히 큰 객체를 mallocing하여 작동합니다. 따라서 구조체의 엄격한 정의를 벗어나 할당 된 메모리 에 액세스 하고 있습니다. 할당을 지나서 쓰는 것은 틀림없는 실수이지만, 할당에 쓰는 것과는 다르지만 "구조체"외부에 있습니다.
dmckee --- 전 중재자 새끼 고양이

2
@James : 여기에서는 특 대형 malloc이 중요합니다. 그것은 구조의 명목상 끝을지나 법적 주소가 있고 구조에 의해 '소유'되고있는 메모리 (즉, 다른 엔티티가 그것을 사용하는 것은 불법 임)가 있음을 보장합니다. 이는 자동 변수에 구조체 해킹을 사용할 수 없음을 의미합니다. 동적으로 할당되어야합니다.
dmckee --- 전 중재자 새끼 고양이

5
@detly : 두 가지를 할당 / 할당 해제하는 것보다 한 가지를 할당 / 할당 해제하는 것이 더 간단합니다. 특히 후자는 처리해야하는 두 가지 실패 방법이 있기 때문입니다. 이것은 한계 비용 / 속도 절감보다 더 중요합니다.
jamesdlin

답변:


52

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

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

과:

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

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

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

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

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


1
OP의 코드에서 p->s배열로 사용되지 않습니다. 로 전달되며 strcpy,이 경우 할당 된 객체 내부 char *로 합법적으로 해석 될 수있는 객체를 가리키는 일반으로 붕괴됩니다 char [100];.
R .. GitHub의 STOP 돕기 ICE

3
아마도 이것을 보는 또 다른 방법은 J.2에 설명 된대로 실제 배열 변수에 액세스하는 방법을 언어가 제한 malloc할 수 있다는 것입니다. void *배열을 포함하는 [구조체]에 대한 포인터. 포인터 char(또는 가급적이면 unsigned char) 를 사용하여 할당 된 객체의 모든 부분에 액세스하는 것은 여전히 ​​유효합니다 .
R .. GitHub의 STOP 돕기 ICE

@아르 자형. -J2가 이것을 어떻게 다루지 않는지 알 수 있지만 6.5.6에서도 다루지 않습니까?
detly

1
물론 가능합니다! 유형 및 크기 정보가 모든 포인터에 포함될 수 있으며 잘못된 포인터 산술이 트랩 (예 : CCured) 으로 만들어 질 수 있습니다 . 더 철학적 인 수준에서, 가능한 구현 이 당신을 잡을 수 있는지 여부는 중요하지 않습니다 . 그것은 여전히 ​​정의되지 않은 동작입니다. 정의되지 않음).
zwol

4
객체는 배열 객체가 아니므로 6.5.6은 관련이 없습니다. 객체는에서 할당 한 메모리 블록입니다 malloc. bs를 내뿜기 전에 표준에서 "개체"를 찾아보십시오.
R .. GitHub STOP HELPING ICE

34

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

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

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

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


2
또한 배열의 크기가 1이면 다음 arr[x] = y;과 같이 다시 작성 될 수 있다고 결정할 수있는 컴파일러를 상상할 수 있습니다 arr[0] = y;. 크기 2의 배열, arr[i] = 4;같이 다시 작성할 수 있습니다 i ? arr[1] = 4 : arr[0] = 4; 나는 컴파일러는 이러한 최적화를 수행 일부 임베디드 시스템에 그들은 매우 생산적이 될 수 본 적이 있지만. 8 비트 데이터 유형을 사용하는 PIC18x에서 첫 번째 명령문의 코드는 16 바이트, 두 번째, 2 또는 4, 세 번째, 8 또는 12입니다. 합법적이라면 나쁜 최적화는 아닙니다.
supercat

표준이 배열 경계 외부의 배열 액세스를 정의되지 않은 동작으로 정의하면 구조체 해킹도 마찬가지입니다. 그러나 표준이 배열 액세스를 포인터 산술 ( a[2] == a + 2)에 대한 구문 설탕으로 정의 하면 그렇지 않습니다. 내가 맞다면 모든 C 표준은 배열 액세스를 포인터 산술로 정의합니다.
yyny

13

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

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

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

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

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

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


2
이걸 찾았다면 +1하지만 여전히 모순이라고 주장합니다. 동일한 객체 (이 경우 주어진 바이트)에 대한 두 개의 포인터는 동일하며, 그에 대한 하나의 포인터 (에서 얻은 전체 객체의 표현 배열에 대한 포인터 malloc)는 추가에서 유효하므로 동일한 포인터가 어떻게 될 수 있습니까? 다른 경로를 통해 얻은 경우 추가에서 유효하지 않습니까? 그들이 그것이 UB라고 주장하고 싶어하더라도, 그것은 꽤 의미가 없습니다. 왜냐하면 구현이 잘 정의 된 사용법과 아마도 정의되지 않은 사용법을 구별 할 계산적으로 방법이 없기 때문입니다.
R .. GitHub의 STOP 돕기 ICE

C 컴파일러가 길이가 0 인 배열의 선언을 금지하기 시작한 것은 너무 나쁩니다. 경우는 그 금지를 위해 많은 컴파일러는 그들이 "해야한다"로 작동하도록 특별한 취급을 할 수 없었을 것이다되지 않은,하지만 여전히 단일 요소 배열을위한 특별한 경우의 코드 (예, 할 수 있었을 것이다 *foo을 포함 단일 요소 배열 boz, 표현식 foo->boz[biz()*391]=9;은)로 단순화 할 수 있습니다 biz(),foo->boz[0]=9;. 불행히도 컴파일러의 요소가 0 인 배열을 거부한다는 것은 많은 코드가 대신 단일 요소 배열을 사용하므로 해당 최적화로 인해 손상 될 수 있음을 의미합니다.
supercat

11

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


현명하게 말하자면, 그것은 구조 해킹이 아닙니다. 구조체 해킹은 유연한 배열 구성원이 아닌 고정 된 크기의 배열을 사용합니다. struct hack은 질문을 받았으며 UB입니다. 유연한 배열 구성원은이 스레드에서 그 사실에 대해 불평하는 사람들을 달래려는 시도처럼 보입니다.
underscore_d

7

누구든지 공식적으로 말하든 그렇지 않든 간에 표준에 의해 정의 되었기 때문에 정의되지 않은 행동이 아닙니다 . 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.


4
포인터 산술의 본질이 관련이 없다는 것은 맞지만 , 선언 된 배열 크기를 초과하는 액세스에 대해서는 틀 렸습니다 . 참고 N1494 (최신 공개 C1x 초안) 섹션 6.5.6 항 8 - 당신도 할 수없는 것 또한 하나 개 이상의 요소를 배열의 선언 크기를지나 포인터를 소요하고 당신이 경우에도 역 참조 할 수 없습니다 과거의 한 요소 일뿐입니다.
zwol

1
@Zack : 객체가 배열이면 사실입니다. 객체가 malloc배열로 액세스되는 할당 된 객체 이거나 요소가 더 큰 구조체 요소의 초기 하위 집합 인 더 작은 구조체에 대한 포인터를 통해 액세스되는 더 큰 구조체이면 사실이 아닙니다. 케이스.
R .. GitHub STOP HELPING ICE

6
+1 malloc포인터 산술로 액세스 할 수있는 메모리 범위를 할당하지 않으면 어떤 용도로 사용됩니까? 그리고 경우 p->s[1]입니다 정의 구문 다음 포인터 연산을 위해 설탕이 대답 단지 reasserts과 표준에 의해 malloc유용하다. 토론해야 할 것은 무엇입니까? :)
Daniel Earwicker

3
원하는만큼 잘 정의되어 있다고 주장 할 수 있지만 그렇지 않다는 사실을 바꾸지는 않습니다. 표준은 배열의 경계를 넘는 액세스에 대해 매우 명확하며이 배열의 경계는 1. 아주 간단합니다.
궤도에서 가벼운 경주

3
@R .., 동일 함을 비교하는 두 포인터가 동일하게 작동해야한다는 귀하의 가정은 잘못된 것 같습니다. 고려 int m[1]; int n[1]; if(m+1 == n) m[1] = 0;가정 if지점이 입력됩니다. 이것은 n내가 읽은 6.5.6 p8 (마지막 문장)에 따라 UB (초기화 보장이 아님 )입니다. 관련 : 6.5.9 p6 with footnote 109. (참조는 C11 n1570에 있습니다.) [...]
mafso

7

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

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

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

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

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


1
(2)에서는 s[1]정의되지 않은 동작이 아닙니다. 이 같은입니다 *(s+1)같은 인 *((char *)p + offsetof(struct T, s) + 1)A와 유효한 포인터 인 char할당 된 객체가.
R .. GitHub의 STOP 돕기 ICE

반면에 (3)은 정의되지 않은 동작이라고 거의 확신합니다. 해당 주소에있는 이러한 구조체에 의존하는 작업을 수행 할 때마다 컴파일러는 구조체의 모든 부분에서 읽는 기계어 코드를 자유롭게 생성 할 수 있습니다. 쓸모 없거나 엄격한 할당 검사를위한 안전 기능이 될 수 있지만 구현이이를 수행 할 수없는 이유는 없습니다.
R .. GitHub의 STOP 돕기 ICE

R : 배열이 크기를 갖도록 선언 된 경우 (의 foo[]구문 설탕이 *foo아님), 포인터 산술이 수행 된 방법에 관계없이 선언 된 크기 및 할당 된 크기 보다 작은 액세스 는 UB입니다.
zwol

1
@Zack, 당신은 여러 가지에 대해 틀 렸습니다. foo[]구조체에서 구문 설탕이 아닙니다 *foo. C99 유연한 배열 구성원입니다. 나머지는 내 답변과 다른 답변에 대한 의견을 참조하십시오.
R .. GitHub의 STOP 돕기 ICE

6
문제는위원회의 일부 구성원 이이 "핵"이 UB가 되기를 간절히 원한다는 것입니다. 왜냐하면 그들은 C 구현이 포인터 경계를 적용 할 수있는 어떤 동화의 나라를 구상하기 때문입니다. 그러나 좋든 나쁘 든간에, 그렇게하면 표준의 다른 부분과 충돌하게 unsigned char [sizeof object]됩니다. . 나는 pre-C99의 유연한 배열 멤버 "hack"이 잘 정의 된 동작을 가지고 있다는 내 주장을지지합니다.
R .. GitHub STOP HELPING ICE

3

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

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


예를 들어 줄 수 있습니까?
Tal

1

컴파일러가 다음과 같은 것을 받아들이면

typedef struct {
  int len;
  char dat [];
};

나는 그것이 그 길이를 넘어서 'dat'에 대한 아래 첨자를 받아 들일 준비가되어 있어야한다는 것이 꽤 분명하다고 생각한다. 반면에 누군가 다음과 같은 코드를 작성하면

typedef struct {
  int 무엇이든;
  char dat [1];
} MY_STRUCT;

그리고 나중에 somestruct-> dat [x]에 액세스합니다. 나는 컴파일러가 x의 큰 값으로 작동하는 주소 계산 코드를 사용할 의무가 있다고 생각하지 않습니다. 정말로 안전하고 싶다면 적절한 패러다임은 다음과 같을 것입니다.

# LARGEST_DAT_SIZE 0xF000 정의
typedef struct {
  int 무엇이든;
  문자 데이터 [LARGEST_DAT_SIZE];
} MY_STRUCT;

그런 다음 (sizeof (MYSTRUCT) -LARGEST_DAT_SIZE + desired_array_length) 바이트의 malloc을 수행하십시오 (desired_array_length가 LARGEST_DAT_SIZE보다 크면 결과가 정의되지 않을 수 있음을 염두에 두십시오).

덧붙여서, 길이가 0 인 배열은 컴파일러가 더 큰 인덱스에서 작동하는 코드를 생성해야한다는 신호로 간주 될 수 있기 때문에 길이가 0 인 배열을 금지하기로 한 결정은 불행한 것이라고 생각합니다 (Turbo C와 같은 일부 오래된 방언이 지원함). .

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.