Blog05 — C++ Cốt Lõi
Bố cục bài viết
Trang này
Ngày tạo
05/08/2026
Cập nhật
Lượt xem
3
Đọc
11m
05 — C++ Cốt Lõi
Mục tiêu: Chuyển từ C sang C++ một cách có chọn lọc. Nhiều telecom stack hiện đại (một phần Wireshark, srsRAN, một số 5GC, công cụ phân tích) dùng C++. C++ cho bạn RAII (tự quản lý tài nguyên — diệt tận gốc memory leak/UAF) và STL (container/string an toàn hơn mảng C). Ta học C++ như “C có thêm công cụ an toàn”, không sa đà vào mọi ngóc ngách.
1. C++ khác C ở đâu — bức tranh lớn
| Khía cạnh | C | C++ |
|---|---|---|
| Quản lý bộ nhớ | thủ công (malloc/free) | RAII + smart pointer (tự động) |
| Chuỗi | char* + \0 (nguy hiểm) | std::string (biết độ dài, tự cấp phát) |
| Mảng động | malloc/realloc thủ công | std::vector (tự lớn, kiểm tra biên với .at()) |
| Trừu tượng | struct + function pointer | class, kế thừa, template |
| Lỗi | mã trả về | exception (và mã trả về) |
| Kiểu | yếu hơn | mạnh hơn (static_cast, nullptr, enum class) |
Tư duy đúng: C++ không thay thế kiến thức C của bạn — nó xây trên đó. Mọi thứ về con trỏ, bộ nhớ, endianness vẫn nguyên giá trị. C++ chỉ thêm lớp an toàn và biểu đạt.
Biên dịch:
g++ -std=c++20 -Wall -Wextra -g -O0 -fsanitize=address,undefined main.cpp -o app
2. Những nâng cấp nhỏ mà dùng ngay
#include <iostream>
#include <cstdint>
int main() {
uint32_t teid = 0xDEADBEEF;
std::cout << "TEID = " << std::hex << teid << "\n"; // in kiểu C++
int *p = nullptr; // dùng nullptr, KHÔNG dùng NULL/0
auto len = ntohs(x); // auto: suy luận kiểu tự động (gọn, an toàn)
const int N = 16;
uint8_t buf[N]{}; // {} khởi tạo zero toàn bộ
return 0;
}
| Tính năng | Thay cho (C) | Lợi ích |
|---|---|---|
nullptr | NULL | An toàn kiểu, không nhầm với số 0 |
auto | gõ kiểu dài | Gọn, tránh sai kiểu |
std::cout | printf | Type-safe (không sai format specifier) |
enum class | enum | Có phạm vi, không ngầm chuyển int |
static_cast<T> | cast kiểu C (T)x | Tường minh, an toàn, dễ tìm |
enum class Cmd : uint32_t { CER = 257, DWR = 280, DPR = 282 };
Cmd c = Cmd::CER;
// if (c == 257) ... // LỖI biên dịch — phải static_cast, tránh so sánh nhầm
3. Reference (&) — bí danh an toàn hơn con trỏ
Reference là một tên khác cho một biến đã tồn tại — không thể null, không thể “trỏ lại” chỗ khác.
void inc(uint32_t &x) { x++; } // x là reference tới biến gốc
uint32_t v = 5;
inc(v); // v thành 6 — KHÔNG cần &v như con trỏ
// const reference: truyền dữ liệu lớn mà không copy, lại không cho sửa
void process(const std::vector<uint8_t> &packet); // không copy 1500 byte, read-only
| | Con trỏ T* | Reference T& |
|–|————–|—————-|
| Có thể null? | Có | Không |
| Trỏ lại được? | Có | Không |
| Cú pháp dùng | *p, p-> | như biến thường |
| Dùng khi | có thể “không có gì”, số học con trỏ | luôn có giá trị, tham số/return |
Khuyến nghị: trong C++, ưu tiên
const T&cho tham số lớn (read-only) vàT&cho output. Dùng con trỏ khi giá trị có thể vắng mặt (hoặc khi giao tiếp với API C).
4. Class & OOP — đóng gói dữ liệu + hành vi
#include <cstdint>
#include <vector>
class DiameterMessage {
private: // ẩn — chỉ trong class truy cập được
uint32_t command_code_;
std::vector<uint8_t> raw_; // dữ liệu thô, tự quản lý bộ nhớ
public:
// Constructor — gọi khi tạo object
DiameterMessage(uint32_t code, std::vector<uint8_t> raw)
: command_code_(code), raw_(std::move(raw)) {} // member init list
// Method
uint32_t command_code() const { return command_code_; } // const = không sửa object
size_t size() const { return raw_.size(); }
};
DiameterMessage msg(257, {0x01, 0x00, 0x00, 0x14});
std::cout << msg.command_code() << "\n"; // 257
Khái niệm cốt lõi:
private/public/protected: kiểm soát truy cập (encapsulation). Field nội bộ đểprivate.- Constructor / Destructor: hàm chạy khi tạo / hủy object. Destructor là chìa khóa của RAII (mục 6).
constmethod: cam kết “không sửa object” — vừa an toàn vừa tự tài liệu.- Quy ước đặt tên: nhiều codebase dùng hậu tố
_cho member private (raw_).
Kế thừa & hàm ảo (đa hình) — mô hình “một protocol, nhiều message type”
class Message {
public:
virtual ~Message() = default; // destructor ảo — BẮT BUỘC cho base class
virtual void parse(const uint8_t *b, size_t n) = 0; // = 0: thuần ảo (abstract)
virtual const char *name() const = 0;
};
class CER : public Message {
public:
void parse(const uint8_t *b, size_t n) override { /* ... */ }
const char *name() const override { return "CER"; }
};
Liên hệ an ninh: đa hình hiện thực bằng vtable (bảng con trỏ hàm ẩn trong mỗi object). Đây là mục tiêu của vtable hijacking / vtable injection — một kỹ thuật khai thác C++ kinh điển. Hiểu vtable giúp bạn vừa khai thác (lab) vừa hiểu mitigations (vtable verification, CFI). Ta nhắc lại ở
09.
5. STL Containers — thay thế an toàn cho mảng/buffer C
5.1. std::vector<uint8_t> — buffer động, BẠN SẼ DÙNG NHIỀU NHẤT
#include <vector>
std::vector<uint8_t> packet; // buffer rỗng, tự lớn
packet.push_back(0x01); // thêm byte
packet.resize(20); // đổi kích thước
packet.size(); // độ dài (LUÔN biết, khác mảng C!)
packet.data(); // con trỏ uint8_t* tới dữ liệu thô (để gọi API C/socket)
uint8_t b = packet[3]; // truy cập KHÔNG kiểm tra biên (nhanh)
uint8_t b = packet.at(3); // truy cập CÓ kiểm tra biên (ném exception nếu vượt)
Vì sao quan trọng cho security:
std::vectorbiết độ dài của mình và.at()kiểm tra biên. Phần lớn buffer overflow của C biến mất khi dùng vector đúng cách. Khi nhận dữ liệu từ socket,vector<uint8_t>là lựa chọn mặc định..data()+.size()cho bạn cặp (ptr, len) để giao tiếp với hàm C.
5.2. Các container khác & khi nào dùng
#include <string> // std::string — chuỗi an toàn (biết độ dài, tự cấp phát)
#include <vector> // std::vector — mảng động → buffer, danh sách AVP
#include <map> // std::map — cây cân bằng (key→value có thứ tự)
#include <unordered_map> // hash map — tra cứu O(1): code→tên, AVP code→định nghĩa
#include <array> // std::array — mảng cố định an toàn (bọc T[N])
#include <set> // tập hợp duy nhất
std::unordered_map<uint32_t, std::string> avp_names = {
{1, "User-Name"}, {263, "Session-Id"}, {264, "Origin-Host"},
};
auto it = avp_names.find(263);
if (it != avp_names.end()) std::cout << it->second << "\n"; // "Session-Id"
| Container | Cấu trúc | Dùng cho |
|---|---|---|
vector | mảng liền | buffer gói tin, danh sách AVP |
string | mảng char | header SIP, hostname, realm |
unordered_map | hash table | tra cứu nhanh code→tên |
map | cây đỏ-đen | cần thứ tự theo key |
array<T,N> | mảng tĩnh | header kích thước cố định |
set | tập duy nhất | danh sách peer, IP whitelist |
5.3. std::string & std::string_view
#include <string>
std::string realm = "epc.mnc001.mcc234.3gppnetwork.org";
realm.size(); // độ dài
realm.substr(0, 3); // "epc"
realm.find("."); // vị trí dấu chấm đầu
// string_view: "nhìn" vào chuỗi mà không copy (C++17) — hiệu năng cao khi parse
#include <string_view>
void check(std::string_view host); // không copy, chỉ con trỏ+độ dài
Với protocol text như SIP,
std::string/std::string_viewan toàn hơn nhiều so vớichar*C. Với protocol nhị phân, dùngstd::vector<uint8_t>(vì có thể chứa byte\0).
6. RAII — viên ngọc của C++ (diệt memory leak & UAF tận gốc)
RAII (Resource Acquisition Is Initialization): tài nguyên (bộ nhớ, file, socket) được giành lấy trong constructor và trả lại trong destructor. Khi object ra khỏi scope, destructor tự chạy → tài nguyên tự được giải phóng. Không thể quên free/close.
// So sánh:
// --- C: dễ quên/leak ---
void c_way() {
uint8_t *buf = malloc(1024);
if (something) return; // LEAK! quên free
free(buf);
}
// --- C++ RAII: không thể leak ---
void cpp_way() {
std::vector<uint8_t> buf(1024); // cấp phát trong constructor
if (something) return; // destructor của buf TỰ ĐỘNG free khi return
} // tự free ở đây
RAII áp dụng cho mọi tài nguyên:
class FileGuard {
FILE *f_;
public:
explicit FileGuard(const char *path) : f_(fopen(path, "rb")) {}
~FileGuard() { if (f_) fclose(f_); } // tự đóng file
FILE *get() { return f_; }
};
// dùng: { FileGuard fg("x.pcap"); ... } → file tự đóng dù return/exception ở đâu
Ý nghĩa an ninh khổng lồ: Phần lớn lỗ hổng memory (leak, UAF, double-free) trong C đến từ quản lý thủ công. RAII + smart pointer (module
06) xóa bỏ cả lớp lỗ hổng này ở mức ngôn ngữ. Khi audit, code C++ dùng RAII đúng cách an toàn hơn nhiều so với C tương đương — nhưng code C++ lai (trộnnew/deletethủ công) lại nguy hiểm hơn vì phức tạp hơn.
7. Exception — xử lý lỗi (dùng có chừng mực trong code mạng)
#include <stdexcept>
void parse(const std::vector<uint8_t> &buf) {
if (buf.size() < 20)
throw std::runtime_error("packet quá ngắn"); // ném
// ...
}
try {
parse(packet);
} catch (const std::exception &e) { // bắt
std::cerr << "lỗi parse: " << e.what() << "\n";
}
Lưu ý hiệu năng/độ tin cậy: trong đường dẫn xử lý gói tốc độ cao, nhiều dự án tránh exception (chi phí khi ném, khó kiểm soát luồng) và trả mã lỗi hoặc dùng
std::optional/std::expected(C++23). Hãy theo phong cách của codebase bạn làm việc.
8. Ví dụ tổng hợp: lớp parser buffer an toàn (nền cho 08)
#include <cstdint>
#include <vector>
#include <stdexcept>
#include <span> // C++20: "view" an toàn lên vùng nhớ (ptr+len)
// Đọc tuần tự, có kiểm tra biên — dùng cho mọi parser protocol
class ByteReader {
const uint8_t *data_;
size_t size_;
size_t pos_ = 0;
public:
ByteReader(const uint8_t *d, size_t n) : data_(d), size_(n) {}
explicit ByteReader(std::span<const uint8_t> s) : data_(s.data()), size_(s.size()) {}
size_t remaining() const { return size_ - pos_; }
uint8_t u8() {
if (remaining() < 1) throw std::runtime_error("hết dữ liệu");
return data_[pos_++];
}
uint16_t u16be() { // đọc 16-bit big-endian
if (remaining() < 2) throw std::runtime_error("hết dữ liệu");
uint16_t v = (uint16_t(data_[pos_]) << 8) | data_[pos_+1];
pos_ += 2; return v;
}
uint32_t u32be() {
if (remaining() < 4) throw std::runtime_error("hết dữ liệu");
uint32_t v = (uint32_t(data_[pos_]) << 24) | (uint32_t(data_[pos_+1]) << 16) |
(uint32_t(data_[pos_+2]) << 8) | uint32_t(data_[pos_+3]);
pos_ += 4; return v;
}
void skip(size_t n) {
if (remaining() < n) throw std::runtime_error("skip vượt biên");
pos_ += n;
}
};
Lớp
ByteReadernày gói gọn kiểm tra biên vào một chỗ — mọiu8/u16be/u32beđều an toàn. Đây chính là pattern khiến parser C++ khó bị overflow hơn parser C “tay không”. Ta sẽ dùng nó để parse Diameter/GTP ở08.
9. Bài tập / Lab module 05
- Vector thay mảng: viết lại bài “đọc file vào buffer” (module 02) bằng
std::vector<uint8_t>thay chomalloc/realloc. Chú ý: không cònfree, không leak. Chạy ASan để xác nhận sạch. - enum class command: định nghĩa
enum class Cmd : uint32_t, viết hàmstd::string name(Cmd). Thử so sánhCmd::CER == 257và xác nhận nó không biên dịch (an toàn kiểu). - Map tra cứu AVP: dựng
unordered_map<uint32_t,std::string>cho ~10 AVP code phổ biến (User-Name=1, Host-IP-Address=257, Auth-Application-Id=258, Session-Id=263, Origin-Host=264, Origin-Realm=296…). Viết hàm tra cứu trả “Unknown” nếu không thấy. - Class DiameterMessage: mở rộng class ở mục 4 với method
flags(),application_id()đọc từraw_. Dùngconstmethod đúng chỗ. - RAII guard: viết
SocketGuardRAII bọc một file descriptor (int), đóngclose(fd)trong destructor. (Chuẩn bị cho module 07.) - ByteReader: hoàn thiện
ByteReader(mục 8), viết test: feed một buffer hợp lệ và một buffer cụt (thiếu byte), xác nhận buffer cụt ném exception thay vì đọc tràn. So sánh trải nghiệm với việc tự kiểm tra biên thủ công ở C — thấy rõ C++ giảm bug thế nào.
Cột mốc module 05: Bạn dùng
std::vector/std::string/mapthay cho mảng/chuỗi C thô, hiểu và áp dụng RAII để loại bỏ leak/UAF, dựng được class parser an toàn (ByteReader) đóng gói kiểm tra biên. Bạn đã có công cụ C++ đủ để viết tooling an ninh chắc chắn.
➡️ Tiếp theo: 06-Cpp-Nang-Cao.md — template, smart pointer, move semantics, C++ hiện đại.
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ớ