Go, canlı işaretçileri belirlemek için tüm canlı işaretçileri tanımlamak zorunda olan eşzamanlı bir çöp toplayıcıya sahiptir. C'den farklı olarak, Go, uintptr'ı gösterici meta verileri taşımayan opak bir tam sayı türü olarak ele alır; bu, çöp toplayıcının kök taraması ve işaretçi geçişi sırasında bu türdeki değerleri görmezden geldiği anlamına gelir. Bu tasarım, adresler üzerinde tam sayı aritmetiğine izin verir ancak geçerli bellek referanslarının yalnızca sayılar olarak görünmesiyle birlikte bir tehlikeli boşluk yaratır; bu, çalışma zamanının canlılık takibi için görünmezdir.
Geliştiriciler adres hesaplamaları yaptıklarında - örneğin, dizi elemanlarına sınır kontrolleri olmadan erişme veya belleği hizalama - genellikle unsafe.Pointer'ı uintptr'a dönüştürür, ofset uygular ve ardından geri döner. Bu adımlar birden fazla ifade veya işlev çağrısı arasında gerçekleşiyorsa, ara uintptr değeri bellek referansının tek kanıtı haline gelir. Çöp toplayıcı, bir işaretçi görmediğinde, temel nesnenin ulaşılamaz olduğunu varsayabilir ve onu geri alabilir; bu da, son işaretçi dönüşümü geçersiz belleğe erişmeye çalıştığında kullanım sonrası serbest kalma hatalarına veya veri bozulmalarına yol açar.
Go, unsafe.Pointer'dan uintptr'a ve geriye dönüşümün aynı ifade içinde gerçekleşmesini, ara depolama veya işlev çağrısı olmadan şart koşar. Bu model, derleyicinin orijinal işaretçiyi aritmetik işlem boyunca canlı tutmasını sağlar; bu, eşzamanlı çöp toplama döngülerinin referans edilen nesneyi geri almasını engeller. Klasik form (*T)(unsafe.Pointer(uintptr(p) + offset)) olup, tüm hesaplama tek bir değerlendirme olarak kalır.
Yüksek verimli bir paket işleme sistemi, protokol başlıklarını doğrudan bir bayt diliminden ayrıştırmak için Go'nun sınır kontrolü yükünden kaçınmak zorundaydı. Mühendislik ekibi, sıcak yolda nanosaniyeleri sıkıştırmak ve katı 10Gbps hat hızı gereksinimlerini karşılamak için 1500 baytlık bir MTU tamponundaki 8. bayta erişimine ihtiyaç duydu.
Bir yaklaşım, ara adres hesaplamasını netlik için yerel bir değişkende saklamaktı: addr := uintptr(unsafe.Pointer(&buf[0])) + 8 hesaplayarak ve daha sonra *(*uint64)(unsafe.Pointer(addr)) ile derefere ederek. Bu, okunabilirliği artırıp adres değeri için kesme noktasında hata ayıklama sağlamış olsa da, fatal bir yarış koşulu yaratıyordu - çöp toplayıcı, atama ve derefere işlemi arasında çalıştırılabilirdi, tüpleri yeni bir yığma konumuna taşınabilir ve addr'ı eski adrese işaret eden sarkık bir referans haline getirebilir, bu da segmentasyon ihlalleri veya veri bozulmasına neden olabilirdi.
Bir alternatif strateji, aritmetiği unsafe.Pointer ve ofset alan bir yardımcı işlev içine sarmaktı; o işlev içinde dönüşümü gerçekleştirerek. Ancak, işlev çağrıları zamanlama noktaları olarak hareket eder ve yığın büyümesine veya çöp toplayıcının çalışmasına neden olabileceğinden, işaretçiyi işlev argümanları aracılığıyla geçirmek, derleyicinin yardımcı işlevin çalışması boyunca orijinal işaretçinin canlılığını korumaya devam edeceğini garanti etmezdi; bu da kodu erken toplama karşı açığa çıkarıyordu.
Ekip, bir //go:nosplit montaj tarzı sarıcı içinde kapsülleyerek, *(*uint64)(unsafe.Pointer(uintptr(unsafe.Pointer(&buf[0])) + 8)) tek ifade biçimini seçti. Bu, çalışma zamanının perspektifinden atomik olarak işaretçi aritmetiğinin gerçekleşmesini sağladı ve çöp toplayıcının ara uintptr durumunu gözlemlemesini engelledi. Çözüm, bazı hata ayıklama yeteneklerinden feragat etmesine karşın, geçerliliği sağladı ve hatalı dönüşümlerin tespit edilmesi için geniş kapsamlı birim testleri ve CI sırasında checkptr etkinleştirilmiş derlemeleri kullanarak doğrulandı.
Paket işleyici, sıfır atımlı sıcak yollara sahip olarak kararlı alt mikro saniye gecikmesi sağladı. Üretimde çöp toplayıcıya bağlı hiçbir çökme meydana gelmedi; bu, hizmetin stres testleri sırasında GODEBUG=checkptr=1 altında çalıştırılmasıyla doğrulandı ve hiçbir unsafe.Pointer ihlali tespit edilmedi.
Neden unsafe.Pointer'ı uintptr'a dönüştürmek ve geri dönmeden önce bir değişkende saklamak, Go'nun bellek güvenliği garantilerini ihlal eder?
Go çöp toplayıcısı eşzamanlı çalışır ve herhangi bir tahsis noktasında tetiklenebilir. uintptr'ı bir değişkende sakladığınızda, nesne yalnızca bir tam sayı ile referans edildiği bir pencere yaratmış olursunuz. uintptr değerleri kökler olarak taranmadığından, GC bu pencerede nesneyi geri alabilir ve ardından gelen işaretçi dönüşümünün serbest belleğe erişmesine neden olabilir.
checkptr bayrağı, unsafe.Pointer aritmetiği ile nasıl etkileşir ve geçerli bir kod neden GODEBUG=checkptr=2 altında panik tetikleyebilir?
checkptr enstrümantasyonu, unsafe.Pointer dönüşümlerinin hizalama ve tahsis sınırlarına saygı gösterdiğini doğrular. checkptr=2 altında, derleyici, aritmetiğin orijinal nesne içinde tutulduğunu doğrulamak için çalışma zamanı kontrol noktaları ekler. Geçerli bir kod, aritmetik bir nesnenin ortasına işaretçiye neden olursa veya çok ifade içeren bir uintptr hesaplamasından türetilirse panik yapabilir; çünkü checkptr sınır ötesi ifadelerde canlılık garantilerini doğrulayamaz.
Geçici işaretçilerle ilgili unsafe.Pointer kuralları ile cgo işaretçi geçişi kuralları arasındaki fark nedir ve bu kurallara aykırı davranmak Go'nun yığın büyümesi sırasında neden çökmesine neden olabilir?
unsafe.Pointer atomik dönüşümleri gerektirirken, cgo, C'ye geçirilen işaretçilerin sabit kalmasını gerektiren ek kısıtlamalar getirir. Adaylar, Go işaretçilerini C belleğinde uintptr olarak saklamanın güvenli olduğunu varsayıyor; ancak, Go yığın büyümesi veya GC sırasında bu işaretçiler geçersiz hale gelebilir. Çözüm, runtime.Pinner kullanmak veya C çağrılarının Go'ya döneceğinden önce tamamlanmasını sağlamaktır; bu da yabancı işlev yürütmesi sırasında erişilebilirlik değişkenlerinin korunmasını sağlar.