C에서 컴파일러는 각 멤버가 올바르게 정렬되도록 멤버 사이 또는 마지막 멤버 뒤에 패딩 바이트를 삽입하여 선언 된 순서대로 구조체 멤버를 배치합니다.
gcc는 언어 확장을 제공하는데 __attribute__((packed))
, 이는 컴파일러에게 패딩을 삽입하지 않도록 지시하여 구조체 멤버가 잘못 정렬되도록합니다. 예를 들어, 시스템에서 일반적으로 모든 int
객체에 4 바이트 정렬 __attribute__((packed))
이 필요한 경우 int
구조체 멤버가 홀수 오프셋으로 할당 될 수 있습니다 .
gcc 문서 인용하기 :
`packed ‘속성은`aligned’속성으로 더 큰 값을 지정하지 않는 한 변수 또는 구조 필드가 가능한 가장 작은 정렬을 갖도록 지정합니다 (변수의 경우 1 바이트, 필드의 경우 1 비트).
분명히이 확장을 사용하면 컴파일러가 (일부 플랫폼에서) 잘못 정렬 된 멤버에 한 번에 한 바이트 씩 액세스하는 코드를 생성해야하기 때문에 데이터 요구 사항은 더 작아 지지만 코드는 느려질 수 있습니다.
그러나 이것이 안전하지 않은 경우가 있습니까? 컴파일러는 항상 올바른 (더 느린) 코드를 생성하여 잘못 정렬 된 묶음 구조체 멤버에 액세스합니까? 모든 경우에 그렇게 할 수 있습니까?
답변
예, __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;
| ^~~~~~~~~
답변
위에서 말했듯이 압축 된 구조체의 멤버를 가리 키지 마십시오. 이것은 단순히 불을 가지고 노는 것입니다. 당신이 말 __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
).
답변
항상 .
점이나 ->
표기법을 통해 구조체를 통해 값에 액세스하는 한 완벽하게 안전 합니다.
무엇 되지 안전은 계정에 그것을 복용하지 않고 접속하시면 다음 정렬되지 않은 데이터의 포인터를 복용하고 있습니다.
또한 구조체의 각 항목이 정렬되지 않은 것으로 알려져 있지만 특정 방식 으로 정렬되지 않은 것으로 알려져 있으므로 컴파일러가 예상하거나 문제가있는 것처럼 구조체 전체를 정렬해야합니다 (일부 플랫폼에서는 또는 앞으로는 정렬되지 않은 액세스를 최적화하기위한 새로운 방법이 발명 된 경우).
답변
이 속성을 사용하는 것은 확실히 안전하지 않습니다.
그것이 깨는 한 가지 특별한 점은 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 s1
과 struct s2
는 “공통의 초기 시퀀스”가, 포장은 해당 구성원이 동일한 오프셋 바이트 살지 않는다 그 이전의 수단에 적용. 표준에 따르면 동일해야하지만 결과는 member에 쓴 값이 member x.b
에서 읽은 값과 같지 않습니다 y.b
.
답변
패킹 된 구조체의 주요 용도 중 하나는 의미를 제공 할 데이터 스트림 (예 : 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 바이트의 데이터를 참조하므로 해석되어야합니다.’와 별도의 구조화 된 데이터 구성,