Skip to content
LIVE FEED

Blog01 — C Cơ Bản

Bố cục bài viết

Trang này

Sunex 05/08/2026 · 12 phút đọc

Ngày tạo

05/08/2026

Cập nhật

Lượt xem

8

Đọc

12m

01 — C Cơ Bản

Mục tiêu: Nắm chắc cú pháp, kiểu dữ liệu, toán tử, luồng điều khiển, hàm và I/O. Đây là nền móng — mọi thứ về protocol parsing sau này đều xây trên đây. Chúng ta học nhanh phần “ngôn ngữ chung” và dừng kỹ ở chỗ liên quan đến byte, kiểu số nguyên và overflow (mảnh đất của an ninh).


1. Chương trình C đầu tiên & vòng đời biên dịch

#include <stdio.h>   // khai báo printf

int main(void) {
    printf("Hello, telecom security\n");
    return 0;        // 0 = thành công (quy ước POSIX)
}

Biên dịch & chạy:

gcc -std=c17 -Wall -Wextra -g -O0 hello.c -o hello
./hello

Bốn giai đoạn của trình biên dịch (phải hiểu — sau này debug và đọc CVE cần)

hello.c
  │  (1) Preprocessor  — xử lý #include, #define, #ifdef
  ▼
hello.i  (mã C đã khai triển macro)
  │  (2) Compiler      — dịch sang assembly
  ▼
hello.s  (assembly)
  │  (3) Assembler     — dịch sang mã máy (object)
  ▼
hello.o  (object file, chưa link)
  │  (4) Linker        — nối với thư viện (libc...) → file thực thi
  ▼
hello    (ELF executable)

Xem từng bước:

gcc -E hello.c          # chỉ chạy preprocessor
gcc -S hello.c          # dừng ở assembly (.s)
gcc -c hello.c          # dừng ở object (.o)
gcc hello.o -o hello    # link

Liên hệ an ninh: Khi phân tích một telecom binary đã biên dịch (vd. một probe LI đóng kín), bạn sẽ làm ngược quy trình này: ELF → disassembly → suy luận C. Hiểu chiều xuôi giúp bạn đọc chiều ngược.


2. Kiểu dữ liệu & kích thước — phần cực kỳ quan trọng cho parsing

Trong viễn thông, mỗi field protocol có độ rộng cố định (8/16/24/32 bit). Bạn phải chọn đúng kiểu.

2.1. Kiểu nguyên thủy và độ rộng (KHÔNG đảm bảo cố định!)

KiểuĐộ rộng điển hình trên Linux x86-64Lưu ý
char8 bitCó dấu hay không tùy nền tảng → nguồn bug
short16 bit
int32 bit
long64 bit (Linux) / 32 bit (Windows)Không di động
long long64 bit
float32 bitHiếm dùng trong protocol
double64 bit

Chuẩn C chỉ đảm bảo char ≤ short ≤ int ≤ long ≤ long long và char ≥ 8 bit, short/int ≥ 16 bit, long ≥ 32 bit. Tuyệt đối không giả định kích thước cố định khi parse byte.

2.2. Dùng kiểu độ rộng cố định — <stdint.h> (BẮT BUỘC trong code protocol)

#include <stdint.h>

uint8_t  flags;     // đúng 8 bit không dấu
uint16_t app_id;    // 16 bit — vd. Diameter Application-Id 16-bit? (thực ra 32)
uint32_t teid;      // 32 bit — GTP TEID
int32_t  delta;     // 32 bit có dấu
uint64_t imsi_num;  // 64 bit
KiểuÝ nghĩaDùng cho
uint8_t1 byte không dấubyte thô, flags, octet
uint16_t2 byte không dấuport, length 16-bit
uint32_t4 byte không dấuTEID, Application-Id, Command Code 24-bit (chứa trong 32)
uint64_t8 byte không dấutimestamp, counter lớn
int8_t…int64_tbản có dấugiá trị có dấu
printf("kích thước uint32_t = %zu byte\n", sizeof(uint32_t));  // 4

Quy tắc thép #2: Trong mọi struct mô tả gói tin, chỉ dùng uintN_t/intN_t. Không bao giờ int/long cho field protocol.

2.3. Số có dấu vs không dấu — cạm bẫy an ninh số một

uint8_t len = msg[2];          // độ dài lấy từ gói (kẻ tấn công kiểm soát!)
if (len - 4 > 0) { /* ... */ } // BUG tiềm tàng

Nếu len là uint8_t và bằng 2, biểu thức len - 4 bị integer promotion lên int → -2 (đúng). Nhưng nếu bạn so sánh số không dấu:

uint32_t remaining = buf_len - header_len;   // nếu header_len > buf_len → wrap-around!
// remaining trở thành SỐ KHỔNG LỒ (vd 0xFFFFFFF8) thay vì âm
if (remaining >= avp_len) memcpy(dst, src, avp_len);  // → buffer overflow

Đây là integer underflow → heap overflow, một lớp lỗ hổng kinh điển trong parser Diameter/GTP. Ta sẽ mổ xẻ kỹ ở 09.

// AN TOÀN: kiểm tra trước khi trừ
if (buf_len < header_len) return ERR_TRUNCATED;
uint32_t remaining = buf_len - header_len;   // giờ chắc chắn không underflow

3. Hằng, biến, và const

#define MAX_AVP   1024          // hằng preprocessor (không có kiểu, thay thế văn bản)
const uint16_t DIAMETER_PORT = 3868;  // hằng có kiểu — ưu tiên dùng cái này
enum { HEADER_LEN = 20 };       // hằng nguyên trong enum (idiom phổ biến)

uint32_t teid = 0xDEADBEEF;     // hex literal
uint8_t  mask = 0b10000000;     // binary literal (C23/GNU) — bit cao nhất
Cách khai báo hằngCó kiểu?Phạm viKhuyến nghị
#defineKhôngToàn file sau dòng định nghĩaCho cờ biên dịch, macro
constCóTheo scopeƯu tiên cho giá trị dữ liệu
enumintTheo scopeCho tập hằng nguyên liên quan

4. Toán tử

4.1. Số học & gán

a + b   a - b   a * b   a / b   a % b      // % chỉ cho số nguyên
a += 1; a -= 1; a *= 2; a /= 2; a %= 8;
i++; ++i; i--; --i;                        // hậu tố vs tiền tố

4.2. So sánh & logic

==  !=  <  >  <=  >=
&&  ||  !                                  // logic, có short-circuit

4.3. Bitwise — trái tim của protocol parsing (sẽ đào sâu ở 04)

a & b    // AND   — tách (mask) bit
a | b    // OR    — gộp bit
a ^ b    // XOR   — đảo bit có chọn lọc
~a       // NOT   — đảo toàn bộ bit
a << n   // dịch trái n bit (×2^n)
a >> n   // dịch phải n bit

Ví dụ thực tế — tách 2 trường từ 1 byte (kiểu IP version + IHL):

uint8_t b = 0x45;               // gói IPv4 điển hình
uint8_t version = (b >> 4) & 0x0F;   // 4 bit cao  = 4
uint8_t ihl     =  b       & 0x0F;   // 4 bit thấp = 5

4.4. Thứ tự ưu tiên — đừng đoán, hãy đóng ngoặc

if (b & 0x80 == 0)   // SAI! == ưu tiên hơn & → b & (0x80==0) = b & 0
if ((b & 0x80) == 0) // ĐÚNG

Quy tắc thép #3: Khi trộn bitwise với so sánh, luôn đóng ngoặc. Lỗi này gây bug parser hàng đầu.


5. Luồng điều khiển

5.1. if / else if / else

if (cmd == 257) {
    // CER
} else if (cmd == 280) {
    // DWR
} else {
    // unknown
}

5.2. switch — hợp với mã lệnh protocol

switch (command_code) {
    case 257: handle_cer();  break;   // ĐỪNG QUÊN break!
    case 258: handle_cea();  break;
    case 280: handle_dwr();  break;
    case 282: handle_dpr();  break;
    default:  handle_unknown(); break;
}

Bẫy: thiếu break gây “fall-through” — chạy lan sang case dưới. Đôi khi cố ý, nhưng thường là bug. Trình biên dịch hiện đại cảnh báo nếu bật -Wimplicit-fallthrough.

5.3. Vòng lặp

for (size_t i = 0; i < avp_count; i++) { /* duyệt AVP */ }

size_t off = 0;
while (off + 8 <= buf_len) {        // duyệt buffer an toàn theo offset
    /* parse 1 AVP tại buf[off], rồi off += avp_len */
}

do { /* chạy ít nhất 1 lần */ } while (cond);

break (thoát vòng lặp), continue (sang vòng kế tiếp).

Idiom an ninh: Khi duyệt buffer, điều kiện vòng lặp phải bao hàm kiểm tra bound (off + need <= buf_len) — không bao giờ tin vào length field trong gói rồi mới kiểm tra sau.


6. Hàm

#include <stdint.h>
#include <stddef.h>

// Khai báo (prototype) — thường đặt trong .h
int parse_header(const uint8_t *buf, size_t len, uint32_t *out_teid);

// Định nghĩa
int parse_header(const uint8_t *buf, size_t len, uint32_t *out_teid) {
    if (len < 8) return -1;                 // kiểm tra độ dài trước
    // GTP-U TEID nằm ở byte 4..7, big-endian
    *out_teid = ((uint32_t)buf[4] << 24) |
                ((uint32_t)buf[5] << 16) |
                ((uint32_t)buf[6] <<  8) |
                ((uint32_t)buf[7]);
    return 0;                               // 0 = OK
}

Điểm cần khắc cốt:

  • Tham số truyền theo giá trị (pass-by-value): hàm nhận bản sao. Muốn sửa biến gốc → truyền con trỏ (uint32_t *out_teid). (Chi tiết ở 02.)
  • const trong tham số (const uint8_t *buf): cam kết “tôi chỉ đọc, không sửa buffer”. Vừa an toàn vừa là tài liệu.
  • Quy ước trả về: hàm hệ thống/parser thường trả int (0 = OK, âm = lỗi). Hãy theo quy ước này.
  • void tham số: int main(void) nghĩa là “không tham số”. Khác main() (C cũ, mơ hồ).

Truyền mảng vào hàm — mảng “phân rã” (decay) thành con trỏ

void dump(const uint8_t *data, size_t n);   // KHÔNG nhận được kích thước mảng!
// → phải LUÔN truyền kèm độ dài n. Đây là lý do mọi API C an toàn đều có cặp (ptr, len).

Quy tắc thép #4: Trong C không có “mảng biết độ dài của mình”. Mọi hàm xử lý dữ liệu mạng phải nhận cặp (con trỏ, độ dài) và kiểm tra độ dài trước khi đọc. Quên điều này = lỗ hổng.


7. Phạm vi (scope) & thời gian sống (lifetime)

int g = 10;                 // biến toàn cục — sống suốt chương trình

void f(void) {
    int local = 5;          // biến cục bộ — sống trong f(), nằm trên STACK
    static int counter = 0; // static — khởi tạo 1 lần, GIỮ giá trị giữa các lần gọi
    counter++;
}                           // local biến mất ở đây; counter vẫn còn
LoạiVùng nhớLifetime
Cục bộ (auto)StackTrong khối {}
static cục bộVùng dataToàn chương trình (nhưng phạm vi truy cập = cục bộ)
Toàn cụcVùng dataToàn chương trình
Cấp phát động (malloc)HeapĐến khi free (xem 02)

Bẫy chí mạng (xem kỹ ở 02): trả về con trỏ tới biến cục bộ → dangling pointer, vì biến đã chết khi hàm return.

const char *bad(void) {
    char tmp[8] = "abc";
    return tmp;          // SAI — tmp nằm trên stack, biến mất sau return
}

8. Nhập/Xuất cơ bản

8.1. printf — định dạng (đặc biệt là in hex để soi byte)

printf("%d",   42);        // int thập phân
printf("%u",   42u);       // unsigned
printf("%x",   255);       // hex: ff
printf("%02x", 5);         // hex 2 chữ số, đệm 0: 05  ← dùng in từng byte gói tin
printf("%s",   "hi");      // chuỗi
printf("%p",   (void*)p);  // địa chỉ con trỏ
printf("%zu",  sizeof(x)); // size_t — DÙNG %zu, không %d
printf("%lu",  (unsigned long)v);

Với <inttypes.h> để in uintN_t di động:

#include <inttypes.h>
uint32_t teid = 0xDEADBEEF;
printf("TEID = 0x%08" PRIx32 "\n", teid);   // 0xdeadbeef

8.2. In hex dump một buffer (tiện ích bạn sẽ dùng MÃI MÃI)

#include <stdio.h>
#include <stdint.h>
#include <stddef.h>

void hexdump(const uint8_t *buf, size_t len) {
    for (size_t i = 0; i < len; i++) {
        if (i % 16 == 0) printf("\n%04zx: ", i);
        printf("%02x ", buf[i]);
    }
    printf("\n");
}

8.3. Nhập — scanf và vì sao nên cẩn thận

int n;
scanf("%d", &n);            // đọc 1 số nguyên (chú ý dấu & lấy địa chỉ)

scanf("%s", buf) không giới hạn độ dài → buffer overflow kinh điển. Dùng fgets cho chuỗi:

char line[256];
fgets(line, sizeof(line), stdin);   // an toàn, giới hạn bởi sizeof

9. Lỗi hay gặp của người mới (và vì sao chúng nguy hiểm cho security)

LỗiHệ quả
Quên & trong scanfGhi vào địa chỉ rác → crash/UB
Dùng = thay == trong ifGán nhầm, logic sai (bật -Wparentheses)
Thiếu break trong switchFall-through ngoài ý muốn
So sánh số có dấu/không dấuWrap-around → overflow (xem mục 2.3)
Không kiểm tra độ dài trước khi đọc bufferBuffer over-read (vd. Heartbleed)
Trả con trỏ tới biến cục bộDangling pointer

10. Bài tập / Lab module 01

  1. Hexdump CLI: viết chương trình đọc một file nhị phân (vd. một .pcap hoặc payload raw) và in hexdump 16 byte/dòng (giống xxd). Gợi ý: fopen, fread.
  2. Bóc trường từ byte: cho một byte 0x45, in ra version và IHL (mục 4.3). Mở rộng: cho 2 byte big-endian, ghép thành uint16_t length và in.
  3. Bảng mã lệnh Diameter: viết hàm const char* cmd_name(uint16_t code) dùng switch, trả về tên (“CER”,“CEA”,“DWR”,“DWA”,“DPR”,“DPA”) cho các mã 257/258/280/282. Mã lạ trả “UNKNOWN”.
  4. Chứng minh integer underflow: viết đoạn code cho thấy uint32_t a = 4; uint32_t b = 10; printf("%u", a-b); in ra số khổng lồ. Giải thích bằng lời tại sao — và nó gây lỗ hổng thế nào nếu kết quả dùng làm độ dài memcpy.
  5. Tự build với golden flags: biên dịch lại tất cả bài trên với -Wall -Wextra -fsanitize=address,undefined và dọn sạch mọi cảnh báo.

Cột mốc module 01: Bạn in được hex một buffer, bóc tách bit/byte, và giải thích được tại sao thiếu kiểm tra độ dài và lẫn lộn số có/không dấu là lỗ hổng. Đây đã là nền tảng tư duy security.

➡️ Tiếp theo: 02-C-Con-Tro-Va-Bo-Nho.md — con trỏ và bộ nhớ, phần quan trọng nhất của toàn bộ lộ trình.

Lộ trình học tập

Lập Trình C/C++

  1. 1 01 — C Cơ Bản
  2. 2 02 — Con Trỏ & Bộ Nhớ
  3. 3 03 — Struct, Union, Bitfield & Bố Cục Bộ Nhớ
  4. 4 04 — C Nâng Cao
  5. 5 05 — C++ Cốt Lõi
  6. 6 06 — C++ Hiện Đại
  7. 7 07 — Lập Trình Hệ Thống & Mạng
  8. 8 08 — Phân Tích Giao Thức Telecom Bằng C/C++
  9. 9 09 — An Ninh Bộ Nhớ & Khai Thác Cơ Bản
  10. 10 11 — Lập Trình Nhúng & Real-time (SDR-first)
  11. 11 Bài tập & Pet Project — Lộ trình C/C++ chuyên sâu
  12. 12 Tổng hợp các dạng printf để in giá trị trong C
  13. 13 Bitwise & Mask, Octet, Endian: Hướng dẫn parse protocol byte cho người mới
  14. 14 Tổng hợp: Con trỏ, kích thước kiểu dữ liệu và byte trong bộ nhớ
Contact
Facebook
TOP