3 पॉइंट द्वारा GN⁺ 2023-07-15 | 3 टिप्पणियां | WhatsApp पर शेयर करें
  • Cosmopolitan Libc के साथ अधिक मौजूदा C सॉफ़्टवेयर बिल्ड करने के लिए केवल build system को धोखा देना काफ़ी नहीं था; इसके लिए लगभग 2,000 लाइनों का GCC पैच चाहिए था, ताकि compiler समस्या वाले कोड को अपने-आप बदल सके
  • सबसे बड़ी रुकावट यह थी कि SIGTERM, EINVAL जैसे system values compile-time constants नहीं थे, इसलिए वे C के switch case labels और static/const struct initialization नियमों से टकराते थे
  • sed, Python regex, GCC plugin, यहाँ तक कि preprocessor macro interception तक आज़माया गया, लेकिन case label errors parsing के दौरान आते थे, इसलिए केवल plugin से उन्हें रोकना मुश्किल था
  • अंतिम पैच -fportcosmo flag से चालू होता है, और 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 या cmake build system के साथ निम्न सॉफ़्टवेयर बिल्ड किए जा सकते हैं
    • bash
    • curl
    • git
    • ninja
    • gcc खुद
  • बने हुए executable Linux, FreeBSD, MacOS, OpenBSD, NetBSD और Windows पर भी चलने चाहिए
    • हालाँकि Windows पर चलने की सीधे जाँच नहीं की गई थी
  • GitHub Actions से बने binaries superconfigure releases से डाउनलोड किए जा सकते हैं

Cosmopolitan Libc पोर्टिंग में अटका हुआ हिस्सा

  • 2021 में redbean web server के कारण Cosmopolitan Libc पर ध्यान गया, और उसके बाद यह अहम सवाल बना कि मौजूदा C सॉफ़्टवेयर इस libc पर कितना अच्छी तरह चलता है
  • Lua, Wren, Janet, quickjs जैसे language runtimes को port करते समय वही pattern बार-बार दिखा
  • SIGTERM, EINVAL जैसे system constants को switch statement के case label में इस्तेमाल करने पर C compilation नियमों से टकराव होता था
  • C standard कहता है कि case labels 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 न हों, तो GCC case label does not reduce to an integer constant error देता है
  • पैच switch statements को अपने-आप इस तरह बदल देता है
    • हर case value के लिए if (errno == EINVAL) goto caselabel_EINVAL; जैसे branch जोड़ना
    • default label तक जाने वाला goto जोड़ना
    • मूल case blocks को labels और goto endofthis_switch के साथ फिर से बनाना
  • यह तरीका सबसे elegant नहीं है, लेकिन कई codebases में दिखे मामलों को संभाल सका
  • ifdef और fallthrough मिले-जुले switch को भी संभालना था, इसलिए goto आधारित बदलाव एक सीधा विकल्प था

struct initialization पर भी वही पाबंदी लागू होती है

  • static या const struct initialization में भी compile-time constant की वही समस्या आती है
  • अगर EINVAL compile-time constant नहीं है, तो नीचे दिए गए कुछ initializations C में वैध नहीं रहते
    • function के अंदर सामान्य local struct initialization संभव है
    • const struct
    • static struct
    • global const struct
    • global static struct
  • इसका एक वास्तविक उदाहरण CPython के faulthandler मॉड्यूल में मिलता है
  • समाधान यह था कि पहले struct को dummy values से initialize किया जाए, फिर इस्तेमाल से पहले सही values भरी जाएँ
    • function के अंदर if statement जोड़ा जाता है
    • global या static initialization के लिए __attribute__((constructor)) का उपयोग होता है

टेक्स्ट रिप्लेसमेंट और GCC plugin की सीमाएँ

  • शुरुआत में sed shell script से transformation automate करने की कोशिश हुई, फिर Python script और regex से fallthrough तक संभालने की कोशिश की गई
  • लेकिन C preprocessor और ifdef की वजह से text replacement पूरी तरह भरोसेमंद नहीं था
    • क्योंकि compile time पर कौन-सा ifdef सक्रिय होगा, यह पहले से जानना मुश्किल होता है
  • अगला प्रयास 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 किया जा सकता है
  • समस्या यह थी कि case label 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_reader structure के जरिए 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 और struct initialization 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 की जगह -fportcosmo flag से यह सुविधा चालू होती है
  • GCC जब case is not constant error देने ही वाला होता है, उससे ठीक पहले वह 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 constant errors आने वाली जगहें ढूँढकर ninja भी बिल्ड किया जा सका
  • Python 3.11 के साथ ncurses जोड़ने की कोशिश में एक नई समस्या मिली
    • ncurses में DATA2(PARODD | PARENB, PARODD) जैसा कोड था, जहाँ struct initialization value में binary expression मौजूद थी
    • पुराना patch साधारण constant initialization संभाल लेता था, लेकिन PARODD | PARENB जैसी expressions नहीं संभाल पाता था
  • बाद में patch को बढ़ाकर C के case labels और struct initialization elements में arbitrary expressions को भी संभालने लायक बनाया गया
    • C standard के हिसाब से case labels अब भी compile-time constants ही होने चाहिए
    • patched compiler ऐसे उपयोग पर warning देता है, लेकिन compilation जारी रखता है
    • C++ को g++11 के constexpr handling से मेल न खाने के कारण arbitrary expression replacement से बाहर रखा गया
  • इस बदलाव के बाद ncurses बिना समस्या के बिल्ड हो गया

बची हुई सीमाएँ और व्यावहारिक निष्कर्ष

  • यह patch पूरी तरह perfect नहीं है
    • कुछ anonymous structs को यह संभाल नहीं पाता
    • enum और const int के कुछ मामलों को यह नहीं संभाल पाता
    • initialization expressions में SIGTERM को array index की तरह इस्तेमाल करने वाले मामले नहीं संभलते
    • अजीब static initialization या SIGTERM को static const int8_t में रखने पर कभी-कभी compiler crash अब भी हो सकता है
  • फिर भी कई साफ़ counterexamples हटाए जा चुके हैं, और कई लोकप्रिय सॉफ़्टवेयर अब सहजता से बिल्ड हो जाते हैं
  • अधिक सख़्त test environments आगे और सुधार की जगह दिखा सकते हैं
  • चाहें तो switch statements और struct initializations को हाथ से भी ठीक किया जा सकता है, लेकिन कई मामलों में compiler यह काम अपने-आप कर सकता है
  • अगर कोई C सॉफ़्टवेयर statically build हो सकता है, और खासकर musl के साथ भी बिल्ड होता है, तो उसके Cosmopolitan Libc के साथ भी बिल्ड होने की अच्छी संभावना है

3 टिप्पणियां

 
ryuheechul 2023-07-15

अब क्या neo मैसेज को code block में लपेटने वाले फ़ॉर्मैट में जा रहा है? horizontal scroll करना पड़ता है, और लाल रंग वाले text color भी थोड़ा निराशाजनक हैं...

 
xguru 2023-07-16

आह, code block में wrap हो जाना एक bug है T_T

 
GN⁺ 2023-07-15
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

    • जानना चाहूंगा कि इस patch के GCC upstream में जाने की संभावना है या नहीं। APE executable और cosmopolitan libc बेहद शानदार तकनीक हैं, इसलिए अच्छा होगा अगर इन्हें इस्तेमाल करना और आसान हो जाए
    • अच्छा है। जानना चाहूंगा कि Nim जैसी transpile होने वाली languages के पीछे इसे इस्तेमाल करने पर कोई समस्या आने की संभावना दिखती है या नहीं
    • जानना चाहूंगा कि क्या busybox build करने की भी कोशिश की है
    • हाल ही में lobste.rs पर देखा कि आपको jart का sockpuppet बताकर ban कर दिया गया था, अब जाकर समझ आया कि आप सच में अलग व्यक्ति हैं
      उम्मीद है HN पर आपके साथ बेहतर व्यवहार होगा
    • सिर्फ build हो जाना कोई बहुत बड़ी उपलब्धि नहीं है; ज्यादा दिलचस्प बात यह है कि वह वास्तव में चलता है या नहीं
      उदाहरण के लिए, 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 बनाने से compile करना बहुत आसान हो जाएगा, लेकिन निजी तौर पर मुझे लगता है कि runtime पर existing constants और नए constants के बीच conversion का overhead बहुत ज्यादा होगा
      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 करना मददगार होगा। शायद पहले से मौजूद भी हो

    • vim, emacs, ninja, bash, git, gcc जैसे software build किए जा सकते हैं। इस technique से build करके देखी गई list यहां है: https://github.com/ahgamut/superconfigure
      superconfigure script बस एक wrapper है जो software build करने में इस्तेमाल होने वाली सामान्य configure script को wrap करती है, और --enable-static जैसे flags जोड़ देती है
  • सोच रहा हूँ कि APE का कोई व्यावहारिक उद्देश्य है या नहीं। क्या सच में कहीं ऐसा single executable deploy किया जाता है जिसे कई operating systems पर चलना हो?

    • Logfile Navigator(https://lnav.org) में remote host से communicate करने वाले agent के तौर पर APE executable का इस्तेमाल होता है
      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
    • Java/Clojure/Kotlin programmers, .Net programmers, JS, Python, Ruby, Erlang और सभी virtual machine-based languages के programmers याद आते हैं
      Statically compiled programs हर चीज़ की बुनियाद में हैं, लेकिन application level पर वे कम हैं
      APE बड़ा सवाल उठाता है: “अगर C जैसी low-level ahead-of-time compiled (AOT) language एक बार compile होकर कहीं भी चल सकती है, तो virtual machine की जरूरत किसे है?”
    • उदाहरण के लिए maven repository जैसी जगहों पर, जहाँ एक ही id/version combination के लिए कई artifacts रखना झंझट भरा होता है, executable distribution थोड़ा आसान हो सकता है। PyPI में भी शायद ऐसी ही समस्या होगी
      ऊपर से cross compile करना दर्दनाक है। जिन छोटे open source projects के पास सभी BSD variants वाला build farm नहीं है, वे अक्सर उन platforms के लिए binaries publish ही नहीं करते
      kubectl तक के पास Linux binaries हैं, लेकिन BSD के लिए नहीं—अगर OSX को BSD न मानें तो। इसलिए ऐसे projects के लिए यह आसान फायदा लगता है
    • अपने आगे के side projects को इसी तरह release करने का इरादा है। client-only SPA के लिए भी Redbean executable build करता हूँ। यह idea कि ये चीज़ें कई systems पर बस चल जाती हैं, कमाल का है
    • एक shader compiler है जहाँ platform-independent, तुरंत चलने योग्य executable format मददगार होगा
      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

    • मैंने इसका जिक्र करने वाला comment जोड़ा था, लेकिन अभी तक बदला नहीं है। लगता है जल्द बदल जाएगा। क्या यहाँ @dang को call करना ठीक होगा?
  • switch को if में बदलना काफ़ी crude है। इसे platform-neutral constants के set में map करके और runtime symbol को function से dereference करके शायद ठीक किया जा सकता है
    उदाहरण के लिए switch(CosmoMappErrno(EINVAL)) और case COSMO_EINVAL: जैसा इस्तेमाल करें, तो code को goto के ढेर में बदलने की जरूरत नहीं होगी

    • लेकिन उससे unmodified code compile करने में मदद नहीं मिलेगी। Compiler के अंदर generate होने वाले goto की चिंता करने की जरूरत नहीं। Compiler तो वैसे भी जगह-जगह ऐसा करते हैं
      हाँ, अगर मतलब यह है कि 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 बातें भी हैं
    • runtime symbol को function से dereference करने वाला तरीका दिलचस्प लगता है। कोई working example दिखाएँ तो खुशी से try करूँगा
      if-else-goto array मुझे इसलिए पसंद है क्योंकि यह GCC के बाकी हिस्सों के साथ बिल्कुल fit बैठा। मेरा patch[1] देखें तो पता चलेगा कि यह feature जोड़ने के लिए GCC के existing code में बहुत थोड़े बदलाव किए गए हैं
      [1]: https://github.com/ahgamut/gcc/tree/portcosmo-11.2
    • क्या हमें इस बात की भी चिंता करनी चाहिए कि replacement automatically होता है या नहीं?
  • SIGxxx को index के रूप में इस्तेमाल करने वाले code पर इतना हैरान होने की वजह समझ नहीं आती। Signal numbers well-defined हैं, आम तौर पर consecutive होते हैं और 1 से शुरू माने जाते हैं

    • busybox try करने से पहले मैंने git, bash, gcc, curl जैसे कई codebases build किए थे, और ऐसा code नहीं देखा था
      ऊपर से वह 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 इस्तेमाल करने का कोई तरीका नहीं है

    • लगता है problem वही magical macros थे। आम तौर पर macros constants में expand होते हैं, लेकिन अब ऐसा नहीं है, इसलिए कुछ चीज़ें टूट गईं
      std::error_code constant नहीं है, इसलिए लगता है वही problem नहीं होगी