c - X86 निर्देश कैश सिंक्रनाइज़ कैसे किया जाता है?




assembly instructions (4)

मुझे उदाहरण पसंद हैं, इसलिए मैंने सी में स्वयं-संशोधित कोड का थोड़ा सा लिखा है ...

#include <stdio.h>
#include <sys/mman.h> // linux

int main(void) {
    unsigned char *c = mmap(NULL, 7, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|
                            MAP_ANONYMOUS, -1, 0); // get executable memory
    c[0] = 0b11000111; // mov (x86_64), immediate mode, full-sized (32 bits)
    c[1] = 0b11000000; // to register rax (000) which holds the return value
                       // according to linux x86_64 calling convention 
    c[6] = 0b11000011; // return
    for (c[2] = 0; c[2] < 30; c[2]++) { // incr immediate data after every run
        // rest of immediate data (c[3:6]) are already set to 0 by MAP_ANONYMOUS
        printf("%d ", ((int (*)(void)) c)()); // cast c to func ptr, call ptr
    }
    putchar('\n');
    return 0;
}

... जो काम करता है, स्पष्ट रूप से:

>>> gcc -Wall -Wextra -std=c11 -D_GNU_SOURCE -o test test.c; ./test
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29

लेकिन ईमानदारी से, मुझे उम्मीद नहीं थी कि यह बिल्कुल काम करे। मुझे उम्मीद है कि c[2] = 0 को c को पहली कॉल पर कैश किया जाएगा, जिसके बाद c को लगातार सभी कॉल c किए गए दोहराए गए परिवर्तनों को अनदेखा कर देंगी (जब तक कि मैंने किसी भी तरह से स्पष्ट रूप से कैश को अमान्य नहीं किया)। सौभाग्य से, मेरा सीपीयू उस से ज्यादा चालाक प्रतीत होता है।

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

(मुझे शायद इंटेल मैनुअल का संदर्भ लेना चाहिए, लेकिन यह बात हजारों पृष्ठों लंबी है और मैं इसमें खो जाना चाहता हूं ...)


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

11.6 स्वयं-कोडिंग कोड

एक कोड सेगमेंट में स्मृति स्थान पर लिखना जो वर्तमान में प्रोसेसर में कैश किया गया है, संबंधित कैश लाइन (या रेखाएं) को अमान्य करने का कारण बनता है।

लेकिन यह दावा तब तक मान्य है जब तक वही रैखिक पता संशोधित करने और लाने के लिए उपयोग नहीं किया जाता है, जो डीबगर्स और बाइनरी लोडर के मामले में नहीं है क्योंकि वे एक ही पता-स्थान में नहीं चलते हैं:

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

उदाहरण के लिए, सीरियलाइजेशन ऑपरेशन हमेशा पावरपीसी जैसे कई अन्य आर्किटेक्चर द्वारा अनुरोध किया जाता है, जहां इसे स्पष्ट रूप से किया जाना चाहिए ( ई 500 कोर मैनुअल ):

3.3.1.2.1 स्व-संशोधित कोड

जब एक प्रोसेसर किसी भी मेमोरी लोकेशन को संशोधित करता है जिसमें एक निर्देश हो सकता है, तो सॉफ़्टवेयर को यह सुनिश्चित करना चाहिए कि निर्देश कैश डेटा मेमोरी के अनुरूप बना है और संशोधनों को तंत्र लाने के लिए संशोधित किया गया है। यह तब भी किया जाना चाहिए जब कैश अक्षम हो या पृष्ठ को कैशिंग-अवरोधित चिह्नित किया गया हो।

यह ध्यान रखना दिलचस्प है कि कैश अक्षम होने पर भी PowerPC को संदर्भ-सिंक्रनाइज़िंग निर्देश के मुद्दे की आवश्यकता होती है; मुझे संदेह है कि यह लोड / स्टोर बफर जैसे गहरे डेटा प्रोसेसिंग इकाइयों की फ्लश को लागू करता है।

आपके द्वारा प्रस्तावित कोड आर्किटेक्चर पर स्नूपिंग या उन्नत कैश-कोहेरेंसी सुविधाओं के बिना अविश्वसनीय है, और इसलिए असफल होने की संभावना है।

उममीद है कि इससे मदद मिलेगी।


मैं बस अपनी खोज में से एक में इस पृष्ठ पर पहुंचा और लिनक्स कर्नेल के इस क्षेत्र पर अपना ज्ञान साझा करना चाहता हूं!

आपका कोड अपेक्षित रूप से निष्पादित करता है और यहां मेरे लिए कोई आश्चर्य नहीं है। Mmap () syscall और प्रोसेसर कैश कोहेन्सी प्रोटोकॉल आपके लिए यह चाल करता है। झंडे "PROT_READ | PROT_WRITE | PROT_EXEC" आईएमएलबी, एल 1 कैश के डीटीएलबी और इस भौतिक पृष्ठ के एल 2 कैश के टीएलबी को सही ढंग से सेट करने के लिए mmamp () से पूछता है। यह निम्न स्तर का आर्किटेक्चर विशिष्ट कर्नेल कोड प्रोसेसर आर्किटेक्चर (x86, AMD, ARM, SPARC इत्यादि ...) के आधार पर अलग-अलग करता है। यहां कोई भी कर्नेल बग आपके प्रोग्राम को गड़बड़ कर देगा!

यह सिर्फ स्पष्टीकरण उद्देश्य के लिए है। मान लें कि आपका सिस्टम बहुत कुछ नहीं कर रहा है और "ए [0] = 0b01000000;" के बीच कोई प्रक्रिया स्विच नहीं है और "printf (" \ n ") की शुरूआत:" ... यह भी मान लें कि आपके पास 1K L1 iCache है, आपके प्रोसेसर में 1K dCache और कोर में कुछ L2 कैश है। (अब एक दिन ये कुछ एमबी के क्रम में हैं)

  1. mmap () आपके वर्चुअल एड्रेस स्पेस और आईटीएलबी 1, डीटीएलबी 1 और टीएलबी 2 एस सेट करता है।
  2. "एक [0] 0b01000000 =," वास्तव में कर्नेल कोड में ट्रैप (एच / डब्ल्यू जादू) और आपका भौतिक पता स्थापित किया जाएगा और सभी प्रोसेसर टीएलबी कर्नेल द्वारा लोड किए जाएंगे। फिर, आप उपयोगकर्ता मोड में वापस आ जाएंगे और आपका प्रोसेसर वास्तव में 16 बाइट्स (एच / डब्ल्यू जादू एक [0] को [3]) लोड करेगा L1 dCache और L2 कैश में। प्रोसेसर वास्तव में मेमोरी में फिर से जाएगा, केवल तभी जब आप एक [4] और इसी तरह से देखें (अब भविष्यवाणी लोडिंग को अनदेखा करें!)। जब तक आप "एक [7] = 0b11000011;" पूरा करते हैं, तो आपके प्रोसेसर ने शाश्वत बस पर प्रत्येक 16 बाइट्स के दो विस्फोट किए थे। भौतिक स्मृति में अभी भी कोई वास्तविक WRITEs नहीं है। सभी WRITEs L1 dCache (एच / डब्ल्यू जादू, प्रोसेसर जानता है) के भीतर हो रहे हैं और L2 कैश के लिए और DIRTY बिट कैश-लाइन के लिए सेट है।
  3. "एक [3] ++," असेंबली कोड में स्टोर निर्देश होगा, लेकिन प्रोसेसर केवल एल 1 डीसीएसी और एल 2 में स्टोर करेगा और यह भौतिक मेमोरी पर नहीं जाएगा।
  4. चलो फ़ंक्शन कॉल "ए ()" पर आते हैं। फिर प्रोसेसर एल 2 कैश से L1 iCache में निर्देश प्राप्त करता है और इसी तरह।
  5. निम्न स्तर के mmap () syscall और कैश कोहेरेंसी प्रोटोकॉल के सही कार्यान्वयन के कारण, किसी भी प्रोसेसर के तहत किसी भी लिनक्स पर इस उपयोगकर्ता मोड प्रोग्राम का परिणाम समान होगा!
  6. यदि आप एमएमएपी () syscall की ओएस सहायता के बिना किसी भी एम्बेडेड प्रोसेसर पर्यावरण के तहत इस कोड को लिख रहे हैं, तो आपको वह समस्या मिल जाएगी जो आप उम्मीद कर रहे हैं। ऐसा इसलिए है क्योंकि आप या तो एच / डब्ल्यू तंत्र (टीएलबी) या सॉफ्टवेयर तंत्र (मेमोरी बाधा निर्देश) का उपयोग नहीं कर रहे हैं।

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


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





self-modifying