Skip to content
LIVE FEED

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ộ 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:

  1. Mỗi field phải bắt đầu ở offset chia hết cho alignment của nó.
  2. 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:

  1. struct packed + memcpy + ntohs/ntohl (không ép kiểu con trỏ buffer sang struct*, vì dễ vi phạm alignment/strict aliasing);
  2. 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ùng union { uint16_t s; uint8_t b[2]; }, gán s = 0x0102 rồi xem b[0].
  • A2. Viết is_little_endian_memcpy(): làm cùng việc nhưng không dùng union, chỉ dùng memcpy sang mảng byte.
  • A3. In 4 byte của 0x12345678 như đ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_be32 rồi read_be32 phải trả lại đúng giá trị (round-trip).
  • B2. Byte do write_be32 sinh ra phải giống hệt 4 byte của htonl(v) (so bằng memcmp).
  • B3. read_be16 trên {0x00,0x3C} = 60; read_be24 trê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àm bin_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, in as_u32 thô, htonl(as_u32) và read_be32(octets). Giải thích vì sao as_u32 thô là 0x0101a8c0.
  • E2. Chứng minh read_be32(octets) == 0xC0A80101 và 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ặp x &= x - 1. So với __builtin_popcount cho mọi x trong [0, 65535] và vài số lớn.
  • F3. is_pow2(x), lowest_bit(x) (bằng x & -x). Test: is_pow2 trê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):

  1. cộng từng word 16-bit big-endian vào bộ cộng 32-bit;
  2. nếu len lẻ: byte cuối coi là byte cao của một word (byte thấp = 0);
  3. gập carry: while (sum >> 16) sum = (sum & 0xFFFF) + (sum >> 16);
  4. 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 < 20 hoặc 20 + 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_len vẫn là 12.
  • E3. Cắt cụt: thử mọi len từ 0..31. Kỳ vọng: len 0..19 cho IP_ERR_SHORT (20 lần), len 20..31 cho IP_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
  • 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_length thà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ối KET 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ào parse_ipv4 và đố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):

ByteNội dung
0..3code (uint32, big-endian)
4flags (1 byte)
5..7length (24 bit big-endian) = 8 + độ dài data, không tính padding
8..data
cuốipadding 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;. In sizeof(avp_t) và offsetof(avp_t, data); giải thích vì sao data[] không chiếm chỗ trong sizeof.
  • A3. avp_t *avp_new(code, flags, data, len): cấp phát header + data trong một malloc. Từ chối (trả NULL) nếu len > 0xFFFFFF - 8 (field length chỉ có 24 bit). Không gọi memcpy với con trỏ NULL khi len = 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 < 8
  • AVP_E_LEN (-2): length < 8 hoặc length > n
  • AVP_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 (vlen phải = 4, nếu không AVP_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 (vlen phả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 child khi gặp AVP grouped; depth tối đa MAX_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ếu calloc thấ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_foreach in từng AVP; interpret Result-Code=2001, Origin-Host=“a.b”; MSCC trả AVP_E_TYPE (bỏ qua).
  • F3. tree_parse + tree_print ra đúng cây 2 tầng; tree_free khô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_encode với cap quá 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ải length.
  • 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):

ByteNội dung
0version (luôn = 1)
1..3message length, 24 bit, tính cả header
4command flags: bit7 R(equest), bit6 P(roxiable), bit5 E(rror), bit4 T
5..7command code, 24 bit
8..11application-id
12..15hop-by-hop id
16..19end-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).

  1. Include guard DIAMETER_H (và nói được vì sao nếu thiếu thì lỗi).
  2. Hằng DIAMETER_PORT 3868, DIA_HDR_LEN 20u.
  3. enum cờ: DIA_F_REQUEST = 1<<7, DIA_F_PROXYABLE = 1<<6, DIA_F_ERROR = 1<<5, DIA_F_RETRANS = 1<<4.
  4. enum mã trả về: DIA_OK=0 và 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).
  5. dia_header_t (version, flags, length, cmd_code, app_id, hop_by_hop, end_to_end), dia_stats_t (handled, unknown, malformed, requests, answers).
  6. typedef kiểu con trỏ hàm dia_handler_fn: int (*)(const dia_header_t *h, const uint8_t *body, size_t body_len, void *ctx).
  7. 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))).
  8. Macro DIA_DBG(...): gọi dia_log(2, ...) khi build với -DDEBUG, ngược lại thành ((void)0).
  9. Nguyên mẫu: dia_parse_header, dia_build_header, dia_flags_str, dia_dispatch.

Phần B: diameter.c (định nghĩa).

  1. 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.
  2. dia_build_header: ghi đúng 20 byte; trả DIA_E_LENGTH nếu length hoặc cmd_code vượt 24 bit.
  3. 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ụ 0xC0 thành RP--, 0x00 thành ----.
  4. 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.
  5. Bảng dispatch static const struct { uint32_t code; dia_handler_fn fn; } table[] và hàm dia_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/answers theo 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.
  6. Các hàm/macro phụ chỉ dùng nội bộ phải là static (internal linkage). Định nghĩa biến dia_log_level ở đây.

Phần C: main.c.

  1. Hàm make_pkt(out, cmd, flags, body_len) dựng message hợp lệ bằng dia_build_header.
  2. Hàm load_file(path, buf, cap, &n) dùng fopen/fread; nếu lỗi in strerror(errno) kèm mã errno (lưu errno ngay lập tức vào biến).
  3. Xử lý luồng nhiều message nối liền (process_stream): lặp dia_parse_header, dia_dispatch(buf+off, h.length), off += h.length; dừng nếu parse lỗi. Vòng lặp kiểm tra volatile sig_atomic_t g_stop, được set bởi handler SIGINT.
  4. 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).
  5. ./app file.bin xử lý file; ./app khong_ton_tai.bin phải in lỗi từ strerror và 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,undefined cả 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 .o tự 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ửa diameter.h rồi make thì build lại cả hai .o.
  • make debug in thêm các dòng [DBG]; make thường thì không.
  • Tạo được file stream.bin gồm 3 message nối liền và ./app stream.bin xử lý đúng 3 message.
  • Trả lời được: vì sao static giúp giảm bề mặt tấn công? Vì sao bảng dispatch nên là const? Vì sao fmt của dia_log khô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àiKỹ năng chínhDòng kiểm tra cuối
1Nhì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ôngKET QUA: 7/7
2Endianness, shift+mask thay bitfield, union, mẹo bitKET QUA: 16/16
3Parser IPv4/UDP kiểm tra biên đúng thứ tự, internet checksum, thử gói hỏngKET QUA: 21/21
4Flexible array member, tagged union, cây AVP có giới hạn độ sâu, không rò rỉ bộ nhớKET QUA: 18/18
5Header/source, con trỏ hàm, Makefile, sanitizer, errno/volatile/variadicSELFTEST 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ì.

Contact
Facebook
TOP