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

offsetof, container_of và kiểm tra bố cục

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

  • Dùng offsetof đúng cách
  • Cài container_of và giải thích phép trừ trong đó
  • Viết _Static_assert chốt kích thước và độ dời
  • Biết vì sao không nên tự tính độ dời bằng tay

offsetof cho bạn khoảng cách từ đầu struct tới một trường. container_of đi ngược lại: từ địa chỉ một trường, tính ra địa chỉ struct chứa nó. Cặp này là nền của mọi cấu trúc dữ liệu xâm nhập.

#offsetof

#include <stddef.h>

size_t offsetof(type, member);      /* la mot MACRO, khong phai ham */

struct Data { char a; int b; char c; };

offsetof(struct Data, a)     /* 0 */
offsetof(struct Data, b)     /* 4 */
offsetof(struct Data, c)     /* 8 */

/* Ket qua la mot BIEU THUC HANG luc dich, nen dung duoc trong
   _Static_assert, trong kich thuoc mang, va trong nhan case. */
terminal
gcc -std=c11 -O2 -o bocuc.exe bocuc.c && ./bocuc.exe
sizeof   = 12
alignof  = 4
offset a = 0
offset b = 4
offset c = 8

#container_of

container_of
Từ một con trỏ tới trường m của một struct kiểu T, tính ra con trỏ tới chính struct đó. Phép tính là trừ đi độ dời của trường.
Bản đơn giản nhất
#include <stddef.h>

#define VE_CHU(ptr, T, m) \
    ((T *)((char *)(ptr) - offsetof(T, m)))

/* Vi du: */

typedef struct Nut { struct Nut *truoc, *sau; } Nut;

typedef struct {
    int  ma;               /* do doi  0, 4 byte  */
    char ten[16];          /* do doi  4, 16 byte */
                           /* DEM     20, 4 byte, vi Nut can can chinh 8 */
    Nut  moc;              /* do doi 24, 16 byte */
} SinhVien;                /* sizeof 40 */

Nut *n = ...;                              /* con tro toi moc */
SinhVien *sv = VE_CHU(n, SinhVien, moc);   /* con tro toi ca sinh vien */
Vì sao phép trừ đó đúng
/* Bo nho:

   dia chi cua sv                    dia chi cua sv->moc
        |                                    |
        v                                    v
   +----+----------------+------+------------------------+
   | ma |      ten       | DEM  |          moc           |
   +----+----------------+------+------------------------+
   0    4               20     24                       40

   offsetof(SinhVien, moc) = 24

   Nen: dia chi cua sv = dia chi cua sv->moc - 24

   Chu y con so 24 chu khong phai 20: "ten" ket thuc o byte 20,
   nhung Nut chua hai con tro nen can can chinh 8, va 20 khong
   chia het cho 8. Trinh bien dich chen 4 byte dem. Bai 53.1.

   Ep sang char * truoc khi tru la BAT BUOC:
     - so hoc con tro tren SinhVien * se tru theo don vi sizeof(SinhVien)
     - so hoc tren char * tru theo don vi 1 byte, la cai ban muon
     - va so hoc tren void * la mo rong cua GNU, khong phai chuan */
terminal
# Hợp ngữ: container_of biến thành đúng một lệnh
gcc -O2 -S -masm=intel -o - vechu.c
ve_chu:
    lea  rax, -24[rcx]
    ret

#Làm cho nó an toàn về kiểu

Bản không kiểm tra
#define VE_CHU(ptr, T, m) \
    ((T *)((char *)(ptr) - offsetof(T, m)))

/* Khong ai kiem tra rang "ptr" that su tro toi truong "m".
   Neu ban viet nham: */

Nut *n = ...;
SinhVien *sv = VE_CHU(n, SinhVien, ten);   /* SAI truong, dich duoc! */

/* Ket qua la mot con tro tro sai cho, va moi truy cap sau do
   deu la rac. Khong canh bao nao. */
Bản kiểm tra kiểu
/* Ban cua nhan Linux, dung typeof de kiem tra kieu: */

#define VE_CHU(ptr, T, m) __extension__({                          \
    const __typeof__(((T *)0)->m) *_mptr = (ptr);                  \
    (T *)((char *)_mptr - offsetof(T, m));                         \
})

Nut *n = ...;
SinhVien *sv = VE_CHU(n, SinhVien, ten);
/* warning: initialization of 'const char (*)[16]' from incompatible
   pointer type 'Nut *'  */

/* Dong dau gan "ptr" vao mot con tro co KIEU CUA TRUONG.
   Neu kieu khong khop thi trinh bien dich canh bao.

   Chi phi: dung __typeof__ va bieu thuc khoi, ca hai la mo rong GNU.
   Nen bao no trong #if defined(__GNUC__). */
Bản đầy đủ, chạy được ở mọi nơi
#include <stddef.h>

#if defined(__GNUC__) || defined(__clang__)

  #define VE_CHU(ptr, T, m) __extension__({                        \
      const __typeof__(((T *)0)->m) *_mp = (ptr);                  \
      (T *)((char *)_mp - offsetof(T, m));                         \
  })

#else

  #define VE_CHU(ptr, T, m) \
      ((T *)((char *)(ptr) - offsetof(T, m)))

#endif

/* Tren GCC va Clang: co kiem tra kieu.
   Tren trinh bien dich khac: khong co, nhung van chay dung.

   Do la mau tot cho moi macro dung mo rong: ban day du cho
   trinh bien dich chinh, ban toi thieu cho phan con lai. */

#Chốt bố cục lúc dịch

Bốn loại khẳng định đáng đặt
#include <stddef.h>
#include <assert.h>

/* 1. Kich thuoc, khi struct la mot phan ABI hoac dinh dang tep */
_Static_assert(sizeof(BanGhi) == 112,
               "dinh dang tep v1: kich thuoc ban ghi phai la 112");

/* 2. Do doi tung truong */
_Static_assert(offsetof(BanGhi, ma)   == 0,  "bo cuc: ma");
_Static_assert(offsetof(BanGhi, ten)  == 4,  "bo cuc: ten");
_Static_assert(offsetof(BanGhi, diem) == 104, "bo cuc: diem");

/* 3. Truong dau tien, cho ke thua thu cong. Bai 29.5 */
_Static_assert(offsetof(HinhTron, co_so) == 0,
               "co_so phai la truong dau tien");

/* 4. Khong co lo trong nao */
_Static_assert(sizeof(SinhVien) ==
               sizeof(double) + sizeof(long) + 32 + 8 + sizeof(int) + 2,
               "SinhVien co lo trong, xem lai thu tu truong");
terminal
gcc -std=c11 -c gd.c
gd.c:10:1: error: static assertion failed: "ma nay gia dinh int it nhat 3200 bit"
 _Static_assert(sizeof(int) >= 400,
 ^~~~~~~~~~~~~~
Một macro sinh khẳng định cho mọi trường
/* Ket hop voi X Macro cua Bai 54.3: */

#define CAC_TRUONG(X)         \
    X(ma,   0,   4)           \
    X(ten,  4,   64)          \
    X(diem, 104, 8)

typedef struct {
    uint32_t ma;
    char     ten[64];
    uint32_t dem;         /* 4 byte dem, co y */
    double   diem;
} BanGhi;

/* Sinh khang dinh cho tung truong: */
#define X(ten_truong, do_doi, co)                                    \
    _Static_assert(offsetof(BanGhi, ten_truong) == (do_doi),         \
                   "bo cuc: " #ten_truong " sai do doi");            \
    _Static_assert(sizeof(((BanGhi *)0)->ten_truong) == (co),        \
                   "bo cuc: " #ten_truong " sai kich thuoc");
CAC_TRUONG(X)
#undef X

/* Them mot truong vao danh sach la tu dong co hai khang dinh moi.
   Bai 54.3 se noi ky ve ky thuat nay. */

#Ba ứng dụng thật

Một: danh sách xâm nhập

typedef struct Nut { struct Nut *truoc, *sau; } Nut;

typedef struct {
    int ma;
    Nut moc;
} SinhVien;

#define DUYET(bien, dau) \
    for (Nut *bien = (dau)->sau; bien != (dau); bien = bien->sau)

DUYET(n, &dau) {
    SinhVien *sv = VE_CHU(n, SinhVien, moc);
    printf("%d\n", sv->ma);
}

/* Uu diem so voi danh sach thong thuong:
     - khong cap phat rieng cho nut
     - mot doi tuong nam duoc trong NHIEU danh sach cung luc,
       moi danh sach mot truong moc
     - ma danh sach hoan toan doc lap voi kieu du lieu

   Bai 30.3 da noi ky. */

Hai: hàm gọi lại lấy lại ngữ cảnh

typedef struct {
    void (*khi_xong)(struct YeuCau *yc);
} YeuCau;

typedef struct {
    YeuCau  yc;              /* truong dau tien */
    char    duong_dan[256];
    FILE   *tep;
} YeuCauTep;

static void xong(YeuCau *yc) {
    YeuCauTep *t = VE_CHU(yc, YeuCauTep, yc);
    fclose(t->tep);
    free(t);
}

/* He thong su kien chi biet kieu "YeuCau", nhung ham goi lai
   lay lai duoc toan bo ngu canh cua minh.

   Day la cach thay the cho tham so "void *ngu_canh", va no co
   uu diem: khong can cap phat rieng cho ngu canh. */

Ba: nhóm nhiều cấp phát thành một

Struct có phần đuôi biến đổi
typedef struct {
    size_t n;
    double he_so;
    char   du_lieu[];        /* mang do dai bien doi, PHAI o cuoi */
} Goi;

Goi *goi_tao(size_t n) {
    Goi *g = malloc(sizeof *g + n);      /* MOT lan cap phat */
    if (g == NULL) return NULL;

    g->n = n;
    g->he_so = 1.0;
    return g;
}

/* So voi hai lan cap phat:
     Goi *g = malloc(sizeof *g);
     g->du_lieu = malloc(n);

   Uu diem cua mot lan:
     - mot lan malloc va mot lan free thay vi hai
     - du lieu nam ngay canh tieu de, nen cung dong bo dem
     - khong the quen free phan thu hai

   Va sizeof(Goi) KHONG tinh "du_lieu": */

_Static_assert(sizeof(Goi) == 16, "tieu de phai la 16 byte");
_Static_assert(offsetof(Goi, du_lieu) == 16, "du lieu ngay sau tieu de");

/* Chu y: "offsetof(Goi, du_lieu)" co the LON HON sizeof(Goi) neu
   phan tu cua mang can can chinh lon hon can chinh cua struct.
   Nen luon dung offsetof, khong dung sizeof, khi tinh kich thuoc cap phat:

     malloc(offsetof(Goi, du_lieu) + n * sizeof(g->du_lieu[0]))  */

Tự làm thử

  1. In offsetof của mọi trường trong một struct và đối chiếu với phép tính bằng tay.
  2. Viết VE_CHU và xem hợp ngữ của nó ở -O2.
  3. Truyền nhầm tên trường cho VE_CHU ở cả hai bản và so cảnh báo.
  4. Thêm bốn loại khẳng định bố cục vào một struct của bạn.
  5. Cài danh sách xâm nhập cho một kiểu và cho nó nằm trong hai danh sách cùng lúc.
  6. Viết struct có phần đuôi biến đổi và kiểm tra offsetof so với sizeof.

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

  • offsetof là một macro cho biểu thức hằng lúc dịch, dùng được trong _Static_assert.
  • container_of trừ độ dời khỏi địa chỉ trường, và sinh ra đúng một lệnh lea.
  • Bản của nhân Linux dùng __typeof__ để bắt lỗi truyền nhầm tên trường.
  • Chốt bố cục bằng _Static_assert chỉ khi nó là hợp đồng: định dạng tệp, ABI, thanh ghi, kế thừa thủ công.
  • Với struct có phần đuôi biến đổi, tính kích thước cấp phát bằng offsetof chứ không bằng sizeof.