Actually Portable Executables बिल्ड के लिए GCC को पैच करना
(ahgamut.github.io)- Cosmopolitan Libc के साथ अधिक मौजूदा C सॉफ़्टवेयर बिल्ड करने के लिए केवल build system को धोखा देना काफ़ी नहीं था; इसके लिए लगभग 2,000 लाइनों का GCC पैच चाहिए था, ताकि compiler समस्या वाले कोड को अपने-आप बदल सके
- सबसे बड़ी रुकावट यह थी कि
SIGTERM,EINVALजैसे system values compile-time constants नहीं थे, इसलिए वे C केswitch caselabels औरstatic/const structinitialization नियमों से टकराते थे sed, Python regex, GCC plugin, यहाँ तक कि preprocessor macro interception तक आज़माया गया, लेकिनcaselabel errors parsing के दौरान आते थे, इसलिए केवल plugin से उन्हें रोकना मुश्किल था- अंतिम पैच
-fportcosmoflag से चालू होता है, और GCC error देने से ठीक पहलेswitchकोif/gotoमें बदल देता है, जबकि struct initialization के लिए runtime पर values भरने का रास्ता अपनाता है - anonymous struct,
enum, कुछconst int, और initialization expressions के भीतर array index जैसे कुछ limits अभी भी हैं, लेकिन इससेbash,curl,git,ninja,gccजैसे सॉफ़्टवेयर को source बदले बिना बिल्ड करने का रास्ता खुल जाता है
GCC पैच का लक्ष्य
- Cosmopolitan Libc के साथ Actually Portable Executables को ज़्यादा आसानी से बनाने के लिए लगभग 2,000 लाइनों का
gccपैच लिखा गया - पैच लागू होने के बाद
./configureयाcmakebuild system के साथ निम्न सॉफ़्टवेयर बिल्ड किए जा सकते हैंbashcurlgitninjagccखुद
- बने हुए executable Linux, FreeBSD, MacOS, OpenBSD, NetBSD और Windows पर भी चलने चाहिए
- हालाँकि Windows पर चलने की सीधे जाँच नहीं की गई थी
- GitHub Actions से बने binaries superconfigure releases से डाउनलोड किए जा सकते हैं
Cosmopolitan Libc पोर्टिंग में अटका हुआ हिस्सा
- 2021 में
redbeanweb server के कारण Cosmopolitan Libc पर ध्यान गया, और उसके बाद यह अहम सवाल बना कि मौजूदा C सॉफ़्टवेयर इस libc पर कितना अच्छी तरह चलता है - Lua, Wren, Janet,
quickjsजैसे language runtimes को port करते समय वही pattern बार-बार दिखा SIGTERM,EINVALजैसे system constants कोswitchstatement केcaselabel में इस्तेमाल करने पर C compilation नियमों से टकराव होता था- C standard कहता है कि
caselabels compile-time constants होने चाहिए- Cosmopolitan Libc में system interface से जुड़े C preprocessor macros का symbolic होना ज़रूरी था
- नतीजे में
switch(errno) { case EINVAL: ... }जैसा आम कोड compile नहीं हो पाता था
switch को if और goto में बदलना
- अगर
switch(errno)के अंदरcase EINVAL,case ENOSYSजैसे labels compile-time constants न हों, तो GCCcase label does not reduce to an integer constanterror देता है - पैच
switchstatements को अपने-आप इस तरह बदल देता है- हर
casevalue के लिएif (errno == EINVAL) goto caselabel_EINVAL;जैसे branch जोड़ना defaultlabel तक जाने वालाgotoजोड़ना- मूल
caseblocks को labels औरgoto endofthis_switchके साथ फिर से बनाना
- हर
- यह तरीका सबसे elegant नहीं है, लेकिन कई codebases में दिखे मामलों को संभाल सका
ifdefऔर fallthrough मिले-जुलेswitchको भी संभालना था, इसलिएgotoआधारित बदलाव एक सीधा विकल्प था
struct initialization पर भी वही पाबंदी लागू होती है
staticयाconst structinitialization में भी compile-time constant की वही समस्या आती है- अगर
EINVALcompile-time constant नहीं है, तो नीचे दिए गए कुछ initializations C में वैध नहीं रहते- function के अंदर सामान्य local
structinitialization संभव है const structstatic struct- global
const struct - global
static struct
- function के अंदर सामान्य local
- इसका एक वास्तविक उदाहरण CPython के
faulthandlerमॉड्यूल में मिलता है - समाधान यह था कि पहले struct को dummy values से initialize किया जाए, फिर इस्तेमाल से पहले सही values भरी जाएँ
- function के अंदर
ifstatement जोड़ा जाता है - global या static initialization के लिए
__attribute__((constructor))का उपयोग होता है
- function के अंदर
टेक्स्ट रिप्लेसमेंट और GCC plugin की सीमाएँ
- शुरुआत में
sedshell script से transformation automate करने की कोशिश हुई, फिर Python script और regex से fallthrough तक संभालने की कोशिश की गई - लेकिन C preprocessor और
ifdefकी वजह से text replacement पूरी तरह भरोसेमंद नहीं था- क्योंकि compile time पर कौन-सा
ifdefसक्रिय होगा, यह पहले से जानना मुश्किल होता है
- क्योंकि compile time पर कौन-सा
- अगला प्रयास GCC plugin के जरिए AST transformation का था
- GCC plugins को
-fplugin=your-plugin.soसे लोड किया जा सकता है PLUGIN_PRE_GENERICIZE,PLUGIN_FINISH_PARSE_FUNCTION,PLUGIN_FINISH_DECLजैसे events पर AST तक पहुँचा जा सकता हैdebug_treeसे AST प्रिंट किया जा सकता है, औरwalk_tree_without_duplicatesसे AST traverse किया जा सकता है
- GCC plugins को
- समस्या यह थी कि
caselabel validation GCC parsing के दौरान ही हो जाती है- plugin को AST तक पहुँच parsing खत्म होने के बाद मिलती है
- गलत
case SIGTERM:label AST में बचता ही नहीं - GCC पहले ही error दे चुका होता है, इसलिए plugin से उसे ठीक करना मुश्किल था
preprocessor macro interception प्रयोग
- केवल plugin से parsing से पहले वाली errors रोकना मुश्किल था, इसलिए C preprocessor के साथ इंटरैक्ट करने का तरीका आज़माया गया
- GCC plugin headers की
cpp_readerstructure के जरिए macro के#define,#undef, और इस्तेमाल होने पर callbacks लगाए जा सकते थे - Cosmopolitan Libc के system value macro structure का इस्तेमाल कर अस्थायी constants डाले गए
- उदाहरण:
extern const int SIGTERM; - उदाहरण:
#define SIGTERM SYMBOLIC(SIGTERM) - plugin के लिए
static const int __tmpcosmo_SIGTERM = ...जैसे temporary values रखे गए
- उदाहरण:
- plugin
SYMBOLIC(SIGTERM)के इस्तेमाल को intercept करके उसे temporary value से बदल देता था, और बाद मेंPLUGIN_PRE_GENERICIZEचरण में AST को फिर सहीVAR_DECLमें बदल देता था - यह macro hack
switchऔरstructinitialization errors को बायपास करने में सफल रहा - इस तरीके से कम-से-कम CPython 3.11 को भी बिल्ड करना संभव हो गया
plugin की जगह GCC को ही बदलने का कारण
- Justine Tunney के साथ चर्चा के बाद, जब यह स्पष्ट हुआ कि समस्या हल की जा सकती है, तब GCC codebase को सीधे modify करने का सरल तरीका सुझाया गया
- plugin और macro hack में बहुत-सा अनावश्यक काम था, और edge cases व crashes को संभालना जटिल था
- उदाहरण के लिए, अगर सिर्फ
case ENOSYSको बदलना होता, तब भी macro replacement की वजह से plugin कोENOSYS + 1याprintf("ENOSYS = %d", ENOSYS)जैसे सामान्य उपयोग भी संभालने पड़ते थे - GCC patch approach में अलग
-fpluginकी जगह-fportcosmoflag से यह सुविधा चालू होती है - GCC जब
case is not constanterror देने ही वाला होता है, उससे ठीक पहले वहflag_portcosmoके सक्रिय होने की जाँच करता है, और अगर यह चालू हो तो ज़रूरी transformation कर देता है - पुराने plugin का AST transformation code GCC के भीतर इस तरह शिफ्ट किया गया कि वह दूसरे plugin callbacks चलने से पहले execute हो सके
- इस बदलाव से macro से जुड़ा hacky code हटाया जा सका
वास्तविक बिल्ड और पैच का विस्तार
- 5 जून 2023 तक patched GCC ने पहले के plugin test cases सब पास कर लिए थे, और Cosmopolitan Libc monorepo में नए binaries जोड़ दिए गए थे
- अधिक कोड compile करते हुए fixes और improvements जारी रहे
luaबिल्ड करना आसान हो गया, औरg++मेंcase constanterrors आने वाली जगहें ढूँढकरninjaभी बिल्ड किया जा सका- Python 3.11 के साथ
ncursesजोड़ने की कोशिश में एक नई समस्या मिलीncursesमेंDATA2(PARODD | PARENB, PARODD)जैसा कोड था, जहाँ struct initialization value में binary expression मौजूद थी- पुराना patch साधारण constant initialization संभाल लेता था, लेकिन
PARODD | PARENBजैसी expressions नहीं संभाल पाता था
- बाद में patch को बढ़ाकर C के
caselabels और struct initialization elements में arbitrary expressions को भी संभालने लायक बनाया गया- C standard के हिसाब से
caselabels अब भी compile-time constants ही होने चाहिए - patched compiler ऐसे उपयोग पर warning देता है, लेकिन compilation जारी रखता है
- C++ को
g++11केconstexprhandling से मेल न खाने के कारण arbitrary expression replacement से बाहर रखा गया
- C standard के हिसाब से
- इस बदलाव के बाद
ncursesबिना समस्या के बिल्ड हो गया
बची हुई सीमाएँ और व्यावहारिक निष्कर्ष
- यह patch पूरी तरह perfect नहीं है
- कुछ anonymous structs को यह संभाल नहीं पाता
enumऔरconst intके कुछ मामलों को यह नहीं संभाल पाता- initialization expressions में
SIGTERMको array index की तरह इस्तेमाल करने वाले मामले नहीं संभलते - अजीब
staticinitialization याSIGTERMकोstatic const int8_tमें रखने पर कभी-कभी compiler crash अब भी हो सकता है
- फिर भी कई साफ़ counterexamples हटाए जा चुके हैं, और कई लोकप्रिय सॉफ़्टवेयर अब सहजता से बिल्ड हो जाते हैं
- अधिक सख़्त test environments आगे और सुधार की जगह दिखा सकते हैं
- चाहें तो
switchstatements औरstructinitializations को हाथ से भी ठीक किया जा सकता है, लेकिन कई मामलों में compiler यह काम अपने-आप कर सकता है - अगर कोई C सॉफ़्टवेयर statically build हो सकता है, और खासकर
muslके साथ भी बिल्ड होता है, तो उसके Cosmopolitan Libc के साथ भी बिल्ड होने की अच्छी संभावना है
3 टिप्पणियां
अब क्या neo मैसेज को code block में लपेटने वाले फ़ॉर्मैट में जा रहा है? horizontal scroll करना पड़ता है, और लाल रंग वाले text color भी थोड़ा निराशाजनक हैं...
आह, code block में wrap हो जाना एक bug है T_T
Hacker News की रायें
मैंने यह लेख लिखा था, लेकिन शीर्षक “Patching GCC to build Actually Portable Executables” होना चाहिए। क्योंकि यह Cosmopolitan Libc और jart के Actually Portable Executable फॉर्मैट की ओर इशारा करता है
मेरे GCC patch से अब vim, emacs, ninja, bash, git, gcc जैसे software को Cosmopolitan Libc के साथ, सामान्य autotools/cmake-style build system के ज़रिए build किया जा सकता है। Build हुए executable files Linux, FreeBSD, MacOS, OpenBSD, NetBSD, Windows पर चलने चाहिए। हालांकि Windows पर अभी test नहीं किया है
इस technique से जिन software को build करके देखा है, उनकी list यहां है: https://github.com/ahgamut/superconfigure
superconfigure script बस एक wrapper है जो software build करने में इस्तेमाल होने वाली सामान्य configure script को wrap करती है, और
--enable-staticजैसे flags जोड़ देती हैअगर Cosmopolitan Libc के साथ GCC build करके देखना चाहते हैं, तो यह repository इस्तेमाल कर सकते हैं: https://github.com/ahgamut/musl-cross-make/tree/gccbuild
उम्मीद है HN पर आपके साथ बेहतर व्यवहार होगा
उदाहरण के लिए, glibc version की तुलना में कोई regression नहीं है, यह साबित करने लायक पर्याप्त testing हुई थी या नहीं, यह जानना चाहूंगा
Go repository में इससे जुड़ा एक issue है: https://github.com/golang/go/issues/51900
कुल मिलाकर यह काफी शानदार है, लेकिन इसका theoretical background जानना चाहूंगा। अगर नया operating system target बनाया जा रहा है, तो logically नए constants define करना सही लगता है
हर underlying operating system के लिए उसके constants को वैसे ही pass करने के बजाय, हर operating system के constants को हर जगह समान बनाने के लिए transform किया जा सकता है। तब constants को array index या case statement में इस्तेमाल करने वाला code भी ठीक से काम करेगा
लेख में बताए गए approach की तुलना में overhead ज्यादा हो सकता है, लेकिन यह कहीं आसान है और Cosmopolitan libc पर नया program compile करने वाले लोगों के लिए भी इस्तेमाल में सरल लगता है
constants की list यहां देख सकते हैं: https://github.com/jart/cosmopolitan/blob/master/libc/sysv/c...
इनमें से किसी भी constant का इस्तेमाल होने पर कई constants को बड़े lookup table के रूप में binary में रखना पड़ेगा, और program में जब भी check की जरूरत होगी, उस table से होकर गुजरना पड़ेगा। शायद यह बहुत धीमा न हो, लेकिन निश्चित रूप से महसूस होगा
लक्ष्य यह था कि libc या port किए जाने वाले software के source code में ज्यादा बदलाव किए बिना porting आसान बने, और performance में समान या बेहतर binary बने। ऐसी constraints में यह GCC patch process को सरल बनाने का सबसे अच्छा तरीका लगा
अगर
SIGHUPको array index initialization में इस्तेमाल करने वाले codebase काफी संख्या में मिले, तो आपके सुझाए तरीके को आजमाकर trade-off measure करने का सोचूंगा। या आप खुद कोशिश करके बता सकते हैं कि अलग constants set बेहतर है या नहींअगर “स्पष्ट counterexamples हटाने में काफी समय लगाया गया और बहुत सारे popular software smoothly build होते हैं”, तो successfully compile होने वाले software की list publish करना मददगार होगा। शायद पहले से मौजूद भी हो
superconfigure script बस एक wrapper है जो software build करने में इस्तेमाल होने वाली सामान्य configure script को wrap करती है, और
--enable-staticजैसे flags जोड़ देती हैसोच रहा हूँ कि APE का कोई व्यावहारिक उद्देश्य है या नहीं। क्या सच में कहीं ऐसा single executable deploy किया जाता है जिसे कई operating systems पर चलना हो?
lnav खुद APE के रूप में build नहीं होता, लेकिन उसके अंदर embedded agent APE के रूप में build होता है। जब user उस host के logs पढ़ना चाहता है, तो वह agent remote पर भेजा जाता है
इससे operating system की type अलग से detect करने या executable के कई versions शामिल करने वाला extra step नहीं चाहिए। इससे जुड़ा छोटा लेख यहाँ है: https://lnav.org/2021/05/03/tailing-remote-files.html
Statically compiled programs हर चीज़ की बुनियाद में हैं, लेकिन application level पर वे कम हैं
APE बड़ा सवाल उठाता है: “अगर C जैसी low-level ahead-of-time compiled (AOT) language एक बार compile होकर कहीं भी चल सकती है, तो virtual machine की जरूरत किसे है?”
ऊपर से cross compile करना दर्दनाक है। जिन छोटे open source projects के पास सभी BSD variants वाला build farm नहीं है, वे अक्सर उन platforms के लिए binaries publish ही नहीं करते
kubectl तक के पास Linux binaries हैं, लेकिन BSD के लिए नहीं—अगर OSX को BSD न मानें तो। इसलिए ऐसे projects के लिए यह आसान फायदा लगता है
User की machine पर on-the-fly build करने के लिए यह बहुत बड़ा है। इसमें कई बड़े third-party C++ dependencies हैं, और hardware के हिसाब से build में 2 मिनट से 20 मिनट तक लगते हैं
इसे macOS x86-64+ARM, Linux x86-64, Windows x86-64 पर चलना है। ARM Linux भी चले तो अच्छा होगा
अभी WASM पर विचार कर रहे हैं, लेकिन उसके लिए installed WASI runtime चाहिए
जिन्हें confusion हो उनके लिए: “Actually Portable Executables” https://justine.lol/ape.html
switchकोifमें बदलना काफ़ी crude है। इसे platform-neutral constants के set में map करके और runtime symbol को function से dereference करके शायद ठीक किया जा सकता हैउदाहरण के लिए
switch(CosmoMappErrno(EINVAL))औरcase COSMO_EINVAL:जैसा इस्तेमाल करें, तो code को goto के ढेर में बदलने की जरूरत नहीं होगीहाँ, अगर मतलब यह है कि compiler ऐसे switch को पहचानकर if tree में rewrite करने के बजाय, हर value के लिए label को cosmo-specific special constant से बदल दे, और switch input value को current runtime platform value से उस cosmo-specific constant में map करने वाले function call से wrap कर दे—और बाकी values को बिना बदले pass कर दे—तो बात अलग है
वह सच में अधिक simple compiler transformation भी हो सकता है। बस complexity यह होगी कि कौन-सा constants set इस्तेमाल हो रहा है, यह पहचानना पड़ेगा, और switch input value पर सही mapping call apply करनी होगी। Library side पर भी ऐसी mappings बनानी पड़ेंगी, इसलिए complexity चाहिए
चूँकि एक implementation पहले से दूसरे transformation तरीके से काम कर रहा है, पता नहीं author इसे implement करना चाहेंगे या नहीं
आखिर में, इस mapping approach में गलत mapping का risk है, और negative errno values इस्तेमाल करने वाले code में mapping से पहले input को invert करना पड़ेगा—ऐसी tricky बातें भी हैं
if-else-goto array मुझे इसलिए पसंद है क्योंकि यह GCC के बाकी हिस्सों के साथ बिल्कुल fit बैठा। मेरा patch[1] देखें तो पता चलेगा कि यह feature जोड़ने के लिए GCC के existing code में बहुत थोड़े बदलाव किए गए हैं
[1]: https://github.com/ahgamut/gcc/tree/portcosmo-11.2
SIGxxxको index के रूप में इस्तेमाल करने वाले code पर इतना हैरान होने की वजह समझ नहीं आती। Signal numbers well-defined हैं, आम तौर पर consecutive होते हैं और 1 से शुरू माने जाते हैंऊपर से वह error मेरे patch के compiler error के साथ आया, जिसे debug करने में काफी समय लगा, इसलिए वह याद रहने वाली हैरानी थी
जानना चाहूँगा कि क्या आपको ऐसा code pattern common लगता है। busybox के अलावा किन codebases में है, यह भी जानना चाहूँगा। अगर examples ज्यादा हों तो शायद ऐसे patterns handle करने के लिए patch update करने में समय लगाऊँ
अगर errno इतना बड़ा problem है, तो समझ नहीं आता कि C++ standard library और std::error_code को चलाने की क्या उम्मीद है
वहाँ magical macros इस्तेमाल करने का कोई तरीका नहीं है
std::error_codeconstant नहीं है, इसलिए लगता है वही problem नहीं होगी