Blog5 bài tập C: Struct, Bố cục bộ nhớ, Endianness, IPv4, AVP và dự án nhiều file (đề chi tiết + lời giải)
Bố cục bài viết
Trang này
Ngày tạo
19/10/2026
Cập nhật
Lượt xem
7
Đọc
66m
Bộ 5 bài tập ôn hai chương 03 — Struct, Union, Bitfield & Bố Cục Bộ Nhớ và 04 — C Nâng Cao. Mỗi bài gồm đề bài chi tiết (bối cảnh, yêu cầu từng phần, ràng buộc, tiêu chí tự chấm) rồi mới đến lời giải có chú thích. Chú thích dùng các thẻ: [NHỚ] quy tắc cốt lõi, [BẪY] chỗ hay sai, [AN NINH] liên hệ khai thác/phòng thủ.
Cách học: đọc đề, tự làm trước, rồi mới mở phần lời giải. Ở bài 1, hãy điền bảng dự đoán trên giấy rồi mới chạy code.
Môi trường: Linux/macOS, 64-bit, little-endian, có <arpa/inet.h>. Mỗi bài build bằng:
gcc -std=c17 -Wall -Wextra -Wpedantic -g -O0 bt1_bo_cuc_bo_nho.c -o bt1
gcc -std=c17 -Wall -Wextra -Wpedantic -g -O1 -fsanitize=address,undefined bt1_bo_cuc_bo_nho.c -o bt1_asan
Bài 1-4 tự kiểm tra bằng macro CHECK và in dòng cuối KET QUA: n/n kiem tra dat (bài 1: 7/7, bài 2: 16/16, bài 3: 21/21, bài 4: 18/18). Bài 5 in SELFTEST OK.
Bài 1 — Bố cục bộ nhớ của struct: alignment, padding, packed, offsetof
Ôn: Chương 03 (mục 1, 2) + _Static_assert, memcpy, hexdump.
Đề bài
Bối cảnh. Muốn map một struct C lên header protocol (GTPv2, IP, Diameter…), bạn phải nhìn struct là nói ngay được: nó chiếm bao nhiêu byte, mỗi field nằm ở byte thứ mấy, và byte đệm (padding) nằm ở đâu. Sai chỗ này thì parser đọc lệch toàn bộ gói tin.
Quy ước máy: 64-bit, little-endian. Alignment bằng kích thước kiểu: uint8_t:1, uint16_t:2, uint32_t:4, uint64_t:8.
Cho 4 struct:
struct S1 { uint8_t a; uint32_t b; uint8_t c; };
struct S2 { uint32_t b; uint8_t a; uint8_t c; };
struct S3 { uint8_t a; uint64_t d; uint8_t c; uint32_t b; };
struct S4 { uint64_t d; uint32_t b; uint8_t a; uint8_t c; };
Phần A (làm trên giấy trước khi chạy bất cứ thứ gì). Với mỗi struct, điền bảng: offset từng field, sizeof, số byte padding. Áp dụng hai quy tắc:
- Mỗi field phải bắt đầu ở offset chia hết cho alignment của nó.
sizeof(struct)phải chia hết cho alignment lớn nhất của các field (để phần tử kế tiếp trong mảng cũng được căn).
Phần B: in layout bằng macro. Viết macro SHOW_FIELD(T, f) in tên struct, tên field, offset, kích thước field. Yêu cầu dùng offsetof (<stddef.h>), toán tử # (stringify) và sizeof của một member mà không cần tạo biến. Viết hàm summary() in sizeof, tổng kích thước các field, số byte padding. Áp dụng cho S1..S4. Quan sát: S3 và S4 có cùng field nhưng sizeof khác nhau bao nhiêu byte?
Phần C: packed và khóa layout lúc biên dịch. Khai báo bản packed của S1 (S1P) và S3 (S3P) bằng __attribute__((packed)). In sizeof và offsetof của chúng. Dùng _Static_assert để khóa layout: nếu sau này ai đó sửa struct làm lệch sizeof/offset thì build phải lỗi.
Phần D: nhìn tận mắt padding. Tạo mảng struct S1 arr[2], memset toàn bộ về 0xAA, gán a, b, c cho từng phần tử, hexdump 24 byte. Các byte còn 0xAA chính là padding. In khoảng cách (byte) giữa &arr[1] và &arr[0] và giải thích vì sao padding ở cuối struct tồn tại.
Phần E: map struct lên gói tin. Cho gói GTPv2 12 byte: 48 20 00 10 11 22 33 44 00 00 01 00 (flags=0x48, msg_type=0x20, length=0x0010, teid=0x11223344, seq_spare=0x00000100 nên seq = 24 bit cao = 1). Parse bằng hai cách rồi chứng minh kết quả khớp nhau:
- struct packed +
memcpy+ntohs/ntohl(không ép kiểu con trỏ buffer sangstruct*, vì dễ vi phạm alignment/strict aliasing); - thủ công: đọc từng byte theo offset, ghép big-endian bằng shift.
Sau đó cố tình map gói 6 byte 01 11 22 33 44 02 vào S1 (có padding) và vào S1P (packed) để thấy S1 đọc sai, S1P đúng (nhưng vẫn cần ntohl).
Phần F: câu hỏi (trả lời bằng comment).
- F1. Vì sao gửi nguyên một struct có padding qua mạng/ghi file là nguy hiểm về mặt an ninh (ngoài việc sai layout)?
- F2. Vì sao sắp xếp field từ lớn đến nhỏ thường làm struct nhỏ hơn?
- F3. Vì sao không nên lấy địa chỉ một field của struct packed?
Ràng buộc: build sạch với -std=c17 -Wall -Wextra -Wpedantic; không dùng #pragma pack (dùng __attribute__((packed))); không ép kiểu (struct X *)buf trên buffer gói tin.
Tiêu chí tự chấm:
- Bảng dự đoán ở Phần A khớp với số máy in ra.
- Chương trình in
KET QUA: 7/7 kiem tra dat. - Trả lời được F1-F3 mà không nhìn đáp án.
Lời giải
#define _DEFAULT_SOURCE /* cho htons/ntohl... khi dùng -std=c17 */
#include <arpa/inet.h> /* ntohs, ntohl */
#include <stddef.h> /* offsetof */
#include <stdint.h>
#include <stdio.h>
#include <string.h>
/* ---------------------------------------------------------------------------
* 4 struct của đề. Lời giải Phần A (đã kiểm chứng bằng _Static_assert bên dưới):
*
* S1 {a:1, b:4, c:1} a@0 | pad 3 | b@4..7 | c@8 | pad 3 => sizeof 12
* S2 {b:4, a:1, c:1} b@0..3 | a@4 | c@5 | pad 2 => sizeof 8
* S3 {a:1, d:8, c:1, b:4} a@0 | pad 7 | d@8..15 | c@16 | pad 3 | b@20..23 => 24
* S4 {d:8, b:4, a:1, c:1} d@0..7 | b@8..11 | a@12 | c@13 | pad 2 => sizeof 16
*
* [NHỚ] Cùng 4 field của S3/S4 (tổng 14 byte dữ liệu): S3 tốn 24, S4 chỉ 16.
* Chỉ vì đổi thứ tự khai báo. Quy tắc: xếp field từ LỚN đến NHỎ.
* ------------------------------------------------------------------------- */
struct S1 { uint8_t a; uint32_t b; uint8_t c; };
struct S2 { uint32_t b; uint8_t a; uint8_t c; };
struct S3 { uint8_t a; uint64_t d; uint8_t c; uint32_t b; };
struct S4 { uint64_t d; uint32_t b; uint8_t a; uint8_t c; };
/* Phần C: bản packed — bỏ hết padding, field nằm sát nhau */
struct __attribute__((packed)) S1P { uint8_t a; uint32_t b; uint8_t c; };
struct __attribute__((packed)) S3P { uint8_t a; uint64_t d; uint8_t c; uint32_t b; };
/* Header GTPv2 (Phần E): packed để khớp wire. Thực ra các field đã tự căn nên
* packed hay không đều = 12, nhưng khai báo packed thể hiện ý định "khớp wire". */
struct __attribute__((packed)) gtpv2_header {
uint8_t flags;
uint8_t msg_type;
uint16_t length; /* BIG-ENDIAN trên wire */
uint32_t teid; /* BIG-ENDIAN trên wire */
uint32_t seq_spare; /* seq(24 bit cao) + spare(8 bit thấp), BIG-ENDIAN */
};
/* ---------------------------------------------------------------------------
* Phần C: KHÓA layout lúc biên dịch. Sửa struct làm lệch -> build FAIL ngay.
* (Nếu đổi sang máy 32-bit, uint64_t có thể căn 4 và các số S3/S4 sẽ khác:
* chính là lý do cần static assert để phát hiện sớm.)
* ------------------------------------------------------------------------- */
_Static_assert(sizeof(struct S1) == 12, "S1 phai 12 byte");
_Static_assert(offsetof(struct S1, b) == 4, "S1.b phai o offset 4");
_Static_assert(offsetof(struct S1, c) == 8, "S1.c phai o offset 8");
_Static_assert(sizeof(struct S2) == 8, "S2 phai 8 byte");
_Static_assert(offsetof(struct S2, a) == 4, "S2.a phai o offset 4");
_Static_assert(offsetof(struct S2, c) == 5, "S2.c phai o offset 5");
_Static_assert(sizeof(struct S3) == 24, "S3 phai 24 byte");
_Static_assert(offsetof(struct S3, d) == 8, "S3.d phai o offset 8");
_Static_assert(offsetof(struct S3, c) == 16, "S3.c phai o offset 16");
_Static_assert(offsetof(struct S3, b) == 20, "S3.b phai o offset 20");
_Static_assert(sizeof(struct S4) == 16, "S4 phai 16 byte");
_Static_assert(offsetof(struct S4, b) == 8, "S4.b phai o offset 8");
_Static_assert(offsetof(struct S4, a) == 12, "S4.a phai o offset 12");
_Static_assert(offsetof(struct S4, c) == 13, "S4.c phai o offset 13");
_Static_assert(sizeof(struct S1P) == 6, "S1P phai 6 byte");
_Static_assert(offsetof(struct S1P, b) == 1, "S1P.b phai o offset 1");
_Static_assert(offsetof(struct S1P, c) == 5, "S1P.c phai o offset 5");
_Static_assert(sizeof(struct S3P) == 14, "S3P phai 14 byte");
_Static_assert(offsetof(struct S3P, d) == 1, "S3P.d phai o offset 1");
_Static_assert(offsetof(struct S3P, c) == 9, "S3P.c phai o offset 9");
_Static_assert(offsetof(struct S3P, b) == 10, "S3P.b phai o offset 10");
_Static_assert(sizeof(struct gtpv2_header) == 12, "GTPv2 header phai 12 byte");
/* ---------------------------------------------------------------------------
* Phần B: macro in layout.
* #T, #f : biến token thành chuỗi ("struct S1", "b")
* offsetof : vị trí byte của field tính từ đầu struct
* sizeof(((T *)0)->f) : kích thước của member f mà không cần có biến thật.
* (Con trỏ NULL chỉ dùng trong sizeof -> KHÔNG được đánh giá,
* không dereference thật, hợp lệ.)
* ------------------------------------------------------------------------- */
#define SHOW_FIELD(T, f) \
printf(" %-10s %-2s offset=%-2zu size=%zu\n", #T, #f, offsetof(T, f), sizeof(((T *)0)->f))
static void summary(const char *name, size_t size, size_t sum_fields) {
printf(" %-10s sizeof=%-2zu tong_field=%-2zu padding=%zu\n\n",
name, size, sum_fields, size - sum_fields);
}
static void hexdump(const char *label, const void *p, size_t n) {
const uint8_t *b = p;
printf(" %s", label);
for (size_t i = 0; i < n; i++) printf("%02x ", b[i]);
putchar('\n');
}
/* ---- bộ đếm kiểm tra ----------------------------------------------------- */
static int g_pass = 0, g_total = 0;
/* [BẪY macro] 'cond' phải được đánh giá ĐÚNG MỘT LẦN: nếu viết (cond) hai lần
* trong macro thì biểu thức có tác dụng phụ (gọi hàm cấp phát...) sẽ chạy 2 lần. */
#define CHECK(cond, ...) do { \
int ok_ = (cond) ? 1 : 0; \
g_total++; \
if (ok_) g_pass++; \
printf(" [%s] ", ok_ ? "OK " : "LOI"); \
printf(__VA_ARGS__); \
putchar('\n'); \
} while (0)
/* ---- Phần E: parse GTPv2 bằng 2 cách ------------------------------------- */
struct gtp_view {
uint8_t version, t_flag, msg_type;
uint16_t length;
uint32_t teid, seq;
};
/* Cách 1: struct packed + memcpy + ntohs/ntohl.
* memcpy thay vì ép kiểu con trỏ: buffer có thể không căn, ép kiểu là UB. */
static void parse_gtp_packed(const uint8_t *buf, struct gtp_view *v) {
struct gtpv2_header h;
memcpy(&h, buf, sizeof h);
v->version = (uint8_t)((h.flags >> 5) & 0x07);
v->t_flag = (uint8_t)((h.flags >> 3) & 0x01);
v->msg_type = h.msg_type;
v->length = ntohs(h.length); /* BẮT BUỘC đổi endianness */
v->teid = ntohl(h.teid);
v->seq = ntohl(h.seq_spare) >> 8; /* 24 bit cao là seq, 8 bit thấp là spare */
}
/* Cách 2: thủ công, không phụ thuộc layout/endianness/alignment của máy. */
static void parse_gtp_manual(const uint8_t *b, struct gtp_view *v) {
v->version = (uint8_t)((b[0] >> 5) & 0x07);
v->t_flag = (uint8_t)((b[0] >> 3) & 0x01);
v->msg_type = b[1];
v->length = (uint16_t)(((uint16_t)b[2] << 8) | b[3]);
v->teid = ((uint32_t)b[4] << 24) | ((uint32_t)b[5] << 16) |
((uint32_t)b[6] << 8) | (uint32_t)b[7];
v->seq = ((uint32_t)b[8] << 16) | ((uint32_t)b[9] << 8) | b[10]; /* 3 byte đầu */
}
static int same_view(const struct gtp_view *x, const struct gtp_view *y) {
return x->version == y->version && x->t_flag == y->t_flag &&
x->msg_type == y->msg_type && x->length == y->length &&
x->teid == y->teid && x->seq == y->seq;
}
int main(void) {
/* ---------------- Phần B: layout ---------------- */
printf("== B. Layout cua S1..S4 ==\n");
SHOW_FIELD(struct S1, a); SHOW_FIELD(struct S1, b); SHOW_FIELD(struct S1, c);
summary("struct S1", sizeof(struct S1), 1 + 4 + 1);
SHOW_FIELD(struct S2, b); SHOW_FIELD(struct S2, a); SHOW_FIELD(struct S2, c);
summary("struct S2", sizeof(struct S2), 4 + 1 + 1);
SHOW_FIELD(struct S3, a); SHOW_FIELD(struct S3, d); SHOW_FIELD(struct S3, c); SHOW_FIELD(struct S3, b);
summary("struct S3", sizeof(struct S3), 1 + 8 + 1 + 4);
SHOW_FIELD(struct S4, d); SHOW_FIELD(struct S4, b); SHOW_FIELD(struct S4, a); SHOW_FIELD(struct S4, c);
summary("struct S4", sizeof(struct S4), 8 + 4 + 1 + 1);
CHECK(sizeof(struct S3) - sizeof(struct S4) == 8,
"S3 va S4 cung field nhung S3 to hon %zu byte (do thu tu khai bao)",
sizeof(struct S3) - sizeof(struct S4));
/* ---------------- Phần C: packed ---------------- */
printf("\n== C. Ban packed ==\n");
SHOW_FIELD(struct S1P, a); SHOW_FIELD(struct S1P, b); SHOW_FIELD(struct S1P, c);
summary("struct S1P", sizeof(struct S1P), 1 + 4 + 1);
SHOW_FIELD(struct S3P, d); SHOW_FIELD(struct S3P, b);
summary("struct S3P", sizeof(struct S3P), 1 + 8 + 1 + 4);
CHECK(sizeof(struct S1P) == 6 && sizeof(struct S3P) == 14,
"packed: S1P=%zu (khong padding), S3P=%zu (chi con du lieu thuan)",
sizeof(struct S1P), sizeof(struct S3P));
/* ---------------- Phần D: nhìn padding ---------------- */
printf("\n== D. Nhin tan mat padding ==\n");
struct S1 arr[2];
memset(arr, 0xAA, sizeof arr); /* tô 0xAA: byte nào còn 0xAA = padding */
arr[0].a = 1; arr[0].b = 0x11223344; arr[0].c = 2;
arr[1].a = 3; arr[1].b = 0x55667788; arr[1].c = 4;
hexdump("arr[2] trong RAM: ", arr, sizeof arr);
/* 01 | aa aa aa | 44 33 22 11 | 02 | aa aa aa <- phần tử 0 (12 byte)
* 03 | aa aa aa | 88 77 66 55 | 04 | aa aa aa <- phần tử 1
* b ghi little-endian (44 33 22 11); 3 byte 'aa' giữa a và b, và 3 byte
* 'aa' cuối mỗi phần tử là padding. */
size_t stride = (size_t)((const uint8_t *)&arr[1] - (const uint8_t *)&arr[0]);
CHECK(stride == sizeof(struct S1), "&arr[1] - &arr[0] = %zu byte = sizeof(S1)", stride);
/* F1 [AN NINH]: write(fd, &s, sizeof s) gửi cả byte padding. Chúng chứa
* rác còn sót trên stack/heap (khóa, con trỏ, dữ liệu người dùng khác)
* => rò rỉ thông tin (info leak). Cách đúng: serialize TỪNG field, hoặc
* memset struct về 0 trước khi điền.
* F2: field lớn đặt trước thì các field nhỏ "lấp" vào chỗ trống phía sau,
* ít phải chèn padding giữa các field; chỉ còn padding cuối struct.
* (Padding cuối struct tồn tại để phần tử arr[1] cũng bắt đầu ở địa chỉ
* căn đúng: stride = sizeof phải là bội của alignment lớn nhất.) */
/* ---------------- Phần E: map lên gói tin ---------------- */
printf("\n== E. Map struct len goi tin ==\n");
static const uint8_t wire12[12] = { 0x48, 0x20, 0x00, 0x10, 0x11, 0x22,
0x33, 0x44, 0x00, 0x00, 0x01, 0x00 };
struct gtp_view v1, v2;
parse_gtp_packed(wire12, &v1);
parse_gtp_manual(wire12, &v2);
printf(" version=%u T=%u msg_type=%u length=%u teid=0x%08x seq=%u\n",
v1.version, v1.t_flag, v1.msg_type, v1.length, (unsigned)v1.teid, (unsigned)v1.seq);
CHECK(same_view(&v1, &v2), "2 cach parse (packed+ntoh vs thu cong) cho ket qua giong nhau");
CHECK(v1.version == 2 && v1.t_flag == 1 && v1.msg_type == 32 && v1.length == 16 &&
v1.teid == 0x11223344u && v1.seq == 1,
"gia tri dung: version=2 T=1 type=32 length=16 teid=0x11223344 seq=1");
static const uint8_t wire6[6] = { 0x01, 0x11, 0x22, 0x33, 0x44, 0x02 };
struct S1 bad;
memset(&bad, 0, sizeof bad);
memcpy(&bad, wire6, sizeof wire6); /* 6 byte tràn vào struct có padding */
struct S1P good;
memcpy(&good, wire6, sizeof good); /* 6 byte khớp đúng layout packed */
printf(" S1 (co padding): a=%u b=0x%08x c=%u <- SAI (b va c bi lech)\n",
bad.a, (unsigned)bad.b, bad.c);
printf(" S1P (packed) : a=%u b=0x%08x c=%u <- b chua ntohl\n",
good.a, (unsigned)good.b, good.c);
CHECK(bad.b != 0x11223344u, "S1 co padding doc SAI: b=0x%08x (mong doi 0x11223344)", (unsigned)bad.b);
CHECK(good.a == 1 && ntohl(good.b) == 0x11223344u && good.c == 2,
"S1P packed + ntohl doc DUNG: a=1 b=0x11223344 c=2");
/* F3: &good.b là con trỏ uint32_t* trỏ vào địa chỉ LỆCH (offset 1). CPU
* yêu cầu căn có thể chậm, thậm chí crash (ARM cũ) hoặc UB theo chuẩn C.
* GCC cảnh báo -Waddress-of-packed-member đúng vì lý do này. Luôn đọc
* qua memcpy hoặc đọc theo byte. */
printf("\nKET QUA: %d/%d kiem tra dat\n", g_pass, g_total);
return (g_pass == g_total) ? 0 : 1;
}
Bài 2 — Endianness, bit (shift + mask), bitfield, union
Ôn: Chương 03 (mục 3, 4, 5) + Chương 04 (mục 3.3 mẹo bit).
Đề bài
Bối cảnh. Mọi field nhiều byte trên wire là big-endian, còn máy của bạn (x86, ARM) là little-endian. Nhiều field lại nhồi nhiều trường nhỏ vào một byte. Bài này luyện hai kỹ năng sống còn khi parse protocol: đổi thứ tự byte và bóc/đóng gói bit, rồi tự kiểm chứng bằng cách đối chiếu với hàm chuẩn.
Phần A: phát hiện endianness.
- A1. Viết
is_little_endian_union(): dùngunion { uint16_t s; uint8_t b[2]; }, gáns = 0x0102rồi xemb[0]. - A2. Viết
is_little_endian_memcpy(): làm cùng việc nhưng không dùng union, chỉ dùngmemcpysang mảng byte. - A3. In 4 byte của
0x12345678như đang nằm trong RAM. Kiểm tra hai hàm trên trả cùng kết quả.
Phần B: tự viết hàm đọc/ghi big-endian (không dùng ntohl/htonl). Viết read_be16, read_be24, read_be32 (đọc từ con trỏ byte) và write_be16, write_be32 (ghi ra con trỏ byte). Chỉ dùng shift và OR. Kiểm thử:
- B1. Với các giá trị
{0, 1, 0xFF, 0x12345678, 0x80000001, 0xFFFFFFFF}:write_be32rồiread_be32phải trả lại đúng giá trị (round-trip). - B2. Byte do
write_be32sinh ra phải giống hệt 4 byte củahtonl(v)(so bằngmemcmp). - B3.
read_be16trên{0x00,0x3C}= 60;read_be24trên{0x00,0x01,0x00}= 256 và{0xFF,0xFF,0xFF}= 16777215 (field 24 bit: length/command-code của Diameter, seq của GTPv2).
Phần C: bóc/đóng gói byte flags GTPv2. Layout: bit7..5 = version, bit4 = P, bit3 = T, bit2..0 = spare.
- C1.
gtp_flags_unpack(byte, &f)vàgtp_flags_pack(&f)bằng shift + mask. - C2. Với các byte
{0x48, 0x40, 0x58, 0x5F, 0xFF}: in version/P/T/spare và dạng nhị phân của từng field (viết hàmbin_str). - C3. Round-trip cho tất cả 256 giá trị byte:
pack(unpack(b)) == b.
Phần D: bitfield so với shift+mask.
- D1. Khai báo struct bitfield tương ứng (
spare:3, t:1, p:1, version:3). - D2. Với cả 256 giá trị byte, so bitfield với kết quả unpack ở C1, đếm số trường hợp khác nhau và in ra.
- D3. (comment) Vì sao không dùng bitfield để parse byte thật?
Phần E: union nhiều cách nhìn.
- E1.
union ip_addr { uint32_t as_u32; uint8_t octets[4]; }. Gán octets = 192.168.1.1, in 4 byte trong RAM, inas_u32thô,htonl(as_u32)vàread_be32(octets). Giải thích vì saoas_u32thô là0x0101a8c0. - E2. Chứng minh
read_be32(octets) == 0xC0A80101vàas_u32 == htonl(0xC0A80101).
Phần F: mẹo bit (nền tảng cho flags, bitmap).
- F1. Viết macro
SET_BIT,CLEAR_BIT,TOGGLE_BIT,TEST_BIT(x, n). Từ x = 0: set bit 3, set bit 0 cho 9; clear bit 3 cho 1; toggle bit 7 cho 129; test bit 7 cho 1. - F2.
popcount_kernighan(x): đếm bit 1 bằng vòng lặpx &= x - 1. So với__builtin_popcountcho mọi x trong [0, 65535] và vài số lớn. - F3.
is_pow2(x),lowest_bit(x)(bằngx & -x). Test:is_pow2trên{0,1,2,3,64,96}cho{0,1,1,0,1,0};lowest_bit(0xB4) = 0x04.
Ràng buộc: build sạch; Phần B không được gọi ntohl/htonl bên trong hàm của bạn (chỉ dùng trong phần test để đối chiếu); với số có dấu trong phép dịch/đảo bit hãy dùng kiểu unsigned.
Tiêu chí tự chấm: mọi dòng kiểm tra [OK ], dòng cuối KET QUA: 16/16; giải thích được vì sao as_u32 của 192.168.1.1 là 0x0101a8c0 trên x86.
Lời giải
#define _DEFAULT_SOURCE
#include <arpa/inet.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
static int g_pass = 0, g_total = 0;
/* [BẪY macro] 'cond' phải được đánh giá ĐÚNG MỘT LẦN: nếu viết (cond) hai lần
* trong macro thì biểu thức có tác dụng phụ (gọi hàm cấp phát...) sẽ chạy 2 lần. */
#define CHECK(cond, ...) do { \
int ok_ = (cond) ? 1 : 0; \
g_total++; \
if (ok_) g_pass++; \
printf(" [%s] ", ok_ ? "OK " : "LOI"); \
printf(__VA_ARGS__); \
putchar('\n'); \
} while (0)
/* ===========================================================================
* PHẦN A — phát hiện endianness
* [NHỚ] little-endian: byte THẤP nằm ở địa chỉ THẤP => 0x0102 lưu "02 01".
* ========================================================================= */
static int is_little_endian_union(void) {
union { uint16_t s; uint8_t b[2]; } u = { .s = 0x0102 };
return u.b[0] == 0x02; /* byte đầu tiên là byte thấp? => little-endian */
}
static int is_little_endian_memcpy(void) {
uint16_t v = 0x0102;
uint8_t b[2];
memcpy(b, &v, sizeof b); /* memcpy: cách chắc chắn đúng để "nhìn byte" */
return b[0] == 0x02;
}
/* ===========================================================================
* PHẦN B — đọc/ghi BIG-ENDIAN bằng shift + OR
* [NHỚ] Cách này không phụ thuộc endianness của máy, không cần căn địa chỉ,
* không cần struct. Wireshark (tvb_get_ntohl) cũng làm kiểu này.
* [BẪY] Phải ép từng byte sang uint32_t TRƯỚC khi dịch: nếu không, byte
* (kiểu int sau promotion) dịch trái 24 bit có thể tràn số có dấu (UB).
* ========================================================================= */
static uint16_t read_be16(const uint8_t *p) {
return (uint16_t)(((uint16_t)p[0] << 8) | p[1]);
}
static uint32_t read_be24(const uint8_t *p) {
return ((uint32_t)p[0] << 16) | ((uint32_t)p[1] << 8) | p[2];
}
static uint32_t read_be32(const uint8_t *p) {
return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) |
((uint32_t)p[2] << 8) | (uint32_t)p[3];
}
static void write_be16(uint8_t *p, uint16_t v) {
p[0] = (uint8_t)(v >> 8);
p[1] = (uint8_t)(v & 0xFF);
}
static void write_be32(uint8_t *p, uint32_t v) {
p[0] = (uint8_t)(v >> 24);
p[1] = (uint8_t)(v >> 16);
p[2] = (uint8_t)(v >> 8);
p[3] = (uint8_t)(v);
}
/* ===========================================================================
* PHẦN C — flags GTPv2: | version(3) | P(1) | T(1) | spare(3) |
* bit7..5 bit4 bit3 bit2..0
* [NHỚ] Bóc: (b >> vị_trí_thấp_nhất_của_field) & mask_độ_rộng
* Đóng: ((giá_trị & mask) << vị_trí) rồi OR các field lại.
* Mask TRƯỚC khi dịch trái để đầu vào thừa bit không tràn sang field khác.
* ========================================================================= */
typedef struct { uint8_t version, p, t, spare; } gtp_flags_t;
static void gtp_flags_unpack(uint8_t b, gtp_flags_t *f) {
f->version = (uint8_t)((b >> 5) & 0x07); /* 3 bit cao: dịch phải 5, giữ 3 bit */
f->p = (uint8_t)((b >> 4) & 0x01);
f->t = (uint8_t)((b >> 3) & 0x01);
f->spare = (uint8_t)( b & 0x07); /* 3 bit thấp: chỉ cần mask */
}
static uint8_t gtp_flags_pack(const gtp_flags_t *f) {
return (uint8_t)(((f->version & 0x07) << 5) |
((f->p & 0x01) << 4) |
((f->t & 0x01) << 3) |
(f->spare & 0x07));
}
/* In v ở dạng nhị phân 'bits' bit vào buf (buf >= bits+1 byte). */
static const char *bin_str(unsigned v, int bits, char *buf) {
for (int i = 0; i < bits; i++)
buf[i] = ((v >> (bits - 1 - i)) & 1u) ? '1' : '0';
buf[bits] = '\0';
return buf;
}
/* ===========================================================================
* PHẦN D — bitfield (chỉ để HỌC khái niệm)
* [BẪY] Thứ tự bit trong bitfield do compiler/ABI quyết định (GCC khác MSVC,
* little khác big-endian). Trên GCC little-endian, field khai báo ĐẦU
* nằm ở bit THẤP, nên ta khai báo ngược với sơ đồ bit.
* ========================================================================= */
struct __attribute__((packed)) gtp_flags_bf {
unsigned int spare : 3;
unsigned int t : 1;
unsigned int p : 1;
unsigned int version : 3;
};
_Static_assert(sizeof(struct gtp_flags_bf) == 1, "bitfield phai chiem 1 byte");
/* ===========================================================================
* PHẦN E — union
* ========================================================================= */
union ip_addr {
uint32_t as_u32; /* nhìn như 1 số 32-bit */
uint8_t octets[4]; /* nhìn như 4 byte */
};
/* ===========================================================================
* PHẦN F — mẹo bit
* ========================================================================= */
#define SET_BIT(x, n) ((x) |= (1u << (n))) /* OR với mask = bật */
#define CLEAR_BIT(x, n) ((x) &= ~(1u << (n))) /* AND với ~mask = tắt */
#define TOGGLE_BIT(x, n) ((x) ^= (1u << (n))) /* XOR với mask = đảo */
#define TEST_BIT(x, n) (((x) >> (n)) & 1u) /* dịch rồi lấy bit thấp */
/* Kernighan: x & (x-1) xóa đúng MỘT bit 1 thấp nhất => đếm số lần lặp. */
static int popcount_kernighan(uint32_t x) {
int c = 0;
while (x) { x &= x - 1; c++; }
return c;
}
static int is_pow2(uint32_t x) { return x && !(x & (x - 1)); } /* lũy thừa 2 chỉ có đúng 1 bit 1 */
static uint32_t lowest_bit(uint32_t x) { return x & (0u - x); } /* dùng 0u - x để tránh tràn số có dấu */
int main(void) {
char bb[34];
/* ---------------- A ---------------- */
printf("== A. Endianness ==\n");
uint32_t v = 0x12345678;
uint8_t ram[4];
memcpy(ram, &v, 4);
printf(" 0x12345678 trong RAM: %02x %02x %02x %02x\n", ram[0], ram[1], ram[2], ram[3]);
CHECK(is_little_endian_union() == is_little_endian_memcpy(),
"2 cach phat hien cho cung ket qua: may nay la %s-endian",
is_little_endian_union() ? "LITTLE" : "BIG");
/* ---------------- B ---------------- */
printf("\n== B. Doc/ghi big-endian tu viet ==\n");
const uint32_t vals[] = { 0, 1, 0xFF, 0x12345678u, 0x80000001u, 0xFFFFFFFFu };
int rt_ok = 1, std_ok = 1;
for (size_t i = 0; i < sizeof vals / sizeof vals[0]; i++) {
uint8_t w[4];
write_be32(w, vals[i]);
if (read_be32(w) != vals[i]) rt_ok = 0; /* round-trip */
uint32_t n = htonl(vals[i]); /* hàm chuẩn, chỉ dùng để ĐỐI CHIẾU */
if (memcmp(w, &n, 4) != 0) std_ok = 0;
printf(" 0x%08x -> wire %02x %02x %02x %02x\n", (unsigned)vals[i], w[0], w[1], w[2], w[3]);
}
CHECK(rt_ok, "write_be32 -> read_be32 tra lai dung gia tri (6 gia tri)");
CHECK(std_ok, "byte write_be32 sinh ra == byte cua htonl (memcmp)");
const uint8_t l16[2] = { 0x00, 0x3C };
const uint8_t a24[3] = { 0x00, 0x01, 0x00 }, b24[3] = { 0xFF, 0xFF, 0xFF };
CHECK(read_be16(l16) == 60, "read_be16({00,3C}) = %u", read_be16(l16));
CHECK(read_be24(a24) == 256 && read_be24(b24) == 16777215u,
"read_be24({00,01,00}) = %u, read_be24({FF,FF,FF}) = %u",
(unsigned)read_be24(a24), (unsigned)read_be24(b24));
uint8_t w16[2];
write_be16(w16, 0x3C00);
CHECK(w16[0] == 0x3C && w16[1] == 0x00, "write_be16(0x3C00) -> %02x %02x", w16[0], w16[1]);
/* ---------------- C ---------------- */
printf("\n== C. Flags GTPv2 ==\n");
const uint8_t samples[] = { 0x48, 0x40, 0x58, 0x5F, 0xFF };
for (size_t i = 0; i < sizeof samples; i++) {
gtp_flags_t f;
gtp_flags_unpack(samples[i], &f);
char s_ver[8], s_p[4], s_t[4], s_sp[8];
printf(" 0x%02x = %s : version=%s(%u) P=%s T=%s spare=%s(%u)\n",
samples[i], bin_str(samples[i], 8, bb),
bin_str(f.version, 3, s_ver), f.version,
bin_str(f.p, 1, s_p), bin_str(f.t, 1, s_t),
bin_str(f.spare, 3, s_sp), f.spare);
}
gtp_flags_t f48;
gtp_flags_unpack(0x48, &f48);
CHECK(f48.version == 2 && f48.p == 0 && f48.t == 1 && f48.spare == 0,
"0x48 -> version=2 P=0 T=1 spare=0");
int pack_ok = 1;
for (unsigned b = 0; b < 256; b++) {
gtp_flags_t f;
gtp_flags_unpack((uint8_t)b, &f);
if (gtp_flags_pack(&f) != b) pack_ok = 0;
}
CHECK(pack_ok, "pack(unpack(b)) == b voi TAT CA 256 gia tri");
/* ---------------- D ---------------- */
printf("\n== D. Bitfield vs shift+mask ==\n");
int mismatch = 0;
for (unsigned b = 0; b < 256; b++) {
uint8_t byte = (uint8_t)b;
struct gtp_flags_bf bf;
memcpy(&bf, &byte, 1);
gtp_flags_t f;
gtp_flags_unpack(byte, &f);
if (bf.version != f.version || bf.p != f.p || bf.t != f.t || bf.spare != f.spare)
mismatch++;
}
printf(" bitfield khac shift+mask: %d/256 gia tri "
"(0 tren GCC little-endian; compiler/CPU khac co the KHAC)\n", mismatch);
/* D3: thứ tự bit của bitfield là implementation-defined, nên cùng code cho
* kết quả khác giữa GCC/MSVC hay x86/big-endian. Parser đúng trên máy bạn
* có thể sai trên máy khác => lỗi im lặng. Shift+mask thì chuẩn 100%. */
/* ---------------- E ---------------- */
printf("\n== E. Union ==\n");
union ip_addr a;
a.as_u32 = 0;
a.octets[0] = 192; a.octets[1] = 168; a.octets[2] = 1; a.octets[3] = 1;
printf(" octets trong RAM : %02x %02x %02x %02x\n",
a.octets[0], a.octets[1], a.octets[2], a.octets[3]);
printf(" as_u32 tho : 0x%08x (little-endian: byte dau 192 la byte THAP)\n", (unsigned)a.as_u32);
printf(" htonl(as_u32) : 0x%08x\n", (unsigned)htonl(a.as_u32));
printf(" read_be32(octets): 0x%08x (so de doc: C0.A8.01.01)\n", (unsigned)read_be32(a.octets));
CHECK(read_be32(a.octets) == 0xC0A80101u, "read_be32(octets) == 0xC0A80101");
CHECK(a.as_u32 == htonl(0xC0A80101u), "as_u32 == htonl(0xC0A80101) (dung ca little va big-endian)");
/* ---------------- F ---------------- */
printf("\n== F. Meo bit ==\n");
unsigned x = 0;
SET_BIT(x, 3); SET_BIT(x, 0);
CHECK(x == 9, "set bit 3 va bit 0 -> %u", x);
CLEAR_BIT(x, 3);
CHECK(x == 1, "clear bit 3 -> %u", x);
TOGGLE_BIT(x, 7);
CHECK(x == 129 && TEST_BIT(x, 7) == 1 && TEST_BIT(x, 6) == 0,
"toggle bit 7 -> %u, test bit 7 = %u, test bit 6 = %u", x, TEST_BIT(x, 7), TEST_BIT(x, 6));
int pc_ok = 1;
for (uint32_t i = 0; i <= 65535u; i++)
if (popcount_kernighan(i) != __builtin_popcount(i)) pc_ok = 0;
const uint32_t big[] = { 0x80000000u, 0xFFFFFFFFu, 0xAAAAAAAAu, 0x12345678u };
for (size_t i = 0; i < sizeof big / sizeof big[0]; i++)
if (popcount_kernighan(big[i]) != __builtin_popcount(big[i])) pc_ok = 0;
CHECK(pc_ok, "popcount_kernighan == __builtin_popcount (65536 so + 4 so lon)");
CHECK(is_pow2(0) == 0 && is_pow2(1) == 1 && is_pow2(2) == 1 &&
is_pow2(3) == 0 && is_pow2(64) == 1 && is_pow2(96) == 0,
"is_pow2 tren {0,1,2,3,64,96} -> {0,1,1,0,1,0}");
CHECK(lowest_bit(0xB4) == 0x04, "lowest_bit(0xB4) = 0x%02x (tach bit 1 thap nhat)", (unsigned)lowest_bit(0xB4));
printf("\nKET QUA: %d/%d kiem tra dat\n", g_pass, g_total);
return (g_pass == g_total) ? 0 : 1;
}
Bài 3 — Parser IPv4/UDP an toàn + Internet checksum
Ôn: Chương 03 (mục 8: parse IPv4) + Chương 04 (mục 3.2: internet checksum).
Đề bài
Bối cảnh. Bạn viết bộ dựng (build) và bộ phân tích (parse) header IPv4 + UDP, rồi tấn công chính parser của mình bằng gói hỏng để chứng minh nó không đọc ngoài biên và không tin dữ liệu do kẻ gửi điều khiển. Khác biệt giữa parser “chạy được” và parser “không khai thác được” nằm ở các bước kiểm tra biên.
Phần A: Internet checksum (RFC 1071). Viết uint16_t inet_checksum(const uint8_t *data, size_t len):
- cộng từng word 16-bit big-endian vào bộ cộng 32-bit;
- nếu
lenlẻ: byte cuối coi là byte cao của một word (byte thấp = 0); - gập carry:
while (sum >> 16) sum = (sum & 0xFFFF) + (sum >> 16); - trả về bù 1 của kết quả (
~sum).
Tính chất cần kiểm chứng: tính trên header có field checksum = 0 ra giá trị C; ghi C vào header; tính lại trên header đầy đủ phải ra 0.
Phần B: dựng header. Viết int ipv4_build(uint8_t *out, size_t cap, uint32_t src, uint32_t dst, uint8_t proto, uint16_t payload_len):
- Trả về 20 (số byte ghi) hoặc -1 nếu
cap < 20hoặc20 + payload_len > 0xFFFF. - Điền: version=4, IHL=5, TOS=0, total_length=20+payload_len, id=0x1234, flags/fragment=0x4000 (DF), TTL=64, protocol=proto, src, dst.
- Tính checksum đúng và ghi vào byte 10-11.
Phần C: parse header. Viết int parse_ipv4(const uint8_t *b, size_t len, ipv4_info_t *out) trả về một trong các mã lỗi sau, kiểm tra theo đúng thứ tự này:
| Mã | Điều kiện |
|---|---|
IP_ERR_SHORT (-1) | len < 20 |
IP_ERR_VERSION (-2) | version != 4 |
IP_ERR_IHL (-3) | IHL*4 < 20 hoặc IHL*4 > len |
IP_ERR_TOTAL (-4) | total_length < IHL*4 hoặc total_length > len |
IP_ERR_CSUM (-5) | checksum tính trên IHL*4 byte khác 0 |
Thành công trả 0 và điền: ihl_bytes, total_length, protocol, ttl, src, dst, payload_off (= IHL4), payload_len (= total_length - IHL4). Lưu ý: nếu len > total_length (ví dụ Ethernet đệm thêm 0) thì vẫn hợp lệ; payload_len phải tính từ total_length, không phải từ len.
Phần D: parse UDP. Viết int parse_udp(const uint8_t *p, size_t len, udp_info_t *u): trả -1 nếu len < 8; -2 nếu udp_length < 8 hoặc udp_length > len. Điền src_port, dst_port, payload (con trỏ), payload_len = udp_length - 8.
Phần E: bộ test. Dựng frame: IPv4 (10.0.0.1 → 10.0.0.2, proto 17) + UDP (cổng 5060 → 5060, length 12, checksum 0) + payload “ping” (4 byte), tổng 32 byte.
- E1. Parse gói hợp lệ, in các field; checksum header kỳ vọng
0x1497. - E2. Cùng gói nhưng đưa vào parser buffer dài 40 byte (đệm 0): vẫn OK và
payload_lenvẫn là 12. - E3. Cắt cụt: thử mọi
lentừ 0..31. Kỳ vọng:len0..19 choIP_ERR_SHORT(20 lần),len20..31 choIP_ERR_TOTAL(12 lần), không lần nào OK. - E4. Bảng gói bị sửa, mỗi dòng phải trả đúng mã lỗi:
- version=6:
IP_ERR_VERSION - IHL=4 (16 byte):
IP_ERR_IHL - IHL=15 (60 byte > 32):
IP_ERR_IHL - total_length cao = 0xFF:
IP_ERR_TOTAL - total_length = 10:
IP_ERR_TOTAL - TTL 64 thành 63 (không sửa checksum):
IP_ERR_CSUM - byte cuối của dst: 2 thành 3:
IP_ERR_CSUM
- version=6:
- E5. Fuzz nhẹ: với mỗi trong 20 byte header, lật toàn bộ bit (XOR 0xFF) rồi parse. In số gói bị chấp nhận. Kết quả phải là 0/20.
- E6. UDP: parse payload của gói hợp lệ ra “ping”; rồi sửa
udp_lengththành 100 (chỉ có 12 byte) thì phải trả -2.
Ràng buộc: build sạch; không ép kiểu con trỏ buffer sang struct*, đọc field bằng shift (big-endian); chạy sạch dưới -fsanitize=address,undefined.
Tiêu chí tự chấm:
- Mọi dòng kiểm tra
[OK ], dòng cuốiKET QUA: 21/21. - Giải thích được vì sao phải validate IHL trước khi dùng nó làm độ dài.
- (Mở rộng) Lấy một gói thật:
tcpdump -w x.pcap -c1, bỏ 14 byte Ethernet, đưa vàoparse_ipv4và đối chiếu với Wireshark (checksum phải ra “correct”).
Lời giải
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
static int g_pass = 0, g_total = 0;
/* [BẪY macro] 'cond' phải được đánh giá ĐÚNG MỘT LẦN: nếu viết (cond) hai lần
* trong macro thì biểu thức có tác dụng phụ (gọi hàm cấp phát...) sẽ chạy 2 lần. */
#define CHECK(cond, ...) do { \
int ok_ = (cond) ? 1 : 0; \
g_total++; \
if (ok_) g_pass++; \
printf(" [%s] ", ok_ ? "OK " : "LOI"); \
printf(__VA_ARGS__); \
putchar('\n'); \
} while (0)
/* ---- đọc/ghi big-endian (field protocol luôn big-endian) ------------------ */
static uint16_t read_be16(const uint8_t *p) { return (uint16_t)(((uint16_t)p[0] << 8) | p[1]); }
static uint32_t read_be32(const uint8_t *p) {
return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3];
}
static void write_be16(uint8_t *p, uint16_t v) { p[0] = (uint8_t)(v >> 8); p[1] = (uint8_t)v; }
static void write_be32(uint8_t *p, uint32_t v) {
p[0] = (uint8_t)(v >> 24); p[1] = (uint8_t)(v >> 16); p[2] = (uint8_t)(v >> 8); p[3] = (uint8_t)v;
}
/* ===========================================================================
* PHẦN A — Internet checksum
* [NHỚ] one's complement: carry tràn ra khỏi bit 15 được CỘNG NGƯỢC vào bit 0.
* [NHỚ] Kiểm tra hợp lệ: tính trên cả header (gồm checksum) phải ra 0.
* ========================================================================= */
static uint16_t inet_checksum(const uint8_t *data, size_t len) {
uint32_t sum = 0;
for (size_t i = 0; i + 1 < len; i += 2)
sum += ((uint32_t)data[i] << 8) | data[i + 1]; /* word 16-bit big-endian */
if (len & 1) sum += (uint32_t)data[len - 1] << 8; /* byte lẻ cuối = byte CAO */
while (sum >> 16) sum = (sum & 0xFFFF) + (sum >> 16); /* gập carry */
return (uint16_t)~sum; /* bù 1 */
}
/* ===========================================================================
* Mã lỗi + thông tin parse
* ========================================================================= */
enum {
IP_OK = 0,
IP_ERR_SHORT = -1,
IP_ERR_VERSION = -2,
IP_ERR_IHL = -3,
IP_ERR_TOTAL = -4,
IP_ERR_CSUM = -5,
};
static const char *ip_err_str(int e) {
switch (e) {
case IP_OK: return "OK";
case IP_ERR_SHORT: return "IP_ERR_SHORT";
case IP_ERR_VERSION: return "IP_ERR_VERSION";
case IP_ERR_IHL: return "IP_ERR_IHL";
case IP_ERR_TOTAL: return "IP_ERR_TOTAL";
case IP_ERR_CSUM: return "IP_ERR_CSUM";
default: return "?";
}
}
typedef struct {
uint8_t ihl_bytes, protocol, ttl;
uint16_t total_length;
uint32_t src, dst; /* host order sau khi parse */
size_t payload_off, payload_len;
} ipv4_info_t;
typedef struct {
uint16_t src_port, dst_port;
const uint8_t *payload; /* trỏ vào buffer gốc, KHÔNG sao chép */
size_t payload_len;
} udp_info_t;
/* ===========================================================================
* PHẦN B — dựng header
* ========================================================================= */
static int ipv4_build(uint8_t *out, size_t cap, uint32_t src, uint32_t dst,
uint8_t proto, uint16_t payload_len) {
if (cap < 20) return -1;
if ((size_t)payload_len + 20 > 0xFFFF) return -1; /* total_length là 16 bit */
out[0] = 0x45; /* version 4, IHL 5 */
out[1] = 0x00; /* TOS */
write_be16(out + 2, (uint16_t)(20 + payload_len)); /* total length */
write_be16(out + 4, 0x1234); /* identification */
write_be16(out + 6, 0x4000); /* flags: DF, fragment offset 0 */
out[8] = 64; /* TTL */
out[9] = proto;
out[10] = out[11] = 0; /* ZERO checksum TRƯỚC khi tính */
write_be32(out + 12, src);
write_be32(out + 16, dst);
write_be16(out + 10, inet_checksum(out, 20));
return 20;
}
/* ===========================================================================
* PHẦN C — parse header: thứ tự kiểm tra là một phần của thiết kế an toàn
* [NHỚ] Mọi con số lấy từ gói (IHL, total_length) đều do KẺ GỬI điều khiển.
* Phải validate TRƯỚC khi dùng làm độ dài/offset, nếu không là
* đọc-ngoài-biên (over-read) hoặc cấp phát sai.
* ========================================================================= */
static int parse_ipv4(const uint8_t *b, size_t len, ipv4_info_t *out) {
if (len < 20) return IP_ERR_SHORT; /* #1 đủ 20 byte mới được đọc b[0..19] */
if ((b[0] >> 4) != 4) return IP_ERR_VERSION;
size_t ihl = (size_t)(b[0] & 0x0F) * 4; /* IHL đếm theo từ 32-bit */
if (ihl < 20 || ihl > len) return IP_ERR_IHL; /* #2 IHL do kẻ gửi quyết định */
uint16_t total = read_be16(b + 2);
if (total < ihl || total > len) return IP_ERR_TOTAL; /* #3 total_length cũng vậy */
if (inet_checksum(b, ihl) != 0) return IP_ERR_CSUM; /* #4 header phải toàn vẹn */
out->ihl_bytes = (uint8_t)ihl;
out->total_length = total;
out->ttl = b[8];
out->protocol = b[9];
out->src = read_be32(b + 12);
out->dst = read_be32(b + 16);
out->payload_off = ihl;
out->payload_len = total - ihl; /* từ total_length, KHÔNG từ len */
return IP_OK;
}
/* ===========================================================================
* PHẦN D — parse UDP
* ========================================================================= */
static int parse_udp(const uint8_t *p, size_t len, udp_info_t *u) {
if (len < 8) return -1;
uint16_t ulen = read_be16(p + 4);
if (ulen < 8 || ulen > len) return -2;
u->src_port = read_be16(p);
u->dst_port = read_be16(p + 2);
u->payload = p + 8;
u->payload_len = (size_t)ulen - 8;
return 0;
}
static void print_ip(const char *label, uint32_t ip) {
printf(" %-8s= %u.%u.%u.%u\n", label,
(ip >> 24) & 0xFF, (ip >> 16) & 0xFF, (ip >> 8) & 0xFF, ip & 0xFF);
}
int main(void) {
/* ---------------- dựng frame mẫu ---------------- */
uint8_t frame[64];
memset(frame, 0, sizeof frame); /* phần dư = 0 (như Ethernet padding) */
int n = ipv4_build(frame, sizeof frame, 0x0A000001u, 0x0A000002u, 17, 12);
write_be16(frame + 20, 5060); /* UDP src port */
write_be16(frame + 22, 5060); /* UDP dst port */
write_be16(frame + 24, 12); /* UDP length = 8 + 4 */
write_be16(frame + 26, 0); /* UDP checksum 0 = "không dùng" (hợp lệ trong IPv4) */
memcpy(frame + 28, "ping", 4);
const size_t FRAME_LEN = 32;
/* ---------------- A + B + E1 ---------------- */
printf("== A/B. Checksum va build ==\n");
CHECK(n == 20, "ipv4_build tra ve %d byte", n);
uint16_t csum = read_be16(frame + 10);
CHECK(csum == 0x1497, "checksum header = 0x%04x (ky vong 0x1497)", csum);
CHECK(inet_checksum(frame, 20) == 0, "tinh lai tren header day du = 0x%04x (hop le khi = 0)",
inet_checksum(frame, 20));
uint8_t tiny[10];
CHECK(ipv4_build(tiny, sizeof tiny, 1, 2, 17, 0) == -1, "build voi cap=10 bi tu choi (-1)");
CHECK(ipv4_build(frame + 40, 24, 1, 2, 17, 0xFFF0) == -1, "build voi total_length tran 16 bit bi tu choi (-1)");
memset(frame + 40, 0, 24); /* dọn lại vùng dư sau thử nghiệm */
printf("\n== E1. Parse goi hop le ==\n");
ipv4_info_t ip;
int rc = parse_ipv4(frame, FRAME_LEN, &ip);
CHECK(rc == IP_OK, "parse_ipv4 -> %s", ip_err_str(rc));
printf(" ihl=%u total=%u proto=%u ttl=%u payload_off=%zu payload_len=%zu\n",
ip.ihl_bytes, ip.total_length, ip.protocol, ip.ttl, ip.payload_off, ip.payload_len);
print_ip("src", ip.src);
print_ip("dst", ip.dst);
CHECK(ip.total_length == 32 && ip.protocol == 17 && ip.ttl == 64 &&
ip.src == 0x0A000001u && ip.dst == 0x0A000002u && ip.payload_len == 12,
"cac field dung (10.0.0.1 -> 10.0.0.2, UDP, total=32)");
/* ---------------- E2 ---------------- */
printf("\n== E2. Buffer dai hon total_length ==\n");
rc = parse_ipv4(frame, 40, &ip);
CHECK(rc == IP_OK && ip.payload_len == 12,
"len=40 van OK, payload_len=%zu (tinh tu total_length, khong tu len)", ip.payload_len);
/* ---------------- E3 ---------------- */
printf("\n== E3. Cat cut moi do dai 0..31 ==\n");
int c_short = 0, c_total = 0, c_ok = 0, c_other = 0;
for (size_t len = 0; len < FRAME_LEN; len++) {
int r = parse_ipv4(frame, len, &ip);
if (r == IP_ERR_SHORT) c_short++;
else if (r == IP_ERR_TOTAL) c_total++;
else if (r == IP_OK) c_ok++;
else c_other++;
}
printf(" SHORT=%d TOTAL=%d OK=%d khac=%d\n", c_short, c_total, c_ok, c_other);
CHECK(c_short == 20 && c_total == 12 && c_ok == 0 && c_other == 0,
"len 0..19 -> SHORT (20 lan), len 20..31 -> TOTAL (12 lan), khong lan nao OK");
/* ---------------- E4 ---------------- */
printf("\n== E4. Bang goi bi sua ==\n");
static const struct { const char *name; size_t idx; uint8_t val; int expect; } muts[] = {
{ "version=6", 0, 0x65, IP_ERR_VERSION },
{ "IHL=4 (16 byte)", 0, 0x44, IP_ERR_IHL },
{ "IHL=15 (60 byte > 32)", 0, 0x4F, IP_ERR_IHL },
{ "total_length cao = 0xFF", 2, 0xFF, IP_ERR_TOTAL },
{ "total_length = 10", 3, 0x0A, IP_ERR_TOTAL },
{ "TTL 64->63, checksum cu", 8, 63, IP_ERR_CSUM },
{ "dst byte cuoi 2->3", 19, 0x03, IP_ERR_CSUM },
};
for (size_t i = 0; i < sizeof muts / sizeof muts[0]; i++) {
uint8_t c[32];
memcpy(c, frame, sizeof c);
c[muts[i].idx] = muts[i].val;
int r = parse_ipv4(c, sizeof c, &ip);
CHECK(r == muts[i].expect, "%-26s -> %s", muts[i].name, ip_err_str(r));
}
/* ---------------- E5 ---------------- */
printf("\n== E5. Lat bit tung byte header ==\n");
int accepted = 0;
for (size_t i = 0; i < 20; i++) {
uint8_t c[32];
memcpy(c, frame, sizeof c);
c[i] ^= 0xFF;
if (parse_ipv4(c, sizeof c, &ip) == IP_OK) accepted++;
}
CHECK(accepted == 0, "so goi bi CHAP NHAN sau khi lat tung byte: %d/20", accepted);
/* ---------------- E6 ---------------- */
printf("\n== E6. UDP ==\n");
udp_info_t u;
rc = parse_ipv4(frame, FRAME_LEN, &ip); /* parse lại gói hợp lệ để lấy payload_off */
CHECK(rc == IP_OK, "parse lai goi hop le: payload_off=%zu payload_len=%zu", ip.payload_off, ip.payload_len);
rc = parse_udp(frame + ip.payload_off, ip.payload_len, &u);
CHECK(rc == 0 && u.src_port == 5060 && u.dst_port == 5060 && u.payload_len == 4 &&
memcmp(u.payload, "ping", 4) == 0,
"UDP %u -> %u, payload %zu byte = \"%.*s\"", u.src_port, u.dst_port,
u.payload_len, (int)u.payload_len, (const char *)u.payload);
uint8_t lie[12];
memcpy(lie, frame + 20, sizeof lie);
write_be16(lie + 4, 100); /* khai udp_length = 100 nhưng chỉ có 12 byte */
CHECK(parse_udp(lie, sizeof lie, &u) == -2, "udp_length gia (100 > 12 byte that) -> -2");
CHECK(parse_udp(lie, 5, &u) == -1, "UDP cut con 5 byte -> -1");
printf("\nKET QUA: %d/%d kiem tra dat\n", g_pass, g_total);
return (g_pass == g_total) ? 0 : 1;
}
Bài 4 — TLV/AVP: flexible array member, tagged union, cây grouped AVP
Ôn: Chương 03 (mục 5, 6, 7) + Chương 04 (mục 1 callback, mục 2.4 ALIGN4).
Đề bài
Bối cảnh. Diameter (và nhiều protocol khác) mã hóa dữ liệu thành chuỗi AVP độ dài thay đổi. Một số AVP là “grouped”: bên trong chứa AVP con nên tạo thành cây. Bạn tự dựng bộ mã hóa (encode), bộ giải mã (decode) an toàn, bộ diễn giải giá trị (tagged union) và bộ dựng cây có giới hạn độ sâu.
Định dạng AVP (đơn giản hóa từ Diameter):
| Byte | Nội dung |
|---|---|
| 0..3 | code (uint32, big-endian) |
| 4 | flags (1 byte) |
| 5..7 | length (24 bit big-endian) = 8 + độ dài data, không tính padding |
| 8.. | data |
| cuối | padding 0x00 sao cho tổng chiều dài của AVP là bội của 4 (tổng = ALIGN4(length)) |
Ví dụ: AVP Origin-Host “a.b” (3 byte): length = 11, chiếm 12 byte trên wire.
Phần A: ALIGN4 và AVP trong bộ nhớ (flexible array member).
- A1. Viết macro
ALIGN4(n)làm tròn lên bội số 4: 13 thành 16, 12 thành 12, 0 thành 0. - A2. Khai báo
typedef struct { uint32_t code; uint8_t flags; uint32_t data_len; uint8_t data[]; } avp_t;. Insizeof(avp_t)vàoffsetof(avp_t, data); giải thích vì saodata[]không chiếm chỗ trongsizeof. - A3.
avp_t *avp_new(code, flags, data, len): cấp phát header + data trong mộtmalloc. Từ chối (trả NULL) nếulen > 0xFFFFFF - 8(field length chỉ có 24 bit). Không gọimemcpyvới con trỏ NULL khilen = 0.
Phần B: mã hóa. size_t avp_encode(const avp_t *a, uint8_t *out, size_t cap): ghi đúng định dạng ở trên (kể cả padding = 0x00), trả về số byte ghi (đã gồm padding) hoặc 0 nếu cap không đủ. Không được ghi quá cap.
Phần C: giải mã không sao chép (zero-copy view). int avp_decode(const uint8_t *b, size_t n, avp_view_t *v): v trỏ vào buffer gốc (code, flags, val, vlen, consumed). Mã lỗi, kiểm tra theo thứ tự:
AVP_E_SHORT(-1):n < 8AVP_E_LEN(-2):length < 8hoặclength > nAVP_E_PAD(-3):ALIGN4(length) > n(thiếu byte padding)
avp_foreach(buf, n, cb, user): gọi callback cho từng AVP liên tiếp; bước nhảy là consumed (= ALIGN4(length)), không phải length. Dừng và trả mã lỗi nếu có AVP hỏng.
Phần D: giá trị đa kiểu (tagged union). avp_interpret(view, &val) theo code:
- 268 Result-Code:
VT_U32(vlenphải = 4, nếu khôngAVP_E_LEN) - 264 Origin-Host:
VT_STR(sao chép vào mảng 64 byte, tối đa 63 ký tự +'\0', cắt nếu dài hơn, tuyệt đối không tràn) - 257 Host-IP-Address:
VT_IP4(vlenphải = 4) - khác:
AVP_E_TYPE(-6)
Kết quả là struct { vtype_t type; union { u32; str[64]; ip4[4]; } as; }.
Phần E: cây grouped AVP (có giới hạn). Code 456 (MSCC) và 431 (Granted-Service-Unit) là grouped. Viết tree_parse(buf, n, depth, &head) dựng cây node_t { code, vlen, val, child, next }:
- Đệ quy vào
childkhi gặp AVP grouped;depthtối đaMAX_DEPTH = 6, vượt thì trảAVP_E_DEPTH(-4) để chống tràn stack do gói lồng sâu. - Lỗi ở bất kỳ chỗ nào: giải phóng mọi node đã cấp phát rồi trả mã lỗi (không rò rỉ bộ nhớ);
AVP_E_NOMEM(-5) nếucallocthất bại. - Viết
tree_print(thụt lề theo độ sâu) vàtree_free(đệ quy). - Node chỉ trỏ vào buffer gốc (không sao chép) nên buffer phải sống lâu hơn cây.
Phần F: bộ test. Dựng bằng chính avp_new + avp_encode (đừng viết tay byte):
top = Result-Code(268)=2001 | Origin-Host(264)="a.b" | MSCC(456){ Rating-Group(432)=1 | GSU(431){ CC-Total-Octets(421)=4096 } }
- F1. Kích thước: 12 + 12 + 40 = 64 byte.
- F2.
avp_foreachin từng AVP; interpret Result-Code=2001, Origin-Host=“a.b”; MSCC trảAVP_E_TYPE(bỏ qua). - F3.
tree_parse+tree_printra đúng cây 2 tầng;tree_freekhông rò rỉ. - F4. Gói hỏng, mỗi trường hợp phải ra đúng mã lỗi: cắt còn 5 byte (
AVP_E_SHORT); length ở AVP đầu = 0x00FFFF (AVP_E_LEN); length ở AVP đầu = 4 (AVP_E_LEN); cắt mất byte padding của Origin-Host (AVP_E_PAD); lồng 20 tầng grouped (AVP_E_DEPTH). - F5. Result-Code có data 3 byte thì interpret trả
AVP_E_LEN. Origin-Host dài 200 ký tự thì chuỗi nhận được đúng 63 ký tự (không tràn). - F6.
avp_new(len = 0x01000000)trả NULL.avp_encodevớicapquá nhỏ trả 0.
Ràng buộc: build sạch; chạy sạch dưới -fsanitize=address,undefined (không leak, không tràn); mọi độ dài đọc từ gói phải được kiểm tra trước khi dùng.
Tiêu chí tự chấm:
- Mọi dòng kiểm tra
[OK ],KET QUA: 18/18, ASan không báo gì. - Giải thích được vì sao bước nhảy là
ALIGN4(length)chứ không phảilength. - Giải thích được vì sao phải giới hạn độ sâu khi dựng cây.
Lời giải
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int g_pass = 0, g_total = 0;
/* [BẪY macro] 'cond' phải được đánh giá ĐÚNG MỘT LẦN: nếu viết (cond) hai lần
* trong macro thì biểu thức có tác dụng phụ (gọi hàm cấp phát...) sẽ chạy 2 lần. */
#define CHECK(cond, ...) do { \
int ok_ = (cond) ? 1 : 0; \
g_total++; \
if (ok_) g_pass++; \
printf(" [%s] ", ok_ ? "OK " : "LOI"); \
printf(__VA_ARGS__); \
putchar('\n'); \
} while (0)
/* ---- A1: làm tròn lên bội của 4. (n+3) & ~3: cộng 3 rồi xóa 2 bit thấp. ---- */
#define ALIGN4(n) (((n) + 3u) & ~3u)
#define AVP_HDR_LEN 8u
#define AVP_MAX_LEN 0x00FFFFFFu /* length là field 24 bit */
#define MAX_DEPTH 6
enum {
AVP_OK = 0,
AVP_E_SHORT = -1,
AVP_E_LEN = -2,
AVP_E_PAD = -3,
AVP_E_DEPTH = -4,
AVP_E_NOMEM = -5,
AVP_E_TYPE = -6,
};
/* ---- đọc/ghi big-endian ---------------------------------------------------- */
static uint32_t read_be24(const uint8_t *p) { return ((uint32_t)p[0] << 16) | ((uint32_t)p[1] << 8) | p[2]; }
static uint32_t read_be32(const uint8_t *p) {
return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3];
}
static void write_be24(uint8_t *p, uint32_t v) { p[0] = (uint8_t)(v >> 16); p[1] = (uint8_t)(v >> 8); p[2] = (uint8_t)v; }
static void write_be32(uint8_t *p, uint32_t v) {
p[0] = (uint8_t)(v >> 24); p[1] = (uint8_t)(v >> 16); p[2] = (uint8_t)(v >> 8); p[3] = (uint8_t)v;
}
/* ===========================================================================
* PHẦN A — AVP trong bộ nhớ (host representation, KHÔNG phải wire format)
* [NHỚ] data[] (flexible array member) phải là field CUỐI, không tính vào
* sizeof; cấp phát header + n byte trong MỘT khối => một malloc, một free.
* ========================================================================= */
typedef struct {
uint32_t code;
uint8_t flags;
uint32_t data_len;
uint8_t data[];
} avp_t;
static avp_t *avp_new(uint32_t code, uint8_t flags, const void *data, size_t len) {
if (len > AVP_MAX_LEN - AVP_HDR_LEN) return NULL; /* #1 field length chỉ 24 bit */
avp_t *a = malloc(sizeof *a + len);
if (!a) return NULL; /* LUÔN kiểm tra malloc */
a->code = code;
a->flags = flags;
a->data_len = (uint32_t)len;
if (len) memcpy(a->data, data, len); /* memcpy(NULL, ..., 0) là UB => chặn */
return a;
}
/* ===========================================================================
* PHẦN B — mã hóa ra wire
* ========================================================================= */
static size_t avp_encode(const avp_t *a, uint8_t *out, size_t cap) {
size_t alen = AVP_HDR_LEN + a->data_len; /* length ghi vào header: KHÔNG gồm padding */
size_t total = ALIGN4(alen); /* số byte thật chiếm trên wire */
if (cap < total) return 0; /* bound check TRƯỚC khi ghi */
write_be32(out, a->code);
out[4] = a->flags;
write_be24(out + 5, (uint32_t)alen);
if (a->data_len) memcpy(out + AVP_HDR_LEN, a->data, a->data_len);
memset(out + alen, 0, total - alen); /* padding = 0x00 */
return total;
}
/* ===========================================================================
* PHẦN C — giải mã zero-copy + duyệt bằng callback
* ========================================================================= */
typedef struct {
uint32_t code;
uint8_t flags;
const uint8_t *val; /* trỏ vào buffer gốc */
uint32_t vlen;
size_t consumed; /* ALIGN4(length): bước nhảy tới AVP kế tiếp */
} avp_view_t;
static int avp_decode(const uint8_t *b, size_t n, avp_view_t *v) {
if (n < AVP_HDR_LEN) return AVP_E_SHORT;
uint32_t alen = read_be24(b + 5);
if (alen < AVP_HDR_LEN || alen > n) return AVP_E_LEN; /* length do kẻ gửi quyết định */
size_t total = ALIGN4(alen);
if (total > n) return AVP_E_PAD; /* thiếu byte padding */
v->code = read_be32(b);
v->flags = b[4];
v->val = b + AVP_HDR_LEN;
v->vlen = alen - AVP_HDR_LEN;
v->consumed = total;
return AVP_OK;
}
typedef void (*avp_cb)(const avp_view_t *v, void *user);
static int avp_foreach(const uint8_t *b, size_t n, avp_cb cb, void *user) {
while (n > 0) {
avp_view_t v;
int rc = avp_decode(b, n, &v);
if (rc != AVP_OK) return rc;
cb(&v, user);
b += v.consumed; /* nhảy qua CẢ padding */
n -= v.consumed;
}
return AVP_OK;
}
/* ===========================================================================
* PHẦN D — tagged union: type cho biết đang dùng nhánh nào của union
* ========================================================================= */
typedef enum { VT_U32, VT_STR, VT_IP4 } vtype_t;
typedef struct {
vtype_t type;
union { uint32_t u32; char str[64]; uint8_t ip4[4]; } as;
} avp_val_t;
static int avp_interpret(const avp_view_t *v, avp_val_t *out) {
switch (v->code) {
case 268: /* Result-Code */
if (v->vlen != 4) return AVP_E_LEN;
out->type = VT_U32;
out->as.u32 = read_be32(v->val);
return AVP_OK;
case 264: { /* Origin-Host */
size_t cap = sizeof out->as.str - 1; /* chừa 1 byte cho '\0' */
size_t n = v->vlen < cap ? v->vlen : cap;
memcpy(out->as.str, v->val, n);
out->as.str[n] = '\0'; /* data trên wire KHÔNG có '\0' => tự thêm */
out->type = VT_STR;
return AVP_OK;
}
case 257: /* Host-IP-Address */
if (v->vlen != 4) return AVP_E_LEN;
out->type = VT_IP4;
memcpy(out->as.ip4, v->val, 4);
return AVP_OK;
default:
return AVP_E_TYPE;
}
}
/* ===========================================================================
* PHẦN E — cây grouped AVP
* [NHỚ] "con đầu - anh em kế": mỗi node chỉ cần child (danh sách con) và
* next (AVP kế tiếp CÙNG cấp).
* ========================================================================= */
typedef struct node {
uint32_t code, vlen;
const uint8_t *val;
struct node *child, *next;
} node_t;
static int is_grouped(uint32_t code) { return code == 456 || code == 431; }
static void tree_free(node_t *n) {
while (n) {
node_t *next = n->next; /* LƯU next trước khi free (tránh use-after-free) */
tree_free(n->child);
free(n);
n = next;
}
}
static int tree_parse(const uint8_t *b, size_t n, int depth, node_t **out) {
if (depth > MAX_DEPTH) return AVP_E_DEPTH; /* gói lồng sâu bất thường => từ chối */
node_t *head = NULL, **tail = &head;
while (n > 0) {
avp_view_t v;
int rc = avp_decode(b, n, &v);
if (rc != AVP_OK) { tree_free(head); return rc; }
node_t *nd = calloc(1, sizeof *nd); /* calloc: child/next = NULL sẵn */
if (!nd) { tree_free(head); return AVP_E_NOMEM; }
nd->code = v.code; nd->vlen = v.vlen; nd->val = v.val;
*tail = nd; /* gắn NGAY để lỗi sau còn free được node này */
tail = &nd->next;
if (is_grouped(v.code)) {
rc = tree_parse(v.val, v.vlen, depth + 1, &nd->child);
if (rc != AVP_OK) { tree_free(head); return rc; } /* nd->child còn NULL: không double-free */
}
b += v.consumed;
n -= v.consumed;
}
*out = head;
return AVP_OK;
}
static const char *avp_name(uint32_t code) {
switch (code) {
case 257: return "Host-IP-Address";
case 264: return "Origin-Host";
case 268: return "Result-Code";
case 421: return "CC-Total-Octets";
case 431: return "Granted-Service-Unit";
case 432: return "Rating-Group-Id";
case 456: return "Multiple-Services-Credit-Control";
default: return "?";
}
}
static void tree_print(const node_t *n, int depth) {
for (; n; n = n->next) {
printf(" %*s- %u %s", depth * 2, "", (unsigned)n->code, avp_name(n->code));
if (is_grouped(n->code)) printf(" [grouped]");
else if (n->code == 264) printf(" = \"%.*s\"", (int)n->vlen, (const char *)n->val);
else if (n->vlen == 4) printf(" = %u", (unsigned)read_be32(n->val));
printf("\n");
tree_print(n->child, depth + 1);
}
}
/* ===========================================================================
* Hàm dựng dữ liệu test: luôn đi qua avp_new + avp_encode
* ========================================================================= */
static size_t put_avp(uint8_t *out, size_t cap, uint32_t code, const void *data, size_t len) {
avp_t *a = avp_new(code, 0x40, data, len);
if (!a) return 0;
size_t n = avp_encode(a, out, cap);
free(a); /* một khối => một free */
return n;
}
static size_t put_u32(uint8_t *out, size_t cap, uint32_t code, uint32_t val) {
uint8_t d[4];
write_be32(d, val);
return put_avp(out, cap, code, d, 4);
}
/* Bọc một leaf trong 'levels' tầng grouped(431) */
static int build_nested(uint8_t *out, size_t cap, int levels, size_t *outlen) {
uint8_t a[512], b[512];
size_t n = put_u32(a, sizeof a, 421, 1);
for (int i = 0; i < levels; i++) {
size_t m = put_avp(b, sizeof b, 431, a, n);
if (m == 0) return -1;
memcpy(a, b, m);
n = m;
}
if (n > cap) return -1;
memcpy(out, a, n);
*outlen = n;
return 0;
}
static void print_cb(const avp_view_t *v, void *user) {
int *count = user; /* ép void* về đúng kiểu */
(*count)++;
avp_val_t val;
int rc = avp_interpret(v, &val);
printf(" AVP %-3u flags=0x%02x vlen=%-2u consumed=%-2zu ",
(unsigned)v->code, v->flags, (unsigned)v->vlen, v->consumed);
if (rc == AVP_OK && val.type == VT_U32) printf("-> U32 %u\n", (unsigned)val.as.u32);
else if (rc == AVP_OK && val.type == VT_STR) printf("-> STR \"%s\"\n", val.as.str);
else if (rc == AVP_OK && val.type == VT_IP4) printf("-> IP4 %u.%u.%u.%u\n", val.as.ip4[0], val.as.ip4[1], val.as.ip4[2], val.as.ip4[3]);
else printf("-> (khong dien giai, ma %d)\n", rc);
}
int main(void) {
/* ---------------- A ---------------- */
printf("== A. ALIGN4 va flexible array member ==\n");
CHECK(ALIGN4(13u) == 16 && ALIGN4(12u) == 12 && ALIGN4(0u) == 0 && ALIGN4(1u) == 4,
"ALIGN4: 13->%u 12->%u 0->%u 1->%u", ALIGN4(13u), ALIGN4(12u), ALIGN4(0u), ALIGN4(1u));
printf(" sizeof(avp_t)=%zu offsetof(data)=%zu (data[] khong chiem cho trong sizeof)\n",
sizeof(avp_t), offsetof(avp_t, data));
CHECK(avp_new(1, 0, NULL, 0x01000000u) == NULL, "avp_new(len=0x01000000) bi tu choi (NULL)");
avp_t *empty = avp_new(1, 0, NULL, 0);
CHECK(empty != NULL && empty->data_len == 0, "avp_new(len=0) khong goi memcpy(NULL) va thanh cong");
uint8_t smallbuf[8];
CHECK(avp_encode(empty, smallbuf, 4) == 0, "avp_encode voi cap=4 < 8 -> 0 (khong ghi qua cap)");
free(empty);
/* ---------------- dựng top ---------------- */
uint8_t top[256], mscc_body[128], gsu_in[32];
size_t n_oct = put_u32(gsu_in, sizeof gsu_in, 421, 4096); /* 12 byte */
size_t off = put_u32(mscc_body, sizeof mscc_body, 432, 1); /* 12 byte */
off += put_avp(mscc_body + off, sizeof mscc_body - off, 431, gsu_in, n_oct); /* 8+12 = 20 */
size_t t = 0;
t += put_u32(top + t, sizeof top - t, 268, 2001); /* 12 byte */
t += put_avp(top + t, sizeof top - t, 264, "a.b", 3); /* length 11 -> 12 byte */
t += put_avp(top + t, sizeof top - t, 456, mscc_body, off); /* 8 + 32 = 40 */
printf("\n== F1. Kich thuoc ==\n");
CHECK(t == 64, "tong buffer = %zu byte (12 + 12 + 40)", t);
/* Origin-Host bắt đầu ở offset 12 (sau Result-Code 12 byte): byte thấp của length ở 12+7,
* byte padding (1 byte) ở 12+11 */
CHECK(top[12 + 7] == 11 && top[12 + 11] == 0,
"Origin-Host: length ghi 11 (khong gom padding), byte padding = 0x00 o cuoi");
/* ---------------- F2 ---------------- */
printf("\n== F2. Duyet bang callback + tagged union ==\n");
int count = 0;
int rc = avp_foreach(top, t, print_cb, &count);
CHECK(rc == AVP_OK && count == 3, "avp_foreach: rc=%d, so AVP top-level=%d", rc, count);
/* ---------------- F3 ---------------- */
printf("\n== F3. Cay grouped AVP ==\n");
node_t *root = NULL;
rc = tree_parse(top, t, 0, &root);
CHECK(rc == AVP_OK && root != NULL, "tree_parse -> %d", rc);
tree_print(root, 0);
tree_free(root);
/* ---------------- F4 ---------------- */
printf("\n== F4. Goi hong ==\n");
avp_view_t v;
CHECK(avp_decode(top, 5, &v) == AVP_E_SHORT, "cat con 5 byte -> AVP_E_SHORT");
uint8_t bad[256];
memcpy(bad, top, t);
write_be24(bad + 5, 0x00FFFF);
CHECK(avp_decode(bad, t, &v) == AVP_E_LEN, "length = 0x00FFFF > buffer -> AVP_E_LEN");
write_be24(bad + 5, 4);
CHECK(avp_decode(bad, t, &v) == AVP_E_LEN, "length = 4 < 8 -> AVP_E_LEN");
CHECK(avp_decode(top + 12, 11, &v) == AVP_E_PAD, "mat byte padding cua Origin-Host -> AVP_E_PAD");
uint8_t nest[512];
size_t nlen = 0;
node_t *tr = NULL;
CHECK(build_nested(nest, sizeof nest, 2, &nlen) == 0 &&
tree_parse(nest, nlen, 0, &tr) == AVP_OK, "long 2 tang -> parse OK");
tree_free(tr);
tr = NULL;
CHECK(build_nested(nest, sizeof nest, 20, &nlen) == 0 &&
tree_parse(nest, nlen, 0, &tr) == AVP_E_DEPTH && tr == NULL,
"long 20 tang -> AVP_E_DEPTH (da don sach, khong ro ri)");
/* Lỗi giữa chừng khi cây đã có nhiều node: buffer cắt ngang trong grouped */
CHECK(tree_parse(top, 52, 0, &tr) == AVP_E_LEN && tr == NULL,
"cat ngang trong MSCC -> AVP_E_LEN, cay dang dung do duoc giai phong (ASan se bat neu ro ri)");
/* ---------------- F5 ---------------- */
printf("\n== F5. Dien giai an toan ==\n");
uint8_t buf[300];
size_t m = put_avp(buf, sizeof buf, 268, "abc", 3);
avp_val_t val;
CHECK(m > 0 && avp_decode(buf, m, &v) == AVP_OK && avp_interpret(&v, &val) == AVP_E_LEN,
"Result-Code data 3 byte -> AVP_E_LEN");
char longname[200];
memset(longname, 'x', sizeof longname);
m = put_avp(buf, sizeof buf, 264, longname, sizeof longname);
CHECK(m > 0 && avp_decode(buf, m, &v) == AVP_OK && avp_interpret(&v, &val) == AVP_OK &&
strlen(val.as.str) == 63,
"Origin-Host 200 ky tu -> chuoi nhan duoc dai %zu (cat o 63, khong tran)", strlen(val.as.str));
uint8_t ip[4] = { 10, 1, 2, 3 };
m = put_avp(buf, sizeof buf, 257, ip, 4);
CHECK(m > 0 && avp_decode(buf, m, &v) == AVP_OK && avp_interpret(&v, &val) == AVP_OK &&
val.type == VT_IP4 && val.as.ip4[0] == 10 && val.as.ip4[3] == 3, "Host-IP-Address -> 10.1.2.3");
printf("\nKET QUA: %d/%d kiem tra dat\n", g_pass, g_total);
return (g_pass == g_total) ? 0 : 1;
}
Điểm đáng nhớ: macro CHECK ở bản đầu từng đánh giá điều kiện hai lần, khiến tree_parse trong điều kiện chạy hai lần và làm rò rỉ một cây (ASan báo ngay). Đó chính là bẫy “macro đánh giá hai lần” của chương 04.
Bài 5 — Dự án nhiều file: parser Diameter + dispatch table + Makefile
Ôn: Chương 04 (mục 1 con trỏ hàm, mục 2 preprocessor, mục 3.1 cờ bit, mục 4 nhiều file/static/extern, mục 5 Makefile, mục 6 sanitizer, mục 7 errno/volatile/variadic).
Đề bài
Bối cảnh. Bạn xây module đọc header Diameter và chuyển message tới đúng hàm xử lý theo command code, như cách các telecom stack thật làm. Trọng tâm không phải thuật toán mà là cách tổ chức một dự án C: header/source tách bạch, bảng con trỏ hàm, build tự động, và kiểm tra bộ nhớ. Dự án gồm 4 file: diameter.h, diameter.c, main.c, Makefile.
Header Diameter (20 byte, big-endian):
| Byte | Nội dung |
|---|---|
| 0 | version (luôn = 1) |
| 1..3 | message length, 24 bit, tính cả header |
| 4 | command flags: bit7 R(equest), bit6 P(roxiable), bit5 E(rror), bit4 T |
| 5..7 | command code, 24 bit |
| 8..11 | application-id |
| 12..15 | hop-by-hop id |
| 16..19 | end-to-end id |
Command code dùng trong bài: 257 CER/CEA, 258 RAR/RAA, 272 CCR/CCA, 274 ASR/ASA, 280 DWR/DWA, 282 DPR/DPA. Request và Answer chung command code, phân biệt bằng cờ R.
Phần A: diameter.h (chỉ khai báo).
- Include guard
DIAMETER_H(và nói được vì sao nếu thiếu thì lỗi). - Hằng
DIAMETER_PORT 3868,DIA_HDR_LEN 20u. enumcờ:DIA_F_REQUEST = 1<<7,DIA_F_PROXYABLE = 1<<6,DIA_F_ERROR = 1<<5,DIA_F_RETRANS = 1<<4.enummã trả về:DIA_OK=0và các lỗi âm:DIA_E_SHORT(-1)buffer < 20;DIA_E_VERSION(-2);DIA_E_LENGTH(-3)length < 20 hoặc > buffer;DIA_E_ALIGN(-4)length không chia hết 4;DIA_E_UNKNOWN_CMD(-5);DIA_E_HANDLER(-6).dia_header_t(version, flags, length, cmd_code, app_id, hop_by_hop, end_to_end),dia_stats_t(handled, unknown, malformed, requests, answers).typedefkiểu con trỏ hàmdia_handler_fn:int (*)(const dia_header_t *h, const uint8_t *body, size_t body_len, void *ctx).extern int dia_log_level;và hàm biến tham sốdia_log(int level, const char *fmt, ...)có__attribute__((format(printf,2,3))).- Macro
DIA_DBG(...): gọidia_log(2, ...)khi build với-DDEBUG, ngược lại thành((void)0). - Nguyên mẫu:
dia_parse_header,dia_build_header,dia_flags_str,dia_dispatch.
Phần B: diameter.c (định nghĩa).
dia_parse_header: kiểm tra theo thứ tự SHORT, VERSION, LENGTH, ALIGN. Mọi field đọc bằng shift (big-endian), không ép kiểu con trỏ sang struct.dia_build_header: ghi đúng 20 byte; trảDIA_E_LENGTHnếu length hoặc cmd_code vượt 24 bit.dia_flags_str(flags, out[5]): trả chuỗi 4 ký tựRPET(chữ nếu cờ bật,-nếu tắt). Ví dụ0xC0thànhRP--,0x00thành----.- 6 handler với cùng chữ ký
dia_handler_fn, in tên command (CER hay CEA tùy cờ R). Dùng một macro nhiều dòng (dấu\) để sinh 6 hàm thay vì chép tay. - Bảng dispatch
static const struct { uint32_t code; dia_handler_fn fn; } table[]và hàmdia_dispatch(buf, len, stats):- parse header; lỗi thì
stats->malformed++, in mã lỗi, trả mã lỗi; - in một dòng
cmd=... flags=... len=...; - đếm
requests/answerstheo cờ R; - tra bảng bằng vòng lặp (dùng macro
ARRAY_LEN), gọi handler qua con trỏ hàm, thành công thìhandled++; - không thấy thì
unknown++, in thông báo, trảDIA_E_UNKNOWN_CMD.
- parse header; lỗi thì
- Các hàm/macro phụ chỉ dùng nội bộ phải là
static(internal linkage). Định nghĩa biếndia_log_levelở đây.
Phần C: main.c.
- Hàm
make_pkt(out, cmd, flags, body_len)dựng message hợp lệ bằngdia_build_header. - Hàm
load_file(path, buf, cap, &n)dùngfopen/fread; nếu lỗi instrerror(errno)kèm mãerrno(lưuerrnongay lập tức vào biến). - Xử lý luồng nhiều message nối liền (
process_stream): lặpdia_parse_header,dia_dispatch(buf+off, h.length),off += h.length; dừng nếu parse lỗi. Vòng lặp kiểm travolatile sig_atomic_t g_stop, được set bởi handlerSIGINT. - Self-test chạy 10 message: 5 command hợp lệ, 1 command lạ (999), 4 gói hỏng (version=2; length=21; length=256 > buffer; cắt còn 10 byte).
./app file.binxử lý file;./app khong_ton_tai.binphải in lỗi từstrerrorvà thoát mã 1.
Phần D: Makefile.
- Biến
CC,CFLAGS(-std=c17 -Wall -Wextra -Wpedantic -g -O0),SRCS,OBJS := $(SRCS:.c=.o),TARGET := app. - Target:
all(mặc định),debug(thêm-DDEBUG),asan(thêm-fsanitize=address,undefinedcả khi compile lẫn link),clean, khai báo.PHONY. - Quy tắc mẫu
%.o: %.c diameter.hđể sửa header thì mọi.otự build lại. - Dòng lệnh trong Makefile phải bắt đầu bằng ký tự TAB.
Ràng buộc: build sạch bằng make; make asan rồi chạy không có lỗi ASan/UBSan; .c không được #include một .c khác; không dùng biến toàn cục nào ngoài dia_log_level và g_stop.
Kết quả kỳ vọng: ./app in dòng cuối SELFTEST OK với thống kê handled=5 unknown=1 malformed=4 requests=5 answers=1.
Tiêu chí tự chấm:
make,make debug,make asan,make cleanđều chạy; sửadiameter.hrồimakethì build lại cả hai.o.make debugin thêm các dòng[DBG];makethường thì không.- Tạo được file
stream.bingồm 3 message nối liền và./app stream.binxử lý đúng 3 message. - Trả lời được: vì sao
staticgiúp giảm bề mặt tấn công? Vì sao bảng dispatch nên làconst? Vì saofmtcủadia_logkhông bao giờ được lấy từ dữ liệu gói tin?
Mở rộng (tùy chọn): thêm một command của riêng bạn vào bảng (chỉ thêm 1 dòng + 1 handler); chuyển sang CMake (add_executable(app main.c diameter.c)); chạy valgrind --leak-check=full ./app và gdb ./app (break dia_dispatch, x/20xb buf, bt).
Lời giải
diameter.h
/*
* diameter.h — GIAO DIỆN CÔNG KHAI của module Diameter (chỉ KHAI BÁO).
* Quy tắc: .h chứa khai báo (kiểu, hằng, nguyên mẫu hàm); .c chứa định nghĩa.
*/
#ifndef DIAMETER_H /* INCLUDE GUARD: nếu file bị #include 2 lần thì lần 2 bị bỏ qua, */
#define DIAMETER_H /* nếu không sẽ lỗi "định nghĩa lặp" (redefinition). */
#include <stddef.h>
#include <stdint.h>
#define DIAMETER_PORT 3868
#define DIA_HDR_LEN 20u /* header Diameter luôn 20 byte */
/* Cờ Command Flags (1 byte sau Version+Length): mỗi cờ là một BIT. */
enum {
DIA_F_REQUEST = 1 << 7, /* 0x80 R: Request (không đặt = Answer) */
DIA_F_PROXYABLE = 1 << 6, /* 0x40 P */
DIA_F_ERROR = 1 << 5, /* 0x20 E */
DIA_F_RETRANS = 1 << 4, /* 0x10 T */
};
/* Mã trả về: 0 = ok, âm = lỗi. */
enum {
DIA_OK = 0,
DIA_E_SHORT = -1, /* buffer < 20 byte */
DIA_E_VERSION = -2, /* version != 1 */
DIA_E_LENGTH = -3, /* length < 20 hoặc length > buffer */
DIA_E_ALIGN = -4, /* length không chia hết cho 4 */
DIA_E_UNKNOWN_CMD = -5, /* command code không có trong bảng */
DIA_E_HANDLER = -6, /* handler báo lỗi */
};
typedef struct {
uint8_t version;
uint8_t flags;
uint32_t length; /* 24 bit trên wire, tính cả header */
uint32_t cmd_code; /* 24 bit trên wire */
uint32_t app_id;
uint32_t hop_by_hop;
uint32_t end_to_end;
} dia_header_t;
typedef struct {
unsigned handled, unknown, malformed, requests, answers;
} dia_stats_t;
/* Kiểu con trỏ hàm cho handler: nhận header đã parse, phần body, và ctx tùy ý. */
typedef int (*dia_handler_fn)(const dia_header_t *h, const uint8_t *body,
size_t body_len, void *ctx);
/* extern = "biến này ĐỊNH NGHĨA ở file .c khác" (ở đây: diameter.c) */
extern int dia_log_level;
/* attribute format: để compiler kiểm tra chuỗi format như printf và cảnh báo
* khi dùng sai (cũng giúp phát hiện lỗi format-string). */
void dia_log(int level, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
/* DIA_DBG: chỉ có tác dụng khi build với -DDEBUG; ngược lại biến mất hoàn toàn. */
#ifdef DEBUG
#define DIA_DBG(...) dia_log(2, __VA_ARGS__)
#else
#define DIA_DBG(...) ((void)0)
#endif
int dia_parse_header(const uint8_t *buf, size_t len, dia_header_t *h);
int dia_build_header(uint8_t out[DIA_HDR_LEN], const dia_header_t *h);
const char *dia_flags_str(uint8_t flags, char out[5]);
int dia_dispatch(const uint8_t *buf, size_t len, dia_stats_t *stats);
#endif /* DIAMETER_H */
diameter.c
/*
* diameter.c — ĐỊNH NGHĨA của module Diameter.
*/
#include "diameter.h" /* "ngoặc kép" = header của dự án; <ngoặc nhọn> = header hệ thống */
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
/* Định nghĩa biến toàn cục đã khai báo extern trong diameter.h. */
int dia_log_level = 1;
/* Hàm biến tham số (variadic) như printf: va_start -> vprintf -> va_end.
* [AN NINH] fmt LUÔN là chuỗi cố định do lập trình viên viết, không bao giờ là
* dữ liệu từ gói tin (tránh lỗ hổng format-string). */
void dia_log(int level, const char *fmt, ...) {
if (level > dia_log_level) return;
va_list ap;
va_start(ap, fmt);
vprintf(fmt, ap);
va_end(ap);
}
/* static ở phạm vi file = internal linkage: các hàm/biến này CHỈ thấy trong
* diameter.c, file khác không gọi được và không thể đụng tên. Thu nhỏ bề mặt
* tấn công + tránh xung đột tên khi link. */
#define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0]))
static uint32_t rd24(const uint8_t *p) { return ((uint32_t)p[0] << 16) | ((uint32_t)p[1] << 8) | p[2]; }
static uint32_t rd32(const uint8_t *p) {
return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3];
}
static void wr24(uint8_t *p, uint32_t v) { p[0] = (uint8_t)(v >> 16); p[1] = (uint8_t)(v >> 8); p[2] = (uint8_t)v; }
static void wr32(uint8_t *p, uint32_t v) {
p[0] = (uint8_t)(v >> 24); p[1] = (uint8_t)(v >> 16); p[2] = (uint8_t)(v >> 8); p[3] = (uint8_t)v;
}
/* Header Diameter (20 byte, big-endian):
* 0 : version (=1)
* 1..3 : message length (24 bit)
* 4 : command flags (R P E T ....)
* 5..7 : command code (24 bit)
* 8..11 : application-id
* 12..15 : hop-by-hop id
* 16..19 : end-to-end id
*/
int dia_parse_header(const uint8_t *b, size_t len, dia_header_t *h) {
if (len < DIA_HDR_LEN) return DIA_E_SHORT; /* đủ 20 byte mới được đọc */
h->version = b[0];
h->length = rd24(b + 1);
h->flags = b[4];
h->cmd_code = rd24(b + 5);
h->app_id = rd32(b + 8);
h->hop_by_hop = rd32(b + 12);
h->end_to_end = rd32(b + 16);
if (h->version != 1) return DIA_E_VERSION;
if (h->length < DIA_HDR_LEN || h->length > len) return DIA_E_LENGTH; /* length do kẻ gửi quyết định */
if (h->length & 3u) return DIA_E_ALIGN; /* Diameter luôn căn 4 byte */
return DIA_OK;
}
int dia_build_header(uint8_t out[DIA_HDR_LEN], const dia_header_t *h) {
if (h->length > 0xFFFFFFu || h->cmd_code > 0xFFFFFFu) return DIA_E_LENGTH; /* field 24 bit */
out[0] = 1; /* version luôn 1 */
wr24(out + 1, h->length);
out[4] = h->flags;
wr24(out + 5, h->cmd_code);
wr32(out + 8, h->app_id);
wr32(out + 12, h->hop_by_hop);
wr32(out + 16, h->end_to_end);
return DIA_OK;
}
/* Cờ -> chuỗi "RPET": chữ in nếu bật, '-' nếu tắt. Kiểm tra cờ bằng AND với mask. */
const char *dia_flags_str(uint8_t f, char out[5]) {
out[0] = (f & DIA_F_REQUEST) ? 'R' : '-';
out[1] = (f & DIA_F_PROXYABLE) ? 'P' : '-';
out[2] = (f & DIA_F_ERROR) ? 'E' : '-';
out[3] = (f & DIA_F_RETRANS) ? 'T' : '-';
out[4] = '\0';
return out;
}
/* ---------------------------------------------------------------------------
* HANDLER: mỗi command một hàm cùng chữ ký dia_handler_fn.
* Macro sinh hàm để khỏi lặp lại 6 lần (dấu \ nối dòng trong macro).
* Cùng command code cho cả Request lẫn Answer, phân biệt bằng cờ R.
* ------------------------------------------------------------------------- */
#define DEFINE_HANDLER(fn, REQ, ANS) \
static int fn(const dia_header_t *h, const uint8_t *body, size_t n, void *ctx) { \
(void)body; (void)n; (void)ctx; \
dia_log(1, " -> %s\n", (h->flags & DIA_F_REQUEST) ? (REQ) : (ANS)); \
return 0; \
}
DEFINE_HANDLER(handle_cer, "CER (Capabilities-Exchange-Request)", "CEA (Capabilities-Exchange-Answer)")
DEFINE_HANDLER(handle_rar, "RAR (Re-Auth-Request)", "RAA (Re-Auth-Answer)")
DEFINE_HANDLER(handle_ccr, "CCR (Credit-Control-Request)", "CCA (Credit-Control-Answer)")
DEFINE_HANDLER(handle_asr, "ASR (Abort-Session-Request)", "ASA (Abort-Session-Answer)")
DEFINE_HANDLER(handle_dwr, "DWR (Device-Watchdog-Request)", "DWA (Device-Watchdog-Answer)")
DEFINE_HANDLER(handle_dpr, "DPR (Disconnect-Peer-Request)", "DPA (Disconnect-Peer-Answer)")
/* BẢNG DISPATCH: thay switch khổng lồ bằng mảng {code, hàm}. Thêm command mới
* chỉ cần thêm một dòng. 'const' + 'static': bảng nằm ở vùng nhớ chỉ-đọc và
* không bị ghi đè lúc chạy.
* [AN NINH] Bảng con trỏ hàm là mục tiêu khai thác (ghi đè con trỏ = chiếm
* control flow); const/RELRO là lớp phòng thủ. */
static const struct {
uint32_t code;
dia_handler_fn fn;
} table[] = {
{ 257, handle_cer },
{ 258, handle_rar },
{ 272, handle_ccr },
{ 274, handle_asr },
{ 280, handle_dwr },
{ 282, handle_dpr },
};
int dia_dispatch(const uint8_t *buf, size_t len, dia_stats_t *st) {
dia_header_t h;
int rc = dia_parse_header(buf, len, &h);
if (rc != DIA_OK) { /* parse lỗi: đếm, KHÔNG đi tiếp */
st->malformed++;
dia_log(1, " packet hong (ma loi %d)\n", rc);
return rc;
}
char fl[5];
dia_log(1, " cmd=%u flags=%s len=%u\n",
(unsigned)h.cmd_code, dia_flags_str(h.flags, fl), (unsigned)h.length);
DIA_DBG(" [DBG] app_id=%u hop_by_hop=0x%08x end_to_end=0x%08x\n",
(unsigned)h.app_id, (unsigned)h.hop_by_hop, (unsigned)h.end_to_end);
if (h.flags & DIA_F_REQUEST) st->requests++; else st->answers++;
for (size_t i = 0; i < ARRAY_LEN(table); i++) {
if (table[i].code == h.cmd_code) {
int hr = table[i].fn(&h, buf + DIA_HDR_LEN, h.length - DIA_HDR_LEN, st); /* GỌI qua con trỏ hàm */
if (hr != 0) return DIA_E_HANDLER;
st->handled++;
return DIA_OK;
}
}
st->unknown++; /* command lạ: phải xử lý rõ ràng, không bỏ qua */
dia_log(1, " -> UNKNOWN command %u (tu choi)\n", (unsigned)h.cmd_code);
return DIA_E_UNKNOWN_CMD;
}
main.c
/*
* main.c — chương trình thử nghiệm module Diameter.
*
* Cách dùng:
* ./app chạy bộ test dựng sẵn (self-test)
* ./app file.bin đọc file chứa các message Diameter nối liền nhau và xử lý
*/
#include "diameter.h"
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* volatile sig_atomic_t: biến được signal handler sửa; volatile cấm compiler
* "tối ưu bỏ" việc đọc lại nó trong vòng lặp, sig_atomic_t là kiểu ghi nguyên
* tử an toàn trong handler. */
static volatile sig_atomic_t g_stop = 0;
static void on_sigint(int sig) { (void)sig; g_stop = 1; }
/* Đọc cả file vào buf. Lỗi hệ thống -> strerror(errno).
* [BẪY] errno có thể bị ghi đè bởi lệnh tiếp theo => lưu ngay sau khi lỗi. */
static int load_file(const char *path, uint8_t *buf, size_t cap, size_t *n) {
FILE *f = fopen(path, "rb");
if (!f) {
int e = errno;
fprintf(stderr, "Khong mo duoc '%s': %s (errno=%d)\n", path, strerror(e), e);
return -1;
}
*n = fread(buf, 1, cap, f);
fclose(f);
return 0;
}
/* Tạo một message hợp lệ: header + body toàn byte 0xEE. Trả về tổng độ dài. */
static size_t make_pkt(uint8_t *out, uint32_t cmd, uint8_t flags, uint32_t body_len) {
dia_header_t h = {
.version = 1, .flags = flags, .length = DIA_HDR_LEN + body_len, .cmd_code = cmd,
.app_id = 0, .hop_by_hop = 0x1000u + cmd, .end_to_end = 0x2000u + cmd,
};
dia_build_header(out, &h);
memset(out + DIA_HDR_LEN, 0xEE, body_len);
return h.length;
}
static void print_stats(const dia_stats_t *s) {
printf("handled=%u unknown=%u malformed=%u requests=%u answers=%u\n",
s->handled, s->unknown, s->malformed, s->requests, s->answers);
}
/* Xử lý một luồng gồm nhiều message nối liền nhau. */
static void process_stream(const uint8_t *buf, size_t n, dia_stats_t *st) {
size_t off = 0;
while (off < n && !g_stop) { /* g_stop có thể bị đổi bởi Ctrl-C */
dia_header_t h;
if (dia_parse_header(buf + off, n - off, &h) != DIA_OK) {
dia_dispatch(buf + off, n - off, st); /* để dispatch đếm malformed + in lỗi */
break; /* mất đồng bộ khung: dừng */
}
dia_dispatch(buf + off, h.length, st);
off += h.length; /* nhảy đúng length byte */
}
}
#define RUN(title, buf, len) do { printf("[%s]\n", title); dia_dispatch((buf), (len), &st); } while (0)
int main(int argc, char **argv) {
signal(SIGINT, on_sigint);
#ifdef DEBUG
dia_log_level = 2; /* bật log mức debug khi build -DDEBUG */
#endif
dia_stats_t st = { 0 };
if (argc > 1) { /* chế độ file */
static uint8_t big[1 << 16];
size_t n = 0;
if (load_file(argv[1], big, sizeof big, &n) != 0) return 1;
printf("Doc %zu byte tu %s\n", n, argv[1]);
process_stream(big, n, &st);
print_stats(&st);
return 0;
}
uint8_t p[64], q[64];
size_t len;
len = make_pkt(p, 257, DIA_F_REQUEST, 8); RUN("1 CER request", p, len);
len = make_pkt(p, 257, 0, 4); RUN("2 CEA answer", p, len);
len = make_pkt(p, 280, DIA_F_REQUEST, 0); RUN("3 DWR request", p, len);
len = make_pkt(p, 282, DIA_F_REQUEST | DIA_F_PROXYABLE, 0); RUN("4 DPR (R+P)", p, len);
len = make_pkt(p, 272, DIA_F_REQUEST | DIA_F_PROXYABLE, 16); RUN("5 CCR (R+P)", p, len);
len = make_pkt(p, 999, DIA_F_REQUEST, 4); RUN("6 command la", p, len);
len = make_pkt(q, 280, DIA_F_REQUEST, 0);
memcpy(p, q, len); p[0] = 2; RUN("7 version = 2", p, len);
memcpy(p, q, len); p[1] = 0; p[2] = 0; p[3] = 21; RUN("8 length=21 (khong chia het 4)", p, len);
memcpy(p, q, len); p[1] = 0; p[2] = 1; p[3] = 0; RUN("9 length=256 > buffer", p, len);
memcpy(p, q, len); RUN("10 cat con 10 byte", p, 10);
printf("\n");
print_stats(&st);
int ok = st.handled == 5 && st.unknown == 1 && st.malformed == 4 &&
st.requests == 5 && st.answers == 1;
printf("SELFTEST %s\n", ok ? "OK" : "FAIL");
return ok ? 0 : 1;
}
Makefile
Lưu ý: dòng lệnh dưới mỗi target phải thụt bằng ký tự TAB, không phải dấu cách. Khi copy từ trang web, hãy kiểm tra lại các dòng này.
CC := gcc
CFLAGS := -std=c17 -Wall -Wextra -Wpedantic -g -O0
LDFLAGS :=
SRCS := main.c diameter.c
# đổi đuôi .c -> .o
OBJS := $(SRCS:.c=.o)
TARGET := app
all: $(TARGET)
# $@ = tên target (app)
$(TARGET): $(OBJS)
$(CC) $(OBJS) -o $@ $(LDFLAGS)
# Quy tắc mẫu: mỗi .o phụ thuộc .c tương ứng VÀ diameter.h.
# Sửa header -> mọi .o tự build lại (nếu thiếu dòng này sẽ link với object cũ).
# $< = file phụ thuộc đầu tiên (.c)
%.o: %.c diameter.h
$(CC) $(CFLAGS) -c $< -o $@
# make debug: bật macro DEBUG (log mức debug)
debug: CFLAGS += -DDEBUG
debug: clean $(TARGET)
# make asan: bật AddressSanitizer + UBSan (săn lỗi bộ nhớ)
asan: CFLAGS += -fsanitize=address,undefined
asan: LDFLAGS += -fsanitize=address,undefined
asan: clean $(TARGET)
clean:
rm -f $(OBJS) $(TARGET)
# các target này không phải tên file thật
.PHONY: all debug asan clean
Build và chạy
make # build thường
./app # self-test, dòng cuối: SELFTEST OK
make debug # thêm log [DBG]
make asan # bật ASan + UBSan
make clean
Output của ./app (build thường):
[1 CER request]
cmd=257 flags=R--- len=28
-> CER (Capabilities-Exchange-Request)
[2 CEA answer]
cmd=257 flags=---- len=24
-> CEA (Capabilities-Exchange-Answer)
[3 DWR request]
cmd=280 flags=R--- len=20
-> DWR (Device-Watchdog-Request)
[4 DPR (R+P)]
cmd=282 flags=RP-- len=20
-> DPR (Disconnect-Peer-Request)
[5 CCR (R+P)]
cmd=272 flags=RP-- len=36
-> CCR (Credit-Control-Request)
[6 command la]
cmd=999 flags=R--- len=24
-> UNKNOWN command 999 (tu choi)
[7 version = 2]
packet hong (ma loi -2)
[8 length=21 (khong chia het 4)]
packet hong (ma loi -3)
[9 length=256 > buffer]
packet hong (ma loi -3)
[10 cat con 10 byte]
packet hong (ma loi -1)
handled=5 unknown=1 malformed=4 requests=5 answers=1
SELFTEST OK
Tổng kết
| Bài | Kỹ năng chính | Dòng kiểm tra cuối |
|---|---|---|
| 1 | Nhìn struct nói được sizeof/offset/padding; khóa layout bằng _Static_assert; map gói tin bằng packed hoặc parse thủ công | KET QUA: 7/7 |
| 2 | Endianness, shift+mask thay bitfield, union, mẹo bit | KET QUA: 16/16 |
| 3 | Parser IPv4/UDP kiểm tra biên đúng thứ tự, internet checksum, thử gói hỏng | KET QUA: 21/21 |
| 4 | Flexible array member, tagged union, cây AVP có giới hạn độ sâu, không rò rỉ bộ nhớ | KET QUA: 18/18 |
| 5 | Header/source, con trỏ hàm, Makefile, sanitizer, errno/volatile/variadic | SELFTEST OK |
Gợi ý: sau khi tự làm xong, chạy lại từng bài với -fsanitize=address,undefined, rồi thử cố tình phá (bỏ một lần kiểm tra biên, quên một free) để xem sanitizer báo gì.