نكتب محاكي ليست هناك حاجة من قبل أي شخص

يوم جيد.


منذ زمن بعيد كانت هناك رغبة في كتابة محاكي لبعض المعالجات.
وماذا يمكن أن يكون أفضل من اختراع دراجة؟


اسم الدراجة هو V16 ، من الإلتصاق بكلمة Virtual ، وفي الواقع ، عمق البت.



من أين تبدأ؟


وتحتاج إلى البدء ، بالطبع ، مع وصف المعالج.


في البداية ، خططت لكتابة محاكي DCPU-16 ، ولكن هناك أكثر من كافية من هذه المعجزات على الإنترنت ، لذلك قررت التركيز فقط على "لعق" أبسط مع DCPU-16 1.1.


هندسة معمارية


الذاكرة والموانئ


  • يعالج V16 128 كيلو بايت (65536 كلمة) من ذاكرة الوصول العشوائي ، والتي يمكن أيضًا استخدامها كمخازن مؤقتة للجهاز والمكدس.
  • يبدأ المكدس بعنوان FFFF ، لذلك ، لدى RSP قيمة قياسية 0xFFFF
  • تحتوي منافذ الإدخال / الإخراج V16 على 256 منفذًا ، طولها 16 بت. تتم القراءة والكتابة منها من خلال التعليمات IN b, a و OUT b, a .

سجلات


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


تعليمات


يبلغ طول كل التعليمات ثلاث كلمات ويتم تعريفها بالكامل أولاً
تقسم الكلمة الأولى إلى ثلاث قيم: البايت المنخفض هو شفرة التشغيل ، والبايتة العالية في شكل قيمتين 4 بت هي وصف المعاملات.


المقاطعات


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


نطاق القيمةوصف
0x0 ... 0x3القضية كقيمة
0x4 ... 0x7سجل كقيمة في
0x8 ... 0xBسجل + ثابت كقيمة في العنوان
0xCثابت كقيمة في
0xDثابت كقيمة
0xEسجل RIP كقيمة للقراءة فقط
0xFسجل RSP كقيمة

مثال على الكود الكاذب والكلمات التي يجب أن تترجم كل هذا:


 MOV RAX, 0xABCD ; 350D ABCD MOV [RAX], 0x1234 ; 354D 1234 

دورات (دورات)


يمكن لـ V16 تنفيذ تعليمة واحدة في 1 أو 2 أو 3 تدابير. كل وصول الذاكرة هو دورة واحدة على مدار الساعة منفصلة. التعليمات ليست براعة!


لنبدأ الكتابة!


تنفيذ هياكل المعالج الأساسية


  1. مجموعة من السجلات. لا يوجد سوى أربعة سجلات ، لكن الموقف يتحسن بوجود مجموعتين من هذه المجموعات في المعالج. يحدث التبديل باستخدام تعليمات XCR .


     typedef struct Regs { uint16_t rax, rbx; //Primary Accumulator, Base Register uint16_t rcx, rdx; //Counter Register, Data Register } regs_t; 

  2. الأعلام. على عكس DCPU-16 ، يحتوي V16 على قفزات مشروطة ومكالمات روتين فرعي ويعود من نفسه. في الوقت الحالي ، يحتوي المعالج على 8 أعلام ، 5 منها أعلام شرطية.


     //  ,    stdbool.h typedef struct Flags { bool IF, IR, HF; bool CF, ZF; bool EF, GF, LF; } flags_t; 

  3. في الواقع ، المعالج نفسه. كما يصف جدول عناوين المقاطعة ، والذي يمكن أن يسمى واصفات وإيجاد مرجع آخر إلى x86.


     typedef struct CPU { //CPU Values uint16_t ram[V16_RAMSIZE]; //Random Access Memory uint16_t iop[V16_IOPSIZE]; //Input-Output Ports uint16_t idt[V16_IDTSIZE]; //Interrupt vectors table (Interrupt Description Table) flags_t flags; //Flags regs_t reg_m, reg_a; //Main and Alt register files regs_t * reg_current; //Current register file uint16_t rip, rsp, rex; //Internal Registers: Instruction Pointer, Stack Pointer, EXtended Accumulator //Emulator values bool reg_swapped; //Is current register file alt bool running; //Is cpu running uint32_t cycles; //RAM access counter } cpu_t; 

  4. المعامل. عند الحصول على القيم ، نحتاج أولاً إلى القراءة ، ثم التغيير ، ثم كتابة القيمة مرة أخرى إلى حيث حصلنا عليها.


     typedef struct Opd { uint8_t code : 4; uint16_t value; uint16_t nextw; } opd_t; 


وظائف للعمل مع الهياكل


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


 cpu_t * cpu_create(void); //   void cpu_delete(cpu_t *); //   void cpu_load(cpu_t *, const char *); // ROM   void cpu_rswap(cpu_t *); //   uint16_t cpu_nextw(cpu_t *); //RAM[RIP++]. Nuff said void cpu_getop(cpu_t *, opd_t *, uint8_t); //  void cpu_setop(cpu_t *, opd_t *, uint16_t); //  void cpu_tick(cpu_t *); //   void cpu_loop(cpu_t *); // ,    

أيضًا ، لم أذكر تعدادًا كبيرًا برموز التشغيل ، لكن هذا ليس ضروريًا وضروريًا فقط لفهم ما يحدث في كل هذه الفوضى.


القراد () وظيفة


أيضًا ، هناك استدعاءات لوظائف ثابتة مخصصة فقط للاتصال من tick() .


 void cpu_tick(cpu_t *cpu) { //    HLT,      if(cpu->flags.HF) { //      ,      if(!cpu->flags.IF) { cpu->running = false; } return; } //       uint16_t nw = cpu_nextw(cpu); uint8_t op = ((nw >> 8) & 0xFF); uint8_t ob = ((nw >> 4) & 0x0F); uint8_t oa = ((nw >> 0) & 0x0F); //     //   opd_t opdB = { 0 }; opd_t opdA = { 0 }; //    cpu_getop(cpu, &opdB, ob); cpu_getop(cpu, &opdA, oa); //        -  uint16_t B = opdB.value; uint16_t A = opdA.value; uint32_t R = 0xFFFFFFFF; //    bool clearf = true; //       ? //   ! switch(op) { //     . ,   ,    R } //   if(clearf) { cpu->flags.EF = false; cpu->flags.GF = false; cpu->flags.LF = false; } //  ,  32-   16-  //  0xFFFF0000,   0xFFFF << 16 //        32-  if(R != 0xFFFFFFFF) { cpu_setop(cpu, &opdB, (R & 0xFFFF)); cpu->rex = ((R >> 16) & 0xFFFF); cpu->flags.CF = (cpu->rex != 0); cpu->flags.ZF = (R == 0); } return; } 

ماذا تفعل بعد ذلك؟


في محاولة للعثور على إجابة لهذا السؤال ، أعدت كتابة المحاكي خمس مرات من C إلى C ++ ، والعكس بالعكس.


ومع ذلك ، يمكن تحديد الأهداف الرئيسية الآن:


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

استنتاج


آمل أن تكون هذه "المقالة" مفيدة لشخص ما ، شخص ما سيحثهم على كتابة شيء مماثل.


فطائر بلدي يمكن أن ينظر إليها على جيثب .


أيضًا ، حول الرعب ، لدي أداة تجميع للإصدار القديم من هذا المحاكي (لا ، لا تحاول حتى ، فإن المحاكي سوف يشكو على الأقل من تنسيق ROM الخاطئ)

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


All Articles