Menambal GCC untuk build Actually Portable Executables
(ahgamut.github.io)- Untuk membangun lebih banyak perangkat lunak C yang sudah ada dengan Cosmopolitan Libc, ternyata tidak cukup hanya "menipu" build system; compiler juga harus otomatis mentransformasikan kode bermasalah lewat patch GCC sekitar 2.000 baris
- Hambatannya adalah nilai sistem seperti
SIGTERMdanEINVALbukan konstanta waktu kompilasi, sehingga bertabrakan dengan aturan labelswitch casedan inisialisasistatic/const structdalam C - Sudah dicoba mulai dari
sed, regex Python, plugin GCC, sampai mencegat makro preprocessor, tetapi karena error labelcaseterjadi saat parsing, plugin saja sulit untuk mencegahnya - Patch akhirnya diaktifkan dengan flag
-fportcosmo, dan tepat sebelum GCC mengeluarkan error,switchdiubah menjadiif/gotosementara inisialisasi struct dilengkapi dengan pengisian nilai saat runtime - Masih ada batasan seperti struct anonim,
enum, sebagianconst int, dan indeks array di dalam ekspresi inisialisasi, tetapi ini membuka jalan untuk membangun perangkat lunak sepertibash,curl,git,ninja, dangcctanpa mengubah source code
Tujuan patch GCC
- Ditulis sekitar 2.000 baris patch
gccagar lebih mudah membuat Actually Portable Executables dengan Cosmopolitan Libc - Setelah patch diterapkan, software berikut bisa dibangun dengan build system
./configureataucmakebashcurlgitninjagccitu sendiri
- File executable yang dihasilkan seharusnya bisa berjalan di Linux, FreeBSD, MacOS, OpenBSD, NetBSD, dan Windows
- Ada catatan bahwa eksekusi di Windows belum diuji langsung
- Binary hasil build di GitHub Actions bisa diunduh dari rilis superconfigure
Titik hambatan saat porting Cosmopolitan Libc
- Setelah Cosmopolitan Libc menarik perhatian lewat web server
redbeanpada 2021, pertanyaan pentingnya menjadi seberapa baik perangkat lunak C yang sudah ada bisa berjalan di atas libc ini - Pola yang sama berulang saat mem-porting runtime bahasa seperti Lua, Wren, Janet, dan
quickjs - Saat konstanta sistem seperti
SIGTERMdanEINVALdipakai pada labelcasedalamswitch, hal ini berbenturan dengan aturan kompilasi C - Standar C mengharuskan label
caseberupa konstanta waktu kompilasi- Di Cosmopolitan Libc, makro preprocessor C yang terkait antarmuka sistem harus bersifat simbolik
- Akibatnya, kode umum seperti
switch(errno) { case EINVAL: ... }bisa gagal dikompilasi
Mengubah switch menjadi if dan goto
- Jika label seperti
case EINVALataucase ENOSYSdi dalamswitch(errno)bukan konstanta waktu kompilasi, GCC akan mengeluarkan errorcase label does not reduce to an integer constant - Patch ini otomatis mentransformasikan pernyataan
switchmenjadi struktur berikut- Menambahkan percabangan untuk tiap nilai
casedalam bentukif (errno == EINVAL) goto caselabel_EINVAL; - Menambahkan
gotountuk pindah ke labeldefault - Blok
caseasli disusun ulang menjadi label dangoto endofthis_switch
- Menambahkan percabangan untuk tiap nilai
- Cara ini memang bukan yang paling elegan, tetapi berhasil menangani berbagai kasus yang ditemukan di banyak codebase
- Karena juga harus menangani
switchyang bercampurifdefdan fallthrough, transformasi berbasisgotomenjadi pilihan yang sederhana
Inisialisasi struct juga terkena batasan yang sama
- Masalah konstanta waktu kompilasi juga muncul pada inisialisasi
staticatauconst struct - Jika
EINVALbukan konstanta waktu kompilasi, sebagian inisialisasi berikut tidak valid dalam C- inisialisasi
structlokal biasa di dalam fungsi masih bisa const structstatic structconst structglobalstatic structglobal
- inisialisasi
- Salah satu contoh nyata ada di modul
faulthandlermilik CPython - Solusinya adalah menginisialisasi struct dengan nilai dummy lebih dulu, lalu menambahkan kode untuk mengisi nilai yang benar sebelum dipakai
- Di dalam fungsi, ditambahkan pernyataan
if - Untuk inisialisasi global atau statik, digunakan
__attribute__((constructor))
- Di dalam fungsi, ditambahkan pernyataan
Batasan substitusi teks dan plugin GCC
- Awalnya transformasi ingin diotomatisasi dengan shell script
sed, lalu dilanjutkan dengan script Python dan regex agar bisa menangani fallthrough - Namun, karena adanya preprocessor C dan
ifdef, substitusi teks sulit bekerja secara lengkap- Sebab sulit mengetahui lebih dulu
ifdefmana yang akan aktif saat kompilasi
- Sebab sulit mengetahui lebih dulu
- Percobaan berikutnya adalah memakai transformasi AST melalui plugin GCC
- Plugin GCC bisa dimuat dengan
-fplugin=your-plugin.so - AST bisa diakses pada event seperti
PLUGIN_PRE_GENERICIZE,PLUGIN_FINISH_PARSE_FUNCTION, danPLUGIN_FINISH_DECL - AST dapat dicetak dengan
debug_tree, dan ditelusuri denganwalk_tree_without_duplicates
- Plugin GCC bisa dimuat dengan
- Masalahnya, validasi label
casedilakukan GCC saat parsing berlangsung- Plugin baru dapat mengakses AST setelah parsing selesai
- Label salah seperti
case SIGTERM:tidak tersisa di AST - Karena GCC sudah lebih dulu mengeluarkan error, plugin sulit memperbaikinya
Eksperimen mencegat makro preprocessor
- Karena plugin saja sulit mencegah error sebelum parsing, dicoba pendekatan yang berinteraksi dengan preprocessor C
- Melalui struktur
cpp_readerdi header plugin GCC, callback bisa dipasang saat makro di-#define, di-#undef, atau digunakan - Dengan memanfaatkan struktur makro nilai sistem di Cosmopolitan Libc, konstanta sementara bisa disisipkan
- Contoh:
extern const int SIGTERM; - Contoh:
#define SIGTERM SYMBOLIC(SIGTERM) - Untuk plugin, disiapkan nilai sementara seperti
static const int __tmpcosmo_SIGTERM = ...
- Contoh:
- Plugin mencegat penggunaan
SYMBOLIC(SIGTERM)dan menggantinya dengan nilai sementara, lalu pada tahapPLUGIN_PRE_GENERICIZEAST diubah kembali menjadiVAR_DECLyang benar - Hack makro ini
berhasil mengakali error pada inisialisasi struct danswitch`` - Dengan pendekatan ini, build minimal CPython 3.11 juga menjadi mungkin
Mengapa memperbaiki GCC langsung alih-alih plugin
- Setelah berdiskusi dengan Justine Tunney, dan jelas bahwa masalah ini bisa dipecahkan, diusulkan pendekatan yang lebih sederhana: memodifikasi codebase GCC secara langsung
- Plugin dan hack makro menciptakan banyak pekerjaan yang sebenarnya tidak perlu, dan penanganan edge case maupun crash jadi rumit
- Misalnya, walaupun yang perlu diubah hanya
case ENOSYS, akibat substitusi makro plugin juga harus menangani penggunaan normal sepertiENOSYS + 1atauprintf("ENOSYS = %d", ENOSYS) - Pada pendekatan patch GCC, fitur ini diaktifkan dengan flag
-fportcosmoalih-alih-fpluginterpisah - Tepat sebelum GCC mengeluarkan error
case is not constant, compiler memeriksa apakahflag_portcosmoaktif; jika aktif, substitusi yang dibutuhkan dijalankan - Kode transformasi AST dari plugin sebelumnya dipindahkan ke dalam GCC agar dijalankan sebelum callback plugin lain dipanggil
- Perubahan ini memungkinkan penghapusan kode hack terkait makro
Build nyata dan perluasan patch
- Per 5 Juni 2023, GCC yang sudah ditambal lulus semua test case plugin sebelumnya, dan binary baru ditambahkan ke monorepo Cosmopolitan Libc
- Seiring makin banyak kode yang dikompilasi, perbaikan dan penyempurnaan terus dilakukan
- Build
luamenjadi lebih sederhana, danninjajuga bisa dibangun setelah menemukan lokasi munculnya errorcase constantdig++ - Saat mencoba memasangkan
ncurseske Python 3.11, ditemukan masalah baru- Di
ncurses, ada kode sepertiDATA2(PARODD | PARENB, PARODD)yang memasukkan ekspresi biner ke dalam nilai inisialisasi struct - Patch sebelumnya bisa menangani inisialisasi konstanta sederhana, tetapi belum bisa menangani ekspresi seperti
PARODD | PARENB
- Di
- Setelah itu, patch diperluas agar label
casedan elemen inisialisasi struct di C dapat berupa ekspresi arbitrer- Menurut standar C, label
casetetap harus berupa konstanta waktu kompilasi - Compiler yang sudah ditambal akan memberi peringatan untuk penggunaan seperti ini, tetapi kompilasi tetap dilanjutkan
- C++ dikecualikan dari substitusi ekspresi arbitrer karena tidak cocok dengan penanganan
constexprdig++11
- Menurut standar C, label
- Setelah perubahan ini,
ncursesberhasil dibangun tanpa masalah
Batasan yang masih ada dan kesimpulan praktis
- Patch ini belum sempurna
- Belum bisa menangani sebagian struct anonim
- Belum bisa menangani
enumdan sebagian kasusconst int - Belum bisa menangani kasus
SIGTERMsebagai indeks array di dalam ekspresi inisialisasi - Masih ada kemungkinan crash compiler yang jarang pada inisialisasi
staticyang aneh atau saatSIGTERMdimasukkan kestatic const int8_t
- Meski begitu, banyak contoh tandingan yang jelas sudah berhasil dihilangkan, dan banyak software populer bisa dibangun dengan mulus
- Lingkungan pengujian yang lebih ketat mungkin akan memperlihatkan area lain yang masih perlu diperbaiki
- Jika mau, pernyataan
switchdan ekspresi inisialisasistructmasih bisa diperbaiki secara manual, tetapi dalam banyak kasus compiler kini bisa menanganinya secara otomatis - Jika perangkat lunak C bisa dibangun secara statik, dan terutama jika juga bisa dibangun dengan
musl, besar kemungkinan ia juga bisa dibangun dengan Cosmopolitan Libc
3 komentar
Sekarang neo memakai format yang membungkus pesan dengan blok kode? Agak disayangkan karena jadi harus scroll horizontal dan warna teks bernuansa merah juga sedikit kurang cocok...
Aduh, ini terbungkus sebagai blok kode adalah bug T_T
Komentar Hacker News
Saya yang menulis artikel ini, dan judulnya seharusnya adalah “Patching GCC to build Actually Portable Executables”. Ini merujuk pada format Actually Portable Executable milik Cosmopolitan Libc dan jart.
Dengan patch GCC saya, sekarang perangkat lunak seperti vim, emacs, ninja, bash, git, dan gcc bisa dibangun dengan Cosmopolitan Libc melalui sistem build bergaya autotools/cmake seperti biasa. File executable yang dihasilkan seharusnya bisa dijalankan di Linux, FreeBSD, MacOS, OpenBSD, NetBSD, dan Windows. Namun, Windows belum saya uji.
Daftar perangkat lunak yang sudah saya coba build dengan teknik ini ada di sini: https://github.com/ahgamut/superconfigure
Skrip superconfigure hanyalah pembungkus untuk skrip configure umum yang dipakai membangun perangkat lunak, dan menambahkan flag seperti
--enable-static.Jika ingin mencoba membangun GCC dengan Cosmopolitan Libc, Anda bisa memakai repositori ini: https://github.com/ahgamut/musl-cross-make/tree/gccbuild
Semoga Anda mendapat perlakuan yang lebih baik di HN.
Misalnya, saya penasaran apakah sudah ada pengujian yang cukup untuk membuktikan tidak ada regresi dibanding versi glibc.
Ada issue terkait ini di repositori Go: https://github.com/golang/go/issues/51900
Secara keseluruhan ini cukup keren, tapi saya penasaran dengan dasar teorinya. Jika membuat target sistem operasi baru, secara logis tampaknya lebih tepat untuk mendefinisikan konstanta baru.
Untuk setiap sistem operasi turunan, alih-alih meneruskan konstanta milik OS itu apa adanya, Anda bisa mengubah konstanta tiap OS agar menjadi sama di mana-mana. Dengan begitu, kode yang memakai konstanta sebagai indeks array atau di case statement juga akan tetap bekerja.
Mungkin ada overhead lebih besar dibanding pendekatan yang dijelaskan dalam artikel, tetapi tampaknya jauh lebih mudah dan lebih sederhana dipakai dari sudut pandang orang yang ingin mengompilasi program baru ke Cosmopolitan libc.
Daftar konstantanya bisa dilihat di sini: https://github.com/jart/cosmopolitan/blob/master/libc/sysv/c...
Setiap kali salah satu konstanta ini dipakai, Anda harus memasukkan banyak konstanta ke dalam biner sebagai tabel lookup besar, lalu setiap pemeriksaan di program harus melewati tabel itu. Mungkin tidak terlalu lambat, tetapi saya rasa pasti akan terasa.
Tujuannya adalah mempermudah porting tanpa banyak mengubah source code libc atau perangkat lunak yang ingin dipindahkan, sambil tetap menghasilkan biner dengan performa yang setara atau lebih baik. Dengan batasan seperti itu, patch GCC ini tampaknya cara terbaik untuk menyederhanakan prosesnya.
Jika saya cukup sering menemukan codebase yang memakai
SIGHUPuntuk inisialisasi indeks array, saya akan mencoba pendekatan yang Anda usulkan untuk mengukur titik komprominya. Atau Anda juga bisa mencobanya sendiri dan memberi tahu saya apakah kumpulan konstanta terpisah memang lebih baik.Jika “cukup banyak waktu telah dihabiskan untuk menghilangkan contoh tandingan yang jelas, dan banyak perangkat lunak populer bisa di-build dengan mulus”, akan membantu jika ada daftar perangkat lunak yang berhasil dikompilasi. Mungkin sebenarnya sudah ada.
Skrip superconfigure hanyalah pembungkus untuk skrip configure umum yang dipakai membangun perangkat lunak, dan menambahkan flag seperti
--enable-static.Bertanya-tanya apakah APE punya tujuan praktis. Apakah ada pihak yang benar-benar mendistribusikan satu berkas executable yang harus berjalan di banyak sistem operasi?
lnav sendiri tidak dibangun sebagai APE, tetapi agen tertanam di dalamnya dibangun sebagai APE. Saat pengguna ingin membaca log dari host tersebut, agen itu dikirim ke host jarak jauh
Dengan begitu tidak perlu langkah tambahan untuk mendeteksi jenis sistem operasi atau menyertakan beberapa versi executable. Ada tulisan singkat terkait di sini: https://lnav.org/2021/05/03/tailing-remote-files.html
Program yang dikompilasi statis memang menjadi fondasi semuanya, tetapi di level aplikasi jumlahnya minoritas
APE mengajukan pertanyaan besar: “Jika bahasa AOT tingkat rendah seperti C bisa dikompilasi sekali lalu dijalankan di mana saja, siapa yang masih butuh mesin virtual?”
Selain itu, cross-compilation itu menyakitkan. Proyek open source kecil yang tidak punya build farm dengan semua BSD sering kali sama sekali tidak mendistribusikan biner untuk platform seperti itu
Bahkan kubectl punya biner untuk Linux, tetapi tidak untuk BSD. Kecuali jika OSX dihitung sebagai BSD. Jadi untuk proyek seperti itu, ini terlihat seperti keuntungan yang mudah didapat
Terlalu besar untuk dibangun secara ad hoc di mesin pengguna. Ada banyak dependensi C++ pihak ketiga yang besar, dan waktu build berkisar dari 2 hingga 20 menit tergantung perangkat keras
Harus berjalan di macOS x86-64+ARM, Linux x86-64, dan Windows x86-64. Akan bagus jika ARM Linux juga didukung
Saat ini sedang mempertimbangkan WASM, tetapi itu memerlukan runtime WASI yang terpasang
Untuk yang bingung: “Actually Portable Executables” https://justine.lol/ape.html
Mengganti
switchmenjadiifterasa cukup kasar. Sepertinya ini bisa diperbaiki dengan memetakan ke himpunan konstanta netral platform, lalu melakukan dereference simbol runtime sebagai fungsiMisalnya ditulis seperti
switch(CosmoMappErrno(EINVAL))dancase COSMO_EINVAL:sehingga kode tidak perlu diubah menjadi gumpalan gotoTentu, lain ceritanya jika yang dimaksud adalah compiler mengenali switch seperti ini dan, alih-alih menuliskannya ulang menjadi pohon if, mengubah label menjadi konstanta khusus cosmo untuk tiap nilai, lalu membungkus nilai input switch dengan pemanggilan fungsi yang memetakan nilai platform runtime saat ini ke konstanta khusus cosmo yang sesuai. Nilai lain dibiarkan lewat tanpa perubahan
Itu malah bisa jadi transformasi compiler yang lebih sederhana. Hanya saja muncul kompleksitas untuk mengenali himpunan konstanta mana yang sedang dipakai, lalu menerapkan pemanggilan pemetaan yang tepat pada input switch. Di sisi library juga perlu membuat pemetaan seperti itu, jadi kompleksitasnya memang ada
Sudah ada implementasi yang bekerja dengan metode transformasi lain, jadi saya tidak tahu apakah penulisnya ingin mengimplementasikan ini
Terakhir, pendekatan pemetaan seperti ini berisiko salah memetakan, dan juga ada kerumitan seperti harus membalik input sebelum pemetaan untuk kode yang memakai nilai errno negatif
Alasan saya menyukai array if-else-goto adalah karena itu sangat pas dengan bagian lain di GCC. Jika melihat patch saya[1], Anda bisa melihat bahwa untuk menambahkan fitur ini saya hanya mengubah sangat sedikit kode yang sudah ada di GCC
[1]: https://github.com/ahgamut/gcc/tree/portcosmo-11.2
Saya tidak mengerti kenapa orang begitu terkejut dengan kode yang memakai
SIGxxxsebagai indeks. Nomor sinyal itu terdefinisi dengan baik, umumnya berurutan, dan diketahui mulai dari 1Selain itu, error itu muncul bersama error compiler dari patch saya yang butuh waktu lama untuk saya debug, jadi itu menjadi kejutan yang sangat membekas
Saya penasaran apakah pola kode seperti itu dianggap umum. Saya juga ingin tahu codebase lain mana yang memilikinya selain busybox. Kalau contohnya memang banyak, mungkin saya bisa meluangkan waktu untuk memperbarui patch agar menangani pola seperti itu
Kalau errno memang masalah sebesar itu, saya tidak tahu bagaimana harapan untuk menjalankan pustaka standar C++ dan std::error_code
Di sana tidak ada cara untuk memakai makro ajaib seperti itu
std::error_codebukan konstanta, jadi sepertinya tidak akan mengalami masalah yang sama