Hành vi không xác định trong C: Không chỉ là segfault như nhiều người vẫn nghĩ

Công nghệ25 tháng 10, 2026·5 phút đọc

Bài viết phản bác quan điểm cho rằng hành vi không xác định (UB) trong C là vô hại và chỉ gây segfault. Tác giả đưa ra các ví dụ biên dịch thực tế cho thấy UB có thể khiến các hàm chưa được gọi vẫn thực thi, ghi giá trị sai vào bộ nhớ và tạo ra những kết quả hoàn toàn không thể dự đoán.

Hành vi không xác định trong C: Không chỉ là segfault như nhiều người vẫn nghĩ

Hành vi không xác định (undefined behavior - UB) trong ngôn ngữ C từ lâu đã là chủ đề gây tranh cãi. Nhiều lập trình viên cho rằng UB chỉ là chuyện nhỏ, cùng lắm thì chương trình bị segfault rồi dừng. Bài viết mới đây trên blog Habets đã chứng minh điều ngược lại bằng những ví dụ biên dịch cụ thể, cho thấy trình biên dịch có thể làm ra những điều kỳ quái mà không ai lường trước.

"Chocolate nổ tung": khi hàm chưa được gọi vẫn chạy

Tác giả đưa ra một ví dụ với biến volatile int device_command, giả lập một thiết bị ánh xạ bộ nhớ điều khiển động cơ. Hai hàm được định nghĩa: serve_hot_chocolate() gán giá trị 42, còn explode() gán giá trị 3. Trong hàm pha chocolate, tác giả gọi std::unreachable() — một hàm mà theo định nghĩa là kích hoạt hành vi không xác định tại điểm đó.

Kết quả biên dịch bằng GCC 16.2 với cờ -O1 cho ra đoạn assembly bất ngờ:

  • serve_hot_chocolate() chỉ còn một lệnh ghi giá trị 42
  • explode() xuất hiện đầy đủ trong mã máy, dù không hề được gọi ở bất kỳ đâu

Nói cách khác, trình biên dịch đã loại bỏ phần mã phía sau lệnh gọi std::unreachable() vì cho rằng nó không thể chạy tới, đồng thời giữ nguyên hàm explode(). Kết quả là hàm "gây nổ" vẫn tồn tại trong chương trình dù chẳng ai triệu gọi — hiện tượng mà tác giả gọi vui là "chocolate nổ tung".

Truy cập con trỏ null: không chỉ là "chạm vào địa chỉ 0"

Nhiều người lập luận rằng truy cập con trỏ null cũng chẳng sao, cùng lắm là segfault vì địa chỉ 0 không được ánh xạ. Tác giả thay std::unreachable() bằng cặp lệnh int* ptr = nullptr; *ptr = 99; và nhận được kết quả còn kỳ lạ hơn:

Trình biên dịch ghi giá trị 0 thay vì 99 vào địa chỉ bộ nhớ 0, sau đó cố tình làm sập chương trình bằng một lệnh không hợp lệ (ud2). Con số 99 không hề xuất hiện trong chương trình được tạo ra.

Theo tác giả, việc ghi giá trị 0 vào địa chỉ null có thể là cách trình biên dịch tạo tín hiệu rõ ràng hơn cho trình gỡ lỗi. Clang với cùng đoạn mã cũng cho ra kết quả tương tự với hiện tượng "chocolate nổ tung".

Phép cộng con trỏ: khi UB… lại chạy đúng như mong đợi

Điều thú vị là khi tác giả thử ptr += 10 rồi gán 99, trình biên dịch lại sinh ra mã thực hiện đúng điều mà lập trình viên "ngây thơ" mong đợi: ghi 99 vào địa chỉ 40. Dù phép cộng con trỏ trên con trỏ null vi phạm UB vì ít nhất ba lý do khác nhau, lần này kết quả lại trông có vẻ hợp lý.

Tác giả cho rằng đây không phải câu hỏi đúng. Không có trình biên dịch nào có nghĩa vụ phát hiện UB, và vì bài toán dừng (halting problem), trình biên dịch không thể biết chắc đoạn mã có bao giờ được thực thi hay không. Trong trường hợp cụ thể này, GCC và Clang dường như lập luận rằng "truy cập con trỏ null thì không thể xảy ra", nên dừng sinh mã cho phần phía sau để tiết kiệm dung lượng nhị phân.

UB có thể làm bất cứ điều gì

Từ ba ví dụ trên, cùng một điểm kích hoạt UB có thể dẫn tới ba kết quả hoàn toàn khác nhau:

  • Chạy bất cứ thứ gì nằm tiếp theo trong bộ nhớ (chocolate nổ tung)
  • Ghi một giá trị gần như ngẫu nhiên vào địa chỉ 0 rồi cố tình làm sập chương trình
  • Thực hiện đúng như những gì mã nguồn thể hiện một cách bề mặt

Điều này khẳng định lại nguyên tắc nền tảng: UB nghĩa là trình biên dịch được phép làm bất cứ điều gì. Không có bảo đảm nào về segfault, về giá trị được ghi, hay về việc chương trình sẽ dừng ở đâu.

Lời khuyên cho lập trình viên Việt Nam

Với các bạn đang làm việc với C/C++ trong môi trường nhúng, phát triển firmware hay hệ thống điều khiển — những lĩnh vực khá phổ biến tại Việt Nam — bài viết này là lời nhắc nhở quan trọng:

  • Không bao giờ dựa vào hành vi "có vẻ đúng" của mã chứa UB, vì trình biên dịch có thể thay đổi hành vi giữa các phiên bản hoặc tối ưu hóa
  • Bật cảnh báo và công cụ phân tích tĩnh như -Wall -Wextra, AddressSanitizer, UBSan để phát hiện sớm các vấn đề
  • Kiểm tra kỹ mã nguồn khi làm việc với con trỏ null, phép toán con trỏ và các hàm như std::unreachable()
  • Hiểu rằng hành vi không xác định không phải là lỗi thời gian chạy mà là lỗi hợp đồng giữa lập trình viên và trình biên dịch

Tác giả kết bài bằng một liên hệ hài hước với "nasal demons" — hình ảnh ẩn dụ nổi tiếng trong giới lập trình về việc trình biên dịch có thể làm bất cứ điều gì với UB, kể cả triệu hồi quỷ mũi. Dù đùa vui, thông điệp vẫn rất rõ ràng: đừng bao giờ tin rằng UB là vô hại.

Chia sẻ:FacebookX
Nội dung tổng hợp bằng AI, mang tính tham khảo. Xem bài gốc ↗