C में सब कुछ undefined behavior है
(blog.habets.se)- undefined behavior (UB) कोई compiler की दुर्भावनापूर्ण optimization नहीं है, बल्कि यह नियम है कि अगर code को मानक के अनुसार वैध माना जाए, तो असंभव execution path को handle करना ज़रूरी नहीं है
- साधारण न दिखने वाले C/C++ code में सिर्फ double-free या boundary के बाहर access ही नहीं, बल्कि alignment, casting, initialization और type mismatch जैसे सूक्ष्म UB भी व्यापक रूप से छिपे होते हैं
- unaligned
int*याstd::atomic<int>*access पर platform के हिसाब से SIGBUS, kernel fix-up, या सामान्य रूप से काम करता हुआ दिखने वाला परिणाम मिल सकता है, लेकिन standard के अनुसार यह पहले से ही UB है isxdigit()को signedcharदेना,floatकोintमें बदलना, याNULLऔर variadic arguments का गलत उपयोग करना जैसी आम code patterns भी आसानी से standard के बाहर चली जाती हैं- मौजूदा codebase को फेंका नहीं जा सकता, लेकिन LLM-आधारित UB detection और expert verification को मिलाकर बड़े पैमाने पर सुधार करना होगा; यह काम junior developers को सौंपने के लिए बहुत सूक्ष्म है
C/C++ में undefined behavior optimization की समस्या नहीं है
- undefined behavior (UB) का मतलब यह नहीं कि compiler developer की गलती का “दुरुपयोग” करता है, बल्कि यह कि वह मान सकता है कि program standard के अनुसार valid है
- इंसान को intent साफ़ दिखे, तब भी compiler stage या modules के बीच उस intent को व्यक्त करना मुश्किल हो सकता है
- compiler पर यह बाध्यता नहीं है कि वह code generation में उन special cases को संभाले जो “हो ही नहीं सकते”, और hardware समेत execution path में intent से अलग परिणाम आ सकते हैं
- optimization बंद करने से भी UB सुरक्षित नहीं हो जाता, और न ही इस बात की कोई गारंटी है कि वही behavior मौजूदा या भविष्य के compiler/architecture पर बना रहेगा
UB सिर्फ असामान्य code में नहीं होता
- double-free, use-after-free, object boundary के बाहर access, और uninitialized memory access जाने-पहचाने UB हैं, लेकिन industry भर में ये बार-बार दोहराए जाते हैं
- इससे भी अधिक सूक्ष्म और non-intuitive UB बहुत हैं, इसलिए साधारण दिखने वाला C/C++ code भी आसानी से standard के बाहर जा सकता है
- C23 standard में “undefined” शब्द 283 बार आता है, और जिन मामलों में बात साफ़ न कही गई हो लेकिन behavior फिर भी defined न हो, उन्हें जोड़ें तो दायरा और बड़ा हो जाता है
- साधारण से ऊपर के लगभग हर C/C++ code में कहीं न कहीं UB होता है, और इसे सिर्फ individual programmer की लापरवाही मानना कठिन है
unaligned object access
- नीचे जैसे
int*को dereference करने वाला function, pointer सही aligned न होने पर UB बन जाता हैint foo(const int* p) { return *p; } - alignment का मतलब अक्सर
sizeof(int)के गुणज वाले address से हो सकता है, लेकिन असली requirement platform और implementation के अनुसार बदल सकती है - Linux Alpha पर कुछ मामलों में kernel trap पकड़कर software के ज़रिए intended access की नकल कर सकता था, लेकिन दूसरे मामलों में program
SIGBUSसे crash हो सकता है - SPARC पर
SIGBUSआता है, जबकि x86/amd64 पर यह अक्सर बिना समस्या के चलता दिख सकता है या atomic read जैसा लग सकता है - ARM, RISC-V और future architectures पर परिणाम को सामान्यीकृत नहीं किया जा सकता, और भविष्य की architecture में ऐसे special registers भी हो सकते हैं जो
int*के निचले bits का उपयोग ही न करें - अगर compiler अलग load instruction इस्तेमाल करे, तो पहले kernel द्वारा fix-up किया जाने वाला access अब fix-up न हो सके
- compiler पर यह बाध्यता नहीं है कि वह unaligned pointer के लिए भी काम करने वाला assembly बनाए; वह access अपने-आप में ही UB है
atomic type भी गलत alignment पर पहले से UB है
- नीचे जैसे
std::atomic<int>*परstore()याload()call करने पर भी, अगर object सही aligned नहीं है, तो behavior UB हैvoid set_it(std::atomic<int>* p) { p->store(123); } int get_it(std::atomic<int>* p) { return p->load(); } - “क्या unaligned object पर यह operation atomic है?” यह सवाल standard के नज़रिए से बनता ही नहीं
- असली hardware पर atomicity समस्या हो सकती है, लेकिन standard के हिसाब से उससे पहले ही यह UB है
- अगर जिसे atomic पढ़ा जा रहा है वह object page boundary पार कर रहा हो, तो स्थिति और जटिल होती है, लेकिन निष्कर्ष “ठीक है” नहीं बल्कि UB ही रहता है
सिर्फ pointer बनाना भी समस्या हो सकता है
- unaligned pointer में dereference से पहले भी, किसी खास type के pointer में cast करना ही समस्या बन सकता है
bool parse_packet(const uint8_t* bytes) { const int* magic_intp = (const int*)bytes; // UB! int magic_raw = foo(magic_intp); // Probably crashes on SPARC. int magic = ntohl(magic_raw); // this is fine, at least. […] } - यहाँ समस्या
foo()call नहीं, बल्कि(const int*)bytescast है - standard के अनुसार compiler
int*के निचले bits को garbage collection या security tag bits जैसे अर्थ भी दे सकता है
isxdigit() को char देने की समस्या
- नीचे का code सरल दिखता है, लेकिन जिन architectures पर
charsigned है, वहाँ input value 0–127 रेंज से बाहर हो तो यह UB हो सकता हैbool bar(char ch) { return isxdigit(ch); } isxdigit()यह जाँचने वाला function है कि character hexadecimal digit है या नहीं, और यहEOFभी argument के रूप में ले सकता है- C23 7.4p1 के अनुसार
EOFएकintहै, और इससे यह निष्कर्ष निकलता है कि वह ऐसा value है जिसेunsigned charके रूप में represent नहीं किया जा सकता isxdigit()charनहीं बल्किintलेता है, औरcharसेintconversion संभव होने पर भी signedcharकी negative values समस्या पैदा करती हैं- C23 6.2.5 paragraph 20 के अनुसार
charsigned है या नहीं, यह implementation-defined है - नीचे जैसी implementation negative index के साथ अज्ञात memory पढ़ सकती है
int isxdigit(int c) { if (c == EOF) { return false; } return some_array[c]; } - अगर वह memory I/O-mapped क्षेत्र हो, तो यह सिर्फ random value या crash नहीं बल्कि hardware action भी trigger कर सकती है
- यह desktop OS application की तुलना में embedded system में अधिक संभव है, लेकिन user-space network drivers जैसी स्थितियों में सिर्फ user space होने से भी पर्याप्त सुरक्षा नहीं मिलती
float से int में cast करने की समस्या
- seconds वाले
floatको milliseconds वालेintमें बदलने का नीचे जैसा code आम है, लेकिन इसमें UB शामिल हैint milliseconds(float seconds) { int tmp = (int)(seconds * 1000.0); /* WRONG */ return tmp + 1; /* WRONG separately (signed overflow is UB) */ } - C23 6.3.1.4 कहता है कि finite real floating value को integer type में बदलते समय, अगर उसका integral part उस integer type में represent नहीं किया जा सकता, तो behavior undefined है
- non-finite values के लिए भी कोई स्पष्ट परिभाषा नहीं है, इसलिए वह भी UB है
floatकीINT_MAXसे तुलना करना भी सीधा नहीं हैfloatकोintमें cast करने पर वही UB हो सकता है जिससे बचना चाह रहे थेINT_MAXकोfloatमें cast करने पर यह निश्चित नहीं कि वह exactly represent होगा- अगर
INT_MAXfloatमें round होकर ऐसे value में बदल जाए जिसेintमें represent नहीं किया जा सकता, तो comparison representative नहीं रह जाती
- इसे सुरक्षित बनाने के लिए
isfinite()check,INT_MIN + 1000,INT_MAX - 1000जैसी margin comparison, और conversion के बाद addition से पहले extra checks चाहिएint milliseconds(float seconds) { const float ftmp = seconds * 1000.0f; if (!isfinite(ftmp)) { return 0; } if ((float)(INT_MIN + 1000) > ftmp) { return 0; } if ((float)(INT_MAX - 1000) < ftmp) { return 0; } const int tmp = (int)ftmp; if (INT_MAX == tmp) { return 0; } return tmp + 1; } - आप सिर्फ
floatकोintमें बदलना चाहते हैं, लेकिन सुरक्षित code बहुत लंबा हो जाता है
address 0 पर object और null pointer
- OS kernel या embedded code में address 0 पर object रखने की ज़रूरत पड़ सकती है
- लेकिन C standard के अनुरूप वास्तव में address 0 पर object रखने का कोई व्यावहारिक तरीका नहीं माना जा सकता
- C 6.3.2.3 में pointer में convert होने वाला integer constant 0 और
nullptr, “null pointer constant” हैं; यहाँ इन्हेंNULLकहा जा सकता है - C यह निर्धारित नहीं करता कि वास्तविक
NULLpointer machine address 0 की ओर इशारा करता है - C standard hardware नहीं बल्कि C abstract machine को संबोधित करता है, और
NULLतथा 0 की तुलना में सिर्फ इतना guaranteed है कि वे equal होंगे - यह equality इसलिए भी हो सकती है कि integer 0 platform के native
NULLvalue में convert हो, और वह value0xffffभी हो सकती है - null pointer को dereference करना, उसका value कुछ भी हो, UB है; यह C 3.4.3 का canonical example है
- इसलिए यह मानना सही नहीं कि
memset(&ptr, 0, sizeof(ptr));एकNULLpointer बना देगा - structure को zero-initialize करके उसके member pointers को
NULLमान लेना, व्यवहार में भी कई programmers के लिए समस्या बन सकता है - इतिहास में non-zero NULL pointer का उपयोग करने वाली machines भी रही हैं
address 0 पर function होने की धारणा की समस्या
- आधुनिक machines पर भले
NULLaddress 0 की ओर इशारा करे और वहाँ सचमुच कोई object या function मौजूद हो, C 6.3.2.3 कहता है किNULLकिसी भी object या function के बराबर नहीं होता - इसलिए नीचे का code UB है
void (*func_ptr)() = NULL; func_ptr(); - C के नज़रिए से इसका मतलब है “वहाँ कोई function नहीं है”, और compiler के भीतर इस intent को व्यक्त करने का कोई तरीका शायद न हो
- सिर्फ यह मान लेना कि वह all-zero bits वाले address पर call instruction emit करेगा, सही नहीं है
- 16-bit x86 पर तो यह भी स्पष्ट नहीं कि “all zero” का मतलब
0000:0000है याCS:0000
variadic arguments और type mismatch
execl()का आख़िरी argument pointer होना चाहिए, इसलिएNULLmacro या integer 0 को सीधे pass करना UB हो सकता हैexecl("/bin/sh", "sh", "-c", "date", NULL); /* WRONG */ execl("/bin/sh", "sh", "-c", "date", 0); /* WRONG */- सही रूप यह है कि उसे explicitly pointer type में cast किया जाए
execl("/bin/sh", "sh", "-c", "date", (char*)NULL); NULLmacro integer 0 के रूप में interpret हो सकता है, और variadic arguments में ज़रूरी type information नहीं जातीprintf()में भी format specifier और actual argument type match न करें तो UB हैuint64_t blah = 123; printf("%ld\n", blah); /* WRONG */uint64_tको print करने के लिएPRIu64का उपयोग करना चाहिएuint64_t blah = 123; printf("%"PRIu64"\n", blah);uid_tको print करने के लिएuintmax_tमें cast करकेPRIuMAXका उपयोग एक तरीका हो सकता है, लेकिनuid_tunsigned है या नहीं, यह भी निश्चित नहीं- सबसे खराब स्थिति में
-1की जगह कोई निरर्थक value print हो सकती है
0 से divide करना और security problem
- 0 से divide करना UB है, यह बात व्यापक रूप से जानी जाती है, लेकिन जब denominator untrusted input से आता हो तो यह security issue बन जाता है
- महत्वपूर्ण बात यह है कि यह सिर्फ runtime error नहीं, बल्कि input validation boundary पर होने वाला UB हो सकता है
UB नहीं, फिर भी integer promotion खतरनाक है
- integer promotion के rules को code skim करते हुए लागू करना कठिन है, और ये intuition से अलग परिणाम दे सकते हैं
- नीचे के code में
overflowedका value 1 नहीं बल्कि 0 होगाunsigned char a = 0xff; unsigned char b = 1; unsigned char zero = 0; bool overflowed = (a + b) == zero; // overflowed is set to zero, not one. - नीचे के code में सभी variables unsigned जैसे दिखते हैं, फिर भी result
2147483648 (0x80000000)नहीं बल्कि18446744071562067968 (ffffffff80000000)बनता हैunsigned char a = 0x80; uint64_t b = a << 24; // Bonus UB(?) - UB न भी हो, तब भी C/C++ के integer rules intuitive नहीं हैं और defects पैदा करना आसान है
LLM का उपयोग करके UB detection
- आधुनिक LLM से arbitrary C code में UB ढूँढने को कहें तो वह लगभग हमेशा कुछ न कुछ समस्या पकड़ लेता है, और अधिकतर बार सही होता है
- निजी code में UB खोजने के बाद यही तरीका mature और सख़्ती से लिखे गए OpenBSD code पर भी लागू किया गया
- सबसे पहले दिमाग में आए tool
findमें कई समस्याएँ मिलीं - OpenBSD को out-of-bounds write के लिए patch और UB नहीं बल्कि logical bug के लिए patch भेजे गए
- बचे हुए कई UB के लिए patch नहीं भेजे गए
- अतीत में OpenBSD project bug reports को बहुत ग्रहणशील तरीके से नहीं लेता था, ऐसा अनुभव रहा था
- यह भी लगा कि कुछ मामले व्यवहार में शायद ठीक हों
- अगर OpenBSD को codebase से UB हटाना है, तो LLM और project के बीच individual patches भेजने से बड़ा project चाहिए होगा
C/C++ codebase से निपटने की व्यावहारिक दिशा
- मौजूदा C/C++ codebase को फेंका नहीं जा सकता, लेकिन उसे मूल रूप से टूटी अवस्था में छोड़ देना भी विकल्प नहीं है
- AI द्वारा किए गए low-quality बदलाव commit किए बिना, और human reviewers को overwhelm किए बिना, UB को बड़े पैमाने पर ठीक करना होगा
- 2026 में LLM-आधारित UB supervision के बिना C या C++ लिखना SOX violation जैसा माना जा सकता है, और गैर-जिम्मेदाराना समझा जा सकता है
- अगर OpenBSD developers भी 30 साल से अधिक समय में ये सभी समस्याएँ नहीं खोज पाए, तो दूसरे projects की संभावना और भी कम है
- निजी projects में LLM से UB खोजवाना, ज़रूरत पड़ने पर उससे explanation लेना, उससे fix बनवाना, और फिर इंसान द्वारा result verify करना एक कामचलाऊ तरीका हो सकता है
- लेकिन result verify करने के लिए expert चाहिए, और experts आमतौर पर दूसरे कामों में व्यस्त रहते हैं
- यह काम ऊपर से cleanup task जैसा दिखता है, लेकिन पारंपरिक रूप से ऐसे काम करने वाले junior programmers को सौंपने के लिए यह बहुत सूक्ष्म है
1 टिप्पणियां
Hacker News टिप्पणियाँ
C में हैरान करने वाले और अजीब undefined behavior बहुत हैं, लेकिन यह लेख उसे अच्छी तरह दिखा नहीं पाता, बस सतह को हल्का-सा छूता है
इससे भी अजीब उदाहरण
volatile int x = 5; printf("%d in hex is 0x%x.\n", x, x);है। अगरxसिर्फintहो तो ठीक है, लेकिनvolatileहोने पर यह undefined behavior बन जाता है। C standard मेंvolatileaccess केवल पढ़ना भी side effect माना जाता है, और एक ही scalar object पर बिना क्रम वाले side effects undefined behavior होते हैं, जबकि function arguments की evaluation का क्रम आपस में अनिश्चित होता हैआम तौर पर data race का मतलब होता है कि अलग-अलग threads एक ही object को साथ में access करें और उनमें से कम-से-कम एक write हो, लेकिन C में single thread के भीतर भी बिना write के data race जैसी स्थिति बन सकती है
undefinedशब्द जहाँ-जहाँ 283 बार आता है, या omission की वजह से undefined होने वाले हर मामले को गिनाना नहीं हैमुद्दा यह है कि इससे बचा नहीं जा सकता। कम-से-कम 1972 में C आने के बाद से इंसान इसे पूरी तरह टाल नहीं पाए हैं
अगर 54 साल में यह सफल नहीं हुआ, तो “और मेहनत करो” या “गलती मत करो” कोई समाधान नहीं है। Mythos ने OpenBSD में जो एक exploitable flaw पाया, उसे OpenBSD developers के लिए काफ़ी अच्छी रेटिंग मिली थी, लेकिन सबसे सरल code पर tools चलाने से भी undefined behavior की भरमार निकली
उदाहरण के लिए,
findमेंwaitpid(&status)के बादwaitpid()error check करने से पहले uninitialized automatic variablestatusको पढ़ना भी undefined behavior है, हालाँकि ऐसा architecture या compiler कल्पना करना मुश्किल है जहाँ यह exploitable होजैसा लेख में लिखा है, उद्देश्य दुनिया के हर undefined behavior को गिनाना नहीं, बल्कि यह कहना है कि हर non-trivial C/C++ code में undefined behavior होता है
volatileएक type system hack है। इसका समाधान अधिक सिद्धांतपूर्ण होना चाहिए था, और आधुनिक भाषाओं को “C ने ऐसा किया, इसलिए यह अच्छा विचार है” कहकर इसकी नकल नहीं करनी चाहिएशुरुआती C compilers हमेशा values को memory में लिखते थे, इसलिए अगर pointer को memory-mapped I/O hardware पर सेट कर दिया जाए, तो
xबदलने पर हर बार CPU instruction सचमुच memory write करता था और driver code काम करता थालेकिन optimization आने पर compiler ने सोचा कि वह सिर्फ
xको बदल रहा है, इसलिए उसे register में ही रखा, और driver टूट गयाC का
volatilecompiler से कहने का hack है: “वह optimization मत करो”, जबकि सही समाधान, यानी library स्तर पर memory-mapped I/O intrinsics देना, कहीं बड़ा काम होताintrinsics की ज़रूरत इसलिए है कि वे ठीक-ठीक बता सकें कि कौन-सा behavior संभव है और कौन-सा नहीं। कुछ targets पर 1-byte, 2-byte, 4-byte write अलग-अलग क्रियाएँ हैं और hardware भी इन्हें अलग मानता है। कुछ devices 4-byte RGBA write की अपेक्षा करते हैं; अगर आप 1-byte write चार बार भेजें तो वे भ्रमित हो सकते हैं या काम ही न करें। कुछ targets bit-level write भी support करते हैं। केवल
volatileसे यह जानने का कोई तरीका नहीं कि क्या होने वाला है और उसका अर्थ क्या हैC program को compile करने के बाद disassemble करें तो वह ऐसा assembly program बनता है जिसमें undefined behavior नहीं होता, क्योंकि assembly में undefined behavior की अवधारणा नहीं होती
undefined behavior source program का गुण है, executable का नहीं। इसका मतलब है कि जिस भाषा में source लिखा गया है, उसकी specification उस program को कोई अर्थ नहीं देती। दूसरी तरफ compile हुए executable को machine specification अर्थ देती है
race program के behavior का गुण है। इसलिए C program में undefined behavior होने की बात कही जा सकती है, लेकिन यह नहीं कहा जा सकता कि executable में वास्तव में कोई race है। हाँ, compiler undefined behavior वाले program को मनचाहे ढंग से compile कर सकता है, इसलिए race introduce भी कर सकता है, लेकिन यदि वह नए threads बनाए बिना compile करे तो race नहीं होगी
volatileका अर्थ ही यह है कि value किसी और चीज़ द्वारा बदल सकती है। अगर वह global variable है, तो वह चीज़ किसी दूसरे thread के अलावा interrupt या signal handler भी हो सकती है। अगर वह किसी खास address को पढ़ने वाला pointer है, तो value बदलने वाला hardware device register भी हो सकता हैvolatilevariable की अवधारणा अपने-आप में समस्या नहीं है। अगर कोई भाषा interrupt routines और memory-mapped I/O को support करना चाहती है, तो compiler को यह बताने का कोई तरीका होना चाहिए कि एक ही hardware register को दो बार पढ़ना, memory की एक ही location को दो बार पढ़ने जैसा नहीं हैअसली समस्या यह है कि language features और constraints की परस्पर क्रिया को पर्याप्त रूप से व्यवस्थित नहीं किया गया। अगर आपने साफ़ कहा है कि “यह value कभी भी बदल सकती है”, तो उसी वजह से उसके कुछ उपयोगों को undefined behavior मानना मूर्खता है।
volatilevariables के लिए “unordered side effects” की परिभाषा में exception होना चाहिए थाबहुत से लोग भ्रम में रहते हैं कि C और C++ “जो चाहो वह करने देते हैं, इसलिए बहुत flexible हैं।” वास्तव में लगभग हर शक्तिशाली और शानदार दिखने वाली technique undefined behavior का minefield है
unaligned pointer का undefined behavior और भी बुरा है। unaligned pointer केवल access करने पर ही नहीं, pointer अपने-आप में भी undefined behavior है
इसलिए
void* vकोint* iमें implicit cast करना, जैसे C मेंi=vयाint*लेने वालेf(v)को call करना भी undefined behavior है, अगर result pointerintकी alignment requirement पूरी नहीं करतायह C-स्तर की समस्या है, यह महत्वपूर्ण है। अगर किसी C program में undefined behavior है, तो वह program औपचारिक रूप से वैध नहीं है, बल्कि गलत program है। यह hardware की समस्या नहीं है, न ही crash या fault से जुड़ी बात
void*सेint*में cast आम तौर पर hardware code में कुछ नहीं करता, और type केवल C में होते हैं, इसलिए hardware उस cast पर crash भी नहीं करता। आपको लग सकता है कि अगर register में integer value है तो सब ठीक है, लेकिन मुद्दा यह नहीं कि hardware में pointer सचमुच integer है या नहीं; मुद्दा यह है कि unaligned pointer में cast करते ही C program परिभाषा के हिसाब से टूट चुका हैमैं यह भी बताना चाहता था कि undefined behavior hardware में नहीं होता और crash या fault से उसका सीधा संबंध नहीं है। साथ ही मैं उन लोगों को उदाहरण दिखाना चाहता था जो कहते हैं, “देखो, यह तो ठीक चल रहा है”, जबकि वास्तव में ऐसा नहीं है
#pragma pack(push, 1)से struct बनाऊँ, तो क्या इसका मतलब है कि संयोग से aligned member न हो तो member pointer इस्तेमाल ही नहीं कर सकता?इस तरह का undefined behavior ठीक था, और hardware differences के कारण bug आना ज़्यादातर लोगों को बड़ी समस्या नहीं लगता
लेकिन समय के साथ आक्रामक व्याख्याओं ने C को एक तरह की implicit design by contract भाषा में बदल दिया, और constraints अदृश्य हो गए। यह वैसी ही समस्या पैदा करता है जैसी RAII में implicit destructor calls के अदृश्य होने से होती है
C में जब आप pointer dereference करते हैं, compiler function signature में implicit non-null constraint जोड़ देता है। अगर null हो सकने वाला pointer किसी function को दे दिया जाए, तो “कोई check या assertion नहीं है” जैसी error देने के बजाय compiler चुपचाप उस non-null constraint को propagate कर देता है। अगर वह साबित कर दे कि यह constraint झूठा है, तो function को unreachable mark कर देता है, और unreachable function call calling function को भी unreachable बना सकती है
C में undefined behavior सीखने के 5 चरण
इनकार: “मुझे पता है मेरी machine पर signed overflow कैसे होता है”
ग़ुस्सा: “यह compiler बेकार है! जो मैं कहता हूँ वह करता क्यों नहीं?”
मोलभाव: “मैं C को ठीक करने के लिए wg14 को यह proposal भेजूँगा…”
अवसाद: “क्या किसी C code पर भरोसा किया जा सकता है?”
स्वीकृति: “बस undefined behavior मत लिखो”
unaligned access के लिए packed structs का उपयोग करो। compiler जादू की तरह सही code बना देगा। असल में compiler हमेशा सही कर सकता था, बस करता नहीं था
strict aliasing rules के लिए union type punning का उपयोग करो। जो compilers मायने रखते हैं, वे standard कुछ भी कहे, यह काम करता है—ऐसा document करते हैं। या
-fno-strict-aliasingसे इसे बंद कर दो। memory को जैसे चाहो वैसे reinterpret करो; कुछ sharp edges रहेंगी, लेकिन कम-से-कम compiler की ओर से नहींoverflow को
-fwrapvसे define कर लो।+,-,*को__builtin_*_overflowसे बदलो तो explicit error checking भी मुफ़्त मिलती है। functional interface भी अच्छा है और efficient code भी बनता हैअसली स्वीकृति शायद “सामान्य लोग C standard की परवाह नहीं करते” के अधिक करीब है। standard ख़राब है; असली चीज़ compiler है। compilers में ऐसे बहुत उपयोगी features हैं जिनसे इन ज़्यादातर समस्याओं को bypass किया जा सकता है। लोग उनका उपयोग नहीं करते क्योंकि वे “portable”, “standard” C लिखना चाहते हैं; उसी सोच से बाहर निकलना असली स्वीकृति है
मैंने इसी तर्क से freestanding C environment में Lisp interpreter बनाया और UBSan भी pass किया। शुरू में लगा था कि यह फट जाएगा, पर ऐसा नहीं हुआ; अगर मैं कर सकता हूँ, तो कोई भी कर सकता है
जब तक इंसान code लिखते रहेंगे, वह अंतिम अवस्था नहीं हो सकती। कोई भी इंसान C/C++ में undefined behavior को पूरी तरह नहीं टाल सकता
उदाहरण असली undefined behavior कम और ऐसे मामलों जैसे ज़्यादा लगते हैं जो input या परिस्थिति के अनुसार undefined behavior बन सकते हैं
अगर इतना व्यापक लें, तो हर function call भी undefined behavior है क्योंकि वह stack space पार कर सकता है। सच तो यह है कि इस अर्थ में लगभग किसी भी भाषा के लिए ऐसा कहा जा सकता है
C में ध्यान देने लायक वास्तविक खुरदरे हिस्से वैसे भी बहुत हैं; इस तरह की सनसनीखेज़ी खासकर शुरुआती लोगों का ध्यान भटका सकती है और उल्टा नुकसान कर सकती है
STORAGE_ERRORexception define हैhttp://archive.adaic.com/standards/83lrm/html/lrm-11-01.html
उसमें लिखा है कि “subprogram call के execution के दौरान storage पर्याप्त न हो” तो भी यह exception हो सकता है
सबसे पहले, stack space खत्म होने पर क्या होना चाहिए, यह define किया जा सकता है। और सभी programs को मनमाने आकार का stack नहीं चाहिए; कुछ programs को पहले से गणना की जा सकने वाली स्थिर मात्रा ही चाहिए। कुछ language implementations stack का उपयोग ही नहीं करतीं
भाषा शेष stack space जाँचने के tools दे सकती है और उसके आधार पर guarantees भी कर सकती है। या stack खत्म होने पर चलने वाला handler install करने दे सकती है
सही मानसिकता यह है कि undefined behavior शुरू होते ही आप language standard की सुरक्षा-सीमा से बाहर हैं। कुछ समय तक, शायद हमेशा के लिए, सब ठीक चलता दिख सकता है। लेकिन वास्तव में आप अनजाने में toolchain, compiler replacement या upgrade, architecture, runtime, libc version differences की मनमानी पर निर्भर हो जाते हैं
अंततः आप रेत पर नींव खड़ी कर रहे होते हैं, और यही undefined behavior का ख़तरा है
undefined behavior की समस्या यह नहीं कि किसी architecture पर crash हो सकता है
असली समस्या यह है कि compiler मानकर चलता है कि ऐसा code कभी आएगा ही नहीं। फिर भी अगर आप undefined behavior वाला code लिखते हैं, तो compiler, खासकर optimizer, उसे normal path के अनुकूल किसी भी रूप में translate कर सकता है। वह “कुछ भी” कभी-कभी बड़े code blocks हटाने जितना अप्रत्याशित हो सकता है
tail recursion जुड़ जाए तो bug debug build में infinite loop तक पहुँचे ही नहीं, और optimization level बढ़ाने पर ही सामने आए—ऐसा भी हो सकता है
इससे भी बुरे मामलों में program चुपचाप garbage values के साथ चलता रह सकता है, hard disk format कर सकता है, या attacker को पूरे राज्य की चाबी थमा सकता है
जो लोग कहते हैं कि इसे बस define कर दो या unspecified behavior बना दो, वे यह मूल बात चूक जाते हैं कि compiler program के बड़े हिस्से हटा सके—यही मुद्दा है
अगर आप ऐसा code लिखते हैं जो कुछ inputs पर undefined behavior बनता है, तो उन inputs के लिए आपका आशय है कि program का कोई behavior होना ही नहीं चाहिए। आप चाहते हैं कि compiler उस path को optimization से मिटा दे, या defined बाकी मामलों के behavior में मदद करने वाला कोई transformation कर दे
undefined behavior से ही पहुँचे जा सकने वाले log strings डालना, और फिर binary में उन strings का न बचना—यह काफ़ी संतोषजनक है
मैंने पहले एक analysis pass लिखा था जो transformation pipeline के आख़िरी में चलता है—इस धारणा के साथ, और वही उसकी correctness के लिए ज़रूरी था। मैंने सोचा कि अब और optimization नहीं होगी, इसलिए safe है, लेकिन अब मुझे उतना विश्वास नहीं रहा
20 साल से C इस्तेमाल कर रहा हूँ, लेकिन पिछले 6 महीनों में Hacker News पर जितनी undefined behavior की बातें देखीं, उतनी पहले कभी नहीं
असली बातचीत में यह लगभग कभी नहीं आया। code लिखो, न चले तो debug करो, fix करो या workaround लगा दो। समझ नहीं आता कि C के undefined behavior का विषय बार-बार front page पर क्यों चढ़ जाता है
computer science के एक छोटे लेकिन स्थायी वर्ग को नई programming language बनाना या इस्तेमाल करना दुनिया की सबसे रोचक चीज़ लगता है, और उनमें से कुछ लोग हमेशा ऐसा ही सोचते रहते हैं
ऐसे लोगों का language design पक्ष में रुचि लेना स्वाभाविक है, और C का undefined behavior उसी दायरे में आता है। हालाँकि इसकी बहुत-सी बातें मूलतः पुराने CPU architectures को performance loss के बिना accommodate करने की कोशिश से आई थीं, और इसे “design choice” कहना उतना ही अटपटा है जितना पहिये का गोल होना
GCC 3.2 के आसपास compiler ने optimization में undefined behavior का बहुत अधिक आक्रामक उपयोग शुरू किया, जिससे कुछ काफ़ी मशहूर “scandals” हुए, और इसी कारण कई लोग लंबे समय तक GCC 2.95 पर टिके रहे। GCC 3.2, 2002 में आया था
हर कंपनी safety और exposure, यानी news में आने वाली घटनाओं, पर लगातार ज़ोर देती है, इसलिए “unsafe” के खिलाफ़ narrative हद से ज़्यादा बढ़ गया है
नई दुनिया कुछ वैसी है जैसे शहर के वे लोग जिन्होंने कभी कच्ची प्रकृति नहीं देखी, lawn mower देखकर डर जाएँ। ब्लेड घूम रहे हैं? यह तो पागलपन है!
अगर असली target किसी दूरस्थ संचार टॉवर पर लगा छोटा embedded system है, तो “मेरी machine पर चलता है” बेकार है। बेशक ज़्यादातर लोग ऐसा काम नहीं करते, और यहाँ developers का बड़ा हिस्सा शायद web developers है, लेकिन खुद अनुभव न होने पर भी यह रोचक चर्चा है। बल्कि ऐसे मामलों में शायद और भी ज़्यादा
compiler में ऐसे bugs हो सकते हैं जहाँ standard के अनुसार चीज़ काम करनी चाहिए थी लेकिन नहीं करती, standard counterpart के बिना कई extensions होते हैं, और standard में undefined मानी गई चीज़ों के लिए भी implementation-specific अर्थ दिए जाते हैं
शुरुआत से मैं मोटे तौर पर सहमत हूँ, लेकिन उदाहरण अच्छे नहीं हैं और पूरा लेख LLM coding को बढ़ावा देने के लिए पैकेजिंग जैसा लगता है
ऐसा लगता है जैसे कोई व्यक्ति मनमाना code लिखना चाहता हो और चाहता हो कि वह हर environment में बिल्कुल एक जैसा चले। अगर भाषा ऐसी बना दी जाए, तो ज़रूरत पड़ने पर platform-specific लिख पाने का लाभ ही खत्म हो जाएगा
इस लेख का C++ code कुछ जगहों पर 10 साल से भी ज़्यादा समय से idiomatic नहीं रहा, और आज के समय में code smell माना जाएगा
भाषा अपने शुरुआती रूप से काफ़ी बदलकर दूसरी तरह की भाषा बन चुकी है। जैसे ही raw pointers और direct pointer access बहुत मात्रा में दिखे, यह साफ़ हो गया कि लेख के कुछ हिस्सों को सावधानी से पढ़ना चाहिए
एक और साफ़ समस्या यह दृष्टिकोण है कि C और C++ को लगभग एक ही भाषा मानकर मिला दिया गया है। आजकल ये दोनों भाषाएँ वास्तव में काफ़ी दूर जा चुकी हैं
atomic_intनहीं बल्किstd::atomicलिखा थाक्या C के undefined behavior को इस तरह समझना सही है?
program P के पास inputs का एक set A है, जो undefined behavior नहीं पैदा करता, और उसका पूरक set B, जो पैदा करता है
एक सही compiler P को executable P' में compile करता है। A के हर input के लिए P' को P की तरह ही behavior करना चाहिए
लेकिन B के किसी भी input के लिए P' के behavior पर कोई requirement नहीं है
unaligned pointer की वजह से हुए undefined behavior का एक ठोस उदाहरण: https://pzemtsov.github.io/2016/11/06/bug-story-alignment-on...