Là __attribution __ ((đóng gói)) / #pragma của gcc có an toàn không?


164

Trong C, trình biên dịch sẽ bố trí các thành viên của một cấu trúc theo thứ tự mà chúng được khai báo, với các byte đệm có thể được chèn giữa các thành viên hoặc sau thành viên cuối cùng, để đảm bảo rằng mỗi thành viên được căn chỉnh chính xác.

gcc cung cấp một phần mở rộng ngôn ngữ, thông báo __attribute__((packed))cho trình biên dịch không chèn phần đệm, cho phép các thành viên cấu trúc bị sai lệch. Ví dụ, nếu hệ thống thường yêu cầu tất cả intcác đối tượng phải căn chỉnh 4 byte, __attribute__((packed))có thể khiến intcác thành viên cấu trúc được phân bổ ở các độ lệch lẻ.

Trích dẫn tài liệu gcc:

Thuộc tính `pack 'chỉ định rằng một trường biến hoặc cấu trúc nên có sự căn chỉnh nhỏ nhất có thể - một byte cho một biến và một bit cho một trường, trừ khi bạn chỉ định một giá trị lớn hơn với thuộc tính` được căn chỉnh'.

Rõ ràng việc sử dụng phần mở rộng này có thể dẫn đến các yêu cầu dữ liệu nhỏ hơn nhưng mã chậm hơn, vì trình biên dịch phải (trên một số nền tảng) tạo mã để truy cập một thành viên bị sai lệch một byte tại một thời điểm.

Nhưng có trường hợp nào không an toàn? Trình biên dịch luôn tạo mã chính xác (mặc dù chậm hơn) để truy cập các thành viên bị sai lệch của các cấu trúc được đóng gói? Nó thậm chí có thể cho nó làm như vậy trong mọi trường hợp?


1
Báo cáo lỗi gcc hiện được đánh dấu là CỐ ĐỊNH với việc thêm cảnh báo vào việc gán con trỏ (và một tùy chọn để vô hiệu hóa cảnh báo). Chi tiết trong câu trả lời của tôi .
Keith Thompson

Câu trả lời:


148

Có, __attribute__((packed))có khả năng không an toàn trên một số hệ thống. Triệu chứng có thể sẽ không xuất hiện trên x86, điều này chỉ khiến vấn đề trở nên ngấm ngầm hơn; thử nghiệm trên các hệ thống x86 sẽ không tiết lộ vấn đề. (Trên x86, truy cập lệch được xử lý trong phần cứng, nếu bạn dereference một int*con trỏ trỏ đến một địa chỉ lẻ, nó sẽ là một chút chậm hơn so với nếu nó được liên kết đúng, nhưng bạn sẽ nhận được kết quả chính xác.)

Trên một số hệ thống khác, chẳng hạn như SPARC, cố gắng truy cập một intđối tượng sai lệch gây ra lỗi xe buýt, làm hỏng chương trình.

Cũng có những hệ thống trong đó một truy cập sai lệch lặng lẽ bỏ qua các bit thứ tự thấp của địa chỉ, khiến nó truy cập vào đoạn bộ nhớ sai.

Hãy xem xét chương trình sau:

#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;
}

Trên x86 Ubuntu với gcc 4.5.2, nó tạo ra đầu ra sau:

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

Trên SPARC Solaris 9 với gcc 4.5.1, nó tạo ra các mục sau:

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

Trong cả hai trường hợp, chương trình được biên dịch không có tùy chọn bổ sung, chỉ gcc packed.c -o packed.

(Một chương trình sử dụng một cấu trúc đơn thay vì mảng không thể hiện vấn đề một cách đáng tin cậy, vì trình biên dịch có thể phân bổ cấu trúc trên một địa chỉ lẻ để xthành viên được căn chỉnh chính xác. Với một mảng gồm hai struct foođối tượng, ít nhất là một hoặc hai đối tượng khác sẽ có một xthành viên sai lệch .)

(Trong trường hợp này, p0trỏ đến một địa chỉ sai, bởi vì nó trỏ đến một intthành viên được đóng gói theo sau một charthành viên. Tình p1cờ được căn chỉnh chính xác, vì nó trỏ đến cùng một thành viên trong phần tử thứ hai của mảng, vì vậy có hai charđối tượng trước nó - và trên SPARC Solaris, mảng arrdường như được phân bổ tại một địa chỉ chẵn, nhưng không phải là bội số của 4.)

Khi đề cập đến thành viên xcủa một struct footên, trình biên dịch biết rằng xcó khả năng bị sai lệch và sẽ tạo mã bổ sung để truy cập chính xác.

Khi địa chỉ của arr[0].xhoặc arr[1].xđã được lưu trữ trong một đối tượng con trỏ, cả trình biên dịch và chương trình đang chạy đều không biết rằng nó trỏ đến một intđối tượng sai . Nó chỉ giả định rằng nó được căn chỉnh chính xác, dẫn đến (trên một số hệ thống) bị lỗi bus hoặc lỗi tương tự khác.

Sửa lỗi này trong gcc, tôi tin, là không thực tế. Một giải pháp chung sẽ yêu cầu, cho mỗi lần cố gắng hủy bỏ một con trỏ tới bất kỳ loại nào với các yêu cầu căn chỉnh không tầm thường hoặc (a) chứng minh tại thời điểm biên dịch rằng con trỏ không trỏ đến một thành viên bị sai lệch của cấu trúc đóng gói, hoặc (b) tạo mã lớn hơn và chậm hơn có thể xử lý các đối tượng được căn chỉnh hoặc sắp xếp sai.

Tôi đã gửi một báo cáo lỗi gcc . Như tôi đã nói, tôi không tin rằng việc sửa nó là thực tế, nhưng tài liệu nên đề cập đến nó (hiện tại không có).

CẬP NHẬT : Kể từ 2018-12-20, lỗi này được đánh dấu là CỐ ĐỊNH. Bản vá sẽ xuất hiện trong gcc 9 với việc bổ sung -Waddress-of-packed-membertùy chọn mới , được bật theo mặc định.

Khi địa chỉ của thành viên đóng gói của struct hoặc union được lấy, nó có thể dẫn đến một giá trị con trỏ không được sắp xếp. Bản vá này thêm -Waddress-of-thành viên đóng gói để kiểm tra căn chỉnh tại gán con trỏ và cảnh báo địa chỉ không được phân bổ cũng như con trỏ không được chỉ định

Tôi vừa mới xây dựng phiên bản gcc đó từ nguồn. Đối với chương trình trên, nó tạo ra các chẩn đoán:

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
có khả năng bị điều chỉnh sai, và sẽ tạo ra ... cái gì?
Almo

5
Các thành phần cấu trúc bị sai lệch trên ARM thực hiện những thứ kỳ lạ: Một số truy cập gây ra lỗi, một số khác khiến dữ liệu được truy xuất được sắp xếp lại theo trực giác hoặc kết hợp dữ liệu bất ngờ liền kề.
wallyk

8
Có vẻ như việc đóng gói chính nó là an toàn, nhưng cách các thành viên đóng gói được sử dụng có thể không an toàn. Các CPU dựa trên ARM cũ hơn cũng không hỗ trợ truy cập bộ nhớ chưa được phân bổ, các phiên bản mới hơn nhưng tôi biết hệ điều hành Symbian vẫn không cho phép truy cập không được phân bổ khi chạy trên các phiên bản mới hơn này (hỗ trợ bị tắt).
James

14
Một cách khác để khắc phục nó trong gcc là sử dụng hệ thống loại: yêu cầu các con trỏ tới các thành viên của các cấu trúc đóng gói chỉ có thể được gán cho các con trỏ được đánh dấu là đóng gói (nghĩa là có khả năng không được phân bổ). Nhưng thực sự: cấu trúc đóng gói, chỉ cần nói không.
phê

9
@Flavius: Mục đích chính của tôi là lấy thông tin ra khỏi đó. Xem thêm meta.stackexchange.com/questions/17463/ Kẻ
Keith Thompson

62

Như ams đã nói ở trên, đừng lấy một con trỏ tới một thành viên của cấu trúc được đóng gói. Điều này chỉ đơn giản là chơi với lửa. Khi bạn nói __attribute__((__packed__))hoặc #pragma pack(1), những gì bạn thực sự nói là "Này gcc, tôi thực sự biết những gì tôi đang làm." Khi nó bật ra rằng bạn không, bạn không thể đổ lỗi cho trình biên dịch.

Có lẽ chúng ta có thể đổ lỗi cho trình biên dịch cho sự tự mãn của nó. Mặc dù gcc không có -Wcast-aligntùy chọn, nhưng nó không được bật theo mặc định cũng như với -Wallhoặc -Wextra. Điều này rõ ràng là do các nhà phát triển gcc coi loại mã này là một " sự ghê tởm " chết não không đáng để giải quyết - sự khinh thường có thể hiểu được, nhưng nó không giúp ích gì khi một lập trình viên thiếu kinh nghiệm gặp phải nó.

Hãy xem xét những điều sau đây:

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;

Ở đây, loại của alà một cấu trúc đóng gói (như được định nghĩa ở trên). Tương tự, blà một con trỏ đến một cấu trúc đóng gói. Loại biểu thức a.ilà (về cơ bản) một giá trị int l với căn chỉnh 1 byte. cdđều là ints bình thường . Khi đọc a.i, trình biên dịch tạo mã để truy cập không được phân bổ. Khi bạn đọc b->i, bloại của nó vẫn biết nó được đóng gói, vì vậy cũng không có vấn đề gì. elà một con trỏ tới một int được liên kết một byte, vì vậy trình biên dịch cũng biết cách thực hiện chính xác điều đó. Nhưng khi bạn thực hiện bài tập f = &a.i, bạn đang lưu trữ giá trị của một con trỏ int không được sắp xếp trong một biến con trỏ int được căn chỉnh - đó là nơi bạn đã sai. Và tôi đồng ý, gcc nên kích hoạt cảnh báo này bằngmặc định (thậm chí không trong -Wallhoặc -Wextra).


6
+1 để giải thích cách sử dụng con trỏ với các cấu trúc không được sắp xếp!
Soumya

@Soumya Cảm ơn bạn đã cho điểm! :) Hãy nhớ rằng đó __attribute__((aligned(1)))là một phần mở rộng gcc và không thể di động. Theo hiểu biết của tôi, cách thực sự di động duy nhất để thực hiện truy cập không được phân bổ trong C (với bất kỳ kết hợp trình biên dịch / phần cứng nào) là với một bản sao bộ nhớ theo byte (memcpy hoặc tương tự). Một số phần cứng thậm chí không có hướng dẫn để truy cập không được phân bổ. Chuyên môn của tôi là với arm và x86 có thể làm cả hai, mặc dù truy cập không được phân bổ chậm hơn. Vì vậy, nếu bạn cần làm điều này với hiệu suất cao, bạn sẽ cần đánh hơi phần cứng và sử dụng các thủ thuật dành riêng cho vòm.
Daniel Santos

4
@Soumya Đáng buồn thay, __attribute__((aligned(x)))bây giờ dường như bị bỏ qua khi được sử dụng cho con trỏ. :( Tôi chưa có chi tiết đầy đủ về điều này, nhưng __builtin_assume_aligned(ptr, align)dường như sử dụng gcc để tạo mã chính xác. Khi tôi có câu trả lời ngắn gọn hơn (và hy vọng là báo cáo lỗi) Tôi sẽ cập nhật câu trả lời của mình.
Daniel Santos

@DanielSantos: Trình biên dịch chất lượng tôi sử dụng (Keil) nhận ra các vòng loại "đóng gói" cho con trỏ; nếu một cấu trúc được tuyên bố là "đóng gói", lấy địa chỉ của một uint32_tthành viên sẽ mang lại một uint32_t packed*; cố gắng đọc từ một con trỏ như vậy trên vd [mã trong dòng sẽ mất 5 lần cho dù được căn chỉnh hay không được phân bổ].
supercat


49

Nó hoàn toàn an toàn miễn là bạn luôn truy cập các giá trị thông qua cấu trúc thông qua .(dấu chấm) hoặc ->ký hiệu.

Điều không an toàn là lấy con trỏ của dữ liệu chưa được phân bổ và sau đó truy cập nó mà không tính đến điều đó.

Ngoài ra, mặc dù mỗi mục trong cấu trúc được biết là không được phân bổ, nhưng nó được biết là không được sắp xếp theo một cách cụ thể , do đó, toàn bộ cấu trúc phải được căn chỉnh như trình biên dịch mong đợi hoặc sẽ có sự cố (trên một số nền tảng, hoặc trong tương lai nếu một cách mới được phát minh để tối ưu hóa các truy cập không được phân bổ).


Hmm, tôi tự hỏi điều gì xảy ra nếu bạn đặt một cấu trúc đóng gói bên trong một cấu trúc đóng gói khác trong đó sự liên kết sẽ khác nhau? Câu hỏi thú vị, nhưng nó không nên thay đổi câu trả lời.
AMS

GCC sẽ không luôn luôn sắp xếp cấu trúc chính nó. Ví dụ: struct foo {int x; char c; } __attribution __ ((đóng gói)); thanh cấu trúc {char c; cấu trúc foo f; }; Tôi thấy rằng thanh :: f :: x sẽ không nhất thiết phải được căn chỉnh, ít nhất là trên các hương vị nhất định của MIPS.
Anton

3
@antonm: Có, một cấu trúc trong một cấu trúc được đóng gói có thể không được sắp xếp, nhưng, một lần nữa, trình biên dịch biết sự liên kết của từng trường là gì và nó hoàn toàn an toàn miễn là bạn không cố gắng sử dụng các con trỏ vào cấu trúc. Bạn nên tưởng tượng một cấu trúc trong một cấu trúc như một chuỗi các trường phẳng, với tên phụ chỉ để dễ đọc.
am

6

Sử dụng thuộc tính này chắc chắn không an toàn.

Một điều đặc biệt mà nó phá vỡ là khả năng của một uniontrong đó có hai hoặc nhiều cấu trúc để viết một thành viên và đọc một thành viên khác nếu các cấu trúc có một chuỗi thành viên ban đầu chung. Mục 6.5.2.3 của các tiêu chuẩn C11 nêu rõ:

6 Một bảo đảm đặc biệt được thực hiện để đơn giản hóa việc sử dụng các hiệp hội: nếu một liên minh có chứa một số cấu trúc có chung một chuỗi ban đầu (xem bên dưới) và nếu đối tượng hợp nhất hiện có một trong các cấu trúc này, thì được phép kiểm tra phần ban đầu chung của bất kỳ ai trong số họ ở bất cứ nơi nào có thể nhìn thấy một tuyên bố về loại hoàn thành của liên minh. Cấu trúc hai phần chia sẻ một chuỗi ban đầu chung nếu các thành viên tương ứng có các loại tương thích (và, đối với các trường bit, cùng độ rộng) cho một chuỗi gồm một hoặc nhiều thành viên ban đầu.

...

9 VÍ DỤ 3 Sau đây là một đoạn hợp lệ:

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)
/*
...
*/

Khi __attribute__((packed))được giới thiệu nó phá vỡ điều này. Ví dụ sau được chạy trên Ubuntu 16.04 x64 bằng gcc 5.4.0 với tối ưu hóa bị tắt:

#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;
}

Đầu ra:

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

Mặc dù struct s1struct s2có một "chuỗi ban đầu chung", việc đóng gói được áp dụng cho trước đây có nghĩa là các thành viên tương ứng không sống ở cùng một byte bù. Kết quả là giá trị được ghi cho thành viên x.bkhông giống với giá trị được đọc từ thành viên y.b, mặc dù tiêu chuẩn nói rằng chúng phải giống nhau.


Người ta có thể lập luận rằng nếu bạn đóng gói một trong các cấu trúc chứ không phải cái kia, thì bạn sẽ không mong đợi chúng có bố cục nhất quán. Nhưng có, đây là một yêu cầu tiêu chuẩn khác mà nó có thể vi phạm.
Keith Thompson

1

(Sau đây là một ví dụ rất giả tạo được đưa ra để minh họa.) Một cách sử dụng chính của các cấu trúc được đóng gói là nơi bạn có một luồng dữ liệu (giả sử là 256 byte) mà bạn muốn cung cấp ý nghĩa. Nếu tôi lấy một ví dụ nhỏ hơn, giả sử tôi có một chương trình đang chạy trên Arduino của mình, nó gửi qua serial một gói 16 byte có ý nghĩa như sau:

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

Sau đó tôi có thể tuyên bố một cái gì đó như

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

và sau đó tôi có thể tham khảo các byte đíchAddr thông qua aStstall.targetAddr thay vì đấu tranh với số học con trỏ.

Bây giờ với công cụ căn chỉnh xảy ra, việc lấy một con trỏ void * trong bộ nhớ đến dữ liệu nhận được và chuyển nó thành mySturation * sẽ không hoạt động trừ khi trình biên dịch xử lý cấu trúc như được đóng gói (nghĩa là nó lưu trữ dữ liệu theo thứ tự được chỉ định và sử dụng chính xác 16 byte cho ví dụ này). Có các hình phạt về hiệu suất đối với các lần đọc không được phân bổ, do đó, sử dụng các cấu trúc đóng gói cho dữ liệu mà chương trình của bạn đang tích cực làm việc không nhất thiết phải là một ý tưởng hay. Nhưng khi chương trình của bạn được cung cấp một danh sách các byte, các cấu trúc được đóng gói giúp viết các chương trình truy cập nội dung dễ dàng hơn.

Nếu không, bạn kết thúc bằng C ++ và viết một lớp với các phương thức truy cập và công cụ con trỏ số học đằng sau hậu trường. Nói tóm lại, các cấu trúc được đóng gói là để xử lý hiệu quả với dữ liệu được đóng gói và dữ liệu được đóng gói có thể là những gì chương trình của bạn được cung cấp để làm việc với. Đối với hầu hết các phần, mã của bạn nên đọc các giá trị ra khỏi cấu trúc, làm việc với chúng và viết lại chúng khi hoàn thành. Tất cả những thứ khác nên được thực hiện bên ngoài cấu trúc đóng gói. Một phần của vấn đề là những thứ cấp thấp mà C cố gắng che giấu khỏi lập trình viên, và việc nhảy lên cần thiết nếu những điều đó thực sự quan trọng với lập trình viên. (Bạn gần như cần một cấu trúc 'bố cục dữ liệu' khác nhau trong ngôn ngữ để bạn có thể nói 'điều này dài 48 byte, foo đề cập đến dữ liệu 13 byte và nên được hiểu như vậy'; và một cấu trúc dữ liệu có cấu trúc riêng biệt,


Trừ khi tôi thiếu một cái gì đó, điều này không trả lời câu hỏi. Bạn cho rằng việc đóng gói cấu trúc là thuận tiện (đó là), nhưng bạn không giải quyết câu hỏi liệu nó có an toàn không. Ngoài ra, bạn khẳng định rằng hình phạt hiệu suất đối với các lần đọc không được phân bổ; điều đó đúng với x86, nhưng không phải cho tất cả các hệ thống, như tôi đã chứng minh trong câu trả lời của mình.
Keith Thompson
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.