Writing a DOS Clone in 2019. Recently I was lucky enough to take a… | by Andrew Imm

हाल ही में मैं भाग्यशाली था कि मुझे काम से एक महीने की छुट्टी लेने का मौका मिला। जबकि मैंने उस समय का अधिकांश समय यात्रा करने और कंप्यूटर से दूर रहने में बिताया, एक इंजीनियर के लिए इसे पूरी तरह से बंद करना कठिन है। अगर मैं मनोरंजन के लिए कुछ बनाने जा रहा था, तो इसे मेरे दिन के काम से बिल्कुल अलग होना चाहिए। मैंने 80 के दशक में सीधे एक डॉस-संगत ओएस का निर्माण किया।

छवि को पूर्ण आकार में देखने के लिए एंटर दबाएँ या क्लिक करें

ओह क्या? क्यों?

मैं हमेशा से रेट्रो-कंप्यूटिंग का बहुत बड़ा शौकीन रहा हूं। पिछले साल एमुलेटर लिखने के साथ खेलना शुरू करने के बाद, मैंने MOS 65XX एमुलेटर – C64, अटारी 2600, NES की एक श्रृंखला बनाकर उस कट्टरता को पुनर्जीवित किया… इन प्रणालियों के आंतरिक पहलुओं के बारे में सोचने से यह पढ़ने को मिला कि आईबीएम पीसी कितनी जल्दी संचालित होते थे। एक पीसी एमुलेटर बनाने का महत्वपूर्ण कार्य करने के बजाय (मैं मानता हूं कि प्रोसेसर निर्देशों को लागू करने से थोड़ी देर के बाद काफी नींद आ जाती है), मैंने एक डॉस-संगत ओएस लिखने का फैसला किया।

डॉस-संगत का क्या मतलब है, इस पर एक त्वरित प्राइमर/रिफ्रेशर यहां दिया गया है। 80 के दशक में, माइक्रोसॉफ्ट के पास सबसे लोकप्रिय पीसी ऑपरेटिंग सिस्टम था: MS-DOS। जबकि कॉम्पैक जैसी कंपनियों ने पीसी क्लोन बनाए जो आईबीएम के कंप्यूटरों के साथ हार्डवेयर-संगत थे, अन्य कंपनियों ने ऑपरेटिंग सिस्टम बनाए जो एमएस-डॉस के साथ कोड-संगत थे। कुछ में नई प्रतिस्पर्धी सुविधाएँ जोड़ी गईं, अन्य को विशिष्ट हार्डवेयर को ध्यान में रखकर जारी किया गया। जब तक आप MS-DOS के समान API लागू करते हैं, तब तक Microsoft के OS के लिए लिखा गया सॉफ़्टवेयर आपके ओएस पर भी चलेगा। यह तब महत्वपूर्ण था जब आपके कार्यालय के लोगों को आपके ऑफ-ब्रांड सिस्टम पर लोटस 1-2-3 चलाने की आवश्यकता थी।

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

तो मैंने क्या बनाया?

एक महीने की लगातार कोडिंग के बाद, लिखने के समय मेरे पास यह है: एक कर्नेल जो विस्तारित डॉस एपीआई के लगभग आधे हिस्से को लागू करता है; डिस्क ड्राइव, कंसोल और सिस्टम क्लॉक के लिए बुनियादी ड्राइवर समर्थन; एक FAT-12 फ़ाइल सिस्टम कार्यान्वयन; एक COMMAND.COM कमांड प्रॉम्प्ट जो बुनियादी आंतरिक कमांड चलाने, निर्देशिकाओं को सूचीबद्ध करने और अन्य COM प्रोग्राम निष्पादित करने के लिए DOS API का लाभ उठाता है। इसका अधिकांश भाग हैक हो चुका है और आधा-अधूरा है, लेकिन यह कुछ डॉस-फॉर-डॉस प्रोग्राम चलाने में सक्षम होने के प्रारंभिक लक्ष्य को प्राप्त करता है।

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

वास्तविक हो रहा है

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

DOS के पहले संस्करण Intel 8086 के लिए लिखे गए थे, जब रियल मोड ही एकमात्र मोड था। MS-DOS के पूरे जीवनकाल के दौरान, अनुकूलता बनाए रखने के लिए प्रत्येक संस्करण को रियल मोड में लागू किया गया था। परिणामस्वरूप, मेरे डॉस को भी एक रियल मोड प्रोग्राम की आवश्यकता होने वाली थी, जो पिछले 30 वर्षों में लिखे गए सभी ओएस गाइडों के सामने खड़ा हो। जहां हम जा रहे हैं वहां कोई पेजिंग, जीडीटी या रिंग नहीं है।

रियल मोड की सबसे अजीब विचित्रताओं में से एक मेमोरी विभाजन है। रियल मोड मेमोरी एड्रेस के लिए केवल 16-बिट रजिस्टर का उपयोग करता है, लेकिन आपको 64 केबी से अधिक रैम तक पहुंचने की सुविधा देता है। इसे सेगमेंट नामक रजिस्टरों के दूसरे सेट के साथ हासिल किया गया था: एक कोड के लिए, एक डेटा के लिए, और एक स्टैक के लिए। के संयोजन का उपयोग करके पूरी 1 एमबी मेमोरी को संबोधित किया जा सकता था खंड और एक ओफ़्सेट. खंड को चार बिट स्थानांतरित किया गया और 20-बिट पता बनाने के लिए ऑफसेट में जोड़ा गया।

छवि को पूर्ण आकार में देखने के लिए एंटर दबाएँ या क्लिक करें

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

एक बार 32-बिट प्रोसेसर पेश किए जाने के बाद, पूर्ण 4 जीबी को बिना विभाजन के संबोधित किया जा सकता था, और तब से खंडों को काफी हद तक नजरअंदाज कर दिया गया है।

बूट

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

जब BIOS को बूट करने योग्य डिस्क मिलती है, तो यह डिस्क के पहले खंड से पते पर मेमोरी में कोड का एक छोटा सा हिस्सा कॉपी करता है 0x7c00 और उसे चलाना शुरू कर देता है. आदर्श रूप से यह कोड – बूटलोडर – आपके ऑपरेटिंग सिस्टम को डिस्क से मेमोरी में कॉपी करेगा और उस पर पहुंच जाएगा।

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

कर्नेल

कर्नेल का मुख्य कार्य एक सिस्टम कॉल एपीआई स्थापित करना है, जिसमें प्रोग्राम कॉल कर सकते हैं और कुछ प्रभाव ट्रिगर कर सकते हैं। इन सिस्टम कॉलों को लागू करने में काफी जटिलताएं हैं, लेकिन सतह पर मूल रूप से कर्नेल यही करता है। ये सिस्टम कॉल सॉफ़्टवेयर इंटरप्ट को कॉल करके ट्रिगर की जाती हैं 0x21या जैसा कि अक्सर कोड में लिखा जाता था, पूर्णांक 21 घंटे. सिस्टम कॉल के समय अन्य सीपीयू रजिस्टरों के मूल्यों के आधार पर, डॉस यह निर्धारित करेगा कि कौन सी विधि चलानी है।

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

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

इसे जंग के साथ कर रहे हैं

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

मेरा कर्नेल वहां मौजूद कई शानदार रस्ट ओएस ट्यूटोरियल की तरह शुरू हुआ: एक निष्पादन योग्य जो बिना किसी stdlib, बिना किसी मुख्य और बाहरी रूप से स्थित प्रवेश बिंदु के साथ बनाया गया है:

#![no_std]
#![no_main]
#[no_mangle]
pub extern "C" fn _start() {
// kernel code here
}
// Also, we need to override panic behavior
#[no_mangle]
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}

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

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

विकास प्रक्रिया के दौरान, मुझे एक अजीब समस्या का भी सामना करना पड़ा जहां एक निश्चित आकार से पहले कोड को स्थिर रूप से लिंक होने में परेशानी हो रही थी – लिंकर ने कुछ ऑफसेट की गणना करने से इनकार कर दिया। यह अंततः इस बात से प्राप्त हुआ कि कर्नेल को निष्पादन योग्य के रूप में कैसे बनाया जा रहा था। क्योंकि सब कुछ स्थिर है, और मैं निष्पादन योग्य प्रारूपों के लिए किसी विशिष्ट चीज़ पर भरोसा नहीं करता, मुझे वास्तव में “निष्पादन योग्य” होने के लिए कर्नेल की आवश्यकता नहीं है। मैंने बिल्ड स्क्रिप्ट को फिर से व्यवस्थित किया, कर्नेल को एक स्थिर लाइब्रेरी के रूप में संकलित किया, और सब कुछ ठीक काम करता है। मैं ईमानदारी से इसे आगे डीबग करने के लिए एलएलवीएम लिंकर के बारे में पर्याप्त नहीं जानता, लेकिन मेरे वर्तमान समाधान ने मुझे फिलहाल अनब्लॉक कर दिया है।

ढेर, ढेर और स्टैटिक्स

रस्ट की अधिकांश मेमोरी सुरक्षा स्टैक पर आवंटित वेरिएबल्स पर केंद्रित है, जहां उन्हें दायरे से बाहर जाने पर साफ किया जा सकता है, और उनके स्वामित्व को ट्रैक किया जा सकता है। मेरा कर्नेल हीप स्पेस या एलोकेटर का उपयोग नहीं करता है, क्योंकि जिन वस्तुओं से मैं निपटता हूं वे या तो “स्टैक”-आधारित ऑब्जेक्ट हैं जो कर्नेल के पूरे जीवनकाल के लिए रहते हैं, या संकलन समय पर ज्ञात स्थानों के साथ स्थिर ऑब्जेक्ट हैं।

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

ओएस का निर्माण

मैंने पहले ही थोड़ा उल्लेख किया है कि कर्नेल को रस्ट स्रोत से कैसे संकलित किया जाता है, लेकिन मैं इसे और अधिक विस्तार से समझाऊंगा। रस्ट कोड को एक स्थिर प्रोग्राम में संकलित किया जाता है, और एक लिंकर स्क्रिप्ट का उपयोग करके एक साधारण असेंबली एंट्री पॉइंट के साथ जोड़ा जाता है जो सभी अनावश्यक अतिरिक्त को बाहर निकाल देता है। कमांड लाइन जैसे सहायक प्रोग्रामों को भी संकलित किया जाता है और उनके अपने असेंबली हेडर के साथ संयोजित किया जाता है। एक FAT-12 फ़्लॉपी डिस्क छवि प्रारंभ की जाती है, और बूटलोडर को डिस्क के पहले सेक्टर के भाग में कॉपी किया जाता है। अंत में, प्रत्येक सिस्टम फ़ाइल को GNU mtools का उपयोग करके डिस्क छवि पर कॉपी किया जाता है।

फ्लॉपी छवि को QEMU में लोड किया जाता है, जहां बूटलोडर कर्नेल, ड्राइवर और कमांड प्रॉम्प्ट को लोड करना शुरू कर देता है:

छवि को पूर्ण आकार में देखने के लिए एंटर दबाएँ या क्लिक करें

अगला

अद्यतन: मैंने कर्नेल के आंतरिक पहलुओं की खोज के साथ इस पोस्ट को जारी रखा है। एक अन्य पोस्ट में COM फ़ाइलों पर चर्चा की जाएगी कि उन्हें रस्ट में कैसे लिखा जा सकता है, और वे DOS API में वापस कॉल करने में कैसे सक्षम हैं।

एक बार जब मैं काम पर लौटूंगा और स्रोत कोड खोलने की मंजूरी प्राप्त कर लूंगा, तो मैं रेपो को भी सार्वजनिक कर दूंगा।



Leave a Comment