Rust'ta `main`'den önce de çalışan kod vardır
(grack.com)- Rust ikilileri
fn main()öncesinde bir çalışma zamanı başlatma aşamasından geçer; bu aşamada panic·unwinding işlemleri ile program argümanlarının dönüştürülmesi gibi işler yapılır - İşletim sistemi yükleyicisi denetimi entry point'e devrettiğinde C çalışma zamanı ve Rust çalışma zamanı başlatma işlevlerini çalıştırır;
#[unsafe(link_section = "...")]ve constructor yaklaşımıyla pre-main kodu yerleştirilebilir - Linker section'lar, birden fazla crate'in sunduğu verileri ikili oluşturma anında tek bir yerde toplar;
link-sectionise bunu Rust slice'ı gibi ele almayı sağlar ctorilelink-sectionbirlikte kullanıldığında CLI subcommand kaydı, string interning pool sıralaması gibi kalıplarmain'den önce kurulabilir ve sonrasında kilit olmadan okunabilir- Bu yaklaşım allocasyonsuz toplulaştırma ve kontrolün tersine çevrilmesini sağlar; ancak dead code elimination zorluğu, constructor kısıtları, platform farkları ve Miri uyumluluğu sınırları nedeniyle uygulama alanı dikkatle seçilmelidir
Rust ikililerinde main öncesi aşama
- Tüm Rust ikililerinde
fn main()bulunur, ancak gerçek yürütme akışı işletim sistemi yükleyicisi ve çalışma zamanı başlatmasından geçtikten sonramain'e ulaşır - C'de
libcolarak tanınan bir C çalışma zamanı vardır; Rust ise standart kütüphane aracılığıyla kendi çalışma zamanına sahiptir ve C çalışma zamanının üzerinde daha yüksek seviyeli soyutlamalar kurar - Çalışma zamanının amacı geliştirici kodu ile platform işletim sistemini bütünleştirmektir
- C çalışma zamanı,
mainöncesi aşamada allocation, dosya erişimi, thread-local storage gibi çalışma zamanı hizmetlerini yapılandırır - Rust bu noktada panic ve unwinding işlemlerini hazırlar, ayrıca C tarzı program argümanlarını
std::env::argsarayüzüne dönüştürür - Pre-main aşaması kullanıcı kodundan önce çalışır, tek thread'lidir ve sıralaması öngörülebilir bir ortam sunar; bu yüzden deterministik başlatma için uygundur
Entry point
- Bir ikili, işletim sistemi yükleyicisi ikiliyi belleğe yükleyip ortamı kurduktan sonra denetimi devretmesiyle başlar
- Linux'ta entry point ELF başlığındaki
e_entryalanında saklanır ve varsayılan olarak linker_startadlı sembolün adresini yerleştirir - Windows'ta da benzer bir kanca vardır; yürütülebilir dosya
_WinMainCRTStartupişlevinden başlar - İlk çalışma zamanı bootstrap süreci, dosya I/O başlatma ve allocator başlatma gibi statik işlev çağrılarından oluşan bir ağaçtı
- Çalışma zamanı karmaşıklaştıkça bu statik başlatma çağrı ağacı da büyüdü ve ikililer, gerekebilir de gerekmeyebilir de olan daha fazla C çalışma zamanı özelliğini içermeye başladı
- Linker kullanılmayan kodu ikili oluşturulmadan önce kaldırabilir hale gelince, statik başlatma çağrı ağacının yerini alacak bir yönteme ihtiyaç doğdu
- GCC'nin
__attribute__((constructor))yaklaşımı, başlatma işlevi işaretçilerinin listesini ikili içinde bitişik bir alana yerleştiriyor ve C çalışma zamanı başlangıçta bunları dolaşıp çağırıyordu - Constructor'lara öncelik verilebilir hale geldi; örneğin buffered file I/O'dan önce
mallocbaşlatması gerekebilir - Linux'un modern
glibcçalışma zamanı, işlev işaretçilerini.init_arrayiçinde tutar ve sayısal soneklerle çalışma sırası belirlenebilir - Öncelik değeri 100 ve altı çalışma zamanının kendisine ayrılmıştır; bu yüzden C çalışma zamanı kullanan kod 101 ve üstünü kullanmalıdır
- Rust'ta
#[used]ve#[unsafe(link_section = ".init_array.101")]gibi niteliklerle başlatma işlevi işaretçileri yerleştirilebilir
linktime: ctor, link-section vb.
- Örnek Linux ve çeşitli BSD'lerde çalışır, ancak çapraz platform bir örnek olarak tasarlanmamıştır
- macOS
startvestopsembollerini destekler ama adları farklıdır; Windows isestartvestopsembollerini desteklemez, ancak fiilen eşdeğer section sıralama kurallarına sahiptir ctorvelink-section,linktimeprojesinin crate'leridir ve platform farkları ile linker işi karmaşıklığını soyutlarinventoryvelinkmeaynı ilke üzerine kurulmuş yaygın crate'lerdir, ancak örnek için bazı sınırlara sahiptirctorcrate'i, constructor'ları çapraz platform şekilde kaydetmek için gereken boilerplate'i üstlenir#[ctor(unsafe, priority = 101)]gibi bir nitelik eklenen işlev, kod içinde doğrudan çağrılmasa bile linker düzenlemesinden sonra C çalışma zamanı tarafından çağrılır
Section'lar ve linker script'leri
- Derleyici, veri ya da kodu ikili içindeki belirli konumlara yerleştirebilir; çoğu platformda bu alanlara section denir
- Rust da
link_sectionniteliği aracılığıyla aynı düzenleme yeteneğini kullanabilir - Birçok linker, geliştiricinin linker script sağlamasına izin verir; bu metin dosyası object file'ların nasıl birleştirileceğini linker'a bildirir
- Linker script kullanıldığında tek bir C dosyası Linux yürütülebiliri olabilir ya da sabit diskin boot sector'üne yazılan ham assembly bloğu haline gelebilir
- Linker script'leri, kaynak dosyalarda bulunmayan ama C kodunda yüklenmiş ikilinin taban veri işaretçilerine erişmekte kullanılabilen sanal semboller tanımlayabilir
- Örnek linker script'teki
_TEXT_START_ve_TEXT_END_,.textsection'ının başlangıcını ve sonunu gösterecek şekilde tanımlanır _TEXT_START_ = .;ifadesindeki nokta, ikilinin mevcut çıktı adresine yakın bir değer olarak yorumlanan konum sayacını ifade eder
Linker sembolleri
- Linker, başlangıç ve bitiş sembollerinin değerini işaretçi olarak ayarlamaz; bunun yerine aynı adlı
staticöğenin yerleştirileceği adresi belirler - Başlangıç ve bitiş sembolleri
*const Typeişaretçileri değildir; kendi verileri yoktur, yalnızca adres anlamı taşırlar - Section'lar, başlangıç sembolünü içeren ve bitiş sembolünü dışlayan aralıktaki verilerden oluşur
- Birçok linker, yürütülebilir dosyadaki tüm section sınırlarını otomatik tanımlama özelliği kazanmıştır
- GNU toolchain'de
MY_SECTIONadlı bir section için__start_MY_SECTIONve__stop_MY_SECTIONsembolleri otomatik tanımlanır - macOS, her section için
section$startvesection$endsembollerini türeten benzer bir kalıba sahiptir - GNU linker'da linker script içinde açıkça belirtilmeyen section'lara orphan section denir
- Linker,
_startve_stopönekli sembolleri yalnızca section adı C sembol adıyla uyumluysa otomatik tanımlar our_stringsçalışır, ancakour.stringsya da.our_stringsaynı şekilde çalışmaz- Sınır sembollerinde veri yoktur ve yalnızca adres önemlidir; bu yüzden örnekte bunlar
MaybeUninit<()>ile ifade edilir - Stable Rust'ta ideal olan “opaque external type” henüz uygulanmadığından,
MaybeUninitbir vekil görevi görür &raw constişaretçisi birstaticöğe için üretmek her zaman geçerlidir; dolayısıyla değer okunmadan yalnızca adres güvenle alınabilirlink-section, bu linker section ayrıntılarını soyutlar ve standart slice işlemleri kullanılabilen bir Rust slice'ına dönüştürür- Linker section'ların gücü, ikiliye kod sağlayan herhangi bir crate'in aynı section'a öğe ekleyebilmesi ve linker'ın son ikili yazılmadan hemen önce bunların hepsini bir araya getirmesidir
Dependency injection
- Section tabanlı kayıt kalıbı, dependency injection ile aynı ilke üzerinde çalışır
- Dagger ve Spring gibi framework'ler de kayıt verisini tüketenin sağlayıcıya sıkı bağlı olmaması gerektiği ilkesine dayanır
- Sağlayıcı, tanımlandığı yerde veriyi kaydeder; tüketici ise registry'yi okur
- Geleneksel dependency injection'da framework çoğu zaman başlangıçta modül grafiğini dolaşmalı ya da yüklenmiş sınıfları tarayarak sağlayıcı ve tüketicileri bulmalıdır
- Linker section'larda ise sağlayıcı verileri ikili oluşturulurken linker tarafından toplanır ve tüketicinin kolayca okuyabileceği hale getirilir
- CLI subcommand kaydı örneği,
link_section::sectionile subcommand kaydeden bu kalıbın bir örneğidir - Turbopack bu kalıbı string pool sabitleri, serialization·deserialization kayıt mekanizmaları ve turbotask incremental compilation function kayıtları için kullanır
- Varsayımsal bir web sunucusu da route ve middleware bileşenlerini build anında otomatik toplamak için bu kalıbı kullanabilir
Kayıt için section kullanımı
mainöncesi çalışmanın avantajlarından biri, açıkça başlatılmadıkça thread'lerin çalışmamasıdır- Bu ortamda çoğu durumda lock veya senkronizasyon primitive'lerinin karmaşıklığından kaçınılabilir
- Verinin yaşam döngüsü,
mainöncesindeki yazılabilir aşama ilemainsonrasındaki değişmez aşama olarak net biçimde ayrılabilir - Çalışan programda veriye erişirken lock alma ve bırakmadan kaçınmak yapıyı basitleştirip verimliliği artırabilir
- Örnek,
CliSubcommandyapısı,constconstructor işlevi ve#[section]ile subcommand toplar list,add,helpgibi subcommand'lar kodun herhangi bir yerinde bulunabilirmainişlevi yalnızcaCLI_SUBCOMMANDSsection tanımını görüyorsa, kayıtlı subcommand adlarını ve konumlarını bilmeden dinamik dispatch yapabilir- Kayıtlı subcommand yoksa varsayılan subcommand'a dönülür; örnekte varsayılan olarak
helpçalışır
Değişmez verinin ötesi
- Önceki örnek bağlı verinin değişmez olduğunu varsayar, ancak linker tabanlı veri düzenleme değişebilir veri için de kullanılabilir
- Global statik verinin değişebilirliği Rust'ta yaygın bir sorundur ve mutex ya da atomic türler gibi içsel değişebilirlik araçlarıyla çözülür
- Mutex ve atomic türler rekabet olmadığında pahalı değildir, ama tamamen ücretsiz de değildir
- Rust'ta veriyi güvenli biçimde değiştirmek için değişikliğin thread-safe şekilde yapılması ve mutable referans varken aynı veriye başka referans bulunmaması gerekir
- Pre-main ortamı, thread'ler açıkça başlatılmadıkça tek thread'li olduğundan atomic işlemler gerekmez
- Tek thread'li ortamda, değişikliğin daha sonraki okumalardan önce gerçekleştiğini belirten happens-before ilişkisi kendiliğinden sağlanır
mainöncesinde linker section verisini değiştirmek, sonrasında herhangi bir thread'den locksuz biçimde güvenli erişim sağlar- Mutable referans yalnızca
mainöncesinde oluşturulup kapatılırsa, mutable referans varken başka referans olmaması koşulu da sağlanır - Linker section slice'ı, section içindeki statik öğelere alias olduğundan, aliasing kuralları hem slice'a hem de statik öğelere uygulanır
- Slice üzerinden güvenli değişiklik yapmak için statik öğeler mutlaka
UnsafeCelliçine yerleştirilmelidir UnsafeCellile sarılmamış statik öğelerde LLVM değerleri önbelleğe alabilir, yeniden sıralayabilir ya da veri hakkında varsayımlarda bulunabilirUnsafeCellkendi başınaSyncdeğildir; bu yüzden ayrı bir wrapper type gerekir- Örnek, sınır sembollerini ve öğeleri kurmak için
SyncUnsafeCellveMaybeUninit<SyncUnsafeCell<...>>kullanır - Sıralanabilir string interning pool örneği, link anında string pool'u tanımlar ve çalışma zamanının başında slice'ı sıralayarak sonrasında binary search ile string bulur
- Elle yapılan uygulama çok boilerplate içerir; ancak
ctorvelink-sectionileTypedMutableSectionve constructor'lar kullanılarak aynı yapı daha kısa kurulabilir TypedMutableSectionöğelericonstolmalıdır; çünkü içeride, elle yapılan örneğe benzer bir kod kullanılır
Linker section kalıbının avantajları
- Bu kalıp, etiketlenmiş öğeleri garantili şekilde toplulaştırır ve tüm veriyi önceden ayrılmış bitişik belleğe yerleştirir
- Kayıt noktaları kodun her yerine dağıtılabilir
- Section içindeki öğe sayısı garantili olarak elde edilebilir
- Linker section'lar ek allocation gerektirmez
- Aynı yapı linker section olmadan kurulsa
HashMap,Vecveya başka veri yapıları allocate edilmeli ve öğeler toplanırken birçok kez yeniden boyutlandırılmalıydı - Geleneksel toplama yaklaşımında ortak tür modülü, katkı modülleri ve toplama modülü arasındaki bağımlılıklar derin biçimde iç içe geçer
- Linker section kullanıldığında toplayıcı herhangi bir yerde olabilir ve hangi modüllerin veri sağladığını umursamak zorunda kalmaz
scattered-collect, link zamanı desteği olan çeşitli veri yapısı benzerleri sunarScattered*Slice, slice sağlayan çeşitliVecbenzeri yapılardır ve isteğe bağlı sıralamayı desteklerScatteredMapveScatteredSet, minimum pre-main başlatmayla hash tabanlı anahtar-değer sorgulaması sunanHashMapveHashSetbenzeri yapılardır
Bu yaklaşım ne zaman kullanılmamalı
- Link zamanı hesaplama güçlüdür, ancak her zaman doğru araç değildir
- Link zamanı yaklaşımı yerine, veri katkısı yapacak her crate'i görebilen bir crate içinde veriler elle toplanabilir
- Elle toplama zahmetli olabilir; katkı yapanların tek bir merkezi katkı noktasını görmesi yerine, birçok crate referansı taşıyan bir toplama crate'i gerekir
- Dead code elimination zorlaşır
link-sectionvelinkme, öğelere#[used]eklediğinden linker kullanılmayan veriyi kaldıramaz- Intern edilmiş string atomları gibi küçük verilerde bu sorun olmayabilir; ancak ham JSON·JavaScript parçaları veya büyük veri yapıları intern edilirse tespit edilmesi zor çok miktarda dead code birikebilir
- Pre-main constructor işlevlerinin kısıtları vardır
- Constructor işlevleri panic oluşturmamalıdır ve Rust, standart kütüphanedeki tüm işlevlerin kullanılabilir olduğunu garanti etmez
- Aynı öncelik içindeki başlatma işlevlerinin çağrı sırası garanti edilmez ve büyük ölçüde platforma bağlıdır
- Bu kısıtlar dikkatli tasarımla aşılabilir, ancak pre-main yaklaşımı ince detayları ve hata ayıklama zorluğu nedeniyle yine de doğru seçim olmayabilir
- Miri tüm pre-main constructor'ları ve linker section yapılandırmalarını tam olarak desteklemez
- Şu anda Miri, pre-main yürütmeyi çok temel düzeyde görür ve linker section'ları modellemez
- Tanımsız davranış testleri için ASan, TSan gibi LLVM sanitizer'ları önerilir
- Kontrolün tersine çevrilmesi kalıbı, linker section'a veri katkısı yapılan tüm noktaların denetlenmesini zorlaştırabilir
- Geniş dağıtıma sahip ve yoğun kullanılan birçok Rust programı zaten
ctor,link-section,inventory,linkmegibi pre-main özelliklere dayanır
WASM hakkında kısa not
- WASM, geçmişteki tasarım tercihlerinin etkisiyle linker section'ları yerel olarak desteklemez
#[link_section]notasyonu öğeleri gerçek kod section'larına yerleştiremez; bunun yerine onları WASM kodunun kendisinden erişilemeyen WASM custom section'larına koyarlinktimecrate'i WASM'i destekler ve yaklaşımın WASM ikililerinde de çalışmasını sağlayan emülasyon tabanlı bir çözüm sunar- Uygun WASM desteği eklemeye yönelik öneriler ileride gelebilir
Sonuç
mainöncesinde, belirli durumlarda ciddi avantajlar sağlayan pek çok iş yapılabilir- Pre-main ortamı yüksek derecede kontrollü ve yönetilebilir bir sıralama sunduğundan, lock, atomic type ve diğer senkronizasyon primitive'leri olmadan da birçok işlem daha güvenle yapılabilir
- Linker section'lar, ilgili veriyi tüm ikili boyunca serbestçe toplulaştırıp birlikte yerleştirmeyi sağlar ve garip crate bağımlılık sıralarından kaçınmaya yardımcı olur
- Çoğu durumda allocation tamamen önlenebilir; böylece tekrar eden allocation'lardan kaynaklanan parçalanma gibi allocator sorunlarından uzak durulabilir
- İlgili crate'ler arasında
ctor,dtor,link-section,scattered-collectbulunur
1 yorum
Lobste.rs görüşleri
Apple, sistem çağrılarında ABI kararlılığı sınırı olarak libSystem.dylib kullanıyor; NT tabanlı Windows ise sistem çağrıları yerine
ntdll.dll’i ABI kararlılığı sınırı olarak kullanıyor: not syscallsOpenBSD’de Go’nun, yükleyicinin ayarladığı salt okunur
libceşlemesinin dışından sistem çağrısı denemesini çekirdeğin süreç sonlandırma politikasıyla engellemesini aşmak için, NX bitinin zorunlu uygulanmasını kapatan türden metadata bayrakları ayarladığı anlaşılıyorAncak libSystem.dylib contains the functionality which would normally be
libc.soplus other things; bu açıdan BSD ailesindeki “libc kararlılık sınırıdır” yaklaşımıyla aynıAyrıca As of Go 1.16 itibarıyla Go, OpenBSD’nin sistem çağrısı politikasına uymak için libc kullanıyor
Linux, kararlı sistem çağrısı numaralarına sahip olduğu için görece nadir bir örnek; çünkü diğer işletim sistemlerindeki gibi “süreç adres alanına dinamik kütüphane olarak yüklenen çekirdek parçasının, çekirdek modu koduyla kararsız sistem çağrısı
enumtanımlarını paylaşması” şeklinde bir yapıya sahip değil ve Linux ile glibc de başka yerlerde olduğu gibi aynı depoda birlikte geliştirilmiyorWindows’ta C çalışma zamanı ayrıca, MS-DOS’tan kopyalanıp Windows’un alt süreç oluşturma API’sine de miras kalan CP/M tarzı komut dizgesini POSIX tarzı
argvdizisine ayrıştırma işini üstleniyorBu yüzden Python
subprocessbelgelerinde Converting an argument sequence to a string on Windows bölümü var; buradaargvdizisinin, MS C çalışma zamanına gömülü tırnak kurallarına göre bir dizgeye nasıl dönüştürüldüğü anlatılıyor. Çağrılan alt sürecin kendi ayrıştırıcısı isterse bu kurallardan farklı davranabilirLinux’taki
_startda tam olarak bağlayıcının bu isimdeki bir sembolü ikili dosyaya otomatik eklediği anlamına gelmiyor. ELF biçimindeki ikili dosya bir kütüphane değil de çalıştırılabilir dosyaysa, başlıktakie_entryalanı, yani0x18ofseti, yükleyicinin bellek kurulumundan sonra atlayacağı adresi içeriyor_start, libc’nin sağladığı giriş noktasını kullanmadığınızdae_entry’nin işaret edeceği hedefi belirtmek için kullanılan bir GCC geleneği; NASM gibi araçlar da sanırım bunu izliyorWindows’taki
_WinMainCRTStartupda yükleyici tarafından PE header içindekiAddressOfEntryPointüzerinden bulunuyor. Bu alan, PE başlığının başlangıcına göre Offset0x0028konumunda yer alıyor; PE başlığının kendisi ise MZ (DOS EXE) başlığı ve DOS Stub’dan sonra geliyorPE başlığının ayrıntılarını öğrenmek için Making the smallest Windows application ve Tiny PE iyi kaynaklar. Tiny PE, Windows’un kabul ettiği şekillerde PE belirtimini ihlal ediyor; örneğin işletim sisteminin okumadığı bölümleri üst üste bindiriyor ya da kullanılmayan başlık alanlarına kod koyuyor. Bu seviyede, Windows’un kabul ettiği en küçük dosya boyutu kullanılan Windows sürümüne göre değişiyor
Linux’taki çok küçük ELF çalıştırılabilir dosyaları için A Whirlwind Tutorial on Creating Really Teensy ELF Executables for Linux de bakmaya değer
_startile ilgili olarak, a.out sistemlerinde çekirdeğin çalıştırılabilir dosyaya girdiği giriş noktası geleneksel olarak csu/crt0 içinde tanımlananstartidi. Örnek olarak 7th edition, VAX BSD verilebilirO dönemde C derleyicileri genel sembollerin başına
_eklediği için V7’nin_maintanımladığını, BSD’nin ise C’dekistart()için assembly adını süssüzstartolarak tanımladığını görebilirsinizO zamanlar programlar dosyanın başlangıç noktasından başlardı ve
cc’nin bağlayıcı çağrısıcrt0’ın en başta yer almasını sağlayacak şekilde düzenlenirdi.csu, C başlangıç kodu;crt0ise sıfırıncı C çalışma zamanı destek nesnesi anlamına gelirELF’nin geldiği System V’de bunun tam olarak nasıl çalıştığını bulmak daha zor, ancak
startveya_start, csu/crt0 içinde tanımlanan program giriş noktası olarak kullanılmaya devam ettiELF’nin
_önek işlemesini nasıl değiştirdiğini hiçbir zaman tam olarak anlamadım, ama galiba eğlence olsun diye bir katman daha eklenincestartbir nedenle_startolmuş gibi görünüyorDaha net bir eş örnek olarak, ELF sanki
_endde eklemiş gibi; bu BSS’nin üst sınırına karşılık geliyor vemalloc()yığını oluşturmadan öncesbrk(0)’ın döndüreceği konuma denk düşüyormainöncesindeki yaşama ilgi duyuyordum; bunun ne olduğu ve neden faydalı olduğu üzerine tek bir yazı yazmanın iyi olacağını düşündümBağlayıcı toplamasını kullanarak daha hızlı koleksiyonlar oluşturma gibi devam yazısı fikirlerim de var, ama önce bu giriş düzeyi konu hakkında geri bildirim almak istiyorum
no_stdve bazenallocbile olmayan ortamlardamainsadece başka bir fonksiyon oluyor ve ilklendirme büyük ölçüde geliştiricinin sorumluluğunda kalıyorBenzer amaçlarla kod tabanımda epey el yapımı tekrar kodu var; bu yüzden bu crate’lerin gömülü ortamla nasıl örtüştüğünü merak ediyorum