linux - क्या कचरा पैरामीटर के उच्च बिट और x86-64 SysV ABI में रिटर्न वैल्यू रजिस्टरों में अनुमत है?



calling-convention (1)

एक्स 86-64 एसआईएसवी एबीआई अन्य बातों के अलावा निर्दिष्ट करती है कि रजिस्टरों में कैसे कार्य पैरामीटर पारित किए जाते हैं ( rdi में पहले तर्क, फिर rdi और इसी तरह), और कैसे पूर्णांक रिटर्न वैल्यू वापस ( rax और बाद में वास्तव में बड़े मानों के लिए rdx )।

मुझे जो नहीं मिल रहा है, वही है, जब पैरामीटर या रिटर्न वैल्यू रजिस्टरों की उच्च बिट्स 64-बिट्स की तुलना में कम प्रकार के होते हैं

उदाहरण के लिए, निम्न फ़ंक्शन के लिए:

void foo(unsigned x, unsigned y);

... x rdi और rdi में rdi में पास किया जाएगा, लेकिन वे केवल 32 बिट्स हैं क्या उच्च 32-बिट rdi और rdi को शून्य होने की जरूरत है? सहजता से, मैं हाँ मानता हूं, लेकिन जीसीसी, झूठ और आईसीसी द्वारा उत्पन्न कोड विशिष्ट बिट निर्देशों की शुरुआत में उच्च बिट्स को शून्य करने के लिए निर्दिष्ट करता है, इसलिए ऐसा लगता है कि कंपाइलर अन्यथा मानते हैं।

इसी तरह, कंपिलर यह मानते हैं कि रिटर्न वैल्यू rax के उच्च बिट्स कचरा बिट्स हो सकते हैं यदि रिटर्न वैल्यू 64 बिट्स से छोटा है। उदाहरण के लिए, निम्न कोड में छोरों:

unsigned gives32();
unsigned short gives16();

long sum32_64() {
  long total = 0;
  for (int i=1000; i--; ) {
    total += gives32();
  }
  return total;
}

long sum16_64() {
  long total = 0;
  for (int i=1000; i--; ) {
    total += gives16();
  }
  return total;
}

... clang में निम्नलिखित को संकलित करें (और अन्य कंपाइलर्स समान हैं):

sum32_64():
...
.LBB0_1:                               
    call    gives32()
    mov     eax, eax
    add     rbx, rax
    inc     ebp
    jne     .LBB0_1


sum16_64():
...
.LBB1_1:
    call    gives16()
    movzx   eax, ax
    add     rbx, rax
    inc     ebp
    jne     .LBB1_1

कॉल को 32-बीट्स पर वापस आने के बाद mov eax, eax और 16-बिट कॉल के बाद movzx eax, ax को movzx eax, ax करें - दोनों को क्रमशः शीर्ष 32 या 48 बिट्स को शून्य करने का असर है। तो इस व्यवहार की कुछ लागत है - 64-बिट रिटर्न मान से निपटने वाला एक ही लूप इस निर्देश को छोड़ देता है।

मैंने x86-64 प्रणाली V ABI दस्तावेज़ को बहुत सावधानी से पढ़ा है, लेकिन मुझे यह पता नहीं चला कि यह व्यवहार मानक में प्रलेखित है या नहीं।

ऐसे निर्णय के क्या लाभ हैं? मुझे लगता है कि स्पष्ट लागतें हैं:

पैरामीटर लागत

पैरामीटर मानों से निपटते समय कैली के कार्यान्वयन पर लागतें लगाई जाती हैं और कार्यों में जब पैरामीटर से निपटने। माना जाता है कि अक्सर यह लागत शून्य होती है क्योंकि फ़ंक्शन प्रभावी रूप से उच्च बिट को अनदेखा कर सकता है या 32-बिट ऑपरेंड आकार के निर्देशों का प्रयोग किया जा सकता है क्योंकि शून्य पर उच्च बिट्स को शून्य रूप से शून्य किया जाता है।

हालांकि, लागत 32-बिट तर्कों को स्वीकार करने वाले कार्यों के मामलों में अक्सर बहुत वास्तविक होते हैं और कुछ गणित करते हैं जो 64-बिट गणित से लाभान्वित हो सकते हैं। उदाहरण के लिए इस फ़ंक्शन को लें:

uint32_t average(uint32_t a, uint32_t b) {
  return ((uint64_t)a + b) >> 2;
}

एक समारोह की गणना करने के लिए 64-बिट गणित का सीधा उपयोग, जो अन्यथा सावधानी से ओवरफ्लो से निपटने के लिए होता है (इस तरह से 32-बिट फ़ंक्शन को बदलने की क्षमता 64-बिट आर्किटेक्चर का अक्सर अनदेखा लाभ होता है)। यह संकलित करता है:

average(unsigned int, unsigned int):
        mov     edi, edi
        mov     eax, esi
        add     rax, rdi
        shr     rax, 2
        ret  

4 निर्देशों में से पूरी तरह से 2 ( ret को छोड़कर) केवल उच्च बिट्स को शून्य करने की आवश्यकता है। यह मोम-उन्मूलन के साथ अभ्यास में सस्ता हो सकता है, लेकिन फिर भी यह भुगतान करने के लिए एक बड़ी लागत लगता है।

दूसरी ओर, मैं वास्तव में कॉल करने वालों के लिए इसी तरह की लागत नहीं देख सकता अगर एबीआई निर्दिष्ट करे कि उच्च बिट्स शून्य हैं। क्योंकि rdi और rdi और अन्य rdi पासिंग रजिस्टरों को खरोंच (यानी, कॉलर द्वारा ओवरराइट किया जा सकता है), आपके पास केवल कुछ परिदृश्य हैं (हम rdi को rdi , लेकिन इसे अपनी पसंद के पैरामीटर के साथ बदलें):

  1. rdi में कार्य करने के लिए मूल्य डाक कॉल कोड में मृत (आवश्यक नहीं) है। उस मामले में, rdi लिए जो भी निर्देश दिया गया है, rdi केवल edi को निर्दिष्ट करना है। न केवल यह मुफ़्त है, यह अक्सर एक बाइट छोटा होता है यदि आप आरएक्स उपसर्ग से बचते हैं

  2. फ़ंक्शन के बाद rdi में फ़ंक्शन को पास किए जाने वाला मान आवश्यक है। उस मामले में, क्योंकि rdi कॉलर-सेव हुआ है, कॉल करने के लिए कॉलर को किसी भी तरह से कैली-सेवर किए गए रजिस्टर में मूल्य के एक mov की ज़रूरत है। आप आम तौर पर इसे व्यवस्थित कर सकते हैं ताकि मूल्य कैली rbx रिजस्टर ( rbx ) में शुरू हो जाए और फिर edi जैसे mov edi, ebx में ले जाया जाता है, इसलिए इसमें कुछ भी लागत नहीं है

मैं कई परिदृश्यों को नहीं देख सकता जहां शून्य से ज्यादा कॉलर की लागत होती है। कुछ उदाहरण यदि 64-बिट गणित की अंतिम अनुदेश में जरूरी है जो rdi सौंपा है। यह काफी दुर्लभ हालांकि लगता है।

वापसी मूल्य की लागत

यहां निर्णय अधिक तटस्थ लगता है। कूल को साफ़ करने के लिए कबाड़ में एक निश्चित कोड होता है (आप कभी कभी mov eax, eax यह करने के लिए mov eax, eax निर्देश देखें), लेकिन अगर कचरा को अनुमति दी जाती है तो कैली को बदल जाता है। कुल मिलाकर, यह अधिक संभावना है कि कॉलर कबाड़ को मुफ्त में साफ़ कर सकता है, जिससे कूड़े को प्रदर्शन के लिए समग्र रूप से हानिकारक नहीं दिखता।

मुझे लगता है कि इस व्यवहार के लिए एक दिलचस्प उपयोग-मामले यह है कि अलग-अलग आकार वाले कार्य एक समान कार्यान्वयन को साझा कर सकते हैं। उदाहरण के लिए, निम्न सभी कार्य:

short sums(short x, short y) {
  return x + y;
}

int sumi(int x, int y) {
  return x + y;
}

long suml(long x, long y) {
  return x + y;
}

वास्तव में एक ही कार्यान्वयन साझा कर सकते हैं 1 :

sum:
        lea     rax, [rdi+rsi]
        ret

1 चाहे इस तरह के तह को वास्तव में फ़ंक्शंस के लिए अनुमति दी जाती है , जिनके पास उनका पता है, बहस के लिए बहुत खुला है


ऐसा लगता है कि आपके पास यहां दो प्रश्न हैं:

  1. लौटने के पहले वापसी मूल्य के उच्च बिट को शून्य करना चाहिए? (और क्या तर्क के उच्च बिट्स को कॉल करने से पहले शून्य करने की आवश्यकता है?)
  2. इस निर्णय से जुड़े लागत / लाभ क्या हैं?

पहले सवाल का जवाब नहीं है, उच्च बिट्स में कचरा हो सकता है , और पीटर कॉर्ड ने इस विषय पर पहले ही बहुत अच्छा जवाब लिखा है।

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

लेकिन मैं एक और विचार को उजागर करना चाहता था: सुरक्षा

सूचना लीक

जब परिणाम के ऊपरी बिट को साफ़ नहीं किया जाता है, तो वे अन्य टुकड़ों के टुकड़ों को बनाए रख सकते हैं, जैसे स्टिक / हीप में फंक्शन पॉइंटर या एड्रेस। यदि उच्च-विशेषाधिकारित कार्यों को निष्पादित करने और बाद में rax (या eax ) का पूर्ण मूल्य प्राप्त करने के लिए कोई तंत्र मौजूद है, तो यह एक सूचना रिसाव पेश कर सकता है । उदाहरण के लिए, सिस्टम कॉल कर्नेल से उपयोगकर्ता स्थान पर एक संकेतक रिसाव कर सकता है, जिससे कर्नेल एएसएलआर की हार हो सकती है। या एक आईपीसी तंत्र एक अन्य प्रक्रिया के बारे में जानकारी लीक कर सकता है 'पता स्थान जो सैंडबॉक्स ब्रेकआउट को विकसित करने में सहायता कर सकता है।

बेशक, कोई तर्क दे सकता है कि सूचना लीक को रोकने के लिए यह एबीआई की जिम्मेदारी नहीं है; यह सही ढंग से अपने कोड को लागू करने के लिए प्रोग्रामर पर निर्भर है जबकि मैं सहमत हूं, यह अनिवार्य है कि कंपाइलर शून्य ऊपरी बिट्स का अभी भी इस जानकारी को नष्ट करने का एक प्रभाव होगा।

आपको अपने इनपुट पर भरोसा नहीं करना चाहिए

दूसरी तरफ, और अधिक महत्वपूर्ण बात, कंपाइलर को आंखों से आश्वस्त नहीं होना चाहिए कि किसी भी प्राप्त मानों के पास ऊपरी बिट्स को शून्य किया गया है या नहीं, यह कार्य अपेक्षित रूप से व्यवहार नहीं कर सकता है, और ये शोयोगी परिस्थितियों का भी नेतृत्व कर सकता है। उदाहरण के लिए, निम्नलिखित पर विचार करें:

unsigned char buf[256];
...
__fastcall void write_index(unsigned char index, unsigned char value) {
    buf[index] = value;
}

यदि हमें यह अनुमान लगाने की अनुमति दी गई थी कि index ऊपरी बिट्स शून्य हैं, तो हम इसके बाद के संस्करण को संकलित कर सकते हैं:

write_index:  ;; sil = index, dil = value
    mov rax, offset buf
    mov [rax+rsi], dil
    ret

लेकिन अगर हम इस फ़ंक्शन को अपने कोड से कह सकते हैं, तो हम [0,255] सीमा से [0,255] मूल्य की आपूर्ति कर सकते हैं और बफर की सीमा से परे स्मृति में लिख सकते हैं

बेशक, कंपाइलर वास्तव में इस तरह से कोड उत्पन्न नहीं करेगा, क्योंकि जैसा कि ऊपर बताया गया है, कॉलर की तुलना में, यह शून्य की ज़िम्मेदारी है या अपने तर्कों को साइन-इन करें। यह, मुझे लगता है कि, कोड प्राप्त करने का एक बहुत ही व्यावहारिक कारण है, मान हमेशा मानता है कि ऊपरी बिट में कचरा है और इसे स्पष्ट रूप से हटा दें





calling-convention