Có phải cuộc gọi giả định đó đã kích hoạt hành vi không xác định?
Đúng. "Uninitialized" chỉ là một giá trị khác mà một byte trong Máy Tóm tắt Rust có thể có, bên cạnh 0x00 - 0xFF thông thường. Hãy để chúng tôi viết byte đặc biệt này là 0xUU. (Xem bài đăng trên blog này để có thêm một chút nền tảng về chủ đề này .) 0xUU được bảo toàn bởi các bản sao giống như bất kỳ giá trị có thể nào khác mà một byte có thể có được giữ bởi các bản sao.
Nhưng các chi tiết phức tạp hơn một chút. Có hai cách để sao chép dữ liệu xung quanh trong bộ nhớ trong Rust. Thật không may, các chi tiết cho điều này cũng không được nhóm ngôn ngữ Rust quy định rõ ràng, vì vậy những gì diễn ra sau đây là cách giải thích cá nhân của tôi. Tôi nghĩ những gì tôi đang nói là không gây tranh cãi trừ khi được đánh dấu khác, nhưng tất nhiên đó có thể là một ấn tượng sai.
Unyped / byte-khôn ngoan sao chép
Nói chung, khi một phạm vi byte được sao chép, phạm vi nguồn sẽ ghi đè lên phạm vi đích - vì vậy nếu phạm vi nguồn là "0x00 0xUU 0xUU 0xUU", thì sau khi sao chép, phạm vi đích sẽ có danh sách byte chính xác đó.
Đây là những gì memcpy
/ memmove
trong C hành xử như thế nào (theo cách giải thích của tôi về tiêu chuẩn, không rõ ràng ở đây không may). Trong Rust, ptr::copy{,_nonoverlapping}
có thể thực hiện một bản sao theo byte, nhưng hiện tại nó không thực sự được chỉ định chính xác và một số người có thể muốn nói rằng nó cũng được gõ. Điều này đã được thảo luận một chút trong vấn đề này .
Bản đánh máy
Thay thế là một "bản sao được gõ", đó là những gì xảy ra trên mọi phép gán thông thường ( =
) và khi truyền các giá trị đến / từ một hàm. Một bản sao được gõ sẽ giải thích bộ nhớ nguồn ở một số loại T
và sau đó "tái tuần tự hóa" giá trị của loại đó T
vào bộ nhớ đích.
Sự khác biệt chính đối với bản sao theo byte là thông tin không liên quan ở loại T
bị mất. Về cơ bản, đây là một cách phức tạp để nói rằng một bản sao được gõ "quên" phần đệm và đặt lại nó thành không được khởi tạo một cách hiệu quả. So với một bản sao chưa được đánh dấu, một bản sao được đánh máy sẽ mất nhiều thông tin hơn. Các bản sao được đánh dấu giữ nguyên biểu diễn bên dưới, các bản sao được nhập chỉ bảo toàn giá trị được biểu diễn.
Vì vậy, ngay cả khi bạn chuyển đổi 0usize
sang PaddingDemo
, một bản sao được nhập của giá trị đó có thể đặt lại giá trị này thành "0x00 0xUU 0xUU 0xUU" (hoặc bất kỳ byte nào có thể khác cho phần đệm) - giả sử data
nằm ở offset 0, không được bảo đảm (thêm #[repr(C)]
nếu bạn muốn sự đảm bảo đó).
Trong trường hợp của bạn, ptr::write
lấy một đối số kiểu PaddingDemo
và đối số được truyền qua một bản sao được gõ. Vì vậy, tại thời điểm đó, các byte đệm có thể thay đổi tùy ý, đặc biệt chúng có thể trở thành 0xUU.
Chưa hoàn thành usize
Việc mã của bạn có UB hay không phụ thuộc vào một yếu tố khác, cụ thể là việc có một byte chưa được khởi tạo trong một usize
là UB hay không. Câu hỏi là, một phạm vi bộ nhớ chưa được khởi tạo (một phần) đại diện cho một số nguyên? Hiện tại, nó không và do đó có UB . Tuy nhiên, liệu đó có phải là trường hợp được tranh luận nhiều hay không và dường như cuối cùng chúng ta sẽ cho phép nó.
Tuy nhiên, nhiều chi tiết khác vẫn chưa rõ ràng - ví dụ, việc chuyển "0x00 0xUU 0xUU 0xUU" sang một số nguyên cũng có thể dẫn đến một số nguyên chưa được khởi tạo hoàn toàn , nghĩa là các số nguyên có thể không thể bảo toàn "khởi tạo một phần". Để bảo toàn các byte được khởi tạo một phần trong các số nguyên, về cơ bản chúng ta phải nói rằng một số nguyên không có "giá trị" trừu tượng, nó chỉ là một chuỗi các byte (có thể chưa được khởi tạo). Điều này không phản ánh cách các số nguyên được sử dụng trong các hoạt động như thế nào /
. (Một số điều này cũng phụ thuộc vào các quyết định LLVM xung quanh poison
vàfreeze
; LLVM có thể quyết định rằng khi thực hiện tải ở loại số nguyên, kết quả là hoàn toàn poison
nếu có bất kỳ byte đầu vào nàopoison
.) Vì vậy, ngay cả khi mã không phải là UB vì chúng tôi cho phép các số nguyên chưa được khởi tạo, nó có thể không hoạt động như mong đợi vì dữ liệu bạn muốn chuyển đang bị mất.
Nếu bạn muốn chuyển byte thô xung quanh, tôi khuyên bạn nên sử dụng một loại phù hợp với điều đó, chẳng hạn như MaybeUninit
. Nếu bạn sử dụng một kiểu số nguyên, mục tiêu sẽ là chuyển các giá trị nguyên - tức là số.