सी

2008-10-21 17 views
11

ओपन सोर्स program I wrote में दो बार सुरक्षित रूप से दंडित करने के लिए, मैं फ़ाइल से बाइनरी डेटा (किसी अन्य प्रोग्राम द्वारा लिखित) पढ़ रहा हूं और इनट्स, युगल, और अन्य मिश्रित डेटा प्रकारों को आउटपुट कर रहा हूं। चुनौतियों में से एक यह है कि इसे दोनों एंडियननेस की 32-बिट और 64-बिट मशीनों पर चलने की आवश्यकता है, जिसका अर्थ है कि मैं बहुत कम स्तर के बिट-ट्विडलिंग करने के लिए समाप्त होता हूं। मुझे एक (बहुत) टाइपिंग और सख्त एलियासिंग टाइप करने के बारे में कुछ पता है और यह सुनिश्चित करना चाहता हूं कि मैं सही तरीके से काम कर रहा हूं।सी

असल में, यह एक चार * से विभिन्न आकार के एक पूर्णांक कन्वर्ट करने के लिए आसान है:

int64_t snativeint64_t(const char *buf) 
{ 
    /* Interpret the first 8 bytes of buf as a 64-bit int */ 
    return *(int64_t *) buf; 
} 

और मैं सहायता कार्यों की एक डाली बाइट आदेश स्वैप करने के लिए आवश्यकतानुसार ऐसे के रूप में है,:

int64_t swappedint64_t(const int64_t wrongend) 
{ 
    /* Change the endianness of a 64-bit integer */ 
    return (((wrongend & 0xff00000000000000LL) >> 56) | 
      ((wrongend & 0x00ff000000000000LL) >> 40) | 
      ((wrongend & 0x0000ff0000000000LL) >> 24) | 
      ((wrongend & 0x000000ff00000000LL) >> 8) | 
      ((wrongend & 0x00000000ff000000LL) << 8) | 
      ((wrongend & 0x0000000000ff0000LL) << 24) | 
      ((wrongend & 0x000000000000ff00LL) << 40) | 
      ((wrongend & 0x00000000000000ffLL) << 56)); 
} 

रनटाइम पर, कार्यक्रम मशीन के endianness पता लगाता है और एक समारोह सूचक को ऊपर की एक प्रदान करती है:

int64_t (*slittleint64_t)(const char *); 
if(littleendian) { 
    slittleint64_t = snativeint64_t; 
} else { 
    slittleint64_t = sswappedint64_t; 
} 

अब, मुश्किल हिस्सा तब आता है जब मैं एक char * को दो बार डालने की कोशिश कर रहा हूं।

union 
{ 
    double d; 
    int64_t i; 
} int64todouble; 

int64todouble.i = slittleint64_t(bufoffset); 
printf("%lf", int64todouble.d); 

हालांकि, कुछ compilers "int64todouble.i" काम दूर अनुकूलन और कार्यक्रम को तोड़ सकते थे: मैं तो जैसे endian-गमागमन कोड का फिर से उपयोग करना चाहते हैं। पर विचार करते समय ऐसा करने का एक सुरक्षित तरीका है कि इस कार्यक्रम को प्रदर्शन के लिए अनुकूलित किया जाना चाहिए, और यह भी कि मैं बदल सकता हूं कि परिवर्तनों के समांतर सेट को चार * से को सीधे डालने के लिए नहीं लिखना चाहिए? यदि दंड की यूनियन विधि सुरक्षित है, तो क्या मुझे का उपयोग करने के लिए snativeint64_t जैसे कार्यों को दोबारा लिखना चाहिए?

snativeint64_t: 
     movq (%rdi), %rax 
     ret 
:

int64_t snativeint64_t(const char *buf) 
{ 
    /* Interpret the first 8 bytes of buf as a 64-bit int */ 
    int64_t output; 
    memcpy(&output, buf, 8); 
    return output; 
} 

अपने मूल कोड के रूप में ठीक उसी कोडांतरक में संकलित किया:


मैं क्योंकि रूपांतरण कार्यों को फिर से लिखा Steve Jessop's जवाब का उपयोग कर तो की तरह, memcpy उपयोग करने के लिए समाप्त हो गया

दो में से, memcpy संस्करण स्पष्ट रूप से व्यक्त करता है कि मैं क्या करने की कोशिश कर रहा हूं और यहां तक ​​कि सबसे बेवकूफ कंपाइलरों पर भी काम करना चाहिए।

एडम, आपका उत्तर भी अद्भुत था और मैंने इससे बहुत कुछ सीखा। पोस्ट करने का शुक्रिया!

+0

पूर्णांक रूपांतरण केवल तभी सुरक्षित है जब चार सूचक पर्याप्त रूप से गठबंधन हो। –

+0

इस कार्यक्रम में, यह हमेशा होगा। –

+0

रनटाइम पर एंडियन-नेस का पता क्यों लगा? मैं कल्पना करता हूं कि कार्यक्रम केवल उस आर्क पर काम कर सकता है जिसे संकलित किया गया था जिसके लिए एक विशिष्ट एंडियन-नेस होगा (मुझे पता है कि कुछ आपको चुनने देते हैं), तो क्यों नॉटल और पसंद की तरह नहीं करते हैं और इसे संकलित समय निर्णय लेते हैं? –

उत्तर

2

चूंकि आपको यह सुनिश्चित करने के लिए अपने कार्यान्वयन के बारे में पर्याप्त जानकारी है कि int64_t और डबल समान आकार हैं, और उपयुक्त संग्रहण प्रस्तुतियां हैं, तो आप एक memcpy को खतरे में डाल सकते हैं। तब आपको अलियासिंग के बारे में भी सोचना नहीं है।

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

int64_t i = slittleint64_t(buffoffset); 
double d; 
memcpy(&d,&i,8); /* might emit no code if you're lucky */ 
printf("%lf", d); 

परिणामी कोड की जांच करें, या बस इसे प्रोफाइल करें। संभावना सबसे खराब स्थिति में भी धीमी नहीं होगी।

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

आम तौर पर आप sprintf और sscanf का उपयोग करके अपने युगल को स्टोर करने पर विचार कर सकते हैं, लेकिन आपकी प्रोजेक्ट के लिए फ़ाइल प्रारूप आपके नियंत्रण में नहीं हैं। लेकिन यदि आपका एप्लिकेशन एक प्रारूप में एक इनपुट फ़ाइल से किसी अन्य प्रारूप में एक आउटपुट फ़ाइल में आईईईई युगल को झुका रहा है (सुनिश्चित नहीं है कि यह है, क्योंकि मुझे प्रश्न में डेटाबेस प्रारूपों को नहीं पता है, लेकिन यदि ऐसा है), तो शायद आप इस तथ्य को भूल सकते हैं कि यह एक दोगुना है, क्योंकि आप इसे अंकगणित के लिए उपयोग नहीं कर रहे हैं। बस इसे एक अपारदर्शी char [8] के रूप में देखें, केवल फ़ाइल स्वरूप भिन्न होने पर बाइटवॉपिंग की आवश्यकता होती है।

+0

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

12

मुझे अत्यधिक सुझाव है कि आप Understanding Strict Aliasing पढ़ लें। विशेष रूप से, "एक संघ के माध्यम से कास्टिंग" लेबल वाले अनुभाग देखें। इसमें कई अच्छे उदाहरण हैं। जबकि लेख सेल प्रोसेसर के बारे में एक वेबसाइट पर है और पीपीसी असेंबली उदाहरणों का उपयोग करता है, लगभग सभी यह x86 सहित अन्य आर्किटेक्चर के लिए समान रूप से लागू होते हैं।

+0

धन्यवाद! यही वह चीज है जिसे मैं ढूंढ रहा था। मैं अब पढ़ने के लिए बंद हूँ। –

+0

@ryan_s: धन्यवाद, तय –

2

मानक कहता है कि एक संघ के एक क्षेत्र को लिखना और उससे तुरंत पढ़ना अपरिभाषित व्यवहार है। तो यदि आप नियम पुस्तिका से जाते हैं, तो यूनियन आधारित विधि काम नहीं करेगी।

मैक्रोज़ आमतौर पर एक बुरा विचार है, लेकिन यह नियम के लिए अपवाद हो सकता है।पैरामीटर के रूप में इनपुट और आउटपुट प्रकार का उपयोग कर मैक्रोज़ के एक सेट का उपयोग कर सी में टेम्पलेट जैसी व्यवहार प्राप्त करना संभव होना चाहिए।

+0

जीसीसी मैनुअल का कहना है कि "यहां तक ​​कि -फ्रिक्ट-एलाइजिंग के साथ, टाइप-पनिंग की अनुमति है, बशर्ते स्मृति प्रकार के माध्यम से स्मृति का उपयोग किया जाए।" यह इतना अच्छा कहने के लिए आकर्षक है, लेकिन मुझे कंपाइलर-विशिष्ट कोड लिखने से नफरत है। एक मैक्रो उदाहरण के लिए एक सूचक मिला? –

0

एक बहुत ही छोटे उप-सुझाव के रूप में, मेरा सुझाव है कि आप 64-बिट मामले में मास्किंग और स्थानांतरण को स्वैप कर सकते हैं या नहीं। चूंकि ऑपरेशन बाइट्स को स्वैप कर रहा है, इसलिए आप हमेशा 0xff के मुखौटा से दूर रह सकते हैं। इससे तेज, अधिक कॉम्पैक्ट कोड हो सकता है, जब तक कि संकलक अपने आप को समझने के लिए पर्याप्त स्मार्ट न हो।

संक्षेप में, यह बदल रहा है:

(((wrongend & 0xff00000000000000LL) >> 56) 

इस में:

((wrongend >> 56) & 0xff) 

एक ही परिणाम उत्पन्न करनी चाहिए।

+0

यह केवल पहले मास्क-एंड-शिफ्ट ऑपरेशन के लिए काम करेगा क्योंकि अन्य सभी आउटपुट के बीच में बिट्स को ले जा रहे हैं। –

+0

सच है, तो आपको मास्किंग के बाद इसे वापस ले जाना होगा। मैं शायद ऐसा करना पसंद करूंगा, क्योंकि विशाल स्थिरांक (मेरे लिए) से परहेज करना अच्छा है। जब आप बाइट निकालने और तब बाइट-फॉर-बाइट के साथ कुछ और कर रहे होते हैं तो बस ऑर्डर स्वैप करना बेहतर होता है। – unwind

-1

संपादित करें:
निकाला गया के बारे में टिप्पणी कैसे प्रभावी ढंग से डेटा हमेशा बड़ा endian और मशीन endianess करने की अदला-बदली को स्टोर करने के प्रश्नकर्ता के रूप में उल्लेख नहीं किया गया है किसी अन्य प्रोग्राम अपने डेटा (जो महत्वपूर्ण जानकारी है) लिखते हैं।

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


डबल/नाव के बारे में, सिर्फ स्मृति कास्टिंग द्वारा ints करने के लिए उन्हें स्टोर:

double d = 3.1234; 
printf("Double %f\n", d); 
int64_t i = *(int64_t *)&d; 
// Now i contains the double value as int 
double d2 = *(double *)&i; 
printf("Double2 %f\n", d2); 

एक समारोह में यह लपेटें

int64_t doubleToInt64(double d) 
{ 
    return *(int64_t *)&d; 
} 

double int64ToDouble(int64_t i) 
{ 
    return *(double *)&i; 
} 

प्रश्नकर्ता इस लिंक प्रदान की:

http://cocoawithlove.com/2008/04/using-pointers-to-recast-in-c-is-bad.html

साबित करने के रूप में कि कास्टिंग खराब है ... दुर्भाग्य से मैं केवल इस पृष्ठ के अधिकांश से असहमत हूं। उद्धरण और टिप्पणियां:

के रूप में आम एक सूचक है के माध्यम से कास्टिंग के रूप में, यह वास्तव में बुरा व्यवहार और संभावित जोखिम भरा कोड है। एक पॉइंटर के माध्यम से कास्टिंग को पनिंग प्रकार की वजह से बग बनाने की क्षमता है।

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

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

यह सच है, लेकिन दुर्भाग्य से मेरे कोड से पूरी तरह से असंबंधित है।

वह करने के लिए क्या दर्शाता है इस तरह कोड है:

int64_t * intPointer; 
: 
// Init intPointer somehow 
: 
double * doublePointer = (double *)intPointer; 

अब doublePointer और intPointer एक ही स्मृति स्थान के लिए दोनों बिंदु है, लेकिन एक ही प्रकार के रूप में इस का इलाज। यह वह स्थिति है जिसे आपको वास्तव में संघ के साथ हल करना चाहिए, और कुछ भी बहुत बुरा है। बुरा यह नहीं है कि मेरा कोड क्या करता है!

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

int64_t intValue = 12345; 
double doubleValue = int64ToDouble(intValue); 
// The statement below will not change the value of doubleValue! 
// Both are not pointing to the same memory location, both have their 
// own storage space on stack and are totally unreleated. 
intValue = 5678; 

मेरा कोड मेमोरी प्रति से अधिक कुछ नहीं है, केवल बाहरी कार्य के बिना सी में लिखा गया है।

int64_t doubleToInt64(double d) 
{ 
    return *(int64_t *)&d; 
} 

int64_t doubleToInt64(double d) 
{ 
    int64_t result; 
    memcpy(&result, &d, sizeof(d)); 
    return result; 
} 

के रूप में लिखा जा सकता है ऐसा नहीं है कि अधिक से अधिक कुछ भी नहीं है, इसलिए किसी भी प्रकार दृष्टि में भी कहीं भी punning है। और यह ऑपरेशन भी पूरी तरह से सुरक्षित है, क्योंकि एक ऑपरेशन के रूप में सुरक्षित सी में हो सकता है। डबल को हमेशा 64 बिट के रूप में परिभाषित किया जाता है (इंटेल के विपरीत यह आकार में भिन्न नहीं होता है, यह 64 बिट पर तय होता है), इसलिए यह हमेशा फिट होगा एक int64_t आकार परिवर्तनीय में।

+0

आपके पहले बिंदु पर, कार्यक्रम किसी अन्य प्रोग्राम द्वारा उत्पन्न डेटा पढ़ता है। दूसरी तरफ, यह इस पर फंस गया प्रतीत होता है: http://cocoawithlove.com/2008/04/using-pointers-to-recast-in-c-is-bad.html और जो मैं पूछ रहा हूं उसका हिस्सा है क्या मुझे इसे पूरी तरह से दूर करना चाहिए। –

+0

ऊपर अपडेट देखें। आपके लिंक किए गए पृष्ठ के दावे के रूप में शामिल नहीं होने के बावजूद कोई भी प्रकार की दंड नहीं है, न कि करीब भी। और आपके कोड के विपरीत मैंने कभी भी किसी भी चीज़ के लिए चार पॉइंटर नहीं डाला (जैसा कि यह असुरक्षित असुरक्षित है!), मैं मूल्य से सभी डेटा पास करता हूं (संदर्भ द्वारा कभी नहीं!) और मैं केवल उसी आकार के लिए गारंटी देता हूं जो समान आकार – Mecki

+0

मूल्य से गुजरता है प्रदर्शन कारणों से मेरे कोड में असंभव है। मैंने कभी एक चार सूचक नहीं डाला; मैंने अपनी सामग्री डाली। अंत में, ntoh * केवल बड़े-अंत मूल्यों को कास्टिंग करने पर काम करता है। छोटे-अंत मूल्यों के लिए कोई संबंधित कार्य नहीं है। –

 संबंधित मुद्दे

  • कोई संबंधित समस्या नहीं^_^