Actually Portable Executables Derlemek İçin GCC’yi Yamalamak
(ahgamut.github.io)- Cosmopolitan Libc ile mevcut C yazılımlarının daha fazlasını derleyebilmek için, derleme sistemini kandırmanın ötesine geçilmesi ve yaklaşık 2.000 satırlık bir GCC yamasıyla derleyicinin sorunlu kodu otomatik dönüştürmesi gerekti
- Engel,
SIGTERM,EINVALgibi sistem değerlerinin derleme zamanı sabiti olmaması ve bu yüzden C’ninswitch caseetiketleri ilestatic/const structbaşlatma kurallarına takılmasıydı sed, Python regex’leri, GCC eklentileri ve önişlemci makrolarını yakalama gibi yollar denendi; ancakcaseetiketi hatası ayrıştırma sırasında oluştuğu için bunu yalnızca eklentiyle engellemek zordu- Son yama
-fportcosmobayrağıyla etkinleştiriliyor; GCC hata vermeden hemen önceswitchifadeleriniif/gotoyapısına çeviriyor ve yapı başlatmalarını çalışma zamanında değer doldurarak tamamlıyor - Anonim yapılar,
enum, bazıconst intkullanımları ve başlatma ifadeleri içindeki dizi indeksleri gibi sınırlamalar kalsa da,bash,curl,git,ninja,gccgibi yazılımları kaynak kodu değiştirmeden derlemenin yolu açıldı
GCC yamasının hedefi
- Cosmopolitan Libc ile Actually Portable Executables oluşturmayı kolaylaştırmak için yaklaşık 2.000 satırlık bir
gccyaması yazıldı - Yama uygulandıktan sonra
./configureveyacmakederleme sistemleriyle şu yazılımlar derlenebiliyorbashcurlgitninjagcc’nin kendisi
- Derlenen çalıştırılabilir dosyaların Linux, FreeBSD, MacOS, OpenBSD, NetBSD ve Windows’ta da çalışabilmesi gerekiyor
- Windows’ta çalıştırmanın doğrudan test edilmediği notu düşülüyor
- GitHub Actions ile derlenen ikili dosyalar superconfigure releases üzerinden indirilebiliyor
Cosmopolitan Libc portlamasında takılınan nokta
- Cosmopolitan Libc 2021’de
redbeanweb sunucusuyla dikkat çektikten sonra, mevcut C yazılımlarının bu libc üzerinde ne kadar iyi çalıştığı önemli bir soru haline geldi - Lua, Wren, Janet,
quickjsgibi dil runtime’larını portlama sürecinde aynı kalıp tekrarlandı SIGTERM,EINVALgibi sistem sabitleriswitchdeyimindekicaseetiketlerinde kullanıldığında C’nin derleme kurallarıyla çakışıyor- C standardı,
caseetiketlerinin derleme zamanı sabiti olmasını şart koşuyor- Cosmopolitan Libc’de sistem arayüzüyle ilgili C önişlemci makrolarının sembolik olması gerekiyordu
- Bunun sonucunda
switch(errno) { case EINVAL: ... }gibi yaygın kodlar derlenmeyebiliyordu
switch ifadelerini if ve goto ile değiştirmek
switch(errno)içindekicase EINVAL,case ENOSYSgibi etiketler derleme zamanı sabiti değilse GCCcase label does not reduce to an integer constanthatası veriyor- Yama,
switchdeyimlerini otomatik olarak şu yapıya dönüştürüyor- Her
casedeğeri içinif (errno == EINVAL) goto caselabel_EINVAL;biçiminde bir dal ekliyor defaultetiketine giden birgotoekliyor- Özgün
casebloklarını etiketler vegoto endofthis_switchile yeniden düzenliyor
- Her
- Bu yöntem en zarif çözüm olmasa da, birçok kod tabanında görülen örnekleri işleyebildi
ifdefve fallthrough iç içe geçmişswitchdeyimlerini de ele almak gerektiğinden,gototabanlı dönüşüm basit bir seçenekti
struct başlatmaları da aynı kısıta takılıyor
staticveyaconst structbaşlatmalarında da derleme zamanı sabiti sorunu ortaya çıkıyorEINVALderleme zamanı sabiti değilse aşağıdaki başlatmaların bir kısmı C’de geçerli olmuyor- Fonksiyon içindeki normal yerel
structbaşlatması mümkün const structstatic struct- Global
const struct - Global
static struct
- Fonksiyon içindeki normal yerel
- Gerçek bir örnek olarak CPython’ın
faulthandlermodülü var - Çözüm, yapıyı sahte değerlerle başlatıp kullanılmadan önce doğru değerleri dolduran kod eklemek
- Fonksiyon içinde
ifdeyimi ekleniyor - Global veya statik başlatmalarda
__attribute__((constructor))kullanılıyor
- Fonksiyon içinde
Metin değiştirme ve GCC eklentilerinin sınırları
- Başta dönüşümü otomatikleştirmek için
sedshell script’i denenmiş, ardından Python script’i ve regex’lerle fallthrough durumları da işlenmeye çalışılmıştı - Ancak C önişlemcisi ve
ifdefnedeniyle metin değiştirme kusursuz çalışmakta zorlandı- Çünkü derleme sırasında hangi
ifdef’in etkin olacağını önceden bilmek zor
- Çünkü derleme sırasında hangi
- Sonraki deneme, GCC eklentisi kullanarak AST dönüşümü yapmaktı
- GCC eklentileri
-fplugin=your-plugin.soile yüklenebiliyor PLUGIN_PRE_GENERICIZE,PLUGIN_FINISH_PARSE_FUNCTION,PLUGIN_FINISH_DECLgibi olaylarda AST’ye erişilebiliyordebug_treeile AST yazdırılabiliyor,walk_tree_without_duplicatesile AST üzerinde dolaşılabiliyor
- GCC eklentileri
- Sorun,
caseetiketi doğrulamasının GCC ayrıştırması sırasında yapılmasıydı- Eklenti, ayrıştırma bittikten sonra AST’ye erişiyor
- Hatalı
case SIGTERM:etiketi AST’de kalmıyor - GCC zaten hata vermiş olduğu için eklenti tarafında düzeltmek zorlaşıyor
Önişlemci makrolarını yakalama deneyi
- Yalnızca eklentiyle ayrıştırma öncesi hatayı engellemek zor olduğundan, C önişlemcisiyle etkileşime giren bir yöntem denendi
- GCC eklenti başlıklarındaki
cpp_readeryapısı üzerinden makrolar#define,#undefedildiğinde veya kullanıldığında callback bağlanabiliyordu - Cosmopolitan Libc’nin sistem değeri makro yapısından yararlanılarak geçici sabitler eklendi
- Örnek:
extern const int SIGTERM; - Örnek:
#define SIGTERM SYMBOLIC(SIGTERM) - Eklenti için
static const int __tmpcosmo_SIGTERM = ...gibi geçici değerler tutuldu
- Örnek:
- Eklenti,
SYMBOLIC(SIGTERM)kullanımını yakalayıp geçici değerle değiştiriyor; ardındanPLUGIN_PRE_GENERICIZEaşamasında AST’yi tekrar doğruVAR_DECLhaline çeviriyordu - Bu makro hack’i
switchvestructbaşlatma hatalarını aşmayı başardı - Bu yöntemle en azından CPython 3.11 derlenebilir hale geldi
Eklenti yerine GCC’nin kendisini düzeltme nedeni
- Justine Tunney ile yapılan görüşmeden sonra, sorunun çözülebileceği anlaşılınca GCC kod tabanını doğrudan değiştiren daha basit bir yaklaşım önerildi
- Eklenti ve makro hack’i gereksiz çok iş çıkarıyor; kenar durumları ve çökmeleri ele almak karmaşıklaşıyordu
- Örneğin yalnızca
case ENOSYSdönüştürülmesi gereken bir durumda bile, makro ikamesi nedeniyleENOSYS + 1veyaprintf("ENOSYS = %d", ENOSYS)gibi normal kullanımları da eklentinin işlemesi gerekiyordu - GCC yaması yaklaşımında ayrı bir
-fpluginyerine özellik-fportcosmobayrağıyla açılıyor - GCC,
case is not constanthatasını vermeden hemen önceflag_portcosmo’nun etkin olup olmadığını kontrol ediyor; etkinse gerekli ikameleri yapıyor - Mevcut eklentideki AST dönüşüm kodu, GCC içinde diğer eklenti callback’leri çağrılmadan önce çalışacak şekilde taşındı
- Bu değişiklikle makroyla ilgili hack kodları kaldırılabildi
Gerçek derlemeler ve yamanın genişletilmesi
- 5 Haziran 2023 itibarıyla yamalanmış GCC, önceki eklenti test vakalarının tamamını geçti ve Cosmopolitan Libc monorepo’suna yeni bir binary eklendi
- Daha fazla kod derlendikçe düzeltmeler ve iyileştirmeler sürdü
luaderlemesi basitleşti;g++içindecase constanthatasının oluştuğu yer bulunarakninjada derlenebilir hale geldi- Python 3.11’e
ncursesbağlama sürecinde yeni bir sorun bulunduncursesiçindeDATA2(PARODD | PARENB, PARODD)gibi, yapı başlatma değerlerinde ikili ifadeler içeren kodlar vardı- Mevcut yama basit sabit başlatmaları işliyordu ama
PARODD | PARENBgibi ifadeleri işleyemiyordu
- Daha sonra yama, C’deki
caseetiketleri ve yapı başlatma öğeleri rastgele ifadeler olabilecek şekilde genişletildi- C standardına göre
caseetiketleri hâlâ derleme zamanı sabiti olmalı - Yamalanmış derleyici bu tür kullanımlarda uyarı veriyor ama derlemeye devam ediyor
- C++ tarafında
g++11’inconstexprişlemesiyle uyuşmadığı için rastgele ifade ikamesinin kapsamı dışında bırakıldı
- C standardına göre
- Bu değişiklikten sonra
ncursessorunsuz derlendi
Kalan kısıtlar ve pratik sonuç
- Yama kusursuz değil
- Bazı anonim yapıları işleyemiyor
enumveconst intiçin bazı durumları işleyemiyor- Başlatma ifadeleri içinde
SIGTERM’in dizi indeksi olarak kullanıldığı durumları işleyemiyor - Tuhaf
staticbaşlatmalar veyaSIGTERM’instatic const int8_tiçine konduğu durumlarda nadir derleyici çökmeleri kalabilir
- Yine de belirgin karşı örneklerin çoğu ortadan kaldırıldı ve birçok popüler yazılım sorunsuz derleniyor
- Daha sıkı bir test ortamı, ek iyileştirme noktalarını ortaya çıkarabilir
- İstenirse
switchdeyimleri vestructbaşlatma ifadeleri elle de düzeltilebilir; ancak çoğu durumda derleyici bunu otomatik ele alabiliyor - C yazılımı statik olarak derlenebiliyorsa ve özellikle
muslile de derleniyorsa, Cosmopolitan Libc ile de derlenebilme olasılığı var
3 yorum
Artık neo, mesajları kod bloğuna sarma formatına mı geçiyor? Yatay kaydırma yapmak gerekmesi ve kırmızı tonlardaki metin renginin biraz hayal kırıklığı yaratması üzücü...
Ah, bunun kod bloğuna alınması bir bug hüzün
Hacker News yorumları
Bu yazıyı ben yazdım; başlık “Patching GCC to build Actually Portable Executables” olmalı. Çünkü Cosmopolitan Libc’ye ve jart’ın Actually Portable Executable biçimine atıfta bulunuyor.
GCC yamam sayesinde artık vim, emacs, ninja, bash, git, gcc gibi yazılımlar Cosmopolitan Libc ile, alışılmış autotools/cmake tarzı build sistemleri üzerinden derlenebiliyor. Üretilen yürütülebilir dosyalar Linux, FreeBSD, MacOS, OpenBSD, NetBSD ve Windows’ta çalışabilmeli. Ancak Windows’u henüz test etmedim.
Bu teknikle derlemeyi denediğim yazılımların listesi burada: https://github.com/ahgamut/superconfigure
superconfigure betiği, yazılım derlemek için kullanılan normal configure betiğini saran ve
--enable-staticgibi bayrakları ekleyen bir wrapper’dan ibaret.GCC’yi Cosmopolitan Libc ile derlemeyi denemek isterseniz şu depoyu kullanabilirsiniz: https://github.com/ahgamut/musl-cross-make/tree/gccbuild
HN’de daha iyi muamele görmenizi umarım.
Örneğin glibc sürümüyle karşılaştırıldığında regresyon olmadığını kanıtlayacak kadar yeterli test yapılıp yapılmadığını merak ediyorum.
Go deposunda bununla ilgili bir issue var: https://github.com/golang/go/issues/51900
Genel olarak epey havalı, ama bunun teorik arka planını merak ediyorum. Yeni bir işletim sistemi hedefi oluşturulacaksa, mantıken yeni sabitler tanımlamak daha doğru görünüyor.
Alt işletim sistemlerinin her biri için o işletim sisteminin sabitlerini aynen geçirmek yerine, her işletim sisteminin sabitlerini her yerde aynı olacak şekilde dönüştürmek yeterli olur. Böylece sabitleri dizi indeksi veya case ifadesinde kullanan kod da düzgün çalışır.
Yazıda anlatılan yaklaşıma göre daha fazla overhead oluşturabilir, ama çok daha kolay ve Cosmopolitan libc’ye yeni program derlemek isteyenler açısından kullanımı da daha basit görünüyor.
Sabitlerin listesine buradan bakabilirsiniz: https://github.com/jart/cosmopolitan/blob/master/libc/sysv/c...
Bu sabitlerden biri her kullanıldığında, çok sayıda sabiti büyük bir lookup table olarak ikili dosyaya koymak ve programın kontrol etmesi gerektiği her seferinde o tablodan geçmek gerekir. Aşırı yavaş olmayabilir, ama kesinlikle hissedileceğini düşünüyorum.
Amaç, libc’nin veya port etmek istediğimiz yazılımın kaynak kodunu çok değiştirmeden port etmeyi kolaylaştırmak ve performansı benzer ya da daha iyi olan ikili dosyalar üretmekti. Bu kısıtlar altında, bu GCC yaması süreci basitleştirmenin en iyi yolu gibi göründü.
SIGHUP’ı dizi indeksi ilklendirmesinde kullanan yeterince çok kod tabanıyla karşılaşırsam, önerdiğiniz yöntemi deneyip ödünleşimi ölçmeyi düşünüyorum. Ya da siz kendiniz deneyip ayrı bir sabit kümesinin daha iyi olup olmadığını bildirebilirsiniz.“Bariz karşı örnekleri ortadan kaldırmak için epey zaman harcadıysanız ve birçok popüler yazılım sorunsuzca derleniyorsa”, başarıyla derlenen yazılımların listesini paylaşmak faydalı olabilir. Belki zaten vardır.
superconfigure betiği, yazılım derlemek için kullanılan normal configure betiğini saran ve
--enable-staticgibi bayrakları ekleyen bir wrapper’dan ibaret.APE’nin pratik bir amacı olup olmadığını merak ediyorum. Birden fazla işletim sisteminde çalışması gereken tek bir çalıştırılabilir dosyayı gerçekten dağıtan bir yer var mı?
lnav’ın kendisi APE olarak derlenmiyor, ama içine gömülü ajan APE olarak derleniyor. Kullanıcı o ana makinedeki günlükleri okumak istediğinde bu ajan uzaktan gönderiliyor
Böylece işletim sistemi türünü ayrıca belirleme ya da çalıştırılabilir dosyanın birden fazla sürümünü dahil etme gibi ek adımlara gerek kalmıyor. Konuyla ilgili kısa yazı burada: https://lnav.org/2021/05/03/tailing-remote-files.html
Statik olarak derlenen programlar her şeyin en altında yer alıyor, ama uygulama düzeyinde azınlıktalar
APE, “C kadar düşük seviyeli bir AOT dili bir kez derlenip her yerde çalışabiliyorsa sanal makineye kimin ihtiyacı var?” gibi büyük bir soru ortaya atıyor
Üstelik cross-compile yapmak acı verici. Çeşitli BSD’lerin hepsine sahip bir build farm’ı olmayan küçük açık kaynak projeleri, çoğu zaman bu platformlar için ikili dosya hiç dağıtmıyor
kubectl’in bile Linux için ikili dosyaları var ama BSD için yok. OSX’i BSD saymazsak tabii. Bu yüzden böyle projeler için kolay bir kazanım gibi görünüyor
Kullanıcının makinesinde anında derlemek için fazla büyük. Birkaç büyük üçüncü taraf C++ bağımlılığı içeriyor ve donanıma göre derleme 2 dakikadan 20 dakikaya kadar sürüyor
macOS x86-64+ARM, Linux x86-64, Windows x86-64 üzerinde çalışması gerekiyor. ARM Linux da olsa iyi olur
Şu anda WASM’i değerlendiriyoruz, ancak kurulu bir WASI runtime’ı gerektiriyor
Kafası karışanlar için: “Actually Portable Executables” https://justine.lol/ape.html
switch’iifile değiştirmek epey kaba bir çözüm. Platformdan bağımsız bir sabit kümesine eşleyip runtime sembolünü bir fonksiyonla dereference ederek düzeltilebilir gibiÖrneğin
switch(CosmoMappErrno(EINVAL))vecase COSMO_EINVAL:gibi kullanılırsa kodu bir goto yığınına çevirmek gerekmezElbette derleyicinin böyle bir switch’i tanıyıp if ağacı olarak yeniden yazmak yerine, label’ları her değer için cosmo’ya özel sabitlerle değiştirmesini ve switch girdisini mevcut runtime platform değerinden ilgili cosmo’ya özel sabite eşleyen bir fonksiyon çağrısıyla sarmalamasını kastediyorsanız, o başka. Diğer değerleri de değiştirmeden geçirerek
Bu aslında daha basit bir derleyici dönüşümü de olabilir. Ancak hangi sabit kümelerinden birinin kullanıldığını tanıma ve switch girdisine uygun eşleme çağrısını uygulama karmaşıklığı ortaya çıkar. Kütüphane tarafında da bu eşlemeleri oluşturmak gerekeceğinden karmaşıklık ister
Zaten başka bir dönüşüm yöntemiyle çalışan bir uygulama olduğuna göre, yazarın bunu uygulamak isteyip istemeyeceğini bilmiyorum
Son olarak böyle bir eşleme yaklaşımında yanlış eşleme riski var; negatif errno değerleri kullanan kodlarda da eşlemeden önce girdiyi tersine çevirmek gibi zahmetli durumlar söz konusu
if-else-goto dizisini sevmemin nedeni, GCC’nin diğer bölümleriyle tam oturmasıydı. Patch’ime[1] bakarsanız bu özelliği eklemek için GCC’nin mevcut kodunda çok az değişiklik yaptığımı görebilirsiniz
[1]: https://github.com/ahgamut/gcc/tree/portcosmo-11.2
SIGxxx’i indeks olarak kullanan koda neden bu kadar şaşırıldığını bilmiyorum. Sinyal numaraları iyi tanımlıdır; genelde ardışık oldukları ve 1’den başladıkları bilinirÜstelik o hata, debug etmesi uzun süren patch’imdeki derleyici hatasıyla birlikte ortaya çıktığı için akılda kalan bir sürpriz oldu
Böyle bir kod kalıbının yaygın olduğunu düşünüp düşünmediğinizi merak ediyorum. busybox dışında hangi codebase’lerde olduğunu da bilmek isterim. Çok örnek varsa patch’i bu kalıbı işleyecek şekilde güncellemeye zaman ayırabilirim
errno bu kadar büyük bir sorunsa, C++ standart kütüphanesini ve std::error_code’u çalıştırma umudu nedir bilmiyorum
Orada sihirli makrolar kullanmanın bir yolu yok
std::error_codebir sabit olmadığı için aynı sorunu yaşamaz gibi görünüyor