BlogBài tập & Pet Project — Lộ trình C/C++ chuyên sâu
Bố cục bài viết
Trang này
Ngày tạo
30/07/2026
Cập nhật
Lượt xem
9
Đọc
11m
Bài tập & Pet Project — Lộ trình C/C++ chuyên sâu
Đi kèm với lộ trình 5 giai đoạn. Mỗi giai đoạn có: bài tập nhỏ (rèn từng kỹ năng riêng lẻ, làm trong 1 buổi) và pet project (gộp nhiều kỹ năng, làm trong vài ngày–1 tuần). Ưu tiên dùng dữ liệu/bối cảnh telecom thật của bạn (GTPv2-C, Diameter, SDR) thay vì bài tập trừu tượng.
GIAI ĐOẠN 0 — Nền tảng C
Bài tập nhỏ (rèn từng kỹ năng)
Con trỏ & bộ nhớ
- Viết hàm
void swap(int *a, int *b)— kiểm tra hiểu đúng pass-by-pointer. - Viết hàm tự cài
strlen,strcpy,strcatbằng con trỏ thuần (không dùng<string.h>) — bắt buộc dùng pointer arithmetic, không dùng chỉ số mảng[]. - Viết một hàm nhận
void *buf, size_t lenvà in ra dạng hex dump (giống output củaxxd) — đây là công cụ bạn sẽ dùng lại nhiều lần khi debug packet buffer. - Cấp phát một mảng động bằng
malloc, cố tình gây leak, chạy Valgrind để thấy report — sau đó fix và chạy lại để thấy report sạch. - Viết một hàm cố tình gây double-free, quan sát crash — hiểu tại sao nó nguy hiểm trước khi học exploit ở Giai đoạn 3.
Struct & bitfield
6. Định nghĩa struct cho GTPv2-C header cố định (8 byte đầu: Version/P/T flags, Message Type, Message Length, TEID) bằng bitfield, sau đó dùng sizeof() kiểm tra padding compiler chèn vào có đúng như bạn dự đoán không.
7. Viết union chứa 2 kiểu diễn giải khác nhau của cùng 4 byte (ví dụ IPv4 address dưới dạng uint32_t và dạng 4×uint8_t) — thử in ra cả 2 cách để thấy union chia sẻ vùng nhớ thế nào.
Preprocessor
8. Viết file header định nghĩa toàn bộ Message Type code của GTPv2-C (#define GTP_ECHO_REQUEST 1, GTP_CREATE_SESSION_REQUEST 32…) kèm header guard đúng chuẩn.
Pet project: Parser TLV cho GTPv2-C / Diameter AVP
- Input: một file capture
.pcaphoặc buffer hex bạn tự tạo, chứa message GTPv2-C thật (bạn có sẵn từ công việc). - Output: chương trình đọc từng byte, tách Message Type, Length, TEID, và duyệt qua các Information Element (IE) dạng TLV, in ra tên IE + giá trị.
- Yêu cầu kỹ thuật: dùng struct + bitfield cho header cố định, dùng con trỏ thuần để duyệt phần IE có độ dài biến đổi, không dùng thư viện parse có sẵn.
- Mở rộng (optional): thêm phần đọc Diameter AVP header (khác cấu trúc TLV ở chỗ AVP Code 4 byte, Flags 1 byte, Length 3 byte) để so sánh 2 kiểu TLV khác nhau.
GIAI ĐOẠN 1 — Compiler / Linker / OS
Bài tập nhỏ
Build pipeline
- Lấy một file
.cđơn giản, chạy lần lượtgcc -E,gcc -S,gcc -c, quan sát output ở mỗi bước để thấy rõ 4 giai đoạn compile. - Viết 2 file
.ccùng định nghĩa một hàm trùng tên (khôngstatic) → build thử, xem linker báo lỗi gì → sửa bằngstatic, build lại thành công.
Memory layout
3. Viết chương trình in ra địa chỉ của: một biến local (stack), một biến global (data/bss), một vùng malloc (heap) — so sánh thứ tự địa chỉ để thấy layout thực tế trên máy bạn.
4. Chạy chương trình đó nhiều lần, quan sát địa chỉ heap/stack thay đổi mỗi lần chạy — đây chính là ASLR đang hoạt động.
Debugging 5. Cố tình viết một hàm đệ quy vô hạn (không có điều kiện dừng) → dùng GDB attach vào lúc nó đang crash vì stack overflow, xem backtrace. 6. Lấy lại parser TLV ở Giai đoạn 0, cố tình đưa input packet bị cắt cụt (truncated) để gây segfault → dùng GDB tìm chính xác dòng code và giá trị con trỏ gây lỗi.
Syscalls & socket
7. Viết chương trình mở một file bằng syscall open() trực tiếp (không dùng fopen), đọc nội dung bằng read(), in ra bằng write() — không dùng <stdio.h> chút nào.
8. Viết chương trình fork() một tiến trình con, tiến trình con in “con”, tiến trình cha wait() rồi in “cha” — quan sát thứ tự output.
Pet project: Raw socket sniffer cho GTP-U
- Dùng
socket(AF_PACKET, SOCK_RAW, ...)để bắt trực tiếp Ethernet frame trên một interface (hoặc đọc từ file.pcapnếu không có quyền raw socket). - Tự parse layer bằng layer: Ethernet header → IP header → UDP header → GTP-U header (port 2152) → in ra TEID và độ dài payload.
- Không dùng libpcap hay bất kỳ thư viện parse có sẵn nào — tự viết struct cho từng layer, dùng kiến thức Giai đoạn 0.
- Build bằng Makefile tối thiểu (2-3 target: build, clean, run) thay vì gọi
gcctay. - Chạy Valgrind trên chương trình này để đảm bảo không leak khi xử lý hàng nghìn packet liên tục.
GIAI ĐOẠN 2 — C++ hiện đại
Bài tập nhỏ
RAII & smart pointer
- Viết một class
Bufferwrapchar*cấp phát bằngnew[], destructor tựdelete[]— so sánh với việc quên gọifreeở bản C. - Refactor class
Bufferở trên để dùngstd::unique_ptr<char[]>thay vì raw pointer + destructor thủ công. - Viết một class
Sessionmô phỏng phiên GTP, dùngstd::shared_ptr<Session>để 2 object khác nhau (ví dụSessionTablevàLogger) cùng giữ tham chiếu tới cùng 1 session mà không double-free.
Templates & STL
4. Viết một template function parseField<T>(const uint8_t* buf, size_t offset) đọc một giá trị kiểu T (uint8_t/uint16_t/uint32_t) từ buffer ở offset cho trước, tự xử lý byte order (network to host).
5. Dùng std::unordered_map<uint32_t, std::string> để build một bảng tra cứu Message Type code → tên message (thay thế chuỗi if-else hoặc switch-case dài).
Move semantics
6. Viết một class chứa std::vector<uint8_t> lớn, thêm constructor/method để log lại khi nào copy constructor được gọi và khi nào move constructor được gọi — thử truyền object đó theo 2 cách (copy vs std::move) để thấy khác biệt runtime rõ ràng.
Pet project: Session correlation table (bản đơn giản)
- Đây chính là bài toán bạn đang làm thật: correlate IMSI/MSISDN ↔ UE IP ↔ F-TEID.
- Dùng
unordered_map<uint32_t /*TEID*/, shared_ptr<Session>>làm cấu trúc dữ liệu chính. Sessionlà class RAII quản lý toàn bộ state của 1 phiên (IMSI, IP, TEID, timestamp tạo/cập nhật).- Input: đọc tuần tự các message GTPv2-C giả lập (Create Session Request/Response) từ file, mỗi message cập nhật hoặc tạo mới entry trong bảng.
- Output: khi có message Delete Session Request, in ra toàn bộ thông tin session trước khi xóa entry (dùng RAII để đảm bảo không leak khi session bị remove khỏi map).
- Mở rộng (optional): thêm timeout tự động xóa session không hoạt động quá N giây — bài tập tốt để chuẩn bị tư duy cho phần RTOS/timer ở Giai đoạn 4.
GIAI ĐOẠN 3 — Nhánh Security
Bài tập nhỏ
Memory corruption cơ bản
- Viết một hàm
vulnerable_copy(char *dest, const char *src)dùngstrcpykhông kiểm tra độ dài, gọi nó với input dài hơn buffer đích trên stack → build với sanitizer tắt (-fno-stack-protector) để thấy crash, sau đó bật lại stack canary mặc định để thấy chương trình phát hiện và abort. - Viết một hàm có format string bug (
printf(user_input)), truyền input%x %x %x %xvào để thấy nó leak dữ liệu từ stack ra màn hình. - Viết một hàm bị integer overflow khi tính kích thước buffer trước khi
malloc(ví dụmalloc(count * size)vớicountdo user kiểm soát) — cho input khiến phép nhân overflow vàmalloccấp phát buffer nhỏ hơn dự kiến, dẫn tới heap overflow ở bước ghi dữ liệu sau đó.
Sanitizers
4. Lấy lại 3 bài tập trên, build với -fsanitize=address,undefined, so sánh output báo lỗi chi tiết thế nào so với chạy thường (không sanitizer) chỉ thấy segfault trơ trụi.
Disassembly
5. Compile một hàm C đơn giản (ví dụ hàm tính Fibonacci đệ quy) ở 2 mức tối ưu khác nhau (-O0 và -O2), mở bằng Ghidra/objdump, so sánh assembly sinh ra khác nhau thế nào dù source code giống hệt.
Pet project: Fuzzing harness cho parser TLV tự viết
- Lấy lại chính parser GTPv2-C/Diameter AVP ở Giai đoạn 0 (hoặc bản C++ ở Giai đoạn 2).
- Viết một harness function
LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)gọi thẳng vào hàm parse của bạn. - Compile với libFuzzer (
clang -fsanitize=fuzzer,address), chạy fuzzing trong vài giờ, xem có tìm được crash không. - Nếu tìm được crash: dùng GDB + ASan report để xác định chính xác dòng code và điều kiện input gây lỗi, viết lại đoạn code đó cho an toàn (kiểm tra bound trước khi đọc).
- Ghi lại input gây crash làm regression test case, thêm vào bộ test chạy lại mỗi khi sửa code.
- Đây là dự án có giá trị thực tế nhất trong cả lộ trình — kết quả trực tiếp bảo vệ pipeline monitoring GTPv2-C/Diameter khỏi input malformed từ mạng thật.
GIAI ĐOẠN 4 — Nhánh Embedded/Systems
Bài tập nhỏ
Bare-metal & memory-mapped I/O
- Trên board STM32/ESP32: viết chương trình bật/tắt 1 LED bằng cách ghi trực tiếp vào thanh ghi GPIO qua con trỏ
volatile(không dùng HAL library) — bài “hello world” của bare-metal. - Viết linker script tối thiểu định nghĩa vùng Flash và RAM cho MCU cụ thể bạn có, so sánh với linker script mặc định của toolchain để hiểu từng dòng làm gì.
Interrupt
3. Cấu hình một timer interrupt kích hoạt mỗi 1 giây, trong ISR chỉ set một flag (không làm việc nặng trong ISR), main loop poll flag đó để in ra “tick” — bài tập chuẩn để hiểu nguyên tắc “ISR ngắn gọn, main loop xử lý logic”.
4. Tạo race condition có chủ đích: một biến shared giữa ISR và main loop không được bảo vệ, gây corrupt dữ liệu khi tần suất interrupt tăng cao → sửa bằng cách disable interrupt tạm thời hoặc dùng biến volatile + atomic flag.
RTOS
5. Dùng FreeRTOS/Zephyr tạo 2 task đơn giản: task A in “A” mỗi 500ms, task B in “B” mỗi 300ms — quan sát scheduler xen kẽ 2 task.
6. Thêm một mutex bảo vệ một biến đếm chung giữa 2 task, cố tình gây priority inversion (task priority thấp giữ mutex lâu, chặn task priority cao) để hiểu vấn đề trước khi học cách né nó.
SPI/UART 7. Viết driver đọc dữ liệu từ một cảm biến đơn giản qua SPI (accelerometer hoặc tương tự, rẻ và dễ mua), in giá trị đọc được qua UART về máy tính.
Pet project: Data logger RF đơn giản
- Dùng MCU (STM32/ESP32) + module RF hoặc SDR đơn giản (nếu không có sẵn phần cứng phù hợp, có thể thay bằng ADC đọc tín hiệu tương tự bất kỳ để mô phỏng luồng dữ liệu tương tự).
- Kiến trúc: 1 task đọc dữ liệu từ SPI (module RF) theo chu kỳ cố định bằng RTOS, 1 task khác đóng gói dữ liệu và gửi qua UART/network về máy tính, dùng queue/mutex của RTOS để 2 task giao tiếp an toàn.
- Yêu cầu: không được mất sample khi 2 task chạy đồng thời — đây là bài kiểm tra thực tế cho toàn bộ kiến thức interrupt + RTOS + driver ở giai đoạn này.
- Mở rộng (optional, khi đã sẵn sàng đọc source thật): tìm trong srsRAN/OAI một module PHY layer xử lý luồng dữ liệu tương tự (đọc sample từ SDR, đóng gói, đẩy lên layer trên), đọc và note lại cách họ tổ chức code so với pet project của bạn.
Bảng tổng hợp: Pet project theo giai đoạn
| Giai đoạn | Pet project | Kỹ năng chính được kiểm chứng |
|---|---|---|
| 0 | Parser TLV cho GTPv2-C/Diameter AVP | Con trỏ, struct/bitfield, quản lý bộ nhớ |
| 1 | Raw socket sniffer cho GTP-U | Syscall, socket, Makefile, GDB/Valgrind |
| 2 | Session correlation table | RAII, smart pointer, STL, template |
| 3 | Fuzzing harness cho parser tự viết | Memory corruption, fuzzing, sanitizer |
| 4 | Data logger RF đơn giản | Bare-metal, interrupt, RTOS, SPI/UART |
Gợi ý cách dùng file này
- Làm bài tập nhỏ trước, pet project sau — bài tập nhỏ cô lập từng kỹ năng để dễ debug khi sai, pet project gộp lại để thấy chúng phối hợp thế nào trong bài toán thật.
- Mỗi pet project nên viết lại (không chỉ chạy 1 lần) khi chuyển sang giai đoạn kế tiếp — ví dụ parser TLV ở Giai đoạn 0 (C thuần) nên được viết lại bằng C++/RAII ở Giai đoạn 2, rồi đưa vào fuzzing ở Giai đoạn 3. Việc tái sử dụng cùng 1 bài toán qua nhiều giai đoạn giúp thấy rõ tiến bộ thay vì làm project rời rạc không liên quan.
- Luôn bật
-fsanitize=address,undefinedkhi build mọi bài tập/pet project từ Giai đoạn 1 trở đi, kể cả khi bài tập không liên quan trực tiếp đến security.
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ớ