C và cơn ác mộng phân tích chuỗi thành số nguyên

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

Bài viết này khám phá những lỗ hổng và hành vi bất ngờ trong các hàm thư viện chuẩn của ngôn ngữ lập trình C khi chuyển đổi chuỗi thành số nguyên. Từ atol đến sscanf, tác giả chỉ ra rằng hầu hết các phương pháp đều gặp vấn đề với việc xử lý lỗi, đặc biệt là đối với số nguyên không dấu (unsigned). Cuối cùng, giải pháp nằm ở việc sử dụng std::from_chars trong C++ hoặc viết các hàm bao đóng cẩn thận trong C.

Ngôn ngữ lập trình C đã tồn tại hàng thập kỷ và là nền tảng của nhiều hệ thống quan trọng. Tuy nhiên, có một nhiệm vụ cơ bản mà các lập trình viên vẫn phải vật lộn mỗi ngày: chuyển đổi một chuỗi ký tự thành một số nguyên (parsing integers). Một bài phân tích gần đây đã chỉ ra một thực tế đáng báo động: hầu hết các cách thức tiêu chuẩn để làm điều này trong thư viện C đều bị lỗi hoặc không an toàn.

Tiêu chuẩn cho một hàm phân tích đúng

Trước khi đi vào chi tiết kỹ thuật, chúng ta cần thống nhất về hành vi mong đợi. Một hàm chuyển đổi chuỗi sang số "đúng" cần tuân thủ các nguyên tắc cơ bản:

  • Chính xác: Chuỗi "123" phải trở thành số 123.
  • Toàn vẹn: Toàn bộ chuỗi phải được phân tích. "123timmy" không phải là một số hợp lệ, cũng như chuỗi rỗng.
  • Xử lý lỗi: Nếu việc phân tích thất bại, hàm phải báo lỗi rõ ràng cho người gọi, không phải âm thầm trả về một giá trị ngẫu nhiên.

atol(): Vô dụng và nguy hiểm

Hàm atol() có vẻ đơn giản nhất, nhưng nó hoàn toàn không thể sử dụng được cho dữ liệu đầu vào không đáng tin cậy.

  • Nó trả về 0 cho các đầu vào như "timmy" hoặc chuỗi rỗng, khiến bạn không thể phân biệt được lỗi và số 0 thực sự.
  • Khi gặp tràn số (overflow), nó trả về LONG_MAX.
  • Tệ hơn, theo tiêu chuẩn POSIX và C, nếu giá trị không thể được biểu diễn, hành vi là "không xác định" (undefined behavior). Về lý thuyết, nó có thể xóa ổ cứng của bạn, dù trong thực tế nó thường chỉ trả về giá trị sai.

strtol() và strtoul(): Nửa vời

strtol() tốt hơn một chút. Nếu bạn cẩn thận, bạn có thể sử dụng nó để phân tích các số có dấu (signed). Bạn cần kiểm tra errno để phát hiện tràn số và sử dụng con trỏ endptr để đảm bảo toàn bộ chuỗi đã được xử lý.

Tuy nhiên, người anh em strtoul() (dành cho số không dấu) lại là một thảm họa. Vấn đề lớn nhất là nó chấp nhận dấu trừ ở đầu. Điều này dẫn đến những tình huống phi logic:

  • Đầu vào "-1" sẽ trả về ULONG_MAX (giá trị lớn nhất của unsigned long).
  • Điều này tạo ra sự mơ hồ: Bạn không thể biết liệu đầu vào là "-1", một số cực lớn hợp lệ, hay một số bị tràn. Không có cách nào để phát hiện sự khác biệt này.

sscanf(): Không đáng tin cậy

sscanf() dường như là một giải pháp linh hoạt, nhưng nó cũng thất bại trong việc xử lý tràn số một cách nhất quán. Đôi khi nó trả về ULONG_MAX cho các giá trị nằm ngoài phạm vi, nhưng đôi khi lại trả về các giá trị khác. Bạn không thể dựa vào nó để xác nhận tính toàn vẹn của dữ liệu.

Tại sao điều này lại quan trọng?

Có thể bạn sẽ nghĩ "Rác vào, rác ra" (Garbage in, garbage out) là chấp nhận được. Nhưng trong các hệ thống phần mềm nghiêm túc, điều này là không thể chấp nhận.

  • Bảo mật: Các kẻ tấn công có thể khai thác việc phân tích sai để vượt qua các kiểm soát truy cập (ACL) hoặc gây ra sự cố.
  • Tính toàn vẹn của dữ liệu: Khi xử lý tệp dữ liệu, bạn cần dừng lại hoặc bỏ qua dữ liệu lỗi, chứ không phải âm thầm xử lý nó với giá trị sai.
  • Sự rõ ràng: Các API nên được thiết kế để sử dụng đúng. Một con dao sắc bén là tốt, nhưng một con dao không có cán cầm là nguy hiểm.

Giải pháp là gì?

Nếu bạn đang viết mã C++, std::from_chars là cứu tinh. Nó hoạt động chính xác, không chấp nhận dấu trừ cho các kiểu không dấu và báo lỗi rõ ràng.

Đối với lập trình viên C thuần túy, bạn buộc phải viết các hàm bao đóng (wrapper) của riêng mình. Một giải pháp được đề xuất là kiểm tra thủ công các ký tự khoảng trắng và dấu trừ trước khi gọi strtoul(), đảm bảo rằng các trường hợp ngoại lệ bị loại bỏ trước khi chuyển đổi thực sự diễn ra.

Kết luận

Thật đáng buồn khi một ngôn ngữ mạnh mẽ như C lại không có một cách thức chuẩn và an toàn để thực hiện một tác vụ đơn giản như đọc một con số từ chuỗi. Điều này nhắc nhở chúng ta rằng khi làm việc với C, sự thận trọng và kiểm tra kỹ lưỡng là bắt buộc, đặc biệt là khi xử lý các số nguyên không dấu (unsigned integers).

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