यदि ढेर सुरक्षा के लिए शून्य-आरंभीकृत है, तो स्टैक केवल अनधिकृत क्यों है?


15

मेरे डेबियन ग्नू / लिनक्स 9 सिस्टम पर, जब एक बाइनरी निष्पादित किया जाता है,

  • स्टैक असमान है लेकिन
  • ढेर शून्य-प्रारंभिक है।

क्यों?

मुझे लगता है कि शून्य-आरंभीकरण सुरक्षा को बढ़ावा देता है लेकिन, यदि ढेर के लिए है, तो स्टैक के लिए भी क्यों नहीं? क्या ढेर भी सुरक्षा की जरूरत नहीं है?

जहां तक ​​मैं जानता हूं, मेरा प्रश्न डेबियन के लिए विशिष्ट नहीं है।

नमूना सी कोड:

#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>

const size_t n = 8;

// --------------------------------------------------------------------
// UNINTERESTING CODE
// --------------------------------------------------------------------
static void print_array(
  const int *const p, const size_t size, const char *const name
)
{
    printf("%s at %p: ", name, p);
    for (size_t i = 0; i < size; ++i) printf("%d ", p[i]);
    printf("\n");
}

// --------------------------------------------------------------------
// INTERESTING CODE
// --------------------------------------------------------------------
int main()
{
    int a[n];
    int *const b = malloc(n*sizeof(int));
    print_array(a, n, "a");
    print_array(b, n, "b");
    free(b);
    return 0;
}

आउटपुट:

a at 0x7ffe118997e0: 194 0 294230047 32766 294230046 32766 -550453275 32713 
b at 0x561d4bbfe010: 0 0 0 0 0 0 0 0 

सी मानक malloc()स्मृति को आवंटित करने से पहले स्पष्ट करने के लिए नहीं कहता है, बेशक, लेकिन मेरा सी कार्यक्रम केवल चित्रण के लिए है। प्रश्न C के बारे में या C के मानक पुस्तकालय के बारे में प्रश्न नहीं है। बल्कि, यह सवाल एक सवाल है कि कर्नेल और / या रन-टाइम लोडर ढेर क्यों नहीं, बल्कि स्टैक को शून्य कर रहे हैं।

अन्य विशेषज्ञ

मेरा प्रश्न मानकों के दस्तावेजों की आवश्यकताओं के बजाय अवलोकन योग्य GNU / Linux व्यवहार का संबंध है। यदि अनिश्चित है कि मेरा क्या मतलब है, तो इस कोड को आज़माएं, जो आगे अपरिभाषित व्यवहार ( अपरिभाषित, यानी जहां तक ​​सी मानक का संबंध है) को इंगित करने के लिए आमंत्रित करता है :

#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>

const size_t n = 4;

int main()
{
    for (size_t i = n; i; --i) {
        int *const p = malloc(sizeof(int));
        printf("%p %d ", p, *p);
        ++*p;
        printf("%d\n", *p);
        free(p);
    }
    return 0;
}

मेरी मशीन से आउटपुट:

0x555e86696010 0 1
0x555e86696010 0 1
0x555e86696010 0 1
0x555e86696010 0 1

जहां तक ​​सी मानक का संबंध है, व्यवहार अपरिभाषित है, इसलिए मेरा प्रश्न सी मानक का संबंध नहीं है। कॉल को malloc()हर बार एक ही पते पर वापस नहीं करने की आवश्यकता होती है, लेकिन चूंकि यह कॉल malloc()वास्तव में हर बार एक ही पते पर लौटने के लिए होती है, इसलिए यह ध्यान रखना दिलचस्प है कि मेमोरी, जो कि ढेर पर है, हर बार शून्य हो जाती है।

इसके विपरीत, स्टैक शून्य नहीं लग रहा था।

मुझे नहीं पता कि आपके मशीन पर बाद का कोड क्या करेगा, क्योंकि मुझे नहीं पता कि GNU / Linux सिस्टम की कौन सी परत में मनाया गया व्यवहार है। आप इसे आजमा सकते हैं।

अपडेट करें

@ कुसलानंद ने टिप्पणियों में देखा है:

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

कि मेरा परिणाम OpenBSD पर परिणाम से अलग है वास्तव में दिलचस्प है। जाहिरा तौर पर, मेरे प्रयोग एक कर्नेल (या लिंकर) सुरक्षा प्रोटोकॉल की खोज नहीं कर रहे थे, जैसा कि मैंने सोचा था, लेकिन एक सामान्य कार्यान्वयन कलाकृतियों।

इस प्रकाश में, मेरा मानना ​​है कि, एक साथ, @mosvy, @StephenKitt और @AndreasGrapentin के उत्तर मेरे प्रश्न का समाधान करते हैं।

स्टैक ओवरफ्लो पर भी देखें: मॉलोक 0 में मानों को प्रारंभिक क्यों करता है? (credit: @bta)


2
इसके लायक क्या है, आपका सबसे हाल का कोड OpenDD पर चलने पर अलग-अलग पते और (कभी-कभी) अनइंस्टाल्यूटेड (गैर-शून्य) डेटा लौटाता है। यह स्पष्ट रूप से उस व्यवहार के बारे में कुछ नहीं कहता है जिसे आप लिनक्स पर देख रहे हैं।
Kusalananda

कृपया अपने प्रश्न का दायरा न बदलें, और उत्तर और टिप्पणियों को निरर्थक बनाने के लिए इसे संपादित करने का प्रयास न करें। सी में, "हीप" कुछ और नहीं है, लेकिन मैलोडोक () और कॉलोक () द्वारा लौटाए गए मेमोरी, और केवल बाद वाला मेमोरी को शून्य कर रहा है; newसी में ऑपरेटर ++ (भी "ढेर") लिनक्स पर सिर्फ malloc के लिए एक आवरण () है; कर्नेल को पता नहीं है और न ही परवाह है कि "ढेर" क्या है।
मच्छी

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

@ कुसलानंद मैं देख रहा हूं। कि मेरा परिणाम OpenBSD पर परिणाम से अलग है वास्तव में दिलचस्प है। जाहिरा तौर पर, आपने और मोसवी ने दिखाया है कि मेरे प्रयोग कर्नेल (या लिंकर) सुरक्षा प्रोटोकॉल की खोज नहीं कर रहे थे, जैसा कि मैंने सोचा था, लेकिन एक मात्र कार्यान्वयन कलाकारी है।
THB

@ मुझे विश्वास है कि यह एक सही अवलोकन हो सकता है, हाँ।
Kusalananda

जवाबों:


28

मॉलोक () द्वारा लौटाया गया भंडारण शून्य-आरंभिक नहीं है। यह कभी मत मानो।

आपके परीक्षण कार्यक्रम में, यह सिर्फ एक अस्थायी है: मुझे लगता है कि malloc()बस एक ताजा ब्लॉक बंद हो गया है mmap(), लेकिन उस पर भी भरोसा मत करो।

एक उदाहरण के लिए, अगर मैं इस तरह से आपके मशीन पर अपना कार्यक्रम चलाता हूं:

$ echo 'void __attribute__((constructor)) p(void){
    void *b = malloc(4444); memset(b, 4, 4444); free(b);
}' | cc -include stdlib.h -include string.h -xc - -shared -o pollute.so

$ LD_PRELOAD=./pollute.so ./your_program
a at 0x7ffd40d3aa60: 1256994848 21891 1256994464 21891 1087613792 32765 0 0
b at 0x55834c75d010: 67372036 67372036 67372036 67372036 67372036 67372036 67372036 67372036

आपका दूसरा उदाहरण बस mallocglibc में कार्यान्वयन की एक कलाकृति को उजागर करना है ; यदि आप 8 बाइट्स से बड़े बफर के साथ malloc/ बार-बार करते हैं free, तो आप स्पष्ट रूप से देखेंगे कि केवल पहले 8 बाइट्स शून्य हैं, जैसा कि निम्नलिखित नमूना कोड में है।

#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>

const size_t n = 4;
const size_t m = 0x10;

int main()
{
    for (size_t i = n; i; --i) {
        int *const p = malloc(m*sizeof(int));
        printf("%p ", p);
        for (size_t j = 0; j < m; ++j) {
            printf("%d:", p[j]);
            ++p[j];
            printf("%d ", p[j]);
        }
        free(p);
        printf("\n");
    }
    return 0;
}

आउटपुट:

0x55be12864010 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 
0x55be12864010 0:1 0:1 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 
0x55be12864010 0:1 0:1 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 
0x55be12864010 0:1 0:1 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4

2
ठीक है, हाँ, लेकिन यही कारण है कि मैंने स्टैक ओवरफ्लो के बजाय यहां सवाल पूछा है। मेरा प्रश्न सी मानक के बारे में नहीं था लेकिन आधुनिक जीएनयू / लिनक्स सिस्टम के बारे में आमतौर पर बायनेरिज़ को लिंक और लोड करने के बारे में था। आपका LD_PRELOAD हास्य है लेकिन मेरे द्वारा पूछे जाने वाले प्रश्न की तुलना में एक और प्रश्न का उत्तर देता है।
thb

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

23

इस बात पर ध्यान दिए बिना कि स्टैक को कैसे आरंभ किया जाता है, आप एक प्राचीन स्टैक नहीं देख रहे हैं, क्योंकि सी लाइब्रेरी कॉल करने से पहले कई चीजें करती है main, और वे स्टैक को छूते हैं।

जीएनयू सी लाइब्रेरी के साथ, x86-64 पर, _start प्रविष्टि बिंदु पर निष्पादन शुरू होता है , जो __libc_start_mainचीजों को सेट करने के लिए कॉल करता है, और बाद वाला कॉलिंग समाप्त होता है main। लेकिन mainकॉल करने से पहले , यह कई अन्य कार्यों को कॉल करता है, जिससे डेटा के विभिन्न टुकड़े स्टैक पर लिखे जाते हैं। फ़ंक्शन कॉल के बीच स्टैक की सामग्री साफ़ नहीं की जाती है, इसलिए जब आप इसमें जाते हैं, तो mainआपके स्टैक में पिछले फ़ंक्शन कॉल से बचे हुए होते हैं।

यह केवल आपको स्टैक से प्राप्त परिणामों की व्याख्या करता है, अपने सामान्य दृष्टिकोण और मान्यताओं के बारे में अन्य उत्तर देखें।


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

आपके जवाब ने मोसवी के साथ मिलकर मेरा सवाल सुलझा दिया। सिस्टम दुर्भाग्य से मुझे केवल दो में से एक को स्वीकार करने की अनुमति देता है ; अन्यथा, मैं दोनों को स्वीकार करता।
थब

18

दोनों ही मामलों में, आपको असंबद्ध स्मृति मिलती है , और आप इसकी सामग्री के बारे में कोई धारणा नहीं बना सकते।

जब OS को आपकी प्रक्रिया के लिए एक नया पृष्ठ संलग्न करना होता है (चाहे वह इसके स्टैक के लिए हो या इसके द्वारा इस्तेमाल किए गए क्षेत्र के लिए malloc()), यह गारंटी देता है कि यह अन्य प्रक्रियाओं से डेटा को उजागर नहीं करेगा; यह सुनिश्चित करने का सामान्य तरीका है कि इसे शून्य से भरना है (लेकिन यह किसी भी चीज़ के साथ ओवरराइट करने के लिए समान रूप से मान्य है, जिसमें एक पृष्ठ भी शामिल है /dev/urandom- वास्तव में कुछ डिबगिंग malloc()कार्यान्वयन गैर-शून्य पैटर्न लिखते हैं, जैसे गलत धारणाओं को पकड़ने के लिए)।

यदि malloc()इस प्रक्रिया द्वारा पहले से उपयोग और जारी की गई मेमोरी से अनुरोध को संतुष्ट किया जा सकता है, तो इसकी सामग्री को साफ़ नहीं किया जाएगा (वास्तव में, क्लियरिंग का कोई लेना देना नहीं है malloc()और यह नहीं हो सकता है - यह मेमोरी में मैप होने से पहले होना है। आपका पता स्थान)। आपको वह स्मृति मिल सकती है जो पहले आपकी प्रक्रिया / कार्यक्रम (जैसे पहले main()) द्वारा लिखी गई है ।

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

यदि आप वास्तव में समझना चाहते हैं कि ओएस स्तर पर क्या हो रहा है, तो मेरा सुझाव है कि आप सी लाइब्रेरी लेयर को बायपास करें और सिस्टम कॉल जैसे brk()और mmap()इसके बजाय का उपयोग करके बातचीत करें ।


1
एक या दो हफ्ते पहले, मैंने एक अलग प्रयोग, कॉलिंग malloc()और free()बार-बार कोशिश की । हालांकि malloc()हाल ही में प्रयोग में लाए गए उसी भंडारण का पुन: उपयोग करने के लिए कुछ भी करने की आवश्यकता नहीं है , malloc()लेकिन ऐसा करने के लिए ऐसा हुआ। यह हर बार एक ही पते पर लौटने के लिए हुआ, लेकिन हर बार मेमोरी को भी बंद कर दिया, जिसकी मुझे उम्मीद नहीं थी। यह मेरे लिए दिलचस्प था। आगे के प्रयोगों ने आज के प्रश्न को जन्म दिया है।
थब

1
@thb, शायद मैं स्पष्ट रूप से स्पष्ट नहीं हो रहा हूं - अधिकांश कार्यान्वयन वे आपके द्वारा दी गई मेमोरी के साथ malloc()बिल्कुल कुछ नहीं करते हैं - यह या तो पहले से उपयोग किया जाता है, या हौसले से असाइन किया गया (और इसलिए ओएस द्वारा शून्य)। आपके परीक्षण में, आपको स्पष्ट रूप से उत्तरार्द्ध मिला। इसी प्रकार, स्टैक मेमोरी को क्लियर स्टेट में आपकी प्रक्रिया के लिए दिया जाता है, लेकिन आप उन हिस्सों को देखने के लिए इसकी पर्याप्त जांच नहीं करते हैं, जहां आपकी प्रक्रिया अभी तक स्पर्श नहीं हुई है। आपका ढेर स्मृति है इससे पहले कि यह अपनी प्रक्रिया को प्राप्त होने वाली मंजूरी दे दी।
टोबे स्पाईट

2
@TobySpeight: brk और sbrk mmap द्वारा obsoleted हैं। pubs.opengroup.org/onlinepubs/7908799/xsh/brk.html लीगेसी को सबसे ऊपर कहता है।
जोशुआ

2
यदि आपको आरंभिक मेमोरी की आवश्यकता है, तो इसका उपयोग callocएक विकल्प (के बजाय memset) हो सकता है
eckes

2
@thb और टोबी: मजेदार तथ्य: कर्नेल के नए पृष्ठ अक्सर आलसी आवंटित किए जाते हैं, और केवल एक साझा शून्य पृष्ठ पर कॉपी-ऑन-राइट मैप किया जाता है। यह तब mmap(MAP_ANONYMOUS)तक होता है जब तक आप उपयोग नहीं करते हैं MAP_POPULATE। नए स्टैक पेजों को ताजा भौतिक पृष्ठों द्वारा समर्थित और वायर्ड किया गया है (हार्डवेयर पेज टेबल में मैप किया गया है, साथ ही बढ़ते समय कर्नेल के पॉइंटर / लेंथ लिस्ट), क्योंकि आम तौर पर पहली बार छुआ जाने पर नई स्टैक मेमोरी लिखी जा रही है। । लेकिन हाँ, कर्नेल को किसी भी तरह डेटा लीक करने से बचना चाहिए, और शून्य करना सबसे सस्ता और उपयोगी है।
पीटर कॉर्डेस

9

आपका आधार गलत है।

जिसे आप 'सुरक्षा' के रूप में वर्णित करते हैं वह वास्तव में गोपनीयता है , जिसका अर्थ है कि कोई भी प्रक्रिया किसी अन्य प्रक्रिया मेमोरी को नहीं पढ़ सकती है, जब तक कि यह मेमोरी स्पष्ट रूप से इन प्रक्रियाओं के बीच साझा नहीं की जाती है। एक ऑपरेटिंग सिस्टम में, यह समवर्ती गतिविधियों, या प्रक्रियाओं के अलगाव का एक पहलू है।

ऑपरेटिंग सिस्टम इस अलगाव को सुनिश्चित करने के लिए क्या कर रहा है, जब भी मेमोरी को ढेर या स्टैक आवंटन के लिए प्रक्रिया द्वारा अनुरोध किया जाता है, तो यह मेमोरी या तो भौतिक मेमोरी में एक क्षेत्र से आ रही है जो व्हिट जीरो से भरी हुई है, या जो कबाड़ से भरी हुई है उसी प्रक्रिया से आ रहा है

यह सुनिश्चित करता है कि आप केवल कभी जीरो, या अपने स्वयं के कबाड़ को देख रहे हैं, इसलिए गोपनीयता सुनिश्चित की जाती है, और ढेर और स्टैक दोनों 'सुरक्षित' हैं, यद्यपि आवश्यक नहीं (शून्य-) आरंभीकृत।

आप अपने माप में बहुत अधिक पढ़ रहे हैं।


1
प्रश्न का अद्यतन अनुभाग अब आपके स्पष्ट उत्तर का संदर्भ देता है।
थब
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.