RustProgramlamaRust Sistem Geliştiricisi

**MaybeUninit<T>**'in ham hammarettäıcı belleğin derleyicinin geçerlilik varsayımlarından nasıl izole ettiğini ve böyle bir belleğin **T**'nin canlı bir örneğini tuttuğunu iddia ederken programcının hangi özel tehlikeli varsayıyı sağlaması gerektiğini belirtin.

Hintsage yapay zeka asistanı ile mülakatları geçin

Sorunun Yanıtı

Sorunun Geçmişi

Rust 1.36'dan önce geliştiriciler, daha sonra başlatılacak değerler için yığın belleği tahsis etmek amacıyla std::mem::uninitialized'a güvenirlerdi. Bu fonksiyon temelde güvensizdi çünkü derleyiciye o bellek konumunda geçerli bir T bulunduğunu söylüyordu, oysaki bitler rastgeleydi. Güvenlik varsayımlarına sahip türler—örneğin bool, char veya referanslar—için bu, derleyicinin değerin geçerli olduğunu varsayması nedeniyle hemen belirsiz davranışa yol açtı (örneğin, bir bool 0 veya 1 olabilir). RFC 1892, geçerli bir T içermeyen belleği açıkça belirtmek için MaybeUninit<T>'i bir sendika benzeri soyutlama olarak tanıttı ve bu güvensizlik açığını çözdü.

Problem

Ana sorun, LLVM'nin başlatılmamış belleği undef veya poison olarak ele alması ve Rust'ın otomatik bırakma yapıştırıcısı oluşturması ile ilişkilidir. Derleyici, bir T türünde bir değişkenin canlı olduğunu düşünüyorsa, yok edici çağrılar veya niş optimizasyonlar oluşturabilir. Eğer T bir bool ise, başlatılmamış bir byte 2 değerini tutabilir ki bu bit geçerliliği varsayımını ihlal eder. Bunu bırakma kontrolü veya belirleyici muayene sırasında okumak belirsiz bir davranış oluşturur. Ayrıca, eğer başlatma bir dizi boyunca yarıda kalırsa, dizi türünün bırakma yapıştırıcısı, tüm öğeleri bırakmaya çalışır ve başlatılmamış yığın bytes'lerini göstergeler olarak yorumlayarak kullanımdan sonra serbest bırakma veya çift serbest bırakma hatalarına yol açar.

Çözüm

MaybeUninit<T>, geçerli bir T tutan ya da tutmayan tiplenmiş bir kapsayıcıdır. Bu, derleyicinin başlatmayı varsaymasını engeller ve böylelikle bırakma yapıştırıcısının yayılmasını ve geçersiz bit kalıp optimizasyonlarını engeller. Programcı, hangi örneklerin başlatıldığını genellikle ayrı bir indeks veya boolean dizisi aracılığıyla manuel olarak takip etmelidir. Bir değer çıkarmak için, yalnızca geçerli bir T yazdıktan sonra assume_init, assume_init_ref veya std::ptr::read kullanılır. Kritik varsayım, assume_init'in hiçbir zaman tam olarak başlatılmamış bir bellek üzerinde çağrılmaması gerektiğidir ve kısmen başlatılmış bir yapıyı terkettiğinde, programcı yalnızca başlatılan öğeleri ptr::drop_in_place kullanarak manuel olarak bırakmalıdır, böylece kaynak sızıntılarından kaçınılabilir.

use std::mem::{self, MaybeUninit}; use std::ptr; fn init_array_fallible<T, E, const N: usize>( mut f: impl FnMut(usize) -> Result<T, E>, ) -> Result<[T; N], E> { let mut array: [MaybeUninit<T>; N] = unsafe { MaybeUninit::uninit().assume_init() }; let mut i = 0; while i < N { match f(i) { Ok(val) => { array[i].write(val); i += 1; } Err(e) => { for j in 0..i { unsafe { ptr::drop_in_place(array[j].as_mut_ptr()); } } return Err(e); } } } Ok(unsafe { mem::transmute::<[MaybeUninit<T>; N], [T; N]>(array) }) }

Hayatta Bir Durum

Bir yığın sürücüsü geliştirdiğinizi ve yığın tahsisinin yasak olduğu ve gecikmenin belirli olması gerektiğini düşünün. Yığında 1024 Connection nesnesinin sabit boyutlu bir tablosunu tahsis etmeniz gerekiyor. Her Connection'ın başlatılması, NIC bellek tamponu dolu olduğunda başarısız olabilecek bir donanım kaydı yazma sürecini içeriyor. 500. bağlantı başarısız olursa, önceki 499'un düzgün bir şekilde kapatıldığından emin olmak (dosya tanıtıcılarının bırakılması ve DMA eşlemelerinin serbest bırakılması) ve geri kalan 524 alanın dokunulmadan kalmasını sağlamak, başlatılmamış belleği bıraktığınızda herhangi bir belirsiz davranıştan kaçınılmak zorundasınız.

Bir potansiyel yaklaşım, diziyi sentinel değerlerle önceden başlatmak için Default::default() kullanmaktır. Bu, Connection'ın Default'ı uygulamasını gerektirir; bu da problemli çünkü "varsayılan" bir bağlantı hala açıkça serbest bırakılması gereken kernel kaynakları alacaktır ve hata yolunu karmaşıklaştırır. Ayrıca, yalnızca onları yenilemek için 1024 sahte bağlantı oluşturmak, başlatma döngülerini boşa harcamak ve sürücünün arayüzü çevrimiçi getirme için katı zamanlama gereksinimlerini ihlal etmek demektir.

İkinci bir strateji, Vec<Connection> kullanarak with_capacity ile dinamik ekleme yapmayı, ardından sabit bir diziye dönüştürmeyi içerir. Bu, kullanıcı alanı kodunda güvenli ve ideolojik olarak uygun bir yaklaşımdır. Ancak, Vec global bir tahsisatçı gerektirir ki bu bu çekirdek bağlamda mevcut değildir. Ayrıca, çekirdek alanında kabul edilemez olan potansiyel panik yolları ve bellek parçalanmasına neden olur ve sabit boyutlu bir diziye dönüştürme, hata işleme mantığını karmaşıklaştıran çalışma zamanı kontrollerini gerektirir.

Üçüncü yaklaşım, başlatmadan bellek tahsis etmek için MaybeUninit<[Connection; 1024]> kullanmaktır. Başarıyla başlatılan bağlantılar MaybeUninit::write aracılığıyla yazılır ve hata durumu i indeksinde gerçekleşirse, 0'dan i-1'e kadar manuel olarak iterelenir ve her başlatılmış alana ptr::drop_in_place çağrılır. Başarı durumunda, dizinin tamamını başlatılmış türüne dönüştürürüz. Bu çözümü seçtik çünkü sıfır maliyetli yığın tahsisi sağlar, belirli bir performansı temin eder, no_std kısıtlamasını yerine getirir ve yalnızca gerçekten başlatılmış nesneler için kaynakların temizlenmesini garanti eder. Sonuç olarak, kısmi bir hata kurtarma sırasında belirsiz bir davranışı hiç tetiklemeyen ve mikro saniye seviyesinde tutarlı bir başlatma gecikmesi sağlayan sağlam bir sürücü ortaya çıktı.

Adayların Genellikle Atladığı Noktalar


Başlatılmamış bir MaybeUninit<T> üzerinde assume_init çağırmak neden belirsiz bir davranış oluşturur, bu değerin sonra açıkça okunmaması durumunda bile?

Birçok aday, belirsiz davranışın yalnızca veriyi fiziksel olarak eriştiğinizde, örneğin yazdırma veya üzerine dalma gibi durumlarda meydana geldiğini düşünmektedir. Ancak, Rust'ın tip sistemi derleyiciye, assume_init çağrıldığında derhal geçerli bir T bulunduğunu söyler. Niş optimizasyonlara sahip türler (örneğin, bool, char, Option<&T>, veya NonNull<T>) için derleyici, enum çeşitlerini veya geçerliliği belirlemek üzere bit kalıbını inceleyen kodlar üretebilir. Eğer bellek rastgele bitler tutuyorsa (örneğin, bir bool için 0xFF), bu muayene LLVM'de belirsiz davranış tetikler (yükleme poison veya undef). Ayrıca, kapsam sona erdiğinde derleyici, T için bırakma yapıştırıcısı ekler, bu da çöplük verisi üzerinde yıkıcıları çalıştırmaya çalışır ve bu da çöküşlere veya güvenlik açıklarına neden olur. Böylece, assume_init programcının geçerli bir başlatma garantilediği bir sözleşmedir; bunu ihlal etmek, açık okumalara bakılmaksızın, derleyicinin durumunu zehirler.


MaybeUninit::write ile MaybeUninit::as_mut_ptr()'den elde edilen işaretçinin üzerindeki std::ptr::write kullanılması arasındaki fark nedir ve her biri ne zaman uygundur?

MaybeUninit::write, bir T'nin sahipliğini alır ve onu başlatılmamış slot'a yazarak artık başlatılmış veriye bir değiştirilebilir referans döndürdüğü güvenli bir yöntemdir. Değeri hazır olduğunda ve anında güvenli erişim istediğinizde tercih edilir. Buna karşılık, std::ptr::write, eski değeri okumadan veya bırakmadan bir değeri ham bir işaretçiye yazan tehlikesiz bir işlevdir (bu kritik çünkü bellek başlatılmamıştır). as_mut_ptr()'den alınan bir ham işaretçiden yazarak çalışıyorsanız ve write'nin ödünç alma denetleyici kısıtlamalarından kaçınmak istiyorsanız veya yalnızca ham işaretçilerin bulunduğu düşük seviyeli soyutlamaları uygularken ptr::write kullanmalısınız. Temel ayrım, write'nin güvenlik garantileri ve yaşam süresi takibi sağlarken, ptr::write'nin, hedefin geçerli, düzgün bir şekilde hizalanmış ve başlatılmamış olduğu için manuel doğrulamayı gerektirmesidir; böylece kimlik ihlalleri veya erken bırakmalarından kaçınılır.


Kaynak sızıntısı olmadan veya belirsiz davranışı tetiklemeden kısmen başlatılmış bir MaybeUninit<T> dizisini nasıl doğru bir şekilde bırakabiliyorsunuz ve işlemlerin sırası neden kritik?

Başlatma i indeksinde başarısız olduğunda, yalnızca 0..i aralığındaki öğeleri bırakmalısınız. Doğru prosedür, 0'dan i-1'e kadar iterasyon yaparak her birine std::ptr::drop_in_place(array[j].as_mut_ptr()) çağrmasıdır. Bu, T için yıkıcıyı başlatılmamış MaybeUninit kapsayıcısından değer taşımasını sağlamadan çalıştırır (bu da slotu tamamen başlatılmamış bir duruma getirir, ancak teknik olarak hâlâ başlatılmamıştır). Temizlemenin bu başarısızlık anında, hata döndürmeden önce derhal yapılması kritik bir öneme sahiptir, böylece yığın çerçevesi düzgün bir şekilde sarmalanır. Aksi takdirde, dizi üzerinde mem::forget kullanmayı veya basitçe dönmeyi denerseniz, MaybeUninit kapsayıcısı bırakılacak (bu bir işlem olmayacaktır), fakat canlı T örnekleri kaynaklarını sızdırır (dosya tanıtıcıları veya yığın belleği gibi). Tersine, eğer yanlışlıkla i..N öğelerini bırakırsanız, çöp belleği geçerli T örnekleri olarak muamele ettiğinizde belirsiz bir davranış tetiklemiş olursunuz.