BlogBitwise & Mask, Octet, Endian: Hướng dẫn parse protocol byte cho người mới
Bố cục bài viết
Trang này
Ngày tạo
09/09/2026
Cập nhật
Lượt xem
9
Đọc
8m
Bitwise & Mask, Octet, Endian: Hướng dẫn parse protocol byte cho người mới
Tài liệu này tổng hợp kỹ thuật shift + mask để parse các sub-field bit trong một byte/octet của protocol — theo đúng Quy tắc thép #7: dùng shift + mask cho mọi sub-field bit của protocol, bitfield struct chỉ để học khái niệm, không dùng để parse byte thật.
1. Khái niệm nền tảng
1.1 Octet là gì
Octet = 8 bit = 1 byte.
Các RFC dùng từ “octet” thay vì “byte” vì lý do lịch sử: ngày xưa “byte” không phải lúc nào cũng là 8 bit (có máy dùng 6, 7, 9 bit). “Octet” (gốc nghĩa “nhóm 8”) khẳng định chắc chắn 100% là 8 bit. Ngày nay có thể hiểu octet = byte.
1 octet = 1 byte = 8 bit = giá trị từ 0 đến 255 (0x00 - 0xFF)
1.2 Endian là gì
Khi một giá trị cần nhiều hơn 1 byte để biểu diễn, phải quyết định byte nào lưu/gửi trước. Đây thuần túy là quy ước, không phải quy luật kỹ thuật bắt buộc.
Ví dụ số 20 dạng 4-byte (0x00000014):
byte[0] byte[1] byte[2] byte[3]
Big-endian: 00 00 00 14 <- byte LỚN NHẤT (MSB) đi trước
Little-endian: 14 00 00 00 <- byte NHỎ NHẤT (LSB) đi trước
- x86/x86-64 (PC/laptop phổ biến): little-endian
- Giao thức mạng (TCP/IP, hầu hết RFC, Diameter…): quy ước dùng big-endian, gọi là network byte order
Đây là lý do có hàm htons(), ntohl() trong lập trình mạng — chuyển đổi giữa byte order của máy (host) và byte order chuẩn mạng (network).
Cách biết một giao thức dùng endian nào: luôn tra trong RFC/spec, không đoán. Ví dụ RFC 6733 (Diameter) ghi rõ mọi field nhiều byte đều theo network byte order (big-endian).
2. Bitwise & Mask — parse sub-field trong 1 byte
2.1 Bài toán
Một byte 8 bit có thể được chia thành nhiều field nhỏ hơn:
bit: 7 6 5 4 3 2 1 0
field: <-- version --> | P | T | <-- spare -->
Để lấy ra 1 field nằm giữa byte, cần 2 bước: dịch phải (>>) để kéo field về vị trí thấp nhất, rồi AND (&) với mask để lọc bỏ phần dư.
2.2 Dịch phải >>
b >> n: dịch toàn bộ bit sang phải n vị trí, bit trống bên trái lấp bằng 0 (vì uint8_t không dấu → logical shift, không phụ thuộc compiler).
Ví dụ b = 0xB4 = 1011 0100:
b = 1 0 1 1 0 1 0 0
b >> 5 = 0 0 0 0 0 1 0 1
b >> 4 = 0 0 0 0 1 0 1 1 <- vẫn còn dính bit version phía trên
b >> 3 = 0 0 0 1 0 1 1 0 <- vẫn còn dính version + P
2.3 AND & với mask
Mask là số nhị phân toàn 1 ở vị trí muốn giữ, 0 ở vị trí muốn xóa. Quy tắc AND: 1 & 1 = 1, còn lại ra 0.
version = (b >> 5) & 0x07; // 0000 0101 & 0000 0111 = 5
p_flag = (b >> 4) & 0x01; // 0000 1011 & 0000 0001 = 1 (mask cắt bỏ phần version dính theo)
t_flag = (b >> 3) & 0x01; // 0001 0110 & 0000 0001 = 0
2.4 Công thức tính mask
mask = 2^n - 1 (n = số bit của field)
| Số bit field (n) | 2ⁿ - 1 | Mask nhị phân | Mask hex |
|---|---|---|---|
| 1 | 1 | 0b1 | 0x01 |
| 2 | 3 | 0b11 | 0x03 |
| 3 | 7 | 0b111 | 0x07 |
| 4 | 15 | 0b1111 | 0x0F |
| 8 | 255 | 0b11111111 | 0xFF |
Vị trí field trong byte (cao/thấp) không ảnh hưởng mask — đó là việc của shift. Mask chỉ quan tâm field rộng bao nhiêu bit.
Mẹo dùng macro thay vì nhớ bảng:
#define MASK(n) ((1u << (n)) - 1)
uint8_t version = (b >> 5) & MASK(3);
uint8_t p_flag = (b >> 4) & MASK(1);
2.5 Đóng gói ngược lại (encode)
uint8_t out = ((version & 0x07) << 5) |
((p_flag & 0x01) << 4) |
((t_flag & 0x01) << 3);
Mask trước khi shift để phòng giá trị đầu vào tràn ra ngoài số bit cho phép, tránh đè lên field khác.
2.6 Vì sao không dùng bitfield struct
struct { uint8_t version:3; uint8_t p:1; uint8_t t:1; } // chỉ để học, không dùng parse thật
Chuẩn C không quy định thứ tự bit nào nằm trước trong bitfield — do compiler/kiến trúc CPU tự quyết định. Cùng struct, biên dịch trên compiler/platform khác nhau có thể ra layout khác nhau. Ngược lại, shift + mask là toán tử số học thuần túy, kết quả giống hệt nhau trên mọi compiler, mọi CPU → di động 100%.
3. Ví dụ thực tế: parse header Diameter (RFC 6733)
Diameter là giao thức AAA dùng trong mạng viễn thông (4G/5G, VoLTE). Header có field nhiều byte và field không tròn byte (24-bit).
3.1 Cấu trúc header (đơn giản hóa)
byte: 0 1 2 3 4 5 6 7 8 9 10 11
field: Version | ------ Msg Length ------ | Flags | ------ Cmd-Code ------ | -------- App-ID --------
(24 bit) (24 bit) (32 bit)
3.2 Field tròn 1 byte
uint8_t version = buf[0];
3.3 Field nhiều byte — ghép byte bằng shift + OR
Khi field dài hơn 1 byte, phải ghép nhiều byte thành 1 số theo đúng big-endian (network byte order): byte đọc trước dịch trái nhiều hơn.
// Message Length: 24 bit = buf[1], buf[2], buf[3]
uint32_t msg_length = ((uint32_t)buf[1] << 16) |
((uint32_t)buf[2] << 8) |
(uint32_t)buf[3];
Ví dụ buf[1]=0x00, buf[2]=0x00, buf[3]=0x14:
buf[1] << 16 = 0x000000
buf[2] << 8 = 0x000000
buf[3] = 0x000014
OR lại = 0x000014 = 20
Lưu ý ép kiểu (uint32_t) trước khi shift: nếu để uint8_t shift trực tiếp, C tự “promote” lên int — thường vẫn đúng vì int 32-bit trên đa số máy, nhưng đây là hành vi ngầm định dễ gây lỗi khó lường trên platform khác. Ép kiểu tường minh giúp code rõ ràng và di động 100%.
3.4 Command Flags — quay lại shift + mask trong 1 byte
uint8_t flags = buf[4];
uint8_t R = (flags >> 7) & 0x01; // Request flag - bit7
uint8_t P = (flags >> 6) & 0x01; // Proxiable flag - bit6
uint8_t E = (flags >> 5) & 0x01; // Error flag - bit5
uint8_t T = (flags >> 4) & 0x01; // Retransmit flag - bit4
// 4 bit thấp (bit3..bit0): reserved, thường bỏ qua
Mỗi flag rộng 1 bit → mask luôn 0x01, chỉ khác số lần shift để kéo đúng bit về vị trí thấp nhất.
3.5 Application-ID — field 32-bit chuẩn (4 byte tròn)
uint32_t application_id = ((uint32_t)buf[8] << 24) |
((uint32_t)buf[9] << 16) |
((uint32_t)buf[10] << 8) |
(uint32_t)buf[11];
3.6 Đóng gói ngược lại (encode)
buf[0] = version;
buf[1] = (msg_length >> 16) & 0xFF;
buf[2] = (msg_length >> 8) & 0xFF;
buf[3] = msg_length & 0xFF;
buf[4] = (R << 7) | (P << 6) | (E << 5) | (T << 4);
buf[5] = (command_code >> 16) & 0xFF;
buf[6] = (command_code >> 8) & 0xFF;
buf[7] = command_code & 0xFF;
Ở chiều encode: field to → shift phải rồi mask 0xFF để cắt ra từng byte, ghi byte cao nhất trước (đúng big-endian).
3.7 Vì sao không dùng ép kiểu con trỏ
// NGUY HIỂM - đừng làm vậy
uint32_t msg_length = *(uint32_t*)(&buf[1]);
Cách này mượn byte order của CPU đang chạy code, không phải byte order của giao thức. Kết quả đúng trên máy big-endian nhưng sai trên máy little-endian (đa số PC/laptop x86). Ngoài ra còn có nguy cơ lỗi alignment. Đây là lỗi kinh điển khi lập trình mạng — code vẫn compile và chạy bình thường nhưng ra sai số, rất khó phát hiện.
4. So sánh 2 kỹ thuật
| Parse bit trong 1 byte | Parse field nhiều byte | |
|---|---|---|
| Kỹ thuật | shift + & mask | shift + \| OR để ghép byte |
| Đơn vị làm việc | bit | byte (octet) |
| Cần biết endian? | Không | Có — quyết định thứ tự ghép byte |
| Cách sai phổ biến | Quên mask → dính bit field khác | Ép kiểu con trỏ → sai trên máy khác endian |
5. Quy trình thực tế khi gặp giao thức mới
- Mở RFC/tài liệu đặc tả của giao thức
- Tìm dòng “all multi-octet fields are in network byte order” (RFC 6733 section 3 có câu này)
- Xác định big-endian hay little-endian → viết code ghép byte đúng thứ tự
- Không bao giờ dùng bitfield struct hay ép kiểu con trỏ để đọc field từ dữ liệu mạng — luôn dùng shift + mask/OR để tự tay kiểm soát từng bit, từng byte, đảm bảo di động 100% trên mọi compiler và mọi kiến trúc CPU.
Tham khảo
- Quy tắc thép #7: Dùng shift + mask cho mọi sub-field bit của protocol
- RFC 6733 — Diameter Base Protocol
Lộ trình học tập
Lập Trình C/C++
- 1 01 — C Cơ Bản
- 2 02 — Con Trỏ & Bộ Nhớ
- 3 03 — Struct, Union, Bitfield & Bố Cục Bộ Nhớ
- 4 04 — C Nâng Cao
- 5 05 — C++ Cốt Lõi
- 6 06 — C++ Hiện Đại
- 7 07 — Lập Trình Hệ Thống & Mạng
- 8 08 — Phân Tích Giao Thức Telecom Bằng C/C++
- 9 09 — An Ninh Bộ Nhớ & Khai Thác Cơ Bản
- 10 11 — Lập Trình Nhúng & Real-time (SDR-first)
- 11 Bài tập & Pet Project — Lộ trình C/C++ chuyên sâu
- 12 Tổng hợp các dạng printf để in giá trị trong C
- 13 Bitwise & Mask, Octet, Endian: Hướng dẫn parse protocol byte cho người mới
- 14 Tổng hợp: Con trỏ, kích thước kiểu dữ liệu và byte trong bộ nhớ