Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 53.428 phút đọc

Packed struct và cách tuần tự hóa đúng

Sau bài này bạn sẽ làm được

  • Tạo packed struct bằng cú pháp của GCC và của MSVC
  • Kể ba vấn đề của packed
  • Giải thích vì sao lấy địa chỉ một trường trong packed là UB
  • Viết hàm tuần tự hóa thủ công xử lý cả thứ tự byte

packed bỏ hết đệm và cho bạn một struct đúng bằng tổng các trường. Nghe rất hợp lý cho định dạng tệp và giao thức mạng, và đó chính là chỗ bạn không nên dùng nó.

#Cú pháp trên từng trình biên dịch

/* GCC va Clang */
struct __attribute__((packed)) Goi {
    uint8_t  loai;
    uint32_t id;
    uint16_t len;
};

/* MSVC */
#pragma pack(push, 1)
struct Goi {
    uint8_t  loai;
    uint32_t id;
    uint16_t len;
};
#pragma pack(pop)

/* Ca hai, qua macro */
#if defined(_MSC_VER)
  #define GOI_DAU  __pragma(pack(push, 1))
  #define GOI_CUOI __pragma(pack(pop))
  #define GOI_NEN
#else
  #define GOI_DAU
  #define GOI_CUOI
  #define GOI_NEN  __attribute__((packed))
#endif

GOI_DAU
struct GOI_NEN Goi { uint8_t loai; uint32_t id; uint16_t len; };
GOI_CUOI

/* Cu phap khac nhau la mot trong ba van de. */
Cú phápTác dụng
__attribute__((packed))Bỏ hết đệm, căn chỉnh struct thành 1
__attribute__((aligned(4)))Đặt căn chỉnh của struct thành 4
#pragma pack(1)Căn chỉnh tối đa là 1, tức bỏ hết đệm
#pragma pack(4)Căn chỉnh tối đa là 4, vẫn có đệm tới 4 byte
#pragma pack(push, n) và popĐặt rồi khôi phục, để không ảnh hưởng header khác
_Alignas(n) trên trườngChuẩn C11, chỉ TĂNG căn chỉnh được

#Đo thật

packed.c
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>

struct Goi { uint8_t loai; uint32_t id; uint16_t len; };

struct __attribute__((packed)) GoiP {
    uint8_t loai; uint32_t id; uint16_t len;
};

int main(void) {
    printf("sizeof(Goi)  = %d\n", (int)sizeof(struct Goi));
    printf("sizeof(GoiP) = %d\n", (int)sizeof(struct GoiP));
    printf("offset id  thuong = %d, packed = %d\n",
           (int)offsetof(struct Goi, id), (int)offsetof(struct GoiP, id));
    printf("offset len thuong = %d, packed = %d\n",
           (int)offsetof(struct Goi, len), (int)offsetof(struct GoiP, len));
    return 0;
}
terminal
gcc -std=c11 -O2 -Wall -o packed.exe packed.c && ./packed.exe
sizeof(Goi)  = 12
sizeof(GoiP) = 7
offset id  thuong = 4, packed = 1
offset len thuong = 8, packed = 5
Bản đồ byte
struct Goi, khong nen:

byte:  0    1 2 3    4 5 6 7    8 9    10 11
     +----+-------+----------+------+-------+
     |loai|  DEM  |    id    | len  |  DEM  |
     +----+-------+----------+------+-------+
     sizeof = 12


struct GoiP, nen:

byte:  0    1 2 3 4    5 6
     +----+----------+------+
     |loai|    id    | len  |
     +----+----------+------+
     sizeof = 7

7 byte du lieu that. Dung nhu ban muon cho mot goi tin.
Va do la ly do cam do rat lon.

#Ba vấn đề

Một: truy cập chậm hơn

/* Doc "id" trong struct thuong:
     id o do doi 4, can chinh 4  ->  MOT lenh mov

   Doc "id" trong struct nen:
     id o do doi 1, khong can chinh  ->  tren x86: mot lenh mov cham hon
                                          tren ARM: nhieu lenh ghep byte

   Tren ARM32, trinh bien dich sinh ra mot chuoi:
     ldrb r3, [r0, #1]
     ldrb r2, [r0, #2]
     orr  r3, r3, r2, lsl #8
     ldrb r2, [r0, #3]
     orr  r3, r3, r2, lsl #16
     ldrb r2, [r0, #4]
     orr  r3, r3, r2, lsl #24

   Bay lenh thay vi mot. Voi vong lap qua mot mang goi tin thi
   chenh lech rat lon. */

Hai: lấy địa chỉ một trường là hành vi không xác định

pa.c
#include <stdint.h>

struct __attribute__((packed)) P { char c; int i; };

int *f(struct P *p) { return &p->i; }
terminal
# GCC 8.1 không có cờ cảnh báo cho việc này
gcc -Waddress-of-packed-member -c pa.c
gcc.exe: error: unrecognized command line option '-Waddress-of-packed-member'
gcc -std=c11 -Wall -Wextra -c pa.c
(khong co canh bao nao)

Ba: không khả chuyển

/* Ba khac biet giua cac trinh bien dich: */

/* 1. Cu phap khac nhau, nhu muc dau bai */

/* 2. Bo cuc voi #pragma pack(4) khac nhau:
      MSVC va GCC hieu "can chinh toi da la 4" hoi khac nhau
      voi cac truong lon hon 4 byte */

/* 3. Bit field trong packed struct thi hoan toan khong tuong thich */

/* Va mot khac biet lon hon: */

/* Struct nen KHONG giai quyet van de THU TU BYTE.
   Bai 33.7 da noi: mot uint32_t ghi ra tep se co thu tu byte
   cua may ban. Doc tren may lon truoc thi ra so khac.

   Nen ngay ca khi bo cuc khop hoan toan, du lieu van sai. */

#Trường bit cũng không khá hơn

struct Co {
    unsigned bat    : 1;
    unsigned che_do : 3;
    unsigned uu_tien: 4;
    unsigned        : 0;      /* dac biet: sang don vi luu tru moi */
    unsigned dem    : 24;
};

/* Nam thu implementation-defined trong doan tren:

   1. Truong dau nam o bit THAP hay bit CAO cua don vi luu tru
   2. Mot truong co duoc phep VAT QUA ranh gioi don vi luu tru khong
   3. "unsigned x : 3" co dau hay khong (chuan noi la
      implementation-defined neu ban viet "int x : 3")
   4. Kieu nao dung duoc lam truong bit (chuan chi bao dam
      int, unsigned int, _Bool; GCC cho ca cac kieu khac)
   5. Can chinh cua don vi luu tru

   Nen truong bit KHONG dung duoc de anh xa dinh dang tep hay
   thanh ghi phan cung mot cach kha chuyen. */

#Cách đúng: tuần tự hóa thủ công

Ghi thẳng struct
struct __attribute__((packed)) Goi {
    uint8_t  loai;
    uint32_t id;
    uint16_t len;
};

/* Ghi ra tep hoac gui qua mang: */
fwrite(&g, sizeof g, 1, f);
send(sock, &g, sizeof g, 0);

/* Bon van de:
     1. thu tu byte cua may ban
     2. bo cuc phu thuoc trinh bien dich
     3. lay dia chi truong la UB neu can xu ly rieng
     4. khong kiem tra duoc kich thuoc bo dem */
Tuần tự hóa từng trường
typedef struct {          /* struct THUONG, khong nen */
    uint8_t  loai;
    uint32_t id;
    uint16_t len;
} Goi;

enum { GOI_CO_TREN_DAY = 7 };    /* kich thuoc TREN DAY, co dinh */

size_t goi_dong_goi(const Goi *g, uint8_t *ra, size_t cap) {
    if (cap < GOI_CO_TREN_DAY) return 0;

    ra[0] = g->loai;

    ra[1] = (uint8_t)(g->id >> 24);      /* lon truoc, thu tu mang */
    ra[2] = (uint8_t)(g->id >> 16);
    ra[3] = (uint8_t)(g->id >>  8);
    ra[4] = (uint8_t)(g->id      );

    ra[5] = (uint8_t)(g->len >> 8);
    ra[6] = (uint8_t)(g->len     );

    return GOI_CO_TREN_DAY;
}

size_t goi_mo_goi(Goi *g, const uint8_t *tu, size_t co) {
    if (co < GOI_CO_TREN_DAY) return 0;

    g->loai = tu[0];

    g->id = (uint32_t)tu[1] << 24 | (uint32_t)tu[2] << 16
          | (uint32_t)tu[3] <<  8 | (uint32_t)tu[4];

    g->len = (uint16_t)((uint16_t)tu[5] << 8 | tu[6]);

    return GOI_CO_TREN_DAY;
}
Bộ hàm tiện ích nên có
#include <stdint.h>
#include <string.h>

/* Ghi, lon truoc, tuc thu tu mang */

static inline void ghi_u8 (uint8_t *r, uint8_t  v) { r[0] = v; }

static inline void ghi_u16(uint8_t *r, uint16_t v) {
    r[0] = (uint8_t)(v >> 8); r[1] = (uint8_t)v;
}

static inline void ghi_u32(uint8_t *r, uint32_t v) {
    r[0] = (uint8_t)(v >> 24); r[1] = (uint8_t)(v >> 16);
    r[2] = (uint8_t)(v >>  8); r[3] = (uint8_t)v;
}

static inline void ghi_u64(uint8_t *r, uint64_t v) {
    for (int i = 0; i < 8; ++i) r[i] = (uint8_t)(v >> (56 - i * 8));
}

/* Doc */

static inline uint8_t doc_u8(const uint8_t *t) { return t[0]; }

static inline uint16_t doc_u16(const uint8_t *t) {
    return (uint16_t)((uint16_t)t[0] << 8 | t[1]);
}

static inline uint32_t doc_u32(const uint8_t *t) {
    return (uint32_t)t[0] << 24 | (uint32_t)t[1] << 16
         | (uint32_t)t[2] <<  8 | (uint32_t)t[3];
}

/* So thuc: qua bieu dien nhi phan, dung memcpy. Bai 52.3 */

static inline void ghi_f64(uint8_t *r, double v) {
    uint64_t u;
    memcpy(&u, &v, sizeof u);
    ghi_u64(r, u);
}

static inline double doc_f64(const uint8_t *t) {
    uint64_t u = 0;
    for (int i = 0; i < 8; ++i) u = u << 8 | t[i];
    double v;
    memcpy(&v, &u, sizeof v);
    return v;
}

/* Chu y so thuc: cach nay bao dam thu tu byte, nhung van gia dinh
   ca hai may dung IEEE 754. Trong thuc te thi moi may deu vay,
   va __STDC_IEC_559__ noi cho ban biet. */

Tự làm thử

  1. Đo sizeof và offsetof của một struct ở cả hai dạng.
  2. Lấy địa chỉ một trường trong packed struct và thử với -Waddress-of-packed-member nếu trình biên dịch có.
  3. Viết struct { int co : 1; }, gán 1, và in ra.
  4. Viết cặp hàm đóng gói và mở gói cho một cấu trúc bảy byte.
  5. Kiểm thử rằng đóng gói rồi mở gói cho lại đúng giá trị ban đầu.
  6. So hợp ngữ của doc_u32 ở -O2 và tìm lệnh bswap nếu có.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • packed đưa struct từ 12 byte xuống 7 byte, đúng bằng tổng các trường.
  • Ba vấn đề: truy cập chậm hơn, lấy địa chỉ trường là hành vi không xác định, và cú pháp không khả chuyển.
  • Trường bit có tới năm điểm implementation-defined và cho ra cùng số lệnh với mặt nạ, nên không đáng dùng cho dữ liệu ra ngoài.
  • packed không giải quyết vấn đề thứ tự byte, nên nó không đủ cho định dạng tệp hay giao thức mạng.
  • Cách đúng là tuần tự hóa từng trường: định dạng cố định, kiểm tra được kích thước, không UB, và bố cục trong bộ nhớ tự do tối ưu.