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áp | Tá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ường | Chuẩ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ử
- Đo
sizeofvàoffsetofcủa một struct ở cả hai dạng. - Lấy địa chỉ một trường trong packed struct và thử với
-Waddress-of-packed-membernếu trình biên dịch có. - Viết
struct { int co : 1; }, gán 1, và in ra. - Viết cặp hàm đóng gói và mở gói cho một cấu trúc bảy byte.
- Kiểm thử rằng đóng gói rồi mở gói cho lại đúng giá trị ban đầu.
- So hợp ngữ của
doc_u32ở-O2và tìm lệnhbswapnế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.
packedkhô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.