3 पॉइंट द्वारा GN⁺ 2024-07-01 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • C सरल और अभिव्यक्तिपूर्ण है, लेकिन सिर्फ switch स्टेटमेंट और लेबल नियमों से भी ऐसा कोड बनाया जा सकता है जो अनुभवी डेवलपर्स को भी चौंका दे
  • IOCCC-शैली का obfuscation मैक्रो और formatting से कोड को छिपाता है, लेकिन सिर्फ C के अपने grammar से भी पढ़ा जा सकने वाला, फिर भी अजनबी कोड संभव है
  • switch (...) में braces अनिवार्य नहीं हैं, और यह मिलते हुए case लेबल पर jump करता है, इसलिए यह सामान्य block initialization flow से अलग तरह से काम कर सकता है
  • case लेबल को switch block के सबसे ऊपरी स्तर पर ही होना जरूरी नहीं है, इसलिए if (0) case... और else if chain को मिलाकर braces के बिना switch भी compile हो जाता है
  • GNU C का && label-address extension custom switch या label-आधारित loop तक संभव बनाता है, लेकिन कुछ उदाहरण सिर्फ GCC-विशिष्ट हैं और undefined behavior के लिहाज़ से सुरक्षित नहीं हो सकते

C grammar से बनने वाला अजीब कोड

  • C में कई कमियाँ हैं, लेकिन इसकी सरल syntax और अभिव्यक्तिपूर्ण प्रकृति की वजह से यह operating system जैसे बड़े software लिखने के लिए इस्तेमाल की जाती है
  • इसकी संक्षिप्त syntax ने Java से Go तक कई mainstream बाद की भाषाओं की code structure पर असर डाला है
  • code obfuscation का एक प्रतिनिधि उदाहरण IOCCC है
    • IOCCC के विजेता code में अक्सर preprocessor macro, असामान्य formatting, बेकार variable name और उलझे हुए arithmetic expression होते हैं
    • ऐसा code प्रभावित तो करता है, लेकिन क्योंकि उसे सामान्य code में reverse-engineer करना पड़ता है, इसलिए सीखने के लिहाज़ से वह उतना मज़ेदार नहीं भी हो सकता

switch, case, goto से दिखने वाले C के गहरे कोने

  • switch (...) को if (...) या for (...) की तरह braces के बिना लिखा जा सकता है
    • switch (i) case 1: puts("i = 1"); compile हो जाता है
    • braces न हों तो सिर्फ एक statement ही switch से जुड़ता है, इसलिए उसके बाद आने वाला case 2: अब switch के अंदर नहीं रहता और error बन जाता है
  • switch मूल रूप से मिलते हुए case लेबल पर जाने वाली goto के काफ़ी करीब की संरचना है
    • switch block के अंदर int a = 123; और puts(...) होने पर भी, अगर jump default: पर होता है, तो उससे पहले का initialization code नहीं चलता
    • इस स्थिति में a का initialization 123 से नहीं होता, और तकनीकी रूप से यह undefined behavior है
    • उदाहरण Godbolt पर देखा जा सकता है
  • case लेबल का जुड़ा हुआ switch block के सबसे ऊपरी स्तर पर होना जरूरी नहीं है
    • if (0) case 0: puts("i = 0"); जैसी संरचना भी काम करती है
    • क्योंकि switch सीधे उस case पर jump करता है, इसलिए उससे पहले का if (0) छोड़ दिया जाता है
    • उसके बाद puts(...) के बाद आने वाले दूसरे output statement अब भी if (0) शर्त से रुके रहते हैं, इसलिए break के बिना भी fallthrough से बचा जा सकता है
    • उदाहरण Godbolt पर देखा जा सकता है
  • if ... else chain व्याकरण की दृष्टि से एक ही top-level statement की तरह काम करती है, इसलिए इसे switch के साथ जोड़कर braces के बिना अजीब switch बनाया जा सकता है
    • उदाहरण में case 1 ... 10 range case और default भी शामिल हैं
    • उदाहरण Godbolt पर देखा जा सकता है
  • GNU extension && operator से label का address लिया जा सकता है, और उस address पर goto किया जा सकता है
    • इसका उपयोग करके goto *(void*[]){ &&case_0, &&case_1, &&case_2 }[i]; जैसी शैली में custom switch लागू किया जा सकता है
    • उदाहरण Godbolt पर देखा जा सकता है
  • इसी GNU extension का उपयोग करके for (...) के बिना भी variable declaration के अंदर label-आधारित loop लागू किया जा सकता है
    • उदाहरण में i = 0 से i = 5 तक print करने वाला loop labels और goto *&&_ से बनाया गया है
    • उदाहरण Godbolt पर देखा जा सकता है
    • यह आख़िरी snippet सिर्फ GCC के लिए है, और संभव है कि यह undefined behavior के लिहाज़ से सुरक्षित न हो
  • C में बिना जानबूझकर code को obfuscate किए भी पूरी तरह अजनबी और भ्रमित करने वाले रूप बनाए जा सकते हैं

1 टिप्पणियां

 
GN⁺ 2024-07-01
Hacker News टिप्पणियां
  • ऊपर वाले उदाहरण में कहा गया था कि a की वैल्यू प्रिंट होती है लेकिन वह 123 से initialize नहीं होती, पर C में असल में ऐसा हो भी सकता है
    uninitialized variable का इस्तेमाल करने का मतलब “उस memory में संयोग से बची हुई वैल्यू” पढ़ना नहीं, बल्कि undefined behavior है, इसलिए compiler जो चाहे कर सकता है
    उदाहरण के लिए वह उस memory को हमेशा 123 से initialize कर सकता है, या पूरे टुकड़े को undefined behavior मानकर सभी instructions हटा सकता है ताकि कुछ भी print न हो। इससे आगे, वह बाद वाला return या उससे पहले की instructions तक optimization में हटा सकता है, जिससे undefined behavior ऐसा लग सकता है जैसे वह “समय में पीछे जाकर” असर डाल रहा हो

    • C में undefined behavior सचमुच समय में पीछे नहीं जाता
      यह पिछली instructions को प्रभावित कर सकता है, लेकिन code reordering या जटिल transformations undefined behavior के बिना भी होते हैं
    • C में केवल uninitialized automatic storage duration object तक access करना undefined behavior है
      static objects हमेशा initialized होते हैं, इसलिए ऐसी स्थिति पैदा नहीं होती
      जो बचता है वह dynamic objects हैं, जैसे malloc से allocate किए गए struct के uninitialized members; लेकिन uninitialized dynamic memory पढ़ना C में undefined behavior नहीं है, बल्कि आपको uninitialized bits से बनने वाली वैल्यू मिलती है। अगर उस type में trap representation नहीं है, तो यह fail नहीं हो सकता
  • इस तरह का syntax भी संभव है
    switch(k) { if (0) case 0: x = 1; if (0) case 1: x = 2; if (0) default: x = 3; }
    इसे ऐसे switch जैसा बनाया जा सकता है जिसमें हर clause के आखिर में break लिखने की जरूरत न हो, और #define brkcase if (0) case जैसा macro भी संभव है। Compiler को control flow शायद पसंद न आए, लेकिन लगता है वह आम तौर पर इसे अच्छी तरह eliminate कर देगा

    • #define brkcase break;case भी शायद लगभग वैसे ही काम करे, लेकिन तब macro का मकसद थोड़ा खत्म हो जाता है
    • यह तरीका कैसे काम करता है, यह काफी अस्पष्ट है, इसलिए Duff's device-स्टाइल switch abuse के बजाय वही control flow goto से लिखना बेहतर लगेगा
    • यह तरीका तभी काम करता है जब case label की body एक line की हो या braces में wrapped हो
      पहले कभी मैंने यह structure इस अर्थ में इस्तेमाल किया था कि “अगले case label की सिर्फ पहली line skip करो और बाकी में वैसे ही fall through हो जाओ”
      अगर case labels को statements के बीच separators नहीं, बल्कि सिर्फ labels माना जाए, तो सब समझ में आता है
  • C में coroutines implement करने के लिए इस technique का इस्तेमाल किया जा सकता है: https://stackoverflow.com/questions/24202890/switch-based-co...

  • पता नहीं मुझे क्यों नहीं पता था कि case 1 ... 10: valid C है
    मैंने C कई सालों तक इस्तेमाल की है, और सोच रहा हूं कि यह किस standard में आया था

    • अगर यह हाल में standardized नहीं हुआ है, तो यह valid C नहीं बल्कि GNU extension है
    • यह GNU C extension लगता है: https://gcc.gnu.org/onlinedocs/gcc/Case-Ranges.html
      extension का इतिहास नहीं मिला, और मेरी जानकारी में यह standard C में नहीं है। clang को लेकर पक्का नहीं हूं
  • पहले मजे के लिए मैंने 10 से 1 तक countdown करने वाला अजीब C code लिखा था
    C, Python और shell versions सभी && और recursive calls का इस्तेमाल करके one-liner बनाए जा सकते हैं

    • छोटी-सी nitpick, लेकिन sys.stdout.write(f"{n}\n") को print(n) से बदला जा सकता है
      मौजूदा code, f-string को छोड़ दें तो, Python 2 style जैसा दिखता है जहां print statement था। Python 3 में print सामान्य function है और None return करता है जो false evaluate होता है, इसलिए पहले and को भी or से बदलना होगा
  • एक और हैरान करने वाली बात यह है कि 4[arr], arr[4] के बराबर है

    • Array pointer में convert हो जाता है, इसलिए मूल रूप से यह *(array_label+offset) है, और यहां यह *(offset+array_label) बन जाता है
      यानी *(arr+4) और *(4+arr) एक ही हैं
    • इसी principle से arr[i][j] और j[i[arr]] भी बिल्कुल समान हैं
      बस यह जानना काफी है कि a[x][y], (a[x])[y] जैसा है, और a[x], x[a] जैसा है
      arr[i][j](arr[i])[j](i[arr])[j]j[i[arr]]
  • लेख के आखिर का obfuscated code snippet एक और GCC extension दिखाता है: https://stackoverflow.com/questions/34559705/ternary-conditi...

  • इस blog author की ऐसी playful techniques मैंने पहले Twitter पर देखी थीं
    switch statement से loop भी बनाया जा सकता है: https://twitter.com/lcamtuf/status/1807129116980007037

  • लगता है ऐसी switch शरारतें Duff's device का अहम हिस्सा हैं

    • Duff's device इस बात पर निर्भर करता है कि K&R C syntax में switch block और loop को मिलाया जा सकता है, case explicit break न करने पर fall through करता है, और C में loop वापस switch के अंदर jump कर सकता है
      Duff memory-mapped I/O (MMIO) optimize करना चाहते थे, और आज की C में भी यह काम इस तरह नहीं किया जाएगा। अब MMIO, CPU instruction speed जितना तेज नहीं है, और data थोड़ा भी ज्यादा हो तो DMA इस्तेमाल किया जा सकता है
      Modern languages शायद MMIO को सिर्फ pointer indirection की तरह treat भी नहीं करेंगी, लेकिन C ने इसे बनाए रखने के लिए type system में लगातार escape hatches जोड़े हैं
      व्यक्तिगत तौर पर मुझे Tom Duff के “Device” की विरासत WUFFS का iterate loops mechanism लगता है। इसमें loop के N steps को आंशिक रूप से unroll करने का तरीका specify किया जाता है, और promise किया जाता है कि result main loop body को N बार चलाने जैसा ही होगा, लेकिन तेज हो सकता है। Vectorization के लिए intent समझना आसान हो जाता है, और M % N != 0 जैसे झंझट भरे edge cases को इंसान नहीं बल्कि tool सही तरीके से handle करता है