نكتب آلة افتراضية خاصة بنا

في هذا البرنامج التعليمي ، سأوضح لك كيفية كتابة جهازك الظاهري (VM) الذي يمكنه تشغيل برامج المجمّع مثل 2048 (صديقي) أو Roguelike (خاصتي). إذا كنت تعرف كيفية البرمجة ، ولكنك تريد أن تفهم بشكل أفضل ما يحدث داخل الكمبيوتر وكيف تعمل لغات البرمجة ، فهذا المشروع مناسب لك. قد تبدو كتابة الجهاز الافتراضي الخاص بك مخيفة بعض الشيء ، لكنني أعدك أن الموضوع بسيط ومفيد بشكل مدهش.

الكود الأخير هو حوالي 250 سطرًا في C. يكفي أن تعرف أساسيات C أو C ++ فقط ، مثل الحساب الثنائي . أي نظام يونيكس (بما في ذلك ماك) مناسب للبناء والتشغيل. تُستخدم عدة واجهات برمجة التطبيقات لـ Unix لتهيئة إدخال وحدة التحكم وعرضها ، ولكنها ليست ضرورية في الكود الرئيسي. (تنفيذ دعم ويندوز هو موضع تقدير).

ملاحظة: هذا VM هو برنامج المختصة . هذا هو ، كنت تقرأ بالفعل شفرة المصدر الخاصة به الآن! سيتم عرض كل جزء من الشفرة وشرحه بالتفصيل ، بحيث يمكنك التأكد من عدم وجود شيء مفقود. يتم إنشاء رمز النهائي بواسطة الضفيرة من كتل التعليمات البرمجية. مستودع المشروع هنا .

1. المحتويات


  1. جدول المحتويات
  2. مقدمة
  3. العمارة LC-3
  4. أمثلة المجمّع
  5. تنفيذ البرنامج
  6. تنفيذ التعليمات
  7. تعليمات الغش ورقة
  8. إجراءات معالجة المقاطعة
  9. ورقة الغش لروتين المقاطعة
  10. تحميل البرنامج
  11. سجلات الذاكرة المعينة
  12. ميزات النظام الأساسي
  13. بدء تشغيل الجهاز الظاهري
  14. طريقة بديلة في C ++

2. مقدمة


ما هو الجهاز الظاهري؟


الجهاز الظاهري هو برنامج يعمل مثل الكمبيوتر. إنه يحاكي معالجًا مع العديد من مكونات الأجهزة الأخرى ، مما يسمح لك بإجراء العمليات الحسابية والقراءة من الذاكرة والكتابة إليها والتفاعل مع أجهزة الإدخال / الإخراج ، مثل جهاز كمبيوتر فعلي حقيقي. الأهم من ذلك ، يفهم VM لغة الآلة التي يمكنك استخدامها للبرمجة.

يعتمد مقدار الأجهزة التي يحاكيها جهاز VM على الغرض منها. بعض VMs إنتاج سلوك كمبيوتر معين. لم يعد الأشخاص لديهم NES ، ولكن لا يزال بإمكاننا لعب ألعاب NES من خلال محاكاة الأجهزة على مستوى البرنامج. يجب على هذه المحاكيات إعادة إنشاء كل التفاصيل بدقة وكل مكون رئيسي من مكونات الجهاز الأصلي.

أجهزة VM الأخرى لا تتوافق مع أي كمبيوتر معين ، ولكنها تتوافق جزئيًا مع عدة أجهزة في وقت واحد! ويتم ذلك في المقام الأول لتسهيل تطوير البرمجيات. تخيل أنك تريد إنشاء برنامج يعمل على بنيات كمبيوتر متعددة. يوفر الجهاز الظاهري منصة قياسية توفر إمكانية النقل. لا حاجة لإعادة كتابة البرنامج في لهجات مختلفة من المجمع لكل هندسة. يكفي عمل جهاز VM صغير في كل لغة. بعد ذلك ، يمكن كتابة أي برنامج مرة واحدة فقط بلغة التجميع لجهاز ظاهري.



ملاحظة: يقوم برنامج التحويل البرمجي بحل مثل هذه المشكلات عن طريق تجميع لغة قياسية عالية المستوى لأبنية المعالج المختلفة. يقوم VM بإنشاء بنية وحدة المعالجة المركزية القياسية التي يتم محاكاةها على أجهزة مختلفة. إحدى مزايا برنامج التحويل البرمجي هي أنه لا يوجد حمل وقت التشغيل مثل VM. على الرغم من أن برنامج التحويل البرمجي يعمل بشكل جيد ، إلا أن كتابة مترجم جديد للعديد من المنصات أمر صعب للغاية ، لذلك لا تزال أدوات VM مفيدة. في الواقع ، على مستويات مختلفة ، يتم استخدام كل من VM و compilers معا.

يعد Java Virtual Machine (JVM) مثالًا ناجحًا للغاية. JVM نفسها متوسطة الحجم نسبيًا ؛ فهي صغيرة بما يكفي لفهم المبرمج. يتيح لك هذا كتابة رمز لآلاف الأجهزة المختلفة ، بما في ذلك الهواتف. بعد تطبيق JVM على الجهاز الجديد ، يمكن لأي برنامج مكتوب بلغة Java أو Kotlin أو Clojure العمل عليه دون تغييرات. التكاليف الوحيدة ستكون فقط النفقات العامة لجهاز VM نفسه والاستخلاص الإضافي من مستوى الماكينة. هذا هو عادة حل وسط جيد.

لا يجب أن تكون VM كبيرة أو واسعة الانتشار لتوفير فوائد مماثلة. غالبًا ما تستخدم ألعاب الفيديو الأقدم VMs الصغيرة لإنشاء أنظمة نصية بسيطة.

VMs مفيدة أيضًا لعزل البرامج بأمان. تطبيق واحد هو جمع القمامة. لا توجد طريقة بسيطة لتنفيذ تجميع البيانات المهملة تلقائيًا أعلى C أو C ++ ، نظرًا لأن البرنامج لا يمكنه رؤية مكدسه أو متغيراته الخاصة. ومع ذلك ، فإن VM هو "خارج" البرنامج قيد التشغيل ويمكنه مراقبة جميع الإشارات إلى خلايا الذاكرة على المكدس.

يتم توضيح مثال آخر على هذا السلوك من خلال عقود Ethereum الذكية . العقود الذكية هي برامج صغيرة يتم تنفيذها بواسطة كل عقدة تحقق في blockchain. أي أن المشغلين يسمحون بتنفيذ أي برامج مكتوبة بواسطة غرباء تمامًا على أجهزتهم ، دون أي فرصة لدراستها مقدمًا. لمنع الإجراءات الضارة ، يتم تنفيذها على جهاز VM ليس له حق الوصول إلى نظام الملفات أو الشبكة أو القرص ، إلخ. Ethereum هو أيضا مثال جيد على قابلية. بفضل VM ، يمكنك كتابة عقود ذكية دون مراعاة ميزات العديد من المنصات.

3. العمارة LC-3




سيقوم جهاز VM بمحاكاة جهاز كمبيوتر خيالي يسمى LC-3 . أنها تحظى بشعبية لتعليم الطلاب المجمع. هنا ، مجموعة مبسطة من الأوامر مقارنة بـ x86 ، ولكنها تحتفظ بجميع المفاهيم الأساسية المستخدمة في وحدات المعالجة المركزية الحديثة.

أولاً ، تحتاج إلى محاكاة مكونات الأجهزة الضرورية. حاول أن تفهم ماهية كل مكون ، لكن لا تقلق إذا لم تكن متأكدًا من ملاءمته للصورة الكبيرة. لنبدأ بإنشاء ملف في C. يجب وضع كل جزء من التعليمات البرمجية من هذا القسم في النطاق العالمي لهذا الملف.

الذاكرة


يحتوي LC-3 على 65،536 خلية ذاكرة (2 16 ) ، تحتوي كل منها على قيمة 16 بت. هذا يعني أنه يمكن فقط تخزين 128 كيلو بايت - أقل بكثير مما اعتدت عليه! في برنامجنا ، يتم تخزين هذه الذاكرة في مجموعة بسيطة:

/* 65536 locations */ uint16_t memory[UINT16_MAX]; 

سجلات


السجل عبارة عن فتحة لتخزين قيمة واحدة في وحدة المعالجة المركزية. السجلات مثل وحدة المعالجة المركزية "طاولة العمل". حتى تتمكن من العمل مع جزء من البيانات ، يجب أن تكون في أحد السجلات. ولكن نظرًا لوجود عدد قليل فقط من السجلات ، يمكن تنزيل قدر ضئيل فقط من البيانات في أي وقت. تعمل البرامج حول هذه المشكلة عن طريق تحميل القيم من الذاكرة إلى سجلات ، وحساب القيم في سجلات أخرى ، ثم تخزين النتائج النهائية مرة أخرى في الذاكرة.

لا يوجد سوى 10 سجلات في LC-3 ، ولكل منها 16 بت. معظمهم للأغراض العامة ، ولكن البعض منهم أدوار محددة.

  • 8 سجلات للأغراض العامة ( R0-R7 )
  • سجل واحد في عداد الفرق ( PC )
  • سجل علم شرط واحد ( COND )

يمكن استخدام سجلات الأغراض العامة لإجراء أي حسابات برامجية. عداد التعليمات هو عدد صحيح غير موقّع وهو عنوان ذاكرة التعليمة التالية للتنفيذ. تخبرنا علامات الحالة بمعلومات حول الحساب السابق.

 enum { R_R0 = 0, R_R1, R_R2, R_R3, R_R4, R_R5, R_R6, R_R7, R_PC, /* program counter */ R_COND, R_COUNT }; 

مثل الذاكرة ، سنخزن السجلات في صفيف:

 uint16_t reg[R_COUNT]; 

مجموعة التعليمات


التعليمات هي أمر يخبر المعالج بإجراء نوع من المهام الأساسية ، على سبيل المثال ، قم بإضافة رقمين. يحتوي التعليمة على شفرة تشغيل (رمز العملية) تشير إلى نوع المهمة التي يتم تنفيذها ، بالإضافة إلى مجموعة من المعلمات التي توفر مدخلات للمهمة التي يتم تنفيذها.

يمثل كل شفرة تشغيل مهمة واحدة "يعرف" المعالج تنفيذها. هناك 16 شفرة تشغيل في LC-3. يمكن للكمبيوتر حساب تسلسل هذه التعليمات البسيطة فقط. يبلغ طول كل تعليمة 16 بت ، بينما تخزن 4 بتات الأيسر رمز العملية. يتم استخدام الباقي لتخزين المعلمات.

في وقت لاحق سنناقش بالتفصيل ما يفعله كل تعليم. حدد شفرة التشغيل التالية في الوقت الحالي. تأكد من الاحتفاظ بهذا الترتيب للحصول على قيمة التعداد الصحيحة:

 enum { OP_BR = 0, /* branch */ OP_ADD, /* add */ OP_LD, /* load */ OP_ST, /* store */ OP_JSR, /* jump register */ OP_AND, /* bitwise and */ OP_LDR, /* load register */ OP_STR, /* store register */ OP_RTI, /* unused */ OP_NOT, /* bitwise not */ OP_LDI, /* load indirect */ OP_STI, /* store indirect */ OP_JMP, /* jump */ OP_RES, /* reserved (unused) */ OP_LEA, /* load effective address */ OP_TRAP /* execute trap */ }; 

ملاحظة: تحتوي بنية Intel x86 على مئات الإرشادات ، في حين أن بنيات أخرى مثل ARM و LC-3 قليلة للغاية. تسمى مجموعات التعليمات الصغيرة RISC ، بينما تسمى المجموعات الأكبر CISC . مجموعات التعليمات الكبيرة ، كقاعدة عامة ، لا توفر ميزات جديدة بشكل أساسي ، ولكنها غالبًا ما تبسط كتابة رمز المجمّع . يمكن أن يحل أحد تعليمات CISC محل العديد من إرشادات RISC. ومع ذلك ، فإن معالجات CISC أكثر تعقيدًا ومكلفة للتصميم والتصنيع. لا تسمح هذه المقايضات وغيرها باستدعاء التصميم "الأمثل" .

حالة الأعلام


يقوم سجل R_COND بتخزين إشارات الحالة التي توفر معلومات حول آخر حساب تم تنفيذه. يسمح هذا للبرامج بالتحقق من الشروط المنطقية ، مثل if (x > 0) { ... } .

يحتوي كل معالج على العديد من علامات الحالة للإشارة إلى المواقف المختلفة. يستخدم LC-3 ثلاث علامات شرط فقط تُظهر علامة الحساب السابق.

 enum { FL_POS = 1 << 0, /* P */ FL_ZRO = 1 << 1, /* Z */ FL_NEG = 1 << 2, /* N */ }; 

ملاحظة: (يُطلق على الحرف << عامل التشغيل لليسار الأيسر (n << k) ينقل البتات n اليسار من الأماكن k . وهكذا ، 1 << 2 تساوي 4 اقرأ هنا إذا لم تكن معتادًا على المفهوم. سيكون هذا مهمًا جدًا).

لقد انتهينا من تكوين مكونات الأجهزة من الجهاز الظاهري لدينا! بعد إضافة الادراج القياسية (انظر الرابط أعلاه) ، يجب أن يبدو الملف الخاص بك كما يلي:

 {Includes, 12} {Registers, 3} {Opcodes, 3} {Condition Flags, 3} 
فيما يلي ارتباطات إلى أقسام ذات تعداد رقمي في المقالة ، حيث تأتي أجزاء التعليمات البرمجية المطابقة من. للحصول على قائمة كاملة ، انظر برنامج العمل - تقريبا. العابرة

4. أمثلة المجمع


الآن ، دعونا ننظر إلى برنامج المجمّع LC-3 للحصول على فكرة عما يفعله الجهاز الظاهري فعليًا. لا تحتاج إلى معرفة كيفية البرمجة في المجمع ، أو فهم كل شيء هنا. مجرد محاولة للحصول على فكرة عامة عما يحدث. هنا عبارة بسيطة عن "Hello World":

 .ORIG x3000 ; this is the address in memory where the program will be loaded LEA R0, HELLO_STR ; load the address of the HELLO_STR string into R0 PUTs ; output the string pointed to by R0 to the console HALT ; halt the program HELLO_STR .STRINGZ "Hello World!" ; store this string here in the program .END ; mark the end of the file 

كما في C ، ينفذ البرنامج عبارة واحدة من أعلى إلى أسفل. ولكن على عكس C ، لا توجد مناطق متداخلة {} أو هياكل تحكم مثل if أو while ؛ مجرد قائمة بسيطة من المشغلين. لذلك ، فمن الأسهل أداء.

يرجى ملاحظة أن أسماء بعض المشغلين تتوافق مع رموز التشغيل التي حددناها سابقًا. نعلم أن التعليمات هي 16 بت ، لكن يبدو أن كل سطر كما لو كان مع عدد مختلف من الأحرف. كيف يكون هذا التطابق ممكنًا؟

هذا لأن الكود الذي نقرأه مكتوب بلغة التجميع - في شكل نص عادي ، قابل للقراءة وقابل للكتابة. تقوم أداة تسمى المجمّع بتحويل كل سطر من النص إلى تعليمة ثنائية 16 بت يفهمها الجهاز الظاهري. يُطلق على هذا النموذج الثنائي ، الذي يعد أساسًا مجموعة من الإرشادات 16 بت ، رمز الجهاز ويتم تنفيذه بالفعل بواسطة جهاز افتراضي.


ملاحظة: على الرغم من أن المجمع والمجمع يلعبان دورًا مشابهًا في التطوير ، إلا أنهما ليسا متماثلين. يقوم المجمّع ببساطة بتشفير ما كتبه المبرمج في النص ، واستبدال الأحرف بتمثيلها الثنائي وتعبئتها في التعليمات.

يبدو .ORIG و. .STRINGZ تعليمات ، لكن لا. هذه هي توجيهات المجمّع التي تنشئ جزءًا من الكود أو البيانات. على سبيل المثال ، إدراج .STRINGZ سلسلة من الأحرف في موقع محدد في برنامج ثنائي.

يتم تنفيذ الحلقات والشروط باستخدام عبارة تشبه goto. هنا مثال آخر يصل إلى 10.

 AND R0, R0, 0 ; clear R0 LOOP ; label at the top of our loop ADD R0, R0, 1 ; add 1 to R0 and store back in R0 ADD R1, R0, -10 ; subtract 10 from R0 and store back in R1 BRn LOOP ; go back to LOOP if the result was negative ... ; R0 is now 10! 

ملاحظة: هذا البرنامج التعليمي لا يجب أن يتعلم التجميع. ولكن إذا كنت مهتمًا ، فيمكنك كتابة وبناء برامج LC-3 الخاصة بك باستخدام أدوات LC-3 .

5. تنفيذ البرنامج


مرة أخرى ، تعطي الأمثلة السابقة مجرد فكرة عما يفعله VM. لكتابة VM ، لا تحتاج إلى فهم كامل للمجمع. طالما اتبعت الإجراء المناسب لقراءة التعليمات وتنفيذها ، فإن أي برنامج LC-3 سيعمل بشكل صحيح ، بغض النظر عن تعقيده. من الناحية النظرية ، يمكن لـ VM تشغيل متصفح أو نظام تشغيل مثل Linux!

إذا كنت تفكر بعمق ، فهذه فكرة رائعة من الناحية الفلسفية. يمكن أن تنتج البرامج نفسها إجراءات معقدة بشكل تعسفي لم نتوقعها مطلقًا وقد لا نستطيع فهمها. ولكن في الوقت نفسه ، تقتصر جميع وظائفها على رمز بسيط ، والذي سنكتبه! في الوقت نفسه ، لا نعرف كل شيء ولا شيء عن كيفية عمل كل برنامج. ذكر تورينج هذه الفكرة الرائعة:

أعتقد أن الرأي القائل بأن الآلات لا يمكن أن تفاجئ أي شخص بأي شيء يستند إلى خطأ واحد ، وهو ما يعتبر علماء الرياضيات والفلاسفة عرضة له بشكل خاص. أقصد الافتراض بأنه نظرًا لأن بعض الحقائق أصبحت ملكًا للعقل ، فإن جميع عواقب هذه الحقيقة ستصبح على الفور ملكًا للعقل ". - آلان م. تورنج

الإجراء


فيما يلي الوصف الدقيق للإجراء المراد كتابته:

  1. قم بتنزيل تعليمة واحدة من الذاكرة على عنوان سجل PC .
  2. زيادة سجل PC .
  3. عرض شفرة التشغيل لتحديد نوع التعليمات التي يجب اتباعها.
  4. اتبع التعليمات باستخدام المعلمات.
  5. العودة إلى الخطوة 1.

قد تطرح السؤال التالي: "لكن إذا استمرت الحلقة في زيادة العداد في حالة عدم وجود أو if ، هل ستنتهي الإرشادات؟" الجواب هو لا. كما ذكرنا سابقًا ، فإن بعض الإرشادات المشابهة goto تغير تدفق التنفيذ عن طريق القفز حول PC .

نبدأ دراسة هذه العملية كمثال للدورة الرئيسية:

 int main(int argc, const char* argv[]) { {Load Arguments, 12} {Setup, 12} /* set the PC to starting position */ /* 0x3000 is the default */ enum { PC_START = 0x3000 }; reg[R_PC] = PC_START; int running = 1; while (running) { /* FETCH */ uint16_t instr = mem_read(reg[R_PC]++); uint16_t op = instr >> 12; switch (op) { case OP_ADD: {ADD, 6} break; case OP_AND: {AND, 7} break; case OP_NOT: {NOT, 7} break; case OP_BR: {BR, 7} break; case OP_JMP: {JMP, 7} break; case OP_JSR: {JSR, 7} break; case OP_LD: {LD, 7} break; case OP_LDI: {LDI, 6} break; case OP_LDR: {LDR, 7} break; case OP_LEA: {LEA, 7} break; case OP_ST: {ST, 7} break; case OP_STI: {STI, 7} break; case OP_STR: {STR, 7} break; case OP_TRAP: {TRAP, 8} break; case OP_RES: case OP_RTI: default: {BAD OPCODE, 7} break; } } {Shutdown, 12} } 

6. تنفيذ التعليمات


مهمتك الآن هي جعل التنفيذ الصحيح لكل شفرة تشغيل. ويرد وصف مفصل لكل تعليمات في وثائق المشروع . من المواصفات تحتاج إلى معرفة كيفية عمل كل تعليمات وكتابة التنفيذ. هذا أسهل مما يبدو. هنا سأوضح كيفية تنفيذ اثنين منهم. يمكن العثور على رمز الباقي في القسم التالي.

أضف


يأخذ تعليمة ADD رقمين ، ويضيفهم ويخزن النتيجة في السجل. المواصفات موجودة في الوثائق في الصفحة 526. كل تعليمة ADD هي كما يلي:



يوجد سطرين في المخطط ، لأن هناك "وضعين" مختلفين لهذه التعليمات. قبل أن أشرح الأوضاع ، دعونا نحاول إيجاد أوجه التشابه بينهما. كلاهما يبدأ بأربعة بتات متطابقة 0001 . هذه هي قيمة شفرة التشغيل لـ OP_ADD . يتم وضع علامة على البتات الثلاثة التالية في سجل الإخراج. سجل الإخراج هو المكان الذي يتم فيه تخزين المبلغ. البتات الثلاثة التالية هي: SR1 . هذا سجل يحتوي على الرقم الأول المراد إضافته.

وبالتالي ، نحن نعرف مكان حفظ النتيجة ، ونعرف الرقم الأول الذي يجب إضافته. يبقى فقط لمعرفة الرقم الثاني للإضافة. هنا يبدأ الخطان في الاختلاف. لاحظ أن البت 5 يساوي 0 في الأعلى وأن 1. موجود في الأسفل ، وهذا الجزء يتوافق مع الوضع المباشر أو وضع التسجيل . في وضع التسجيل ، يتم تخزين الرقم الثاني في السجل ، مثل الأول. تم وضع علامة على أنه SR2 وهو موجود في البتات من الثاني إلى الصفر. لا يتم استخدام البتات 3 و 4. في المجمع ، سيتم كتابة مثل هذا:

 ADD R2 R0 R1 ; add the contents of R0 to R1 and store in R2. 

في الوضع الفوري ، بدلاً من إضافة محتويات السجل ، يتم تضمين القيمة الفورية في التعليمات نفسها. هذا مناسب لأن البرنامج لا يحتاج إلى تعليمات إضافية لتحميل هذا الرقم في السجل من الذاكرة. بدلاً من ذلك ، هو بالفعل داخل التعليمات عندما نحتاج إليها. المفاضلة هي أنه يمكن تخزين الأرقام الصغيرة فقط هناك. على وجه الدقة ، بحد أقصى 2 5 = 32. هذا مفيد للغاية لزيادة العدادات أو القيم. في المجمع ، يمكنك كتابة مثل هذا:

 ADD R0 R0 1 ; add 1 to R0 and store back in R0 

هنا مقتطف من المواصفات:

إذا كانت قيمة البت [5] تساوي 0 ، فسيتم الحصول على المعامل المصدر الثاني من SR2. إذا كانت البتة [5] 1 ، فسيتم الحصول على المعامل المصدر الثاني عن طريق توسيع imm5 إلى 16 بت. في كلتا الحالتين ، تتم إضافة المعامل المصدر الثاني إلى محتويات SR1 ، ويتم تخزين النتيجة في DR. (ص 526)

هذا مشابه لما ناقشناه. ولكن ما هو "امتداد المعنى"؟ على الرغم من أن القيمة المباشرة في الوضع المباشر لا تحتوي إلا على 5 بتات ، إلا أنه يجب إضافتها برقم 16 بت. يجب توسيع هذه البتات 5 إلى 16 لتتوافق مع رقم آخر. بالنسبة للأرقام الموجبة ، يمكننا ملء البتات المفقودة بالأصفار والحصول على نفس القيمة. ومع ذلك ، بالنسبة للأعداد السالبة لا يعمل هذا. على سبيل المثال ، −1 في خمس بتات هي 1 1111 . إذا ملأته بالأصفار ، فسنحصل على 0000 0000 0001 1111 ، وهو 32! توسيع القيمة يمنع هذه المشكلة عن طريق ملء البتات بالأصفار للأرقام الموجبة والأرقام السالبة.

 uint16_t sign_extend(uint16_t x, int bit_count) { if ((x >> (bit_count - 1)) & 1) { x |= (0xFFFF << bit_count); } return x; } 

ملاحظة: إذا كنت مهتمًا بالأرقام السالبة الثنائية ، فيمكنك القراءة عن الكود الإضافي . ولكن هذا ليس ضروريا. ما عليك سوى نسخ الرمز أعلاه واستخدامه عندما تقول المواصفات لتوسيع القيمة.

المواصفات لديها الجملة الأخيرة:

يتم تعيين رموز الحالة وفقًا لما إذا كانت النتيجة سالبة أو صفر أو موجبة. (ص 526)

في وقت سابق حددنا شرط تعداد العلامات ، والآن حان الوقت لاستخدام هذه العلامات. في كل مرة يتم فيها كتابة قيمة إلى السجل ، نحتاج إلى تحديث الأعلام للإشارة إلى علامة. نكتب وظيفة لإعادة الاستخدام:

 void update_flags(uint16_t r) { if (reg[r] == 0) { reg[R_COND] = FL_ZRO; } else if (reg[r] >> 15) /* a 1 in the left-most bit indicates negative */ { reg[R_COND] = FL_NEG; } else { reg[R_COND] = FL_POS; } } 

نحن الآن على استعداد لكتابة رمز ADD :

 { /* destination register (DR) */ uint16_t r0 = (instr >> 9) & 0x7; /* first operand (SR1) */ uint16_t r1 = (instr >> 6) & 0x7; /* whether we are in immediate mode */ uint16_t imm_flag = (instr >> 5) & 0x1; if (imm_flag) { uint16_t imm5 = sign_extend(instr & 0x1F, 5); reg[r0] = reg[r1] + imm5; } else { uint16_t r2 = instr & 0x7; reg[r0] = reg[r1] + reg[r2]; } update_flags(r0); } 

يحتوي هذا القسم على الكثير من المعلومات ، لذلك دعونا نلخص.

  • يأخذ ADD قيمتين ويخزنهما في سجل.
  • في وضع التسجيل ، القيمة الثانية المراد إضافتها هي في السجل.
  • في الوضع المباشر ، يتم تضمين القيمة الثانية في 5 بتات الصحيحة من التعليمات.
  • يجب توسيع القيم التي تقل عن 16 بت.
  • في كل مرة يتم فيها تغيير التعليمات ، يجب تحديث علامات الحالة.

قد تطغى عليك بكتابة 15 تعليمات أخرى. ومع ذلك ، يمكن إعادة استخدام المعلومات التي تم الحصول عليها هنا. تستخدم معظم الإرشادات مجموعة من توسيع القيمة ، والأنماط المختلفة ، وتحديثات العلم.

LDI


LDI تعني التحميل "غير المباشر" أو "غير المباشر" (التحميل غير المباشر). يستخدم هذا الإرشادات لتحميل القيم من موقع الذاكرة في السجل. مواصفات على الصفحة 532.

إليك ما يبدو عليه الشكل الثنائي:



على عكس ADD ، لا توجد أوضاع وعدد أقل من المعلمات. هذه المرة ، رمز العملية هو 1010 ، والذي يتوافق مع قيمة التعداد OP_LDI . مرة أخرى ، نرى DR ثلاثة بتات (سجل الإخراج) لتخزين القيمة المحملة. يتم تمييز البتات المتبقية على أنها PCoffset9 . هذه هي القيمة المباشرة المضمنة في التعليمات (على غرار imm5 ). نظرًا لأن التعليمة يتم تحميلها من الذاكرة ، فيمكننا تخمين أن هذا الرقم هو نوع من العنوان يوضح مكان تحميل القيمة منه. تشرح المواصفات بمزيد من التفاصيل:

يتم حساب العنوان عن طريق توسيع بتات القيمة [8:0] إلى 16 بت وإضافة هذه القيمة إلى PC الموسع. ما يتم تخزينه في الذاكرة على هذا العنوان هو عنوان البيانات التي سيتم تحميلها في DR . (ص 532)

كما في السابق ، تحتاج إلى توسيع هذه القيمة ذات 9 بت ، لكن هذه المرة أضفها إلى PC الحالي. (إذا نظرت إلى دورة التنفيذ ، زاد PC مباشرة بعد تحميل هذه التعليمات). المجموع الناتج هو عنوان الموقع في الذاكرة ، وهذا العنوان يحتوي على قيمة أخرى ، وهي عنوان قيمة التحميل.

قد يبدو هذا كطريقة ملتوية للقراءة من الذاكرة ، لكن هذا ضروري. يقتصر تعليمة LD على إزاحة عنوان مكونة من 9 بتات ، بينما تتطلب الذاكرة عنوانًا 16 بت. يُعد LDI مفيدًا لتحميل القيم المخزنة في مكان ما خارج الكمبيوتر الحالي ، ولكن لاستخدامها ، يجب تخزين عنوان الموقع النهائي في مكان قريب. يمكنك التفكير في الأمر كمتغير محلي في C ، وهو مؤشر لبعض البيانات:

 // the value of far_data is an address // of course far_data itself (the location in memory containing the address) has an address char* far_data = "apple"; // In memory it may be layed out like this: // Address Label Value // 0x123: far_data = 0x456 // ... // 0x456: string = 'a' // if PC was at 0x100 // LDI R0 0x023 // would load 'a' into R0 

كما كان من قبل ، بعد كتابة القيمة إلى DR ، يجب تحديث العلامات:

يتم تعيين رموز الحالة وفقًا لما إذا كانت النتيجة سالبة أو صفر أو موجبة. (ص 532)

إليك رمز هذه الحالة: ( mem_read مناقشة mem_read في القسم التالي):

 { /* destination register (DR) */ uint16_t r0 = (instr >> 9) & 0x7; /* PCoffset 9*/ uint16_t pc_offset = sign_extend(instr & 0x1ff, 9); /* add pc_offset to the current PC, look at that memory location to get the final address */ reg[r0] = mem_read(mem_read(reg[R_PC] + pc_offset)); update_flags(r0); } 

كما قلت ، في هذا الإرشادات ، استخدمنا جزءًا كبيرًا من الشفرة والمعرفة المكتسبة سابقًا عند كتابة ADD . الشيء نفسه مع بقية التعليمات.

الآن تحتاج إلى تنفيذ بقية التعليمات. اتبع المواصفات واستخدم الكود المكتوب بالفعل. ويرد رمز لجميع التعليمات في نهاية المقال. لن تكون هناك حاجة إلى OP_RTI OP_RES المذكورة سابقًا: OP_RTI و OP_RES . يمكنك تجاهلهم أو إعطاء خطأ إذا تم استدعائهم. عند الانتهاء ، يمكن اعتبار الجزء الأكبر من جهاز VM كاملاً!

7. سرير وفقا للتعليمات


يحتوي هذا القسم على تطبيقات كاملة للتعليمات المتبقية إذا كنت عالقًا.

RTI & RES


(غير مستخدم)

 abort(); 

بت "و"


 { uint16_t r0 = (instr >> 9) & 0x7; uint16_t r1 = (instr >> 6) & 0x7; uint16_t imm_flag = (instr >> 5) & 0x1; if (imm_flag) { uint16_t imm5 = sign_extend(instr & 0x1F, 5); reg[r0] = reg[r1] & imm5; } else { uint16_t r2 = instr & 0x7; reg[r0] = reg[r1] & reg[r2]; } update_flags(r0); } 

لا


 { uint16_t r0 = (instr >> 9) & 0x7; uint16_t r1 = (instr >> 6) & 0x7; reg[r0] = ~reg[r1]; update_flags(r0); } 

فرع


 { uint16_t pc_offset = sign_extend((instr) & 0x1ff, 9); uint16_t cond_flag = (instr >> 9) & 0x7; if (cond_flag & reg[R_COND]) { reg[R_PC] += pc_offset; } } 

القفز


يشار إلى RET منفصلة في المواصفات ، لأن هذا أمر آخر في المجمع. هذا هو في الواقع حالة خاصة من JMP . يحدث RET كلما كانت R1 7.

 { /* Also handles RET */ uint16_t r1 = (instr >> 6) & 0x7; reg[R_PC] = reg[r1]; } 

سجل القفز


 { uint16_t r1 = (instr >> 6) & 0x7; uint16_t long_pc_offset = sign_extend(instr & 0x7ff, 11); uint16_t long_flag = (instr >> 11) & 1; reg[R_R7] = reg[R_PC]; if (long_flag) { reg[R_PC] += long_pc_offset; /* JSR */ } else { reg[R_PC] = reg[r1]; /* JSRR */ } break; } 

تحميل


 { uint16_t r0 = (instr >> 9) & 0x7; uint16_t pc_offset = sign_extend(instr & 0x1ff, 9); reg[r0] = mem_read(reg[R_PC] + pc_offset); update_flags(r0); } 

سجل التحميل


 { uint16_t r0 = (instr >> 9) & 0x7; uint16_t r1 = (instr >> 6) & 0x7; uint16_t offset = sign_extend(instr & 0x3F, 6); reg[r0] = mem_read(reg[r1] + offset); update_flags(r0); } 

عنوان التحميل الفعال


 { uint16_t r0 = (instr >> 9) & 0x7; uint16_t pc_offset = sign_extend(instr & 0x1ff, 9); reg[r0] = reg[R_PC] + pc_offset; update_flags(r0); } 

متجر


 { uint16_t r0 = (instr >> 9) & 0x7; uint16_t pc_offset = sign_extend(instr & 0x1ff, 9); mem_write(reg[R_PC] + pc_offset, reg[r0]); } 

متجر غير مباشر


 { uint16_t r0 = (instr >> 9) & 0x7; uint16_t pc_offset = sign_extend(instr & 0x1ff, 9); mem_write(mem_read(reg[R_PC] + pc_offset), reg[r0]); } 

سجل المتجر


 { uint16_t r0 = (instr >> 9) & 0x7; uint16_t r1 = (instr >> 6) & 0x7; uint16_t offset = sign_extend(instr & 0x3F, 6); mem_write(reg[r1] + offset, reg[r0]); } 

8. مقاطعة إجراءات المناولة


يوفر LC-3 العديد من الإجراءات المحددة مسبقًا لأداء المهام الشائعة والتفاعل مع أجهزة الإدخال / الإخراج. على سبيل المثال ، هناك إجراءات لتلقي خطوط إدخال وإخراج لوحة المفاتيح إلى وحدة التحكم. يطلق عليها إجراءات الاعتراض ، والتي يمكنك التفكير بها كنظام تشغيل أو واجهة برمجة التطبيقات لـ LC-3. يتم تعيين رمز مقاطعة (رمز فخ) لكل برنامج فرعي (يشبه شفرة التشغيل) لكل برنامج فرعي.لتنفيذه ، يتم استدعاء TRAPتعليمة برمز البرنامج الفرعي المطلوب.



قم بتعيين التعداد لكل رمز المقاطعة:

 enum { TRAP_GETC = 0x20, /* get character from keyboard */ TRAP_OUT = 0x21, /* output a character */ TRAP_PUTS = 0x22, /* output a word string */ TRAP_IN = 0x23, /* input a string */ TRAP_PUTSP = 0x24, /* output a byte string */ TRAP_HALT = 0x25 /* halt the program */ }; 

قد تتساءل عن سبب عدم تضمين رموز المقاطعة في التعليمات. هذا لأنهم في الواقع لا يضيفون LC-3 أي وظائف جديدة ، لكنهم يوفرون فقط وسيلة ملائمة لإكمال المهمة (مثل وظائف النظام في C). في محاكي LC-3 الرسمي ، تتم كتابة رموز المقاطعة في المجمع . عند استدعاء رمز المقاطعة ، ينتقل الكمبيوتر إلى عنوان هذا الرمز. تقوم وحدة المعالجة المركزية بتنفيذ تعليمات الإجراء ، وعند الانتهاء ، تتم PCإعادة ضبطها إلى الموقع الذي تم تشغيل المقاطعة منه.

: 0x3000 0x0 . , .

لا توجد مواصفات حول كيفية تنفيذ إجراءات المقاطعة: فقط ما يجب القيام به. في VM الخاص بنا ، سوف نتصرف بشكل مختلف بعض الشيء عن طريق كتابتها في C. عندما يتم استدعاء رمز المقاطعة ، سيتم استدعاء الدالة C. ، وبعد تشغيلها ، سوف تستمر التعليمات.

على الرغم من أنه يمكن كتابة الإجراءات في المجمّع وسوف يكون الكمبيوتر الفعلي LC-3 كذلك ، فإن هذا ليس هو الخيار الأفضل لنظام VM. بدلاً من كتابة إجراءات الإدخال والإخراج البدائية الخاصة بك ، يمكنك استخدام الإجراءات المتاحة على نظام التشغيل الخاص بنا. سيؤدي ذلك إلى تحسين الجهاز الظاهري على أجهزة الكمبيوتر لدينا ، وتبسيط التعليمات البرمجية وتوفير مستوى أعلى من التجريد لقابلية الحمل.

ملاحظة: مثال محدد هو إدخال لوحة المفاتيح. يستخدم إصدار المجمّع حلقة للتحقق باستمرار من إدخال لوحة المفاتيح. ولكن الكثير من وقت المعالج يضيع! باستخدام وظيفة نظام التشغيل المناسبة ، يمكن للبرنامج النوم بسلام قبل إشارة الإدخال.

في المشغل متعدد الخيارات لبرنامج شفرة التشغيل ، TRAPأضف مفتاحًا آخر:

 switch (instr & 0xFF) { case TRAP_GETC: {TRAP GETC, 9} break; case TRAP_OUT: {TRAP OUT, 9} break; case TRAP_PUTS: {TRAP PUTS, 8} break; case TRAP_IN: {TRAP IN, 9} break; case TRAP_PUTSP: {TRAP PUTSP, 9} break; case TRAP_HALT: {TRAP HALT, 9} break; } 

كما هو الحال مع التعليمات ، سأوضح لك كيفية تنفيذ إجراء واحد ، والقيام بالباقي بنفسك.

بوتس


يتم PUTSاستخدام رمز المقاطعة لإرجاع سلسلة مع صفر إنهاء (بالمثل printfفي C). المواصفات في الصفحة 543.

لعرض سلسلة ، يجب أن نعطي روتين المقاطعة سلسلة لعرضها. يتم ذلك عن طريق تخزين عنوان الحرف الأول R0قبل بدء المعالجة.

من المواصفات:

عرض سلسلة أحرف ASCII في عرض وحدة التحكم. يتم احتواء الأحرف في خلايا ذاكرة متعاقبة ، حرف واحد لكل خلية ، بدءًا من العنوان المحدد في R0. ينتهي الإخراج عند مواجهة قيمة في الذاكرة x0000. (ص 543)

لاحظ أنه على عكس السلاسل C ، هنا يتم تخزين الأحرف ليس في بايت واحد ، ولكن في موقع واحد في الذاكرة . موقع ذاكرة LC-3 هو 16 بت ، لذلك كل حرف في السلسلة هو 16 بت. لعرض هذا في وظيفة C ، تحتاج إلى تحويل كل قيمة إلى حرف وطباعتها بشكل منفصل.

 { /* one char per word */ uint16_t* c = memory + reg[R_R0]; while (*c) { putc((char)*c, stdout); ++c; } fflush(stdout); } 

ليس هناك ما هو مطلوب لهذا الإجراء. إجراءات المقاطعة واضحة إلى حد ما إذا كنت تعرف C. الآن عد إلى المواصفات وقم بتنفيذ الباقي. كما هو الحال مع التعليمات ، يمكن العثور على الرمز الكامل في نهاية هذا الدليل.

9. ورقة الغش لروتين المقاطعة


يحتوي هذا القسم على تطبيقات كاملة لإجراءات المقاطعة المتبقية.

إدخال الحرف


 /* read a single ASCII char */ reg[R_R0] = (uint16_t)getchar(); 

إخراج الحرف


 putc((char)reg[R_R0], stdout); fflush(stdout); 

طلب إدخال الحرف


 printf("Enter a character: "); reg[R_R0] = (uint16_t)getchar(); 

خط الانتاج


 { /* one char per byte (two bytes per word) here we need to swap back to big endian format */ uint16_t* c = memory + reg[R_R0]; while (*c) { char char1 = (*c) & 0xFF; putc(char1, stdout); char char2 = (*c) >> 8; if (char2) putc(char2, stdout); ++c; } fflush(stdout); } 

إنهاء البرنامج


 puts("HALT"); fflush(stdout); running = 0; 

10. تنزيل البرامج


تحدثنا كثيرًا عن تحميل وتنفيذ التعليمات من الذاكرة ، ولكن كيف تدخل التعليمات في الذاكرة بشكل عام؟ عند تحويل برنامج المجمّع إلى رمز الجهاز ، تكون النتيجة ملفًا يحتوي على مجموعة من التعليمات والبيانات. يمكن تنزيله ببساطة عن طريق نسخ المحتويات مباشرة إلى عنوان في الذاكرة.

تشير أول 16 بت من ملف البرنامج إلى العنوان في الذاكرة حيث يجب أن يبدأ البرنامج. هذا العنوان يسمى الأصل . يجب قراءتها أولاً ، وبعدها تتم قراءة بقية البيانات في الذاكرة من الملف.

فيما يلي الرمز لتحميل البرنامج في ذاكرة LC-3:

 void read_image_file(FILE* file) { /* the origin tells us where in memory to place the image */ uint16_t origin; fread(&origin, sizeof(origin), 1, file); origin = swap16(origin); /* we know the maximum file size so we only need one fread */ uint16_t max_read = UINT16_MAX - origin; uint16_t* p = memory + origin; size_t read = fread(p, sizeof(uint16_t), max_read, file); /* swap to little endian */ while (read-- > 0) { *p = swap16(*p); ++p; } } 

لاحظ أن لكل قيمة تحميل يسمى swap16. تتم كتابة برامج LC-3 بترتيب بايت مباشر ، لكن معظم أجهزة الكمبيوتر الحديثة تستخدم الترتيب العكسي. نتيجة لذلك ، نحتاج إلى قلب كل واحدة محملة uint16. (إذا استخدمت بطريق الخطأ جهاز كمبيوتر غريب مثل PPC ، فلن يحتاج إلى تغيير).

 uint16_t swap16(uint16_t x) { return (x << 8) | (x >> 8); } 

ملاحظة: يشير ترتيب البايت إلى كيفية ترجمة بايت عدد صحيح. بالترتيب العكسي ، البايت الأول هو الرقم الأقل أهمية ، وفي الترتيب العكسي ، بالعكس. بقدر ما أعرف ، فإن القرار تعسفي في الغالب . اتخذت شركات مختلفة قرارات مختلفة ، لذلك لدينا الآن تطبيقات مختلفة. بالنسبة لهذا المشروع ، لم تعد بحاجة إلى معرفة أي شيء عن ترتيب البايت.

أضف أيضًا وظيفة ملائمة لـ read_image_file، والتي تأخذ مسار السلسلة:

 int read_image(const char* image_path) { FILE* file = fopen(image_path, "rb"); if (!file) { return 0; }; read_image_file(file); fclose(file); return 1; } 

11. السجلات المعينة


بعض السجلات الخاصة غير متوفرة من جدول التسجيل العادي. بدلاً من ذلك ، يتم حجز عنوان خاص لهم في الذاكرة. لقراءة هذه السجلات والكتابة إليها ، يمكنك ببساطة القراءة والكتابة إلى ذاكرتهم. يطلق عليهم سجلات الذاكرة المعينة . عادة ما يتم استخدامها للتفاعل مع أجهزة خاصة.

بالنسبة إلى LC-3 لدينا ، نحتاج إلى تنفيذ سجلين قابلين للتطبيق. هذا هو سجل حالة لوحة المفاتيح ( KBSR) وسجل بيانات لوحة المفاتيح ( KBDR). يشير الأول إلى ما إذا كان المفتاح قد تم ضغطه ، بينما يحدد الثاني المفتاح الذي تم الضغط عليه.

على الرغم من أنه يمكن طلب إدخال لوحة المفاتيح باستخدام GETC، فإنه يمنع التنفيذ حتى يتم تلقي الإدخال. KBSRو KBDRتسمحتحقق من حالة الجهاز مع الاستمرار في تشغيل البرنامج ، بحيث يظل متجاوبًا أثناء انتظار الإدخال.

 enum { MR_KBSR = 0xFE00, /* keyboard status */ MR_KBDR = 0xFE02 /* keyboard data */ }; 

السجلات المعينة تعقد الوصول إلى الذاكرة قليلاً. لا يمكننا القراءة والكتابة إلى صفيف الذاكرة مباشرة ، ولكن بدلاً من ذلك يجب استدعاء وظائف خاصة - واضعة و getter. بعد قراءة الذاكرة من سجل KBSR ، يتحقق getter من لوحة المفاتيح ويقوم بتحديث كلا الموقعين في الذاكرة.

 void mem_write(uint16_t address, uint16_t val) { memory[address] = val; } uint16_t mem_read(uint16_t address) { if (address == MR_KBSR) { if (check_key()) { memory[MR_KBSR] = (1 << 15); memory[MR_KBDR] = getchar(); } else { memory[MR_KBSR] = 0; } } return memory[address]; } 

هذا هو العنصر الأخير من الجهاز الظاهري! إذا كنت قد نفذت بقية إجراءات المقاطعة وإرشاداتها ، فأنت على استعداد تقريبًا لتجربتها!

يجب إضافة كل شيء مكتوب إلى ملف C بالترتيب التالي:

 {Memory Mapped Registers, 11} {TRAP Codes, 8} {Memory Storage, 3} {Register Storage, 3} {Functions, 12} {Main Loop, 5} 

12. منصة الميزات


يحتوي هذا القسم على بعض التفاصيل المملة اللازمة للوصول إلى لوحة المفاتيح والعمل بشكل صحيح. لا يوجد شيء مثير للاهتمام أو بالمعلومات حول تشغيل الأجهزة الافتراضية. لا تتردد نسخة معجون!

إذا كنت تحاول بدء تشغيل VM في نظام تشغيل بخلاف نظام التشغيل Unix ، مثل Windows ، فيجب استبدال هذه الوظائف بوظائف Windows المناظرة.

 uint16_t check_key() { fd_set readfds; FD_ZERO(&readfds); FD_SET(STDIN_FILENO, &readfds); struct timeval timeout; timeout.tv_sec = 0; timeout.tv_usec = 0; return select(1, &readfds, NULL, NULL, &timeout) != 0; } 

رمز لاستخراج المسار من وسيطات البرنامج وإخراج مثال للاستخدام إذا كانت مفقودة.

 if (argc < 2) { /* show usage string */ printf("lc3 [image-file1] ...\n"); exit(2); } for (int j = 1; j < argc; ++j) { if (!read_image(argv[j])) { printf("failed to load image: %s\n", argv[j]); exit(1); } } 

يونيكس محددة محطة التكوين رمز.

 struct termios original_tio; void disable_input_buffering() { tcgetattr(STDIN_FILENO, &original_tio); struct termios new_tio = original_tio; new_tio.c_lflag &= ~ICANON & ~ECHO; tcsetattr(STDIN_FILENO, TCSANOW, &new_tio); } void restore_input_buffering() { tcsetattr(STDIN_FILENO, TCSANOW, &original_tio); } 

عند مقاطعة البرنامج ، نريد إعادة وحدة التحكم إلى إعداداتها العادية.

 void handle_interrupt(int signal) { restore_input_buffering(); printf("\n"); exit(-2); } 

 signal(SIGINT, handle_interrupt); disable_input_buffering(); 

 restore_input_buffering(); 

 {Sign Extend, 6} {Swap, 10} {Update Flags, 6} {Read Image File, 10} {Read Image, 10} {Check Key, 12} {Memory Access, 11} {Input Buffering, 12} {Handle Interrupt, 12} 

 #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #include <signal.h> #include <unistd.h> #include <fcntl.h> #include <sys/time.h> #include <sys/types.h> #include <sys/termios.h> #include <sys/mman.h> 

بدء تشغيل الجهاز الظاهري


الآن يمكنك إنشاء وتشغيل الجهاز الظاهري LC-3!

  1. ترجمة البرنامج مع مترجم المفضلة لديك.
  2. قم بتنزيل النسخة المترجمة من 2048 أو Rogue .
  3. قم بتشغيل البرنامج باستخدام ملف obj كوسيطة:
    lc3-vm path/to/2048.obj
  4. العب في 2048!

 Control the game using WASD keys. Are you on an ANSI terminal (y/n)? y +--------------------------+ | | | | | | | 2 | | | | 2 | | | | | | | +--------------------------+ 

تصحيح الأخطاء


إذا لم يعمل البرنامج بشكل صحيح ، فمن المحتمل أنك قمت بترميز نوع من التعليمات بشكل غير صحيح. يمكن أن يكون من الصعب تصحيح. أوصي بأن تقرأ رمز التجميع للبرنامج في نفس الوقت - وبمساعدة مصحح الأخطاء خطوة بخطوة اتبع إرشادات الجهاز الظاهري واحدة في كل مرة. عند قراءة الكود ، تأكد من أن VM يذهب إلى التعليمات المقصودة. في حالة حدوث عدم تطابق ، ستعرف على الإرشادات التي تسببت في المشكلة. أعد قراءة المواصفات وأعد التحقق من الكود.

14. طريقة بديلة في C ++


إليك طريقة متقدمة لتنفيذ الإرشادات التي تقلل بشكل كبير من حجم الرمز. هذا هو قسم اختياري تماما.

ونظرًا لأن C ++ يدعم الوراثة القوية أثناء عملية التحويل البرمجي ، يمكننا إنشاء أجزاء من التعليمات باستخدام المترجم. تعمل هذه الطريقة على تقليل الازدواجية في الكود وهي في الواقع أقرب إلى مستوى أجهزة الكمبيوتر.

الفكرة هي إعادة استخدام الخطوات الشائعة لكل تعليمات. على سبيل المثال ، تستخدم بعض الإرشادات عنونة أو امتداد غير مباشر لقيمة ما وإضافتها إلى القيمة الحالية PC. توافق ، سيكون من الجميل أن تكتب هذا الرمز مرة واحدة لجميع التعليمات؟

بالنظر إلى التعليمات كسلسلة من الخطوات ، نرى أن كل تعليمة هي مجرد إعادة ترتيب لعدة خطوات أصغر. سنستخدم إشارات البت للإشارة إلى الخطوات التي يجب اتباعها لكل تعليمات. تشير القيمة 1الموجودة في رقم تعليمة bit إلى أنه في هذه الإرشادات ، يجب أن يتضمن المحول البرمجي هذا القسم من التعليمات البرمجية.

 template <unsigned op> void ins(uint16_t instr) { uint16_t r0, r1, r2, imm5, imm_flag; uint16_t pc_plus_off, base_plus_off; uint16_t opbit = (1 << op); if (0x4EEE & opbit) { r0 = (instr >> 9) & 0x7; } if (0x12E3 & opbit) { r1 = (instr >> 6) & 0x7; } if (0x0022 & opbit) { r2 = instr & 0x7; imm_flag = (instr >> 5) & 0x1; imm5 = sign_extend((instr) & 0x1F, 5); } if (0x00C0 & opbit) { // Base + offset base_plus_off = reg[r1] + sign_extend(instr & 0x3f, 6); } if (0x4C0D & opbit) { // Indirect address pc_plus_off = reg[R_PC] + sign_extend(instr & 0x1ff, 9); } if (0x0001 & opbit) { // BR uint16_t cond = (instr >> 9) & 0x7; if (cond & reg[R_COND]) { reg[R_PC] = pc_plus_off; } } if (0x0002 & opbit) // ADD { if (imm_flag) { reg[r0] = reg[r1] + imm5; } else { reg[r0] = reg[r1] + reg[r2]; } } if (0x0020 & opbit) // AND { if (imm_flag) { reg[r0] = reg[r1] & imm5; } else { reg[r0] = reg[r1] & reg[r2]; } } if (0x0200 & opbit) { reg[r0] = ~reg[r1]; } // NOT if (0x1000 & opbit) { reg[R_PC] = reg[r1]; } // JMP if (0x0010 & opbit) // JSR { uint16_t long_flag = (instr >> 11) & 1; pc_plus_off = reg[R_PC] + sign_extend(instr & 0x7ff, 11); reg[R_R7] = reg[R_PC]; if (long_flag) { reg[R_PC] = pc_plus_off; } else { reg[R_PC] = reg[r1]; } } if (0x0004 & opbit) { reg[r0] = mem_read(pc_plus_off); } // LD if (0x0400 & opbit) { reg[r0] = mem_read(mem_read(pc_plus_off)); } // LDI if (0x0040 & opbit) { reg[r0] = mem_read(base_plus_off); } // LDR if (0x4000 & opbit) { reg[r0] = pc_plus_off; } // LEA if (0x0008 & opbit) { mem_write(pc_plus_off, reg[r0]); } // ST if (0x0800 & opbit) { mem_write(mem_read(pc_plus_off), reg[r0]); } // STI if (0x0080 & opbit) { mem_write(base_plus_off, reg[r0]); } // STR if (0x8000 & opbit) // TRAP { {TRAP, 8} } //if (0x0100 & opbit) { } // RTI if (0x4666 & opbit) { update_flags(r0); } } 

 static void (*op_table[16])(uint16_t) = { ins<0>, ins<1>, ins<2>, ins<3>, ins<4>, ins<5>, ins<6>, ins<7>, NULL, ins<9>, ins<10>, ins<11>, ins<12>, NULL, ins<14>, ins<15> }; 

ملاحظة: لقد تعلمت عن هذه التقنية من محاكي NES التي طورتها بيسكويت . إذا كنت مهتمًا بمضاهاة أو NES ، فإنني أوصي بشدة بمقاطع الفيديو التابعة لها.

تستخدم إصدارات أخرى من C ++ الرمز المكتوب بالفعل. النسخة الكاملة هنا .

 {Includes, 12} {Registers, 3} {Condition Flags, 3} {Opcodes, 3} {Memory Mapped Registers, 11} {TRAP Codes, 8} {Memory Storage, 3} {Register Storage, 3} {Functions, 12} int running = 1; {Instruction C++, 14} {Op Table, 14} int main(int argc, const char* argv[]) { {Load Arguments, 12} {Setup, 12} enum { PC_START = 0x3000 }; reg[R_PC] = PC_START; while (running) { uint16_t instr = mem_read(reg[R_PC]++); uint16_t op = instr >> 12; op_table[op](instr); } {Shutdown, 12} } 

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


All Articles