gcc의 __attribute __ ((packed)) / #pragma pack이 안전하지 않습니까?


164

C에서 컴파일러는 각 멤버가 올바르게 정렬되도록 멤버 사이 또는 마지막 멤버 뒤에 패딩 바이트를 삽입하여 선언 된 순서대로 구조체 멤버를 배치합니다.

gcc는 언어 확장을 제공하는데 __attribute__((packed)), 이는 컴파일러에게 패딩을 삽입하지 않도록 지시하여 구조체 멤버가 잘못 정렬되도록합니다. 예를 들어, 시스템에서 일반적으로 모든 int객체에 4 바이트 정렬 __attribute__((packed))이 필요한 경우 int구조체 멤버가 홀수 오프셋으로 할당 될 수 있습니다 .

gcc 문서 인용하기 :

`packed '속성은`aligned'속성으로 더 큰 값을 지정하지 않는 한 변수 또는 구조 필드가 가능한 가장 작은 정렬을 갖도록 지정합니다 (변수의 경우 1 바이트, 필드의 경우 1 비트).

분명히이 확장을 사용하면 컴파일러가 (일부 플랫폼에서) 잘못 정렬 된 멤버에 한 번에 한 바이트 씩 액세스하는 코드를 생성해야하기 때문에 데이터 요구 사항은 더 작아 지지만 코드는 느려질 수 있습니다.

그러나 이것이 안전하지 않은 경우가 있습니까? 컴파일러는 항상 올바른 (더 느린) 코드를 생성하여 잘못 정렬 된 묶음 구조체 멤버에 액세스합니까? 모든 경우에 그렇게 할 수 있습니까?


1
gcc 버그 보고서가 포인터 할당에 경고를 추가하고 경고를 비활성화하는 옵션과 함께 수정 됨으로 표시되었습니다. 내 답변의 세부 사항 .
Keith Thompson

답변:


148

예, __attribute__((packed))일부 시스템에서는 안전하지 않을 수 있습니다. 증상은 아마도 x86에 나타나지 않을 것인데, 이는 문제를 더욱 교활하게 만듭니다. x86 시스템에서 테스트해도 문제가 드러나지 않습니다. x86에서는 잘못 정렬 된 액세스가 하드웨어에서 처리됩니다. int*홀수 주소를 가리키는 포인터를 역 참조하면 올바르게 정렬 된 것보다 약간 느리지 만 올바른 결과를 얻을 수 있습니다.

SPARC와 같은 일부 다른 시스템에서는 잘못 정렬 된 int객체 에 액세스하려고 하면 버스 오류가 발생하여 프로그램이 중단됩니다.

잘못 정렬 된 액세스가 주소의 하위 비트를 조용히 무시하여 잘못된 메모리 청크에 액세스하는 시스템도 있습니다.

다음 프로그램을 고려하십시오.

#include <stdio.h>
#include <stddef.h>
int main(void)
{
    struct foo {
        char c;
        int x;
    } __attribute__((packed));
    struct foo arr[2] = { { 'a', 10 }, {'b', 20 } };
    int *p0 = &arr[0].x;
    int *p1 = &arr[1].x;
    printf("sizeof(struct foo)      = %d\n", (int)sizeof(struct foo));
    printf("offsetof(struct foo, c) = %d\n", (int)offsetof(struct foo, c));
    printf("offsetof(struct foo, x) = %d\n", (int)offsetof(struct foo, x));
    printf("arr[0].x = %d\n", arr[0].x);
    printf("arr[1].x = %d\n", arr[1].x);
    printf("p0 = %p\n", (void*)p0);
    printf("p1 = %p\n", (void*)p1);
    printf("*p0 = %d\n", *p0);
    printf("*p1 = %d\n", *p1);
    return 0;
}

gcc 4.5.2가 포함 된 x86 Ubuntu에서 다음과 같은 출력이 생성됩니다.

sizeof(struct foo)      = 5
offsetof(struct foo, c) = 0
offsetof(struct foo, x) = 1
arr[0].x = 10
arr[1].x = 20
p0 = 0xbffc104f
p1 = 0xbffc1054
*p0 = 10
*p1 = 20

gcc 4.5.1이 설치된 SPARC Solaris 9에서 다음을 생성합니다.

sizeof(struct foo)      = 5
offsetof(struct foo, c) = 0
offsetof(struct foo, x) = 1
arr[0].x = 10
arr[1].x = 20
p0 = ffbff317
p1 = ffbff31c
Bus error

두 경우 모두 프로그램은 추가 옵션없이 컴파일됩니다 gcc packed.c -o packed.

(소위 컴파일러가 홀수 어드레스에 구조체를 할당 할 수 있기 때문에, 어레이는 신뢰성 문제가 발생하지 않는다 라기보다는 하나의 구조체를 사용하는 프로그램 x두개의 배열 부재는 적절하게 정렬된다. struct foo목적, 적어도 하나 이상의 다른 잘못 정렬 된 x멤버가 있습니다.)

(이 경우 멤버 뒤 p0의 패킹 된 int멤버를 가리 키기 때문에 잘못 정렬 된 주소를 가리 킵니다 char. p1배열의 두 번째 요소에서 동일한 멤버를 가리 키므로 올바르게 정렬됩니다. 따라서 그 char앞에 두 개의 오브젝트가 있습니다. SPARC Solaris에서는 어레이 arr가 4의 배수가 아닌 짝수 인 주소에 할당 된 것으로 보입니다.)

이름으로 멤버 x를 참조 할 때 struct foo컴파일러 x는 잠재적으로 잘못 정렬 된 것을 알고 올바르게 액세스하기 위해 추가 코드를 생성합니다.

주소가 포인터 객체에 저장 arr[0].x되거나 arr[1].x포인터 객체에 저장되면 컴파일러 나 실행중인 프로그램은 그것이 잘못 정렬 된 int객체를 가리키는 지 알 수 없습니다 . 버스가 올바르게 정렬되어 버스 오류 또는 이와 유사한 다른 오류가 발생한다고 가정합니다.

gcc에서 이것을 고치는 것은 비실용적이라고 생각합니다. 일반적인 해결책은 (a) 컴파일 타임에 포인터가 패킹 된 구조체의 잘못 정렬 된 멤버를 가리 키지 않는다는 것을 입증하거나 (b) 정렬되거나 잘못 정렬 된 객체를 처리 할 수있는 더 크고 느린 코드 생성

gcc 버그 보고서를 제출했습니다 . 내가 말했듯이, 나는 그것을 고치는 것이 실용적이지 않다고 생각하지만, 문서는 그것을 언급해야한다 (현재는 그렇지 않다).

업데이트 : 2018-12-20 현재이 버그는 수정 된 것으로 표시되어 있습니다. 패치는 gcc 9에 새로운 -Waddress-of-packed-member옵션 이 추가 되어 기본적으로 활성화됩니다.

압축 된 구조체 또는 공용체 멤버 주소를 가져 오면 정렬되지 않은 포인터 값이 발생할 수 있습니다. 이 패치는 -Waddress-of-packed-member를 추가하여 포인터 할당시 정렬을 확인하고 정렬되지 않은 주소와 정렬되지 않은 포인터를 경고합니다.

방금 소스에서 해당 버전의 gcc를 만들었습니다. 위 프로그램의 경우 다음 진단을 생성합니다.

c.c: In function main’:
c.c:10:15: warning: taking address of packed member of struct foo may result in an unaligned pointer value [-Waddress-of-packed-member]
   10 |     int *p0 = &arr[0].x;
      |               ^~~~~~~~~
c.c:11:15: warning: taking address of packed member of struct foo may result in an unaligned pointer value [-Waddress-of-packed-member]
   11 |     int *p1 = &arr[1].x;
      |               ^~~~~~~~~

1
잠재적으로 잘못 정렬되어 있고 무엇을 생성합니까?
Almo

5
ARM의 정렬되지 않은 구조체 요소는 이상한 일을합니다. 일부 액세스는 오류를 유발하고 다른 액세스는 검색된 데이터를 반 직관적으로 재배치하거나 인접한 예기치 않은 데이터를 통합합니다.
wallyk

8
패킹 자체는 안전하지만 패킹 된 멤버가 사용되는 방식은 안전하지 않을 수 있습니다. 이전 ARM 기반 CPU는 정렬되지 않은 메모리 액세스를 지원하지 않았지만 최신 버전은 지원하지 않지만 Symbian OS는 이러한 최신 버전에서 실행할 때 정렬되지 않은 액세스를 여전히 허용하지 않습니다 (지원이 꺼져 있음).
James

14
gcc 내에서 수정하는 또 다른 방법은 유형 시스템을 사용하는 것입니다. 패킹 된 구조체의 멤버에 대한 포인터는 패킹 된 것으로 표시된 (즉, 정렬되지 않은) 포인터에만 할당 할 수 있어야합니다. 그러나 실제로 : 압축 된 구조체는 아니오라고 말합니다.
caf

9
@Flavius ​​: 저의 주요 목적은 정보를 얻는 것이 었습니다. 참조 meta.stackexchange.com/questions/17463/...
키이스 톰슨에게

62

위에서 말했듯이 압축 된 구조체의 멤버를 가리 키지 마십시오. 이것은 단순히 불을 가지고 노는 것입니다. 당신이 말 __attribute__((__packed__))하거나 #pragma pack(1), 당신이 정말로 말하는 것은 "이봐 요, 내가 무슨 짓을하는지 정말 알아요." 그렇지 않은 것으로 판명되면 컴파일러를 올바르게 비난 할 수 없습니다.

아마도 우리는 컴플 라이언시 컴파일러를 비난 할 수 있습니다. GCC는 가지고 있지만 -Wcast-align옵션을, 그것은 기본적으로도 함께 사용할 수 없습니다 -Wall-Wextra. 이는 뇌사 "로 코드의 유형을 고려 GCC 개발자에게 명백하게 가증 주소의"가치없는 - 이해 경멸하지만, 도움이되지 않는 때에 미숙 한 프로그래머 bumbles.

다음을 고려하세요:

struct  __attribute__((__packed__)) my_struct {
    char c;
    int i;
};

struct my_struct a = {'a', 123};
struct my_struct *b = &a;
int c = a.i;
int d = b->i;
int *e __attribute__((aligned(1))) = &a.i;
int *f = &a.i;

여기에서의 유형은 a(위에 정의 된) 압축 구조체입니다. 비슷하게 b압축 된 구조체에 대한 포인터입니다. 식의 유형 a.i은 (기본적으로) 1 바이트 정렬 의 int l- 값 입니다. c그리고 d둘 다 정상 int입니다. 을 읽을 때 a.i컴파일러는 정렬되지 않은 액세스를위한 코드를 생성합니다. 당신이 읽을 때 b->i, b의 유형은 여전히 아무 문제 때문에 그 중 하나, 그것은 포장 알고있다. e는 1 바이트로 정렬 된 int에 대한 포인터이므로 컴파일러는이를 올바르게 역 참조하는 방법을 알고 있습니다. 그러나 할당 f = &a.i을 할 때 정렬되지 않은 int 포인터의 값을 정렬 된 int 포인터 변수에 저장합니다. 그리고 gcc는이 경고를 통해기본값 ( -Wall또는에 포함 되지 않음 -Wextra).


6
정렬되지 않은 구조체에 포인터를 사용하는 방법을 설명하는 +1!
Soumya

@Soumya 포인트 감사합니다! :) 그러나 이것은 __attribute__((aligned(1)))gcc 확장이며 이식성이 없다는 것을 명심하십시오 . 내 지식으로는 C에서 정렬되지 않은 액세스를 수행하는 유일한 휴대용 방법 (컴파일러 / 하드웨어 조합 포함)은 바이트 단위 메모리 사본 (memcpy 또는 이와 유사한 것)을 사용하는 것입니다. 일부 하드웨어에는 정렬되지 않은 액세스에 대한 지침조차 없습니다. 내 전문 지식은 arm 및 x86에 관한 것으로, 정렬되지 않은 액세스는 느리지 만 두 가지를 모두 수행 할 수 있습니다. 따라서 고성능으로이 작업을 수행해야하는 경우 하드웨어를 스니핑하고 아치 별 트릭을 사용해야합니다.
다니엘 산토스

4
@Soumya 슬프게도 __attribute__((aligned(x)))포인터에 사용될 때 무시되는 것으로 보입니다. :( 아직 자세한 내용은 없지만 __builtin_assume_aligned(ptr, align)gcc를 사용하여 올바른 코드를 생성하는 것 같습니다.보다 간결한 답변 (및 버그보고)이 있으면 답변을 업데이트하겠습니다.
Daniel Santos

@DanielSantos : 내가 사용하는 (Keil) 고품질 컴파일러는 포인터에 대한 "packed"한정자를 인식합니다. 구조체가 "packed"로 선언되면 uint32_t멤버 의 주소를 가져 와서 uint32_t packed*; 예를 들어 Cortex-M0에서 이러한 포인터를 읽으려고하면 IIRC는 서브 루틴을 호출합니다 .IIRC는 포인터가 정렬되지 않은 경우 일반 읽기보다 ~ 7x 걸리거나 정렬 된 경우 ~ 3x 걸리지 만 어느 경우 에나 예상대로 작동합니다 [인라인 코드는 정렬되거나 정렬되지 않은 상태에서 5 배의 시간이 걸립니다].
supercat


49

항상 .점이나 ->표기법을 통해 구조체를 통해 값에 액세스하는 한 완벽하게 안전 합니다.

무엇 되지 안전은 계정에 그것을 복용하지 않고 접속하시면 다음 정렬되지 않은 데이터의 포인터를 복용하고 있습니다.

또한 구조체의 각 항목이 정렬되지 않은 것으로 알려져 있지만 특정 방식 으로 정렬되지 않은 것으로 알려져 있으므로 컴파일러가 예상하거나 문제가있는 것처럼 구조체 전체를 정렬해야합니다 (일부 플랫폼에서는 또는 앞으로는 정렬되지 않은 액세스를 최적화하기위한 새로운 방법이 발명 된 경우).


흠, 정렬이 다른 다른 압축 된 구조체 안에 하나의 압축 된 구조체를 넣으면 어떻게 될지 궁금합니다. 흥미로운 질문이지만 답을 바꾸어서는 안됩니다.
ams

GCC는 항상 구조 자체를 정렬하지는 않습니다. 예를 들면 다음과 같습니다. struct foo {int x; 숯불 c; } __attribute __ ((포장)); struct bar {문자 c; struct foo f; }; bar :: f :: x가 MIPS의 특정 특징에 반드시 맞춰지는 것은 아니라는 것을 알았습니다.
Anton

3
@antonm : 예, 압축 된 구조체 내의 구조체는 정렬되지 않을 수도 있지만 컴파일러는 각 필드의 정렬이 무엇인지 알고 있으며 구조체에 포인터를 사용하지 않는 한 완벽하게 안전합니다. 가독성을 위해 여분의 이름을 가진 하나의 일련의 필드로 구조체 내의 구조체를 상상해야합니다.
ams December

6

이 속성을 사용하는 것은 확실히 안전하지 않습니다.

그것이 깨는 한 가지 특별한 점은 union하나의 멤버를 작성하고 구조체에 공통의 초기 멤버 시퀀스가있는 경우 다른 멤버를 읽는 두 개 이상의 구조체를 포함 하는 a의 능력입니다 . C11 표준 상태 6.5.2.3 절 :

6 공용체 사용을 단순화하기 위해 하나의 특별한 보증이 적용됩니다. 공용체에 공통의 초기 시퀀스를 공유하는 여러 구조가 포함되어 있고 (아래 참조) 공용체 개체에 현재 이러한 구조 중 하나가 포함되어 있으면 완료된 유형의 공용체 선언이 보이는 모든 곳에서 공통된 초기 부분. 대응하는 멤버가 하나 이상의 초기 멤버 시퀀스에 대해 호환 가능한 유형 (및 비트 필드의 경우 동일한 너비)을 갖는 경우 두 개의 구조는 공통 초기 시퀀스를 공유합니다.

...

9 예 3 다음은 유효한 조각입니다.

union {
    struct {
        int    alltypes;
    }n;
    struct {
        int    type;
        int    intnode;
    } ni;
    struct {
        int    type;
        double doublenode;
    } nf;
}u;
u.nf.type = 1;
u.nf.doublenode = 3.14;
/*
...
*/
if (u.n.alltypes == 1)
if (sin(u.nf.doublenode) == 0.0)
/*
...
*/

__attribute__((packed))도입 되면 이 문제가 해결됩니다. 다음 예제는 최적화가 비활성화 된 gcc 5.4.0을 사용하여 Ubuntu 16.04 x64에서 실행되었습니다.

#include <stdio.h>
#include <stdlib.h>

struct s1
{
    short a;
    int b;
} __attribute__((packed));

struct s2
{
    short a;
    int b;
};

union su {
    struct s1 x;
    struct s2 y;
};

int main()
{
    union su s;
    s.x.a = 0x1234;
    s.x.b = 0x56789abc;

    printf("sizeof s1 = %zu, sizeof s2 = %zu\n", sizeof(struct s1), sizeof(struct s2));
    printf("s.y.a=%hx, s.y.b=%x\n", s.y.a, s.y.b);
    return 0;
}

산출:

sizeof s1 = 6, sizeof s2 = 8
s.y.a=1234, s.y.b=5678

비록 struct s1struct s2는 "공통의 초기 시퀀스"가, 포장은 해당 구성원이 동일한 오프셋 바이트 살지 않는다 그 이전의 수단에 적용. 표준에 따르면 동일해야하지만 결과는 member에 쓴 값이 member x.b에서 읽은 값과 같지 않습니다 y.b.


구조체 중 하나를 포장하고 다른 구조체는 포장하지 않으면 일관된 레이아웃을 기대하지 않을 것이라고 주장 할 수 있습니다. 그러나 그렇습니다. 이것은 위반할 수있는 또 다른 표준 요구 사항입니다.
키이스 톰슨

1

패킹 된 구조체의 주요 용도 중 하나는 의미를 제공 할 데이터 스트림 (예 : 256 바이트)이있는 곳입니다. 더 작은 예를 들면 Arduino에서 다음과 같은 의미의 16 바이트 패킷을 직렬로 전송하는 프로그램을 실행한다고 가정 해보십시오.

0: message type (1 byte)
1: target address, MSB
2: target address, LSB
3: data (chars)
...
F: checksum (1 byte)

그런 다음과 같은 것을 선언 할 수 있습니다

typedef struct {
  uint8_t msgType;
  uint16_t targetAddr; // may have to bswap
  uint8_t data[12];
  uint8_t checksum;
} __attribute__((packed)) myStruct;

그런 다음 포인터 산술로 조정하는 대신 aStruct.targetAddr을 통해 targetAddr 바이트를 참조 할 수 있습니다.

이제 정렬 작업이 발생 하면 컴파일러가 구조체를 압축 된 것으로 취급 하지 않으면 (즉, 지정된 순서대로 데이터를 저장하고 정확히 16을 사용 하지 않으면) 수신 된 데이터에 대한 메모리의 void * 포인터를 가져 와서 myStruct *에 캐스트하지 않습니다. 이 예제의 바이트). 정렬되지 않은 읽기에는 성능이 저하되므로 프로그램에서 활발하게 작업중인 데이터에 압축 된 구조체를 사용하는 것이 반드시 좋은 생각은 아닙니다. 그러나 프로그램에 바이트 목록이 제공되면 압축 된 구조체를 사용하여 내용에 액세스하는 프로그램을보다 쉽게 ​​작성할 수 있습니다.

그렇지 않으면 C ++을 사용하고 액세서 메소드와 클래스 뒤에서 포인터 산술을 수행하는 클래스를 작성하게됩니다. 간단히 말해서, 팩형 구조체는 팩형 데이터를 효율적으로 처리하기위한 것이며, 팩형 데이터는 프로그램과 함께 제공되는 것일 수 있습니다. 대부분의 경우 코드는 구조에서 값을 읽고, 작업하고, 완료되면 다시 써야합니다. 그 밖의 모든 것은 포장 된 구조 외부에서 수행해야합니다. 문제의 일부는 C가 프로그래머로부터 숨기려고하는 낮은 수준의 것들과, 그러한 것들이 실제로 프로그래머에게 중요한 경우에 필요한 후프 점프입니다. (언어에서 다른 '데이터 레이아웃'구성이 거의 필요하므로 '이것은 48 바이트 길이이고 foo는 13 바이트의 데이터를 참조하므로 해석되어야합니다.'와 별도의 구조화 된 데이터 구성,


내가 빠진 것이 아니라면 질문에 대답하지 않습니다. 당신은 구조 패킹이 편리하다고 주장하지만 그것이 안전한지에 대한 질문은 다루지 않습니다. 또한 정렬되지 않은 읽기에 대한 성능 불이익을 주장합니다. 그것은 x86의 경우에 해당하지만 모든 대답은 아닙니다.
키이스 톰슨
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.