संकेत जटिल हैं, या बाइट में क्या संग्रहीत है?

नमस्कार, हेब्र! मैं आपके लिए लेख "पॉइंटर्स आर कॉम्प्लिकेटेड, या: व्हाट इन ए बाइट?" का अनुवाद प्रस्तुत करता हूँ। राल्फ जंग की लेखनी।


इस गर्मी में मैं फिर से फुलटाइम पर काम कर रहा हूं, और मैं फिर से (अन्य चीजों के बीच) रस्ट / एमआईआर के लिए "मेमोरी मॉडल" पर काम करूंगा। हालांकि, इससे पहले कि मैं अपने विचारों के बारे में बात करूं, मुझे अंततः इस मिथक को दूर करना होगा कि "संकेत सरल हैं: वे बस संख्या हैं।" इस कथन के दोनों भाग त्रुटिपूर्ण हैं, कम से कम असुरक्षित विशेषताओं वाली भाषाओं में, जैसे कि Rust या C: पॉइंटर्स को प्राइम या (साधारण) संख्या नहीं कहा जा सकता है।


मैं स्मृति मॉडल के उस भाग पर भी चर्चा करना चाहूंगा जिसे संबोधित करने से पहले हमें और अधिक जटिल भागों के बारे में बात करनी होगी: स्मृति में संग्रहीत डेटा किस रूप में है? एक मेमोरी में बाइट्स, न्यूनतम पता योग्य इकाइयाँ और सबसे छोटे तत्व होते हैं जिन्हें एक्सेस किया जा सकता है (कम से कम अधिकांश प्लेटफार्मों पर), लेकिन संभावित बाइट मान क्या हैं? फिर, यह पता चला है कि "यह सिर्फ एक 8-बिट संख्या है" एक उत्तर के रूप में उपयुक्त नहीं है।


मुझे उम्मीद है कि इस पोस्ट को पढ़ने के बाद, आप दोनों कथनों के संबंध में मुझसे सहमत होंगे।


संकेत जटिल हैं


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


int test() { auto x = new int[8]; auto y = new int[8]; y[0] = 42; int i = /* -     */; auto x_ptr = &x[i]; *x_ptr = 23; return y[0]; } 

42 की वापसी के साथ y [0] के अंतिम रीड का अनुकूलन करना हमेशा बहुत फायदेमंद होता है। इस अनुकूलन के लिए तर्क यह है कि x_ptr को बदलना जो x को इंगित करता है, y को बदल नहीं सकता है।


हालाँकि, C ++ जैसी निम्न-स्तरीय भाषाओं के साथ काम करते समय, हम i yx मान देकर इस धारणा का उल्लंघन कर सकते हैं। चूँकि & x [i] x + i के समान है, हम 23 & y [0] में लिखते हैं।


बेशक, यह C ++ कंपाइलरों को इस तरह के अनुकूलन करने से नहीं रोकता है। इसे हल करने के लिए, मानक कहता है कि हमारे कोड में यूबी है


सबसे पहले, यह संकेत पर अंकगणितीय संचालन करने की अनुमति नहीं है (जैसा कि & x [i] के मामले में), अगर इस मामले में सूचक सरणी की सीमाओं से परे जाता है । हमारा कार्यक्रम इस नियम का उल्लंघन करता है: x [i] x से आगे जाता है, इसलिए यह यूबी है। दूसरे शब्दों में, यहां तक ​​कि x_ptr मान की गणना भी UB है, इसलिए हम उस स्थान पर भी नहीं पहुंचते जहां हम इस सूचक का उपयोग करना चाहते हैं।


(यह पता चला है कि i = yx भी UB है, क्योंकि केवल उसी मेमोरी एलोकेशन की ओर इशारा करने वाले पॉइंटर्स को घटाया जा सकता है । हालाँकि, हम लिख सकते हैं i = ((size_t) y - (size_t) x) / sizeof (int) बाईपास करने के लिए। यह एक सीमा है।)


लेकिन हम अभी तक नहीं किए गए हैं: इस नियम का एकमात्र अपवाद है जिसे हम अपने लाभ के लिए उपयोग कर सकते हैं। यदि अंकगणित ऑपरेशन सरणी के अंत के बाद सूचक को पते के मूल्य की गणना करता है, तो सब कुछ क्रम में है। (इस अपवाद को C ++ 98 में सबसे आम छोरों के लिए vec.end () की गणना करने की आवश्यकता है।)


आइए उदाहरण को थोड़ा बदलें:


 int test() { auto x = new int[8]; auto y = new int[8]; y[0] = 42; auto x_ptr = x+8; //    if (x_ptr == &y[0]) *x_ptr = 23; return y[0]; } 

अब कल्पना कीजिए कि x और y को एक के बाद एक आवंटित किया गया था, जिसमें y का एक बड़ा पता था। फिर y की शुरुआत के लिए x_ptr अंक! तब स्थिति सही होती है और असाइनमेंट होता है। इसी समय, विदेशों में सूचक के बाहर निकलने के कारण कोई यूबी नहीं है।


ऐसा लगता है कि यह अनुकूलन की अनुमति नहीं देगा। हालांकि, सी ++ मानक में कंपाइलर रचनाकारों की मदद करने के लिए अपनी आस्तीन ऊपर एक और है: वास्तव में, यह हमें x_ptr का उपयोग करने की अनुमति नहीं देता है। मानक के अनुसार पॉइंटर्स में संख्याओं को जोड़ने के बारे में, x_ptr सरणी के अंतिम तत्व के बाद पते पर इंगित करता है। यह किसी अन्य वस्तु के एक विशिष्ट तत्व की ओर इशारा नहीं करता है , भले ही उनका पता समान हो । (कम से कम यह मानक की एक आम व्याख्या है जिसके आधार पर एलएलवीएम इस कोड का अनुकूलन करता है ।)


और फिर भी x_ptr और y [0] एक ही पते की ओर इशारा करते हैं, लेकिन यह उन्हें एक ही पॉइंटर नहीं बनाता है, अर्थात, उनका उपयोग परस्पर नहीं किया जा सकता है: & y [0] y के पहले तत्व को इंगित करता है; x_ptr x के बाद पते की ओर इशारा करता है। यदि हम * x_ptr = 23 को स्ट्रिंग * & y [0] = 0 से प्रतिस्थापित करते हैं, तो हम प्रोग्राम के मान को बदल देंगे, भले ही दो बिंदुओं की समानता के लिए जाँच की गई हो।


यह दोहराने लायक है:


सिर्फ इसलिए कि दो संकेत एक ही पते की ओर इशारा करते हैं, इसका मतलब यह नहीं है कि वे समान हैं और इसका उपयोग परस्पर किया जा सकता है।

हां, यह अंतर मायावी है। वास्तव में, यह अभी भी एलएलवीएम और जीसीसी के साथ संकलित कार्यक्रमों में अंतर का कारण बनता है।


यह भी ध्यान दें कि यह एक के बाद नियम C / C ++ में एकमात्र स्थान नहीं है जहां हम इस तरह के प्रभाव का निरीक्षण कर सकते हैं। एक अन्य उदाहरण सी में प्रतिबंधित कीवर्ड है, जिसका उपयोग यह व्यक्त करने के लिए किया जा सकता है कि संकेत ओवरलैप नहीं हैं (समान नहीं हैं):


 int foo(int *restrict x, int *restrict y) { *x = 42; if (x == y) { *y = 23; } return *x; } int test() { int x; return foo(&x, &x); } 

परीक्षण () कॉल कॉल यूबी, क्योंकि फू में दो मेमोरी एक्सेस एक ही पते पर नहीं होनी चाहिए। * Y को * x के साथ foo में बदलने पर, हम प्रोग्राम के मान को बदल देंगे, और इसे यूबी नहीं कहा जाएगा। एक बार फिर: हालांकि x और y का पता समान है, लेकिन उनका उपयोग परस्पर नहीं किया जा सकता है।


संकेत निश्चित रूप से केवल संख्या नहीं हैं।


सरल सूचक मॉडल


तो एक सूचक क्या है? मुझे पूरा जवाब नहीं पता। वास्तव में, यह अनुसंधान के लिए एक खुला क्षेत्र है।


एक महत्वपूर्ण बिंदु: यहाँ हम एक अमूर्त सूचक मॉडल को देख रहे हैं। बेशक, एक वास्तविक कंप्यूटर पर, पॉइंटर्स नंबर हैं। लेकिन एक वास्तविक कंप्यूटर उन अनुकूलन को नहीं करता है जो आधुनिक सी ++ कंपाइलर करते हैं। यदि हम उपरोक्त कार्यक्रमों को असेंबलर में लिखते हैं, तो कोई यूबी नहीं होगा, कोई अनुकूलन नहीं होगा। C ++ और Rust मेमोरी और पॉइंटर्स के लिए अधिक "उच्च-स्तरीय" दृष्टिकोण लेते हैं, जो प्रोग्रामर को कंपाइलर तक सीमित कर देता है। जब औपचारिक रूप से यह वर्णन करना आवश्यक है कि एक प्रोग्रामर इन भाषाओं में क्या कर सकता है और नहीं कर सकता है, तो संख्या के रूप में संकेत के मॉडल बिखर जाते हैं, इसलिए हमें कुछ और खोजने की आवश्यकता है। यह एक "वर्चुअल मशीन" का उपयोग करने का एक और उदाहरण है जो विनिर्देश उद्देश्यों के लिए एक वास्तविक कंप्यूटर से अलग है - एक विचार जो मैंने पहले के बारे में लिखा था


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


 struct Pointer { alloc_id: usize, offset: isize, } 

एक पॉइंटर (एक पॉइंटर से) में एक संख्या को जोड़ने (घटाना) का संचालन केवल ऑफसेट को प्रभावित करता है, और इसलिए पॉइंटर कभी भी मेमोरी क्षेत्र को नहीं छोड़ सकता है। बिंदुओं को घटाना केवल तभी संभव है जब वे एक ही मेमोरी क्षेत्र ( C ++ के अनुसार) से संबंधित हों।


(जैसा कि हम देख सकते हैं, C ++ मानक इन नियमों को सरणियों पर लागू करता है, मेमोरी क्षेत्रों पर नहीं। हालाँकि, LLVM उन्हें क्षेत्र स्तर पर लागू करता है ।)


यह पता चला (और मिरी एक ही बात दिखाता है) कि यह मॉडल हमारी अच्छी सेवा कर सकता है। हम हमेशा याद रखते हैं कि सूचक किस क्षेत्र की स्मृति से संबंधित है, इसलिए हम एक मेमोरी क्षेत्र के एक के बाद एक सूचक को दूसरे क्षेत्र की शुरुआत में अंतर कर सकते हैं। इस प्रकार मिरी को लग सकता है कि हमारे दूसरे उदाहरण (& x [8]) में यूबी है।


हमारा मॉडल बिखर रहा है


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


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


संक्षेप में, एक दूसरे के लिए संकेत और संख्याओं की जातियां भ्रामक हैं और औपचारिक रूप से निर्धारित करना मुश्किल है, ऊपर वर्णित अनुकूलन को देखते हुए। अनुकूलन के लिए आवश्यक उच्च-स्तरीय दृष्टिकोण और संख्या और इसके विपरीत कास्टिंग पॉइंटर्स का वर्णन करने के लिए आवश्यक निम्न-स्तरीय दृष्टिकोण के बीच एक संघर्ष है। अधिकांश भाग के लिए, हम केवल इस समस्या को miri में अनदेखा करते हैं और, जब भी संभव हो, हम जिस सरल मॉडल के साथ काम करते हैं, उसका उपयोग करने के लिए जितना संभव हो उतना करने की कोशिश करें। C ++ या Rust जैसी भाषाओं की एक पूरी परिभाषा, निश्चित रूप से इतने सरल तरीके से नहीं जा सकती है, यह स्पष्ट करना चाहिए कि वास्तव में क्या हो रहा है। जहां तक ​​मुझे पता है, कोई उपयुक्त समाधान नहीं है, लेकिन अकादमिक अनुसंधान सच्चाई से संपर्क कर रहा है


यही कारण है कि संकेत भी सरल नहीं हैं।


पॉइंटर्स से बाइट्स तक


मुझे उम्मीद है कि मैंने एक तर्कपूर्ण तर्क दिया है कि संख्या केवल डेटा प्रकार पर विचार करने के लिए नहीं है अगर हम औपचारिक रूप से निम्न स्तर की भाषाओं जैसे कि C ++ या Rust (असुरक्षित) भाग का वर्णन करना चाहते हैं। हालाँकि, इसका मतलब है कि मेमोरी से बाइट पढ़ने जैसा एक सरल ऑपरेशन सिर्फ u8 नहीं लौटा सकता है। कल्पना करें कि हम स्रोत के प्रत्येक बाइट को कुछ स्थानीय चर v में बदले में पढ़कर मेमसीपी को लागू करते हैं, और फिर इस मान को लक्ष्य स्थान में संग्रहीत करते हैं। लेकिन क्या होगा अगर यह बाइट एक पॉइंटर का हिस्सा है? यदि सूचक मेमोरी एरिया आईडी और ऑफसेट की एक जोड़ी है, तो इसकी पहली बाइट क्या होगी? हमें यह कहने की आवश्यकता है कि v का मान कितना है, इसलिए हमें किसी तरह इस प्रश्न का उत्तर देना होगा। (और यह गुणा के साथ समस्या की तुलना में एक पूरी तरह से अलग समस्या है, जो पिछले अनुभाग में थी। हम सिर्फ यह मानते हैं कि पॉंटर का कुछ सार प्रकार है।)


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


 enum ByteV1 { Bits(u8), PtrFragment(Pointer, u8), } 

उदाहरण के लिए, PtrFragment (ptr, 0) ptr पॉइंटर के पहले बाइट का प्रतिनिधित्व करता है। इस प्रकार, मेम्सी पॉइंटर को अलग-अलग बाइट्स में "तोड़" सकता है जो मेमोरी में इस पॉइंटर का प्रतिनिधित्व करते हैं, और उन्हें व्यक्तिगत रूप से कॉपी करते हैं। 32-बिट आर्किटेक्चर पर, पूर्ण ptr प्रतिनिधित्व में 4 बाइट्स होंगे:


 [PtrFragment(ptr, 0), PtrFragment(ptr, 1), PtrFragment(ptr, 2), PtrFragment(ptr, 3)] 

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


अनधिकृत स्मृति


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


 enum Byte { Bits(u8), PtrFragment(Pointer, u8), Uninit, } 

हम आवंटित मेमोरी में उन सभी बाइट्स के लिए यूनिनिट वैल्यू का उपयोग करते हैं जिनमें हमने अभी तक कोई मूल्य नहीं लिखा है। समस्याओं के बिना निर्विवाद मेमोरी को पढ़ना संभव है, लेकिन इन बाइट्स (उदाहरण के लिए, संख्यात्मक अंकगणित) के साथ कोई अन्य क्रियाएं यूबी की ओर ले जाती हैं।


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


आपको आश्चर्य हो सकता है कि हमारे पास एक विशेष यूनिनिट मूल्य क्यों है प्रत्येक नई बाइट के लिए कुछ मनमाना b: u8 क्यों न चुनें, और फिर प्रारंभिक मूल्य के रूप में बिट्स (b) का उपयोग करें? यह वास्तव में एक विकल्प है। हालांकि, सबसे पहले, सभी संकलक दृष्टिकोण के लिए एक विशेष मान का उपयोग करते हुए पहुंचे। इस दृष्टिकोण का पालन न करने का मतलब केवल एलएलवीएम के माध्यम से संकलन की समस्याएं नहीं हैं, बल्कि सभी अनुकूलन की समीक्षा करना और यह सुनिश्चित करना है कि वे इस संशोधित मॉडल के साथ सही तरीके से काम करते हैं। यहाँ मुख्य बिंदु: आप हमेशा यूनीट को किसी अन्य मूल्य के साथ सुरक्षित रूप से बदल सकते हैं: इस मूल्य को प्राप्त करने वाले किसी भी ऑपरेशन से किसी भी मामले में यूबी हो जाएगा।


उदाहरण के लिए, यह C कोड यूनीट के साथ अनुकूलित करना आसान है:


 int test() { int x; if (condA()) x = 1; //     ,       ,  condA() //  ,      x. use(x); //  x = 1. } 

यूनिनिट के साथ, हम आसानी से कह सकते हैं कि x का एक यूनीट मान या 1 का मान है, और चूंकि यूनीट को 1 कार्यों के साथ बदलने के बाद, अनुकूलन को आसानी से समझाया गया है। यूनिनिट के बिना, x या तो "किसी प्रकार का मनमाना बिट पैटर्न" या 1 है, और उसी अनुकूलन की व्याख्या करना कठिन है।


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


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


निष्कर्ष


हमने देखा कि C ++ और Rust जैसी भाषाओं में (वास्तविक कंप्यूटरों के विपरीत) पॉइंटर्स अलग-अलग हो सकते हैं भले ही वे एक ही पते पर हों, और यह कि बाइट 0..256 की रेंज में सिर्फ एक संख्या से अधिक है। इसलिए, अगर 1978 में सी भाषा "पोर्टेबल असेंबलर" हो सकती है, तो अब यह एक अविश्वसनीय रूप से गलत बयान है।

Source: https://habr.com/ru/post/hi460953/


All Articles