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

Bitmask và trường bit

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

  • Thiết kế bộ cờ bằng bitmask và thao tác nhiều cờ cùng lúc
  • Trích và ghi một trường nhiều bit bằng mặt nạ
  • Khai báo trường bit và biết bố cục của nó do cài đặt quy định
  • Chọn giữa bitmask thủ công và trường bit

Tám giá trị đúng sai trong một byte thay vì tám byte. C cho bạn hai cách làm việc đó, và một trong hai cách trông đẹp hơn nhưng không dùng được cho định dạng tệp hay giao thức mạng.

#Bitmask: nhiều cờ trong một biến

quyen.h
#ifndef QUYEN_H
#define QUYEN_H

#define BIT(n) (1u << (n))

/* Moi co la MOT bit. Dat ten ro rang, va dung enum
   thay vi define de trinh go loi thay duoc ten. */
enum Quyen {
    Q_DOC      = BIT(0),        /* 0x01 */
    Q_GHI      = BIT(1),        /* 0x02 */
    Q_THUC_THI = BIT(2),        /* 0x04 */
    Q_AN       = BIT(3),        /* 0x08 */
    Q_HE_THONG = BIT(4),        /* 0x10 */

    Q_KHONG    = 0,
    Q_DOC_GHI  = Q_DOC | Q_GHI,
    Q_TAT_CA   = Q_DOC | Q_GHI | Q_THUC_THI | Q_AN | Q_HE_THONG,
};

#endif /* QUYEN_H */
ViệcBiểu thứcGhi chú
Bật một cờq |= Q_GHI
Bật nhiều cờq |= Q_GHI | Q_AN
Tắt một cờq &= ~Q_GHI
Tắt nhiều cờq &= ~(Q_GHI | Q_AN)Ngoặc bắt buộc
Đảoq ^= Q_AN
Có cờ này khôngq & Q_GHIKhác 0 nghĩa là có
Có bất kỳ cờ nào trong nhómq & (Q_DOC | Q_GHI)
Có tất cả cờ trong nhóm(q & can) == canChỗ hay bị nhầm nhất
Xóa hếtq = Q_KHONG
Chỉ giữ nhóm nàyq &= Q_DOC_GHI

In bộ cờ ra chữ để gỡ lỗi

typedef struct { unsigned co; const char *ten; } MucCo;

static const MucCo BANG_CO[] = {
    { Q_DOC,      "DOC"      },
    { Q_GHI,      "GHI"      },
    { Q_THUC_THI, "THUC_THI" },
    { Q_AN,       "AN"       },
    { Q_HE_THONG, "HE_THONG" },
};

void in_quyen(unsigned q) {
    int dau = 1;

    if (q == 0) { printf("KHONG\n"); return; }

    for (size_t i = 0; i < sizeof BANG_CO / sizeof BANG_CO[0]; ++i)
        if (q & BANG_CO[i].co) {
            printf("%s%s", dau ? "" : " | ", BANG_CO[i].ten);
            dau = 0;
            q &= ~BANG_CO[i].co;
        }

    if (q) printf("%s0x%X", dau ? "" : " | ", q);   /* bit la con sot */

    printf("\n");
}
terminal
./quyen
in_quyen(0)              -> KHONG
in_quyen(Q_DOC|Q_GHI)    -> DOC | GHI
in_quyen(Q_TAT_CA)       -> DOC | GHI | THUC_THI | AN | HE_THONG
in_quyen(0x101)          -> DOC | 0x100

#Trường nhiều bit bằng mặt nạ

Che phần không cần trước, rồi kéo về vị trí 0. Đảo thứ tự cũng ra đúng, nhưng quên dấu mặt nạ thì sai.
Ví dụ thật: thanh ghi cấu hình 32 bit
/* Thanh ghi CR1 cua mot bo dinh thoi:
     bit 31..16  chu ky
     bit 15..12  bo chia truoc
     bit 11..8   che do dem
     bit 7       bat ngat
     bit 6..4    nguon xung
     bit 3..1    du tru
     bit 0       bat  */

#define CR1_CHU_KY_HI     31
#define CR1_CHU_KY_LO     16
#define CR1_CHIA_HI       15
#define CR1_CHIA_LO       12
#define CR1_NGUON_HI       6
#define CR1_NGUON_LO       4
#define CR1_NGAT           7
#define CR1_BAT            0

#define MAT_NA(hi, lo)  ((~0u >> (31 - (hi))) & (~0u << (lo)))

static inline unsigned doc_truong(unsigned x, unsigned hi, unsigned lo) {
    return (x & MAT_NA(hi, lo)) >> lo;
}

static inline unsigned ghi_truong(unsigned x, unsigned hi, unsigned lo,
                                  unsigned v) {
    return (x & ~MAT_NA(hi, lo)) | ((v << lo) & MAT_NA(hi, lo));
}

/* Dung: */
unsigned cr1 = 0;

cr1 = ghi_truong(cr1, CR1_CHU_KY_HI, CR1_CHU_KY_LO, 1000);
cr1 = ghi_truong(cr1, CR1_CHIA_HI,   CR1_CHIA_LO,   3);
cr1 = ghi_truong(cr1, CR1_NGUON_HI,  CR1_NGUON_LO,  2);
cr1 |= BIT(CR1_NGAT) | BIT(CR1_BAT);

unsigned chu_ky = doc_truong(cr1, CR1_CHU_KY_HI, CR1_CHU_KY_LO);

#Trường bit của ngôn ngữ

Trường bit
Thành viên struct khai báo kèm số bit sau dấu hai chấm. Trình biên dịch tự sinh mã che và dịch, nên bạn viết a.che_do = 5 thay vì các phép toán bit.
struct Co1 {
    unsigned bat    : 1;
    unsigned che_do : 3;
    unsigned ma     : 8;
};

struct Co1 a;
memset(&a, 0, sizeof a);

a.bat    = 1;
a.che_do = 5;
a.ma     = 0xAB;

unsigned m = a.ma;          /* trinh bien dich tu sinh che va dich */
terminal
gcc -std=c11 -Wall -Wextra -o bf bf.c && ./bf
sizeof(Co1) = 4
sizeof(Co2) = 8  (co unsigned : 0)
sizeof(Co3) = 1  (dung uint8_t)

Co1 { bat=1, che_do=5, ma=0xAB } trong bo nho: BB 0A 00 00 
Khai báosizeof trên x86-64 gccVì sao
unsigned bat:1; unsigned che_do:3; unsigned ma:8;4Mười hai bit vừa trong một unsigned
Như trên, chèn thêm unsigned : 0; ở giữa8Trường rộng 0 ép trường sau sang đơn vị lưu trữ mới
uint8_t bat:1; uint8_t che_do:3; uint8_t ma:4;1Tám bit vừa trong một byte

#Vì sao bit field không dùng cho giao thức

Điều do cài đặt quy địnhHậu quả
Thứ tự xếp bit trong một đơn vị lưu trữgcc trên x86 xếp từ bit thấp lên, trên PowerPC big-endian thì ngược lại
Đơn vị lưu trữ là bao nhiêu byteCùng struct cho sizeof khác nhau giữa các trình biên dịch
Có được phép vắt một trường qua ranh giới đơn vị khôngMột số cài đặt chèn đệm thay vì vắt
int x : 3 là có dấu hay không dấugcc chọn có dấu, một số trình biên dịch khác chọn không dấu
Kiểu nào được dùng làm trường bitChuẩn chỉ bảo đảm int, unsigned int và _Bool. uint8_t là mở rộng
Bit field cho giao thức
/* Doc mot goi tin mang bang bit field */
struct GoiTinIP {
    unsigned phien_ban : 4;
    unsigned do_dai    : 4;
    unsigned dich_vu   : 8;
    unsigned tong_dai  : 16;
};

struct GoiTinIP g;
memcpy(&g, bo_dem, sizeof g);        /* HONG:
     - thu tu bit khac nhau giua x86 va PowerPC
     - sizeof co the la 4 hoac 8
     - thu tu byte cua truong 16 bit cung khac */
Che và dịch thủ công
/* Doc tung byte, tu che va dich */
static void doc_goi_tin(const uint8_t *b, GoiTinIP *g) {
    g->phien_ban = (b[0] >> 4) & 0x0Fu;
    g->do_dai    =  b[0]       & 0x0Fu;
    g->dich_vu   =  b[1];
    g->tong_dai  = ((uint16_t)b[2] << 8) | b[3];   /* lon truoc, dung chuan mang */
}

/* Dai hon, nhung cho ra CUNG KET QUA tren moi may.
   Bai 18.8 da dung dung cach nay cho dinh dang tep. */

#Chọn cách nào

Tiêu chíBitmask thủ côngTrường bit
Đọc mãPhải hiểu phép bitĐọc như trường struct thường
Khả chuyểnHoàn toànBố cục do cài đặt quy định
Dùng cho tệp và mạngĐượcKhông
Lấy địa chỉ trườngKhông cần, nó là biến thườngKhông được
Trình biên dịch kiểm khoảng giá trịKhôngCó, nhưng chỉ với hằng số
Nhiều cờ một lúcDễ, OR các mặt nạPhải gán từng trường
Tốc độNhư nhauNhư nhau, cùng sinh ra lệnh che và dịch
Truyền cả bộ cờ qua tham sốMột unsignedCả struct, hoặc một union

Ví dụ hoàn chỉnh: cả hai cách cho cùng bài toán

/* Bitmask thu cong */
#define CH_BAT        BIT(0)
#define CH_CHE_DO_HI  3
#define CH_CHE_DO_LO  1
#define CH_MA_HI      11
#define CH_MA_LO      4

static inline unsigned ch_che_do(uint32_t x) {
    return doc_truong(x, CH_CHE_DO_HI, CH_CHE_DO_LO);
}

static inline uint32_t ch_dat_che_do(uint32_t x, unsigned v) {
    return ghi_truong(x, CH_CHE_DO_HI, CH_CHE_DO_LO, v);
}

/* Truong bit */
typedef struct {
    unsigned bat    : 1;
    unsigned che_do : 3;
    unsigned ma     : 8;
} CauHinh;

/* Ca hai deu sinh ra khoang cung mot ma may.
   Khac biet nam o KHA CHUYEN va o CACH DOC. */
terminal
# Hai bản sinh ra hợp ngữ giống hệt nhau
gcc -O2 -S -masm=intel -o - ch1.c ch2.c
che va dich, ham f:
    mov  eax, ecx
    shr  eax
    and  eax, 7
    ret

truong bit, ham g:
    mov  eax, ecx
    shr  eax
    and  eax, 7
    ret

Tự làm thử

  1. Viết bộ cờ quyền bằng enum với BIT(n) và thêm _Static_assert kiểm không chồng bit.
  2. Viết in_quyen và chạy nó với 0, hai cờ, tất cả cờ, và một giá trị có bit lạ.
  3. Phân biệt q & can với (q & can) == can bằng một ví dụ cụ thể.
  4. Khai báo struct Co1 ở trên, gán ba giá trị, rồi in bộ nhớ dạng hex và giải mã bằng tay.
  5. Thêm unsigned : 0; vào giữa struct và so sizeof trước sau.
  6. Khai báo struct { int x : 3; }, gán 4, và giải thích vì sao đọc ra -4.

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

  • Bộ cờ khai báo bằng enum với BIT(n), và phải viết rõ giá trị vì enum tự đánh số 0, 1, 2, 3.
  • Kiểm nhiều cờ: q & can là bất kỳ, (q & can) == can là tất cả.
  • Số bit của trường là hi - lo + 1, và dấu cộng một rất hay bị quên.
  • Trường bit đọc dễ hơn nhưng bố cục do cài đặt quy định, nên không dùng cho tệp, mạng hay thanh ghi.
  • int x : 3 là có dấu trên gcc, nên gán 4 đọc ra -4. Luôn viết rõ unsigned.