c++ - यह फ़ंक्शन कॉल टाइप किए गए फ़ंक्शन पॉइंटर के माध्यम से कॉल करने के बाद समझदारी से व्यवहार क्यों करता है?




gcc function-pointers (3)

मेरे पास निम्न कोड है। एक फ़ंक्शन है जो दो इंट 32 लेता है। फिर मैं इसे एक पॉइंटर ले जाता हूं और इसे एक फंक्शन में कास्ट करता हूं जो तीन int8 लेता है और इसे कॉल करता है। मुझे एक रनटाइम त्रुटि की उम्मीद थी लेकिन कार्यक्रम ठीक काम करता है। ऐसा क्यों संभव है?

main.cpp:

#include <iostream>

using namespace std;

void f(int32_t a, int32_t b) {
    cout << a << " " << b << endl;
}

int main() {
    cout << typeid(&f).name() << endl;
    auto g = reinterpret_cast<void(*)(int8_t, int8_t, int8_t)>(&f);
    cout << typeid(g).name() << endl;
    g(10, 20, 30);
    return 0;
}

आउटपुट:

PFviiE
PFvaaaE
10 20

जैसा कि मैं देख सकता हूं कि पहले फ़ंक्शन के हस्ताक्षर के लिए दो ints की आवश्यकता होती है और दूसरे फ़ंक्शन के लिए तीन वर्णों की आवश्यकता होती है। चार इंट से छोटा है और मैंने सोचा कि क्यों ए और बी अभी भी 10 और 20 के बराबर हैं।


जैसा कि अन्य ने बताया है, यह अपरिभाषित व्यवहार है, इसलिए सिद्धांत में क्या हो सकता है, इस बारे में सभी दांव बंद हैं। लेकिन यह मानते हुए कि आप एक x86 मशीन पर हैं, एक प्रशंसनीय स्पष्टीकरण है कि आप इसे क्यों देख रहे हैं।

X86 पर, g ++ संकलक हमेशा स्टैक पर उन्हें धक्का देकर तर्क पारित नहीं करता है। इसके बजाय, यह रजिस्टर में पहले कुछ तर्कों को चुरा लेता है। यदि हम f फ़ंक्शन को f करते हैं, तो ध्यान दें कि पहले कुछ निर्देश रजिस्टरों से बाहर आकर तर्क को स्पष्ट रूप से स्टैक पर ले जाते हैं:

    push    rbp
    mov     rbp, rsp
    sub     rsp, 16
    mov     DWORD PTR [rbp-4], edi  # <--- Here
    mov     DWORD PTR [rbp-8], esi  # <--- Here
    # (many lines skipped)

इसी तरह, ध्यान दें कि कॉल main कैसे उत्पन्न होता है। तर्क उन रजिस्टरों में रखे गए हैं:

    mov     rax, QWORD PTR [rbp-8]
    mov     edx, 30      # <--- Here
    mov     esi, 20      # <--- Here
    mov     edi, 10      # <--- Here
    call    rax

चूंकि तर्कों को रखने के लिए पूरे रजिस्टर का उपयोग किया जा रहा है, इसलिए तर्कों का आकार यहां प्रासंगिक नहीं है।

इसके अलावा, क्योंकि ये तर्क रजिस्टरों के माध्यम से पारित किए जा रहे हैं, स्टैक को गलत तरीके से आकार देने के बारे में कोई चिंता नहीं है। कुछ कॉलिंग कन्वेंशन ( cdecl ) कॉल करने वाले को सफाई करने के लिए छोड़ देते हैं, जबकि अन्य ( stdcall ) कैली को सफाई करने के लिए कहते हैं। हालांकि, न तो वास्तव में यहां कोई मायने रखता है, क्योंकि स्टैक को छुआ नहीं गया है।


जैसा कि अन्य ने बताया है, यह पूरी तरह से अपरिभाषित व्यवहार है, और जो आपको मिलता है वह कंपाइलर पर निर्भर करेगा। यह तभी काम करेगा जब आपके पास एक विशिष्ट कॉल कन्वेंशन हो, जो स्टैक का उपयोग नहीं करता है लेकिन मापदंडों को पारित करने के लिए पंजीकृत करता है।

मैंने असेंबली को देखने के लिए गॉडबोल्ट का उपयोग किया, जिसे आप here पूर्ण रूप से देख सकते here

संबंधित फ़ंक्शन कॉल यहां है:

mov     edi, 10
mov     esi, 20
mov     edx, 30
call    f(int, int) #clang totally knows you're calling f by the way

यह स्टैक पर मापदंडों को धक्का नहीं देता है, यह बस उन्हें रजिस्टरों में डालता है। जो सबसे दिलचस्प है वह यह है कि mov इंस्ट्रक्शन रजिस्टर के निचले 8 बिट्स को नहीं बदलता है, लेकिन यह सभी 32-बिट चाल है। इसका मतलब यह भी है कि इससे पहले कि रजिस्टर में कोई फर्क नहीं पड़ता था, आप हमेशा सही मूल्य प्राप्त करेंगे जब आप 32 बिट्स को वापस करते हैं जैसा कि एफ करता है।

यदि आप आश्चर्य करते हैं कि 32-बिट क्यों चलती है, तो यह पता चलता है कि लगभग हर मामले में, x86 या AMD64 आर्किटेक्चर पर, कंपाइलर हमेशा 32 बिट शाब्दिक चाल या 64 बिट शाब्दिक चाल का उपयोग करेंगे (यदि और केवल यदि मूल्य बहुत बड़ा है 32 बिट्स के लिए)। 8 बिट मूल्य को स्थानांतरित करना रजिस्टर के ऊपरी बिट्स (8-31) को शून्य नहीं करता है, और यह समस्या पैदा कर सकता है अगर मूल्य को बढ़ावा दिया जाएगा। एक 32-बिट शाब्दिक निर्देश का उपयोग करना पहले रजिस्टर को शून्य करने के लिए एक अतिरिक्त निर्देश होने से अधिक सरल है।

एक बात जो आपको याद रखनी है, हालांकि यह वास्तव में f को कॉल करने की कोशिश कर रहा है जैसे कि इसमें 8 बिट्स पैरामीटर थे, इसलिए यदि आप एक बड़ा मूल्य रखते हैं तो यह शाब्दिक रूप से छोटा हो जाएगा। उदाहरण के लिए, 1000 -24 बन जाएगा, क्योंकि 1000 के निचले बिट्स E8 , जो कि हस्ताक्षरित पूर्णांक का उपयोग करते समय -24 । आपको चेतावनी भी मिलेगी

<source>:13:7: warning: implicit conversion from 'int' to 'signed char' changes value from 1000 to -24 [-Wconstant-conversion]

पहला C संकलक, साथ ही साथ अधिकांश संकलक जो C मानक के प्रकाशन से पहले थे, दायें-से-बाएँ क्रम में तर्कों को आगे बढ़ाकर एक फ़ंक्शन कॉल की प्रक्रिया करेंगे, फ़ंक्शन को लागू करने के लिए प्लेटफ़ॉर्म के "कॉल सबरूटीन" निर्देश का उपयोग करें और फिर। उसके बाद जब यह समारोह लौटा, तो जो भी दलील दी गई, पॉप। फ़ंक्शंस, क्रमिक क्रम में अपने तर्कों को संबोधित करेंगे जो कि "कॉल" निर्देश द्वारा धक्का दिए गए थे।

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

मानक से पहले मौजूद भाषा के अधिकांश कार्यान्वयनों में, कोई एक फ़ंक्शन लिख सकता है:

int foo(x,y) int x,y
{
  printf("Hey\n");
  if (x)
  { y+=x; printf("y=%d\n", y); }
}

और इसे आह्वान करें जैसे कि foo(0) या foo(0,0) , पूर्व में थोड़ा तेज होने के साथ। इसे उदाहरण के लिए foo(1); रूप में बुलाने का प्रयास foo(1); निश्चित रूप से स्टैक को भ्रष्ट करेगा, लेकिन यदि फ़ंक्शन ने कभी भी ऑब्जेक्ट y उपयोग नहीं किया है तो इसे पारित करने की कोई आवश्यकता नहीं है। इस तरह के शब्दार्थों का समर्थन करना सभी प्लेटफार्मों पर व्यावहारिक नहीं होगा, हालांकि, और ज्यादातर मामलों में तर्क सत्यापन के लाभ लागत से आगे निकल जाते हैं, इसलिए मानक को यह आवश्यक नहीं है कि कार्यान्वयन उस पैटर्न का समर्थन करने में सक्षम हो, लेकिन उन लोगों को अनुमति देता है जो पैटर्न का समर्थन कर सकते हैं ऐसा करने से भाषा का विस्तार करने में आसानी होती है।





function-pointers