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


89

मुझे एक फ़ंक्शन की आवश्यकता है जो (WinAPI से SecureZeroMemory की तरह) हमेशा मेमोरी को शून्य करता है और दूर अनुकूलित नहीं होता है, भले ही कंपाइलर को लगता है कि मेमोरी उसके बाद फिर से एक्सेस नहीं होने वाली है। अस्थिर के लिए एक आदर्श उम्मीदवार की तरह लगता है। लेकिन मुझे वास्तव में जीसीसी के साथ काम करने के लिए कुछ समस्याएं हो रही हैं। यहाँ एक उदाहरण समारोह है:

void volatileZeroMemory(volatile void* ptr, unsigned long long size)
{
    volatile unsigned char* bytePtr = (volatile unsigned char*)ptr;

    while (size--)
    {
        *bytePtr++ = 0;
    }
}

काफी सरल। लेकिन कोड जिसे जीसीसी वास्तव में उत्पन्न करता है अगर आप कहते हैं कि यह संकलक संस्करण के साथ बेतहाशा भिन्न होता है और आप वास्तव में शून्य करने की कोशिश कर रहे हैं तो बाइट्स की मात्रा। https://godbolt.org/g/cMaQm2

  • जीसीसी 4.4.7 और 4.5.3 कभी भी अस्थिरता को नजरअंदाज नहीं करते हैं।
  • जीसीसी 4.6.4 और 4.7.3 सरणी आकार 1, 2 और 4 के लिए अस्थिरता को अनदेखा करते हैं।
  • 4.9.2 तक जीसीसी 4.8.1 सरणी आकार 1 और 2 के लिए अस्थिरता को नजरअंदाज करता है।
  • जीसीसी 5.1 तक सरणी आकार 1, 2, 4, 8 के लिए वाष्पशील को अनदेखा करें।
  • GCC 6.1 केवल किसी भी सरणी आकार (स्थिरता के लिए बोनस अंक) के लिए इसे अनदेखा करता है।

किसी भी अन्य संकलक का मैंने परीक्षण किया है (क्लैंग, आईसीसी, वीसी) उन स्टोरों को उत्पन्न करता है जिनकी कोई अपेक्षा होती है, किसी भी संकलक संस्करण और किसी भी सरणी आकार के साथ। तो इस बिंदु पर मैं सोच रहा हूं, क्या यह एक (बहुत पुराना और गंभीर?) जीसीसी संकलक बग है, या मानक में अस्थिरता की परिभाषा है जो यह आरोप लगाता है कि यह वास्तव में व्यवहार के अनुरूप है, जिससे पोर्टेबल लिखना अनिवार्य रूप से असंभव है " SecureZeroMemory "फ़ंक्शन?

संपादित करें: कुछ दिलचस्प अवलोकन।

#include <cstddef>
#include <cstdint>
#include <cstring>
#include <atomic>

void callMeMaybe(char* buf);

void volatileZeroMemory(volatile void* ptr, std::size_t size)
{
    for (auto bytePtr = static_cast<volatile std::uint8_t*>(ptr); size-- > 0; )
    {
        *bytePtr++ = 0;
    }

    //std::atomic_thread_fence(std::memory_order_release);
}

std::size_t foo()
{
    char arr[8];
    callMeMaybe(arr);
    volatileZeroMemory(arr, sizeof arr);
    return sizeof arr;
}

CallMeMaybe () से संभव लेखन 6.1 को छोड़कर सभी जीसीसी संस्करण बना देगा अपेक्षित भंडार। मेमोरी बाड़ में टिप्पणी करने से जीसीसी 6.1 भी स्टोर बनाएगा, हालांकि केवल कॉलमैये से संभावित लेखन के संयोजन में ()।

किसी ने भी कैश फ्लश करने का सुझाव दिया है। Microsoft "SecureZeroMemory" में कैश को फ्लश करने की कोशिश नहीं करता है । कैश के वैसे भी बहुत तेजी से अमान्य होने की संभावना है, इसलिए यह शायद एक बड़ी बात नहीं है। इसके अलावा, यदि कोई अन्य प्रोग्राम डेटा की जांच करने की कोशिश कर रहा है, या यदि यह पेज फ़ाइल में लिखा जा रहा है, तो यह हमेशा शून्य संस्करण होगा।

स्टैंडअलोन फ़ंक्शन में मेमसेट () का उपयोग करके जीसीसी 6.1 के बारे में कुछ चिंताएं भी हैं। कुछ लोगों के लिए स्टैंडअलोन फ़ंक्शन के लिए GCC 6.1 कंपाइलर एक टूटे हुए बिल्ड का निर्माण कर सकता है, क्योंकि GCC 6.1 एक सामान्य लूप (जैसे 5.3 गॉडबोल्ट पर करता है) उत्पन्न करता है। (Zwol के जवाब की टिप्पणियाँ पढ़ें।)


4
IMHO का उपयोग volatileकरना बग है जब तक कि अन्यथा सिद्ध न हो। लेकिन सबसे अधिक संभावना एक बग है। volatileइतना अस्वाभाविक है जितना खतरनाक हो - बस इसका उपयोग न करें।
जेसपर जुहल

19
@ जैस्परजहल: नहीं, volatileइस मामले में उचित है।
डायट्रिच एप्प

9
@ नथनऑलिवर: यह काम नहीं करेगा, क्योंकि कंपाइलर मृत दुकानों को ऑप्टिमाइज़ कर सकते हैं भले ही वे उपयोग करें memset। समस्या यह है कि संकलक को पता है कि क्या memsetकरता है।
डायट्रिच एप्प

8
@PaStStelian: यह एक volatileसंकेतक बना देगा , हम एक संकेतक चाहते हैं volatile(हम परवाह नहीं करते हैं कि क्या ++सख्त है, लेकिन क्या *p = 0सख्त है)।
डायट्रिच एप्प

7
@ जेस्पेरुहल: अस्थिर के बारे में कुछ भी निर्दिष्ट नहीं है।
GManNickG

जवाबों:


82

जीसीसी का व्यवहार अनुरूप हो सकता है, और यदि ऐसा नहीं है, तो भी आपको उन volatileमामलों पर भरोसा नहीं करना चाहिए जो आप चाहते हैं। volatileस्मृति-मैप किए गए हार्डवेयर रजिस्टरों के लिए और असामान्य नियंत्रण प्रवाह (जैसे सिग्नल हैंडलर और setjmp) के दौरान संशोधित चर के लिए बनाई गई सी समिति । वे ही चीजें हैं जिनके लिए यह विश्वसनीय है। यह एक सामान्य के रूप में उपयोग करने के लिए सुरक्षित नहीं है "इस बाहर का अनुकूलन न करें" एनोटेशन।

विशेष रूप से, एक महत्वपूर्ण बिंदु पर मानक अस्पष्ट है। (मैंने आपके कोड को C में बदल दिया है; यहाँ C और C ++ के बीच कोई विचलन नहीं होना चाहिए । मैंने मैन्युअल रूप से इनलाइनिंग की है जो कि संदिग्ध अनुकूलन से पहले होगी, यह दिखाने के लिए कि उस बिंदु पर कंपाइलर "देखता है" क्या है। ।)

extern void use_arr(void *, size_t);
void foo(void)
{
    char arr[8];
    use_arr(arr, sizeof arr);

    for (volatile char *p = (volatile char *)arr;
         p < (volatile char *)(arr + 8);
         p++)
      *p = 0;
}

मेमोरी-क्लियरिंग लूप arrएक अस्थिर-योग्य अंतराल के माध्यम से पहुंचता है , लेकिन arrखुद को घोषित नहीं किया जाता है volatile। इसलिए, कम से कम यकीनन सी कंपाइलर को यह अनुमान लगाने की अनुमति है कि लूप द्वारा बनाए गए स्टोर "मृत" हैं, और लूप को पूरी तरह से हटा दें। C Rationale में ऐसा पाठ है जिसका तात्पर्य है कि समिति का मतलब उन दुकानों को संरक्षित करने की आवश्यकता है, लेकिन मानक वास्तव में उस आवश्यकता को पूरा नहीं करता है, जैसा कि मैंने पढ़ा।

मानक क्या करता है या इसकी आवश्यकता नहीं है, इस पर अधिक चर्चा के लिए, देखें कि एक अस्थिर स्थानीय चर को एक अस्थिर तर्क से अलग क्यों अनुकूलित किया जाता है, और ऑप्टिमाइज़र उत्तरार्द्ध से नो-ऑप लूप क्यों उत्पन्न करता है? , क्या एक घोषित संदर्भ / पॉइंटर के माध्यम से घोषित गैर-वाष्पशील वस्तु तक पहुँच प्राप्त करना उक्त पहुँच पर अस्थिर नियमों को प्रदान करता है? , और जीसीसी बग 71793

समिति ने जो सोचाvolatile था उस पर अधिक जानकारी के लिए, "अस्थिर" शब्द के लिए C99 राशनेल को खोजें । जॉन रेगर के पेपर " वाष्पशील गलत हैं " के बारे में विस्तार से बताया गया है कि प्रोग्रामर अपेक्षाओं को कैसे भी volatileकर सकते हैं कि वे संतुष्ट नहीं हैं। निबंध LLVM टीम की श्रृंखला " क्या हर सी प्रोग्रामर चाहिए नो अपरिभाषित व्यवहार के बारे में " विशेष रूप से पर स्पर्श नहीं करता है volatile, लेकिन आप यह समझते हैं कि कैसे और क्यों आधुनिक सी compilers हैं में मदद मिलेगी नहीं "पोर्टेबल अस्सेम्ब्लेर्स"।


एक फ़ंक्शन को लागू करने के व्यावहारिक प्रश्न के लिए, जो आप करना चाहते थे, volatileZeroMemoryउसे लागू करना है: मानक की आवश्यकता के बावजूद या आवश्यकता के लिए था, यह मानने के लिए सबसे बुद्धिमान होगा कि आप इसके volatileलिए उपयोग नहीं कर सकते । वहाँ है एक विकल्प है कि काम करने के लिए पर भरोसा किया जा सकता है, क्योंकि यह अभी तक बहुत ज्यादा अन्य सामान तोड़ अगर यह काम नहीं किया जाएगा:

extern void memory_optimization_fence(void *ptr, size_t size);
inline void
explicit_bzero(void *ptr, size_t size)
{
   memset(ptr, 0, size);
   memory_optimization_fence(ptr, size);
}

/* in a separate source file */
void memory_optimization_fence(void *unused1, size_t unused2) {}

हालाँकि, आपको यह पूरी तरह सुनिश्चित करना चाहिए कि memory_optimization_fenceकिसी भी परिस्थिति में इनलेट न हो। यह अपनी स्वयं की स्रोत फ़ाइल में होना चाहिए और इसे लिंक-टाइम ऑप्टिमाइज़ेशन के अधीन नहीं होना चाहिए।

संकलक एक्सटेंशन पर भरोसा करने वाले अन्य विकल्प हैं, जो कुछ परिस्थितियों में उपयोग करने योग्य हो सकते हैं और तंग कोड उत्पन्न कर सकते हैं (उनमें से एक इस उत्तर के पिछले संस्करण में दिखाई दिया था), लेकिन कोई भी सार्वभौमिक नहीं है।

(मैं फ़ंक्शन को कॉल करने की सलाह देता हूं explicit_bzero, क्योंकि यह एक से अधिक सी लाइब्रेरी में उस नाम के तहत उपलब्ध है। नाम के लिए कम से कम चार अन्य दावेदार हैं, लेकिन प्रत्येक को केवल एकल सी लाइब्रेरी द्वारा अपनाया गया है।)

आपको यह भी पता होना चाहिए कि, यदि आप इसे काम करने के लिए प्राप्त कर सकते हैं, तो भी यह पर्याप्त नहीं हो सकता है। विशेष रूप से, विचार करें

struct aes_expanded_key { __uint128_t rndk[16]; };

void encrypt(const char *key, const char *iv,
             const char *in, char *out, size_t size)
{
    aes_expanded_key ek;
    expand_key(key, ek);
    encrypt_with_ek(ek, iv, in, out, size);
    explicit_bzero(&ek, sizeof ek);
}

एईएस त्वरण निर्देशों के साथ हार्डवेयर मानते हुए, यदि expand_keyऔर encrypt_with_ekइनलाइन हैं, तो कंपाइलर ekपूरी तरह से वेक्टर रजिस्टर फ़ाइल में रखने में सक्षम हो सकता है - जब तक कि कॉल नहीं किया जाता है explicit_bzero, जो इसे हटाने के लिए स्टैक पर संवेदनशील डेटा को कॉपी करने के लिए मजबूर करता है, और इससे भी बदतर, सदिश रजिस्टरों में अभी भी बैठे हैं कि चाबियाँ के बारे में एक झूठा काम नहीं करता है!


6
यह दिलचस्प है ... मैं समिति की टिप्पणियों के संदर्भ को देखने में रुचि रखता हूं।
डायट्रिच एप्प

10
कैसे की 6.7.3 (7) के साथ इस वर्ग की परिभाषा करता है volatileके रूप में [...] इसलिए किसी भी अभिव्यक्ति इस तरह के एक वस्तु की चर्चा करते हुए सख्ती से सार मशीन के नियमों के अनुसार, 5.1.2.3 में वर्णित के रूप में मूल्यांकन किया जाएगा। इसके अलावा, प्रत्येक अनुक्रम में, वस्तु में संग्रहीत अंतिम मान उस सार मशीन द्वारा निर्धारित के साथ सहमत होगा , जो पहले उल्लेख किए गए अज्ञात कार्यों द्वारा संशोधित किया गया था। एक ऐसी वस्तु की पहुँच क्या है जिसके पास अस्थिर-योग्य प्रकार हो, कार्यान्वयन-परिभाषित हो। ?
इविलनोटिक्सिस्ट इडोनोटेक्सिस्ट

15
@IwillnotexistIdonotexist उस मार्ग में मुख्य शब्द ऑब्जेक्ट हैvolatile sig_atomic_t flag;एक वाष्पशील वस्तु है*(volatile char *)fooकेवल एक अस्थिर-योग्य अंतराल के माध्यम से एक पहुंच है और मानक को किसी विशेष प्रभाव के लिए आवश्यक नहीं है।
zwol

3
मानक कहता है कि "अनुपालन" कार्यान्वयन के लिए कुछ मानदंडों को क्या पूरा करना चाहिए। यह वर्णन करने के लिए कोई प्रयास नहीं करता है कि किसी दिए गए प्लेटफ़ॉर्म पर कार्यान्वयन को "अच्छा" कार्यान्वयन या "प्रयोग करने योग्य" होने के लिए क्या मानदंडों को पूरा करना चाहिए। जीसीसी का उपचार volatileइसे "अनुपालन" लागू करने के लिए पर्याप्त हो सकता है, लेकिन इसका मतलब यह नहीं है कि यह "अच्छा" या "उपयोगी" हो। कई तरह के सिस्टम प्रोग्रामिंग के लिए इसे उन मामलों में काफी कमी माना जाना चाहिए।
Supercat

3
C कल्पना भी सीधे तौर पर कहती है "एक वास्तविक कार्यान्वयन को किसी अभिव्यक्ति के भाग का मूल्यांकन करने की आवश्यकता नहीं है यदि यह कटौती कर सकता है कि इसका मूल्य उपयोग नहीं किया गया है और कोई आवश्यक साइड इफेक्ट्स उत्पन्न नहीं होते हैं ( किसी फ़ंक्शन को कॉल करने या किसी अस्थिर ऑब्जेक्ट तक पहुंचने सहित ) । " (मेरा जोर)।
जोहान्स शहाब -

15

मुझे एक फ़ंक्शन की आवश्यकता होती है (जैसे कि WinZI से SecureZeroMemory) हमेशा स्मृति को शून्य करता है और इसे दूर अनुकूलित नहीं किया जाता है,

यह वही है जो मानक फ़ंक्शन memset_sके लिए है।


जैसा कि अस्थिर के साथ यह व्यवहार अनुरूप है या नहीं, यह कहना थोड़ा कठिन है, और अस्थिर को लंबे समय से कीड़े से ग्रस्त बताया गया है।

एक मुद्दा यह है कि चश्मा का कहना है कि "अमूर्त मशीन के नियमों के अनुसार अस्थिर वस्तुओं तक पहुंच का कड़ाई से मूल्यांकन किया जाता है।" लेकिन यह केवल 'अस्थिर वस्तुओं' को संदर्भित करता है, न कि एक गैर-वाष्पशील वस्तु को एक पॉइंटर के माध्यम से एक्सेस करना, जिसमें अस्थिरता को जोड़ा गया है। तो जाहिरा तौर पर अगर एक संकलक बता सकता है कि आप वास्तव में एक अस्थिर वस्तु तक नहीं पहुंच रहे हैं, तो वस्तु को सभी के बाद अस्थिर मानने की आवश्यकता नहीं है।


4
नोट: यह C11 मानक का हिस्सा है, और अभी तक सभी टूलकिन्स में उपलब्ध नहीं है।
डायट्रिच एप्प

5
ध्यान देना चाहिए कि यह फ़ंक्शन C11 के लिए मानकीकृत है, लेकिन C ++ 11, C ++ 14 या C ++ 17 के लिए नहीं। इसलिए तकनीकी रूप से यह C ++ का समाधान नहीं है, लेकिन मैं मानता हूं कि यह व्यावहारिक दृष्टिकोण से सबसे अच्छा विकल्प लगता है। इस बिंदु पर मुझे आश्चर्य है कि क्या जीसीसी से व्यवहार अनुरूप है या नहीं। संपादित करें: वास्तव में, वीएस 2015 में मेमसेट_एस नहीं है, इसलिए यह अभी तक पोर्टेबल नहीं है।
cooky451

2
@ cooky451 मुझे लगा कि C ++ 17 संदर्भ में C11 मानक पुस्तकालय को खींचता है (दूसरा विविध देखें)।
nwp

14
इसके अलावा, memset_sC11- मानक के रूप में वर्णन एक ओवरस्टेटमेंट है। यह अनुलग्नक K का हिस्सा है, जो C11 में वैकल्पिक है (और इसलिए C ++ में भी वैकल्पिक है)। मूल रूप से Microsoft सहित सभी कार्यान्वयनकर्ता, जिनके विचार यह पहले स्थान पर थे (!), ने इसे लेने से मना कर दिया है; पिछले मैंने सुना है कि वे इसे सी-नेक्स्ट में स्क्रैप करने की बात कर रहे थे।
१२:२६ बजे जूल

8
@ cooky451 कुछ हलकों में, Microsoft मूल रूप से हर किसी की आपत्तियों पर सी मानक में सामान मजबूर करने और फिर इसे स्वयं लागू करने के लिए परेशान नहीं है। (इसका सबसे बड़ा उदाहरण C99 नियमों की शिथिलता है कि अंतर्निहित प्रकार क्या है के लिए नियमों में छूट size_tहै। Win64 ABI C90 के अनुरूप नहीं है। यह होता ... ठीक नहीं , लेकिन भयानक नहीं ... यदि। MSVC ने वास्तव में C99 चीजों को उठाया था uintmax_tऔर जैसे %zuसमय पर, लेकिन उन्होंने ऐसा नहीं किया ।)
zwol

2

मैं इस संस्करण को पोर्टेबल C ++ के रूप में प्रस्तुत करता हूं (हालांकि शब्दार्थ सूक्ष्म रूप से भिन्न हैं):

void volatileZeroMemory(volatile void* const ptr, unsigned long long size)
{
    volatile unsigned char* bytePtr = new (ptr) volatile unsigned char[size];

    while (size--)
    {
        *bytePtr++ = 0;
    }
}

अब आपके पास एक वाष्पशील वस्तु तक पहुंच है, न कि वस्तु के अस्थिर दृश्य के माध्यम से बनाई गई गैर-वाष्पशील वस्तु तक पहुंच।

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

अंत में के बजाय किसी ऑब्जेक्ट के जीवनकाल के दौरान इस शून्यिंग का उपयोग करने के लिए, कॉलर को newमूल प्रकार का नया उदाहरण फिर से लगाने के लिए प्लेसमेंट का उपयोग करना चाहिए ।

मूल्य आरंभीकरण का उपयोग करके कोड को कम (स्पष्ट रूप से कम स्पष्ट) किया जा सकता है:

void volatileZeroMemory(volatile void* const ptr, unsigned long long size)
{
    new (ptr) volatile unsigned char[size] ();
}

और इस बिंदु पर यह एक लाइनर और बमुश्किल वारंट एक सहायक समारोह है।


2
यदि फ़ंक्शन निष्पादित होने के बाद ऑब्जेक्ट तक पहुंच प्राप्त करता है, तो वह यूबी को लागू करेगा, इसका मतलब यह है कि इस तरह के एक्सेस से पहले "साफ़" किए गए ऑब्जेक्ट को मान मिल सकता है। यह कैसे सुरक्षा के विपरीत है?
सुपरकैट

0

फ़ंक्शन के पोर्टेबल संस्करण को राइट-हैंड साइड पर एक अस्थिर ऑब्जेक्ट का उपयोग करके और संचयक को स्टोर के लिए मजबूर करने के लिए मजबूर करना संभव होना चाहिए।

void volatileZeroMemory(void* ptr, unsigned long long size)
{
    volatile unsigned char zero = 0;
    unsigned char* bytePtr = static_cast<unsigned char*>(ptr);

    while (size--)
    {
        *bytePtr++ = zero;
    }

    zero = static_cast<unsigned char*>(ptr)[zero];
}

zeroवस्तु घोषित किया जाता है volatileकि यह सुनिश्चित करता संकलक हालांकि यह हमेशा शून्य के रूप में मूल्यांकन करता है अपने मूल्य के बारे में कोई धारणा बना सकते हैं।

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


1
यह बिल्कुल काम नहीं करता है ... बस उस कोड को देखें जो उत्पन्न हो रहा है।
कुकीज्यूथ

1
मेरे उत्पन्न ASM मो को बेहतर ढंग से पढ़ने के बाद, यह फ़ंक्शन कॉल को इनलाइन करने और लूपिंग को बनाए रखने के लिए लगता है, लेकिन *ptrउस लूप के दौरान कोई भी भंडारण नहीं करना चाहिए , या वास्तव में कुछ भी नहीं ... बस लूपिंग। wtf, वहाँ मेरा दिमाग चला जाता है।
अंडरस्कोर_ड

3
@underscore_d यह इसलिए है क्योंकि यह अस्थिर के रीड को संरक्षित करते हुए स्टोर को दूर कर रहा है।
डी क्रूगर

1
हाँ, और यह एक अपरिवर्तनीय परिणाम देता है edx: मुझे यह मिलता है:.L16: subq $1, %rax; movzbl -1(%rsp), %edx; jne .L16
अंडरस्कोर_ड

1
यदि मैं फ़ंक्शन को मनमाने ढंग से volatile unsigned char constभरने वाली बाइट को पारित करने की अनुमति देने के लिए बदलता हूं ... तो यह भी नहीं पढ़ता । जनरेट की गई इनबिल्ट कॉल volatileFill()बस है [load RAX with sizeof] .L9: subq $1, %rax; jne .L9। ऑप्टिमाइज़र (ए) फिल बाइट को फिर से क्यों नहीं पढ़ता है और (बी) लूप को संरक्षित करने में परेशान करता है जहां यह कुछ भी नहीं करता है?
अंडरस्कोर_ड
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.