c++ - क्या अपरिभाषित व्यवहार वाली शाखाओं को पहुंचने योग्य और मृत कोड के रूप में अनुकूलित किया जा सकता है?




language-lawyer undefined-behavior (6)

निम्नलिखित कथन पर विचार करें:

*((char*)NULL) = 0; //undefined behavior

यह स्पष्ट रूप से अपरिभाषित व्यवहार का आह्वान करता है। क्या किसी दिए गए कार्यक्रम में इस तरह के एक बयान का अस्तित्व का मतलब है कि पूरा कार्यक्रम अनिर्धारित है या नियंत्रण प्रवाह केवल इस कथन को हिट करने के बाद ही अपरिभाषित हो जाता है?

यदि उपयोगकर्ता नंबर 3 प्रवेश नहीं करता है तो क्या निम्न प्रोग्राम अच्छी तरह से परिभाषित किया जाएगा?

while (true) {
 int num = ReadNumberFromConsole();
 if (num == 3)
  *((char*)NULL) = 0; //undefined behavior
}

या यह पूरी तरह से अपरिभाषित व्यवहार है चाहे उपयोगकर्ता प्रवेश करता हो?

साथ ही, क्या संकलक मान सकता है कि अपरिभाषित व्यवहार रनटाइम पर कभी निष्पादित नहीं किया जाएगा? वह समय पर पीछे तर्क के लिए अनुमति देगा:

int num = ReadNumberFromConsole();

if (num == 3) {
 PrintToConsole(num);
 *((char*)NULL) = 0; //undefined behavior
}

यहां, संकलक तर्क दे सकता है कि अगर num == 3 हम हमेशा अनिर्धारित व्यवहार का आह्वान करेंगे। इसलिए, यह मामला असंभव होना चाहिए और संख्या को मुद्रित करने की आवश्यकता नहीं है। if पूरा कथन अनुकूलित किया जा सकता है। क्या इस तरह के पीछे की ओर तर्क मानक के अनुसार अनुमति है?


क्या किसी दिए गए कार्यक्रम में इस तरह के एक बयान का अस्तित्व का मतलब है कि पूरा कार्यक्रम अनिर्धारित है या नियंत्रण प्रवाह केवल इस कथन को हिट करने के बाद ही अपरिभाषित हो जाता है?

न तो। पहली स्थिति बहुत मजबूत है और दूसरा बहुत कमजोर है।

ऑब्जेक्ट एक्सेस को कभी-कभी अनुक्रमित किया जाता है, लेकिन मानक समय के बाहर कार्यक्रम के व्यवहार का वर्णन करता है। डेनविल पहले ही उद्धृत किया गया है:

यदि इस तरह के किसी निष्पादन में एक अपरिभाषित ऑपरेशन होता है, तो यह अंतर्राष्ट्रीय मानक उस इनपुट के साथ उस कार्यक्रम को निष्पादित करने के कार्यान्वयन पर कोई आवश्यकता नहीं रखता है (पहले अपरिभाषित ऑपरेशन से पहले संचालन के संबंध में भी नहीं)

इसका व्याख्या किया जा सकता है:

यदि कार्यक्रम का निष्पादन अपरिभाषित व्यवहार उत्पन्न करता है, तो पूरे कार्यक्रम में व्यवहार को अपरिभाषित किया गया है।

तो, यूबी के साथ एक पहुंच योग्य बयान कार्यक्रम यूबी नहीं देता है। एक पहुंच योग्य बयान है कि (इनपुट के मूल्यों के कारण) कभी नहीं पहुंचता है, कार्यक्रम यूबी नहीं देता है। यही कारण है कि आपकी पहली हालत बहुत मजबूत है।

अब, संकलक सामान्य रूप से यह नहीं बता सकता कि यूबी क्या है। इसलिए ऑप्टिमाइज़र को संभावित यूबी के साथ कथन दोबारा ऑर्डर करने की अनुमति देने के लिए जो उनके व्यवहार को परिभाषित किया जाना चाहिए, यूबी को "समय पर वापस पहुंचने" की अनुमति देना और पिछले अनुक्रम बिंदु से पहले गलत होना चाहिए (या सी में ++ 11 शब्दावली, यूबी के लिए उन चीजों को प्रभावित करने के लिए जो यूबी चीज से पहले अनुक्रमित हैं)। इसलिए आपकी दूसरी स्थिति बहुत कमजोर है।

इसका एक बड़ा उदाहरण यह है कि जब अनुकूलक सख्त अलियासिंग पर निर्भर करता है। सख्त एलियासिंग नियमों का पूरा बिंदु संकलक को पुन: क्रमबद्ध करने की अनुमति देना है जिसे वैध रूप से पुन: आदेशित नहीं किया जा सकता है यदि यह संभव था कि प्रश्नकर्ताओं के प्रश्न समान स्मृति को उबाल लें। इसलिए यदि आप अवैध रूप से अलियासिंग पॉइंटर्स का उपयोग करते हैं, और यूबी होता है, तो यह यूबी स्टेटमेंट से पहले "कथन" को आसानी से प्रभावित कर सकता है। जहां तक ​​सार मशीन का संबंध है, यूबी स्टेटमेंट अभी तक निष्पादित नहीं किया गया है। जहां तक ​​वास्तविक ऑब्जेक्ट कोड का संबंध है, इसे आंशिक रूप से या पूरी तरह से निष्पादित किया गया है। लेकिन मानक ऑप्टिमाइज़र के लिए स्टेटमेंट को फिर से ऑर्डर करने के लिए इसका क्या अर्थ है, या यूबी के लिए इसके प्रभाव क्या हैं इसका विवरण देने का प्रयास नहीं करता है। यह केवल जैसे ही इसे पसंद करता है, गलत होने के लिए कार्यान्वयन लाइसेंस देता है।

आप इस बारे में सोच सकते हैं, "यूबी में टाइम मशीन है"।

विशेष रूप से अपने उदाहरणों का उत्तर देने के लिए:

  • यदि व्यवहार पढ़ा जाता है तो व्यवहार केवल अपरिभाषित होता है।
  • यदि मूलभूत ब्लॉक में एक ऑपरेशन निश्चित रूप से अनिर्धारित होता है तो संकलक कोड को मृत के रूप में खत्म कर सकते हैं और कर सकते हैं। उन मामलों में उन्हें अनुमति दी गई है (और मैं अनुमान लगा रहा हूं) जो मूलभूत ब्लॉक नहीं हैं, लेकिन जहां सभी शाखाएं यूबी की ओर ले जाती हैं। यह उदाहरण एक उम्मीदवार नहीं है जब तक कि PrintToConsole(3) किसी भी तरह से वापस लौटने के लिए ज्ञात नहीं है। यह एक अपवाद या जो कुछ भी फेंक सकता है।

आपके दूसरे के लिए एक समान उदाहरण है gcc विकल्प -fdelete-null-pointer-checks , जो इस तरह कोड ले सकता है (मैंने इस विशिष्ट उदाहरण की जांच नहीं की है, इसे सामान्य विचार के दृष्टांत पर विचार करें):

void foo(int *p) {
    if (p) *p = 3;
    std::cout << *p << '\n';
}

और इसे यहां बदलें:

*p = 3;
std::cout << "3\n";

क्यूं कर? क्योंकि यदि p शून्य है तो कोड में यूबी है, इसलिए संकलक मान सकता है कि यह शून्य नहीं है और तदनुसार अनुकूलित करें। लिनक्स कर्नेल इस पर फिसल गया ( https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2009-1897 ) अनिवार्य रूप से क्योंकि यह एक ऐसे मोड में काम करता है जहां एक शून्य सूचक को संदर्भित करना नहीं है यूबी हो, यह एक परिभाषित हार्डवेयर अपवाद के परिणामस्वरूप होने की उम्मीद है कि कर्नेल संभाल सकता है। जब ऑप्टिमाइज़ेशन सक्षम होता है, तो जीसीसी को उस मानक-मानक गारंटी प्रदान करने के लिए -fno-delete-null-pointer-checks का उपयोग करने की आवश्यकता होती है।

पीएस सवाल का व्यावहारिक उत्तर "जब अपरिभाषित व्यवहार हड़ताल करता है?" "दिन के लिए जाने की योजना बनाने से पहले 10 मिनट" है।


"व्यवहार" शब्द का अर्थ है कुछ किया जा रहा है । एक राज्यपाल जिसे कभी निष्पादित नहीं किया जाता है वह "व्यवहार" नहीं होता है।

एक उदाहरण:

*ptr = 0;

क्या यह अनिर्धारित व्यवहार है? मान लीजिए कि हम कार्यक्रम निष्पादन के दौरान कम से कम एक बार 100% निश्चित ptr == nullptr । जवाब हाँ होना चाहिए।

इस बारे में क्या?

 if (ptr) *ptr = 0;

क्या यह अनिर्धारित है? (कम से कम एक बार ptr == nullptr याद रखें?) मुझे यकीन है कि उम्मीद नहीं है, अन्यथा आप किसी भी उपयोगी कार्यक्रम को लिखने में सक्षम नहीं होंगे।

इस उत्तर के निर्माण में कोई भी शब्दकोष नुकसान नहीं पहुंचा था।


अपरिभाषित व्यवहार तब होता है जब कार्यक्रम अपरिभाषित व्यवहार का कारण बनता है इससे कोई फर्क नहीं पड़ता कि आगे क्या होता है। हालांकि, आपने निम्नलिखित उदाहरण दिया है।

int num = ReadNumberFromConsole();

if (num == 3) {
 PrintToConsole(num);
 *((char*)NULL) = 0; //undefined behavior
}

जब तक कंपाइलर PrintToConsole परिभाषा को नहीं जानता, तब तक यह हटा नहीं सकता है if (num == 3) सशर्त। आइए मान लें कि आपके पास LongAndCamelCaseStdio.h की निम्न घोषणा के साथ LongAndCamelCaseStdio.h सिस्टम हेडर है।

void PrintToConsole(int);

कुछ भी बहुत उपयोगी नहीं, ठीक है। अब, देखते हैं कि इस कार्य की वास्तविक परिभाषा की जांच करके विक्रेता कितना बुरा (या शायद इतना बुरा नहीं, अपरिभाषित व्यवहार खराब हो सकता था)।

int printf(const char *, ...);
void exit(int);

void PrintToConsole(int num) {
    printf("%d\n", num);
    exit(0);
}

कंपाइलर को वास्तव में यह मानना ​​है कि किसी भी मनमाने ढंग से कार्य करने वाला संकलक यह नहीं जानता कि यह क्या करता है या बाहर निकलने या अपवाद फेंक सकता है (सी ++ के मामले में)। आप देख सकते हैं कि *((char*)NULL) = 0; निष्पादित नहीं किया जाएगा, क्योंकि PrintToConsole कॉल के बाद निष्पादन जारी नहीं रहेगा।

अनिश्चित व्यवहार तब PrintToConsole जब PrintToConsole वास्तव में वापस आता है। संकलक उम्मीद करता है कि ऐसा न हो (क्योंकि इससे प्रोग्राम अपरिभाषित व्यवहार निष्पादित करेगा, इससे कोई फर्क नहीं पड़ता), इसलिए कुछ भी हो सकता है।

हालांकि, चलिए कुछ और मानते हैं। मान लें कि हम शून्य जांच कर रहे हैं, और शून्य जांच के बाद चर का उपयोग करें।

int putchar(int);

const char *warning;

void lol_null_check(const char *pointer) {
    if (!pointer) {
        warning = "pointer is null";
    }
    putchar(*pointer);
}

इस मामले में, यह ध्यान lol_null_check आसान है कि lol_null_check को एक गैर-नल पॉइंटर की आवश्यकता होती है। वैश्विक गैर-अस्थिर warning चर को असाइन करना ऐसा कुछ नहीं है जो प्रोग्राम से बाहर निकल सकता है या कोई अपवाद फेंक सकता है। pointer भी अस्थिर है, इसलिए यह कार्य के मध्य में अपने मूल्य को जादुई रूप से बदल नहीं सकता है (अगर ऐसा होता है, तो यह अनिर्धारित व्यवहार है)। कॉलिंग lol_null_check(NULL) अपरिभाषित व्यवहार का कारण बन जाएगा जो चर को असाइन नहीं किया जा सकता है (क्योंकि इस बिंदु पर, यह तथ्य कि प्रोग्राम अपरिभाषित व्यवहार निष्पादित करता है) ज्ञात है।

हालांकि, अपरिभाषित व्यवहार का मतलब है कि कार्यक्रम कुछ भी कर सकता है। इसलिए, उस समय में वापस जाने से अपरिभाषित व्यवहार को रोकता है, और int main() निष्पादन की पहली पंक्ति से पहले आपके प्रोग्राम को क्रैश कर देता है। यह अपरिभाषित व्यवहार है, इसे समझ में नहीं आता है। यह 3 टाइप करने के बाद भी क्रैश हो सकता है, लेकिन अपरिभाषित व्यवहार समय पर वापस जायेगा, और इससे पहले कि आप टाइप 3 भी क्रैश हो जाएं। और कौन जानता है, शायद अपरिभाषित व्यवहार आपके सिस्टम रैम को ओवरराइट करेगा, और 2 सप्ताह बाद आपके सिस्टम को क्रैश कर देगा, जबकि आपका अपरिभाषित प्रोग्राम नहीं चल रहा है।


एक निर्देशक उदाहरण है

int foo(int x)
{
    int a;
    if (x)
        return a;
    return 0;
}

वर्तमान जीसीसी और वर्तमान क्लैंग दोनों इस (x86 पर) को अनुकूलित करेंगे

xorl %eax,%eax
ret

क्योंकि वे यह मानते हैं कि x if (x) नियंत्रण पथ में यूबी से हमेशा शून्य होता है। जीसीसी आपको एक अनियमित मूल्य-मूल्य चेतावनी भी नहीं देगा! (क्योंकि उपरोक्त तर्क लागू होता है जो पार से पहले चलता है जो अनियंत्रित-मूल्य चेतावनियां उत्पन्न करता है)


मौजूदा सी ++ वर्किंग ड्राफ्ट 1.9.4 में कहता है

यह अंतर्राष्ट्रीय मानक उन कार्यक्रमों के व्यवहार पर कोई आवश्यकता नहीं लगाता है जिनमें अवांछित व्यवहार शामिल है।

इस पर आधारित, मैं कहूंगा कि किसी भी निष्पादन पथ पर अपरिभाषित व्यवहार वाला एक प्रोग्राम इसके निष्पादन के हर समय कुछ भी कर सकता है।

अपरिभाषित व्यवहार पर दो वास्तव में अच्छे लेख हैं और आमतौर पर कौन से कंपाइलर्स करते हैं:


यदि कार्यक्रम एक ऐसे बयान तक पहुंचता है जो अपरिभाषित व्यवहार का आह्वान करता है, तो किसी भी कार्यक्रम के आउटपुट / व्यवहार पर किसी भी आवश्यकता को नहीं रखा जाता है; इससे कोई फर्क नहीं पड़ता कि वे "पहले" या "बाद" अनिश्चित व्यवहार का आह्वान करेंगे या नहीं।

सभी तीन कोड स्निपेट के बारे में आपका तर्क सही है। विशेष रूप से, एक कंपाइलर किसी भी कथन का इलाज कर सकता है जो बिना शर्त तरीके से अपरिभाषित व्यवहार का आह्वान करता है जिस तरह से जीसीसी __builtin_unreachable() का अनुकूलन करता है: एक अनुकूलन संकेत के रूप में कि कथन पहुंच योग्य नहीं है (और इस प्रकार, बिना शर्त शर्त वाले सभी कोड पथ भी पहुंच योग्य नहीं हैं)। अन्य समान अनुकूलन निश्चित रूप से संभव हैं।







unreachable-code