Blog01 — C Cơ Bản
Bố cục bài viết
Trang này
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-64 | Lưu ý |
|---|---|---|
char | 8 bit | Có dấu hay không tùy nền tảng → nguồn bug |
short | 16 bit | |
int | 32 bit | |
long | 64 bit (Linux) / 32 bit (Windows) | Không di động |
long long | 64 bit | |
float | 32 bit | Hiếm dùng trong protocol |
double | 64 bit |
Chuẩn C chỉ đảm bảo
char ≤ short ≤ int ≤ long ≤ long longvà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ĩa | Dùng cho |
|---|---|---|
uint8_t | 1 byte không dấu | byte thô, flags, octet |
uint16_t | 2 byte không dấu | port, length 16-bit |
uint32_t | 4 byte không dấu | TEID, Application-Id, Command Code 24-bit (chứa trong 32) |
uint64_t | 8 byte không dấu | timestamp, counter lớn |
int8_t…int64_t | bản có dấu | giá 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/longcho 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ằng | Có kiểu? | Phạm vi | Khuyến nghị |
|---|---|---|---|
#define | Không | Toàn file sau dòng định nghĩa | Cho cờ biên dịch, macro |
const | Có | Theo scope | Ưu tiên cho giá trị dữ liệu |
enum | int | Theo scope | Cho 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
breakgâ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.) consttrong 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. voidtham số:int main(void)nghĩa là “không tham số”. Khácmain()(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ại | Vùng nhớ | Lifetime |
|---|---|---|
| Cục bộ (auto) | Stack | Trong khối {} |
static cục bộ | Vùng data | Toàn chương trình (nhưng phạm vi truy cập = cục bộ) |
| Toàn cục | Vùng data | Toà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ùngfgetscho 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ỗi | Hệ quả |
|---|---|
Quên & trong scanf | Ghi vào địa chỉ rác → crash/UB |
Dùng = thay == trong if | Gán nhầm, logic sai (bật -Wparentheses) |
Thiếu break trong switch | Fall-through ngoài ý muốn |
| So sánh số có dấu/không dấu | Wrap-around → overflow (xem mục 2.3) |
| Không kiểm tra độ dài trước khi đọc buffer | Buffer over-read (vd. Heartbleed) |
| Trả con trỏ tới biến cục bộ | Dangling pointer |
10. Bài tập / Lab module 01
- Hexdump CLI: viết chương trình đọc một file nhị phân (vd. một
.pcaphoặc payload raw) và in hexdump 16 byte/dòng (giốngxxd). Gợi ý:fopen,fread. - 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ànhuint16_tlength và in. - Bảng mã lệnh Diameter: viết hàm
const char* cmd_name(uint16_t code)dùngswitch, trả về tên (“CER”,“CEA”,“DWR”,“DWA”,“DPR”,“DPA”) cho các mã 257/258/280/282. Mã lạ trả “UNKNOWN”. - 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àimemcpy. - 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,undefinedvà 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 01 — C Cơ Bản
- 2 02 — Con Trỏ & Bộ Nhớ
- 3 03 — Struct, Union, Bitfield & Bố Cục Bộ Nhớ
- 4 04 — C Nâng Cao
- 5 05 — C++ Cốt Lõi
- 6 06 — C++ Hiện Đại
- 7 07 — Lập Trình Hệ Thống & Mạng
- 8 08 — Phân Tích Giao Thức Telecom Bằng C/C++
- 9 09 — An Ninh Bộ Nhớ & Khai Thác Cơ Bản
- 10 11 — Lập Trình Nhúng & Real-time (SDR-first)
- 11 Bài tập & Pet Project — Lộ trình C/C++ chuyên sâu
- 12 Tổng hợp các dạng printf để in giá trị trong C
- 13 Bitwise & Mask, Octet, Endian: Hướng dẫn parse protocol byte cho người mới
- 14 Tổng hợp: Con trỏ, kích thước kiểu dữ liệu và byte trong bộ nhớ