إنشاء آلة ممر المحاكي. الجزء الأول

الصورة

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

ستحتاج إلى معرفة C ، بالإضافة إلى معرفة المجمع. إذا كنت لا تعرف لغة التجميع ، فإن كتابة محاكي هو أفضل طريقة لتعلمها. ستحتاج أيضًا إلى إتقان الرياضيات العشرية (تُعرف أيضًا باسم الأساس 16 أو "السداسي" فقط). سأتحدث عن هذا الموضوع.

قررت اختيار محاكي لجهاز Space Invaders ، والذي يستخدم المعالج 8080. هذه اللعبة وهذا المعالج شائعان جدًا ، لأنه على الإنترنت يمكنك العثور على الكثير من المعلومات عنها. سوف تحتاجه لإكمال المشروع.

يتم تحميل شفرة المصدر الكاملة للبرنامج التعليمي إلى github . إذا لم تكن قد أتقنت العمل مع git ، فعندئذٍ يوجد على صفحة github زر "Download ZIP" الذي يسمح لك بتنزيل الأرشيف بكل التعليمات البرمجية.

مقدمة للأرقام الثنائية والسداسية العشرية


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

ربما كنت تعرف أو تسمع أن أجهزة الكمبيوتر تعمل مع البيانات الثنائية. يستدعي خبراء الكمبيوتر الرياضيات العشرية الأساسية 10 ، وقاعدة المكالمات الثنائية -2. في التدوين الثنائي ، يمكن أن يكون لكل رقم في عدد قيمتين فقط ، صفر أو واحد. في الشفرة الثنائية ، يكون العد كما يلي: 0 ، 1 ، 10 ، 11 ، 100 ، 101 ، 110 ، 111 ، 1000. هذه ليست أرقامًا عشرية ، لذا لا يمكنك تسميتها "صفر ، واحد ، عشرة ، أحد عشر ، مائة ، مائة وواحد". يتم نطقها على أنها "صفر ، واحد ، واحد صفر ، واحد واحد ، واحد صفر صفر" ، إلخ. نادرًا ما أقرأ الأرقام الثنائية بصوت عال ، ولكن إذا لزم الأمر ، فأنت بحاجة إلى الإشارة بوضوح إلى نظام الأرقام المستخدم. عشرة وأحد عشر ومائة ليس لها معنى في التدوين الثنائي.

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

من الناحية الثنائية ، تصبح سلسلة الأرقام بسرعة طويلة جدًا. لتمثيل الرقم العشري 20000 من حيث ثنائي ، يلزم 16 رقمًا: 0b100111000100000. لإصلاح هذه المشكلة ، من المناسب استخدام نظام أرقام سداسي عشري ، يُعرف أيضًا باسم base-16 (أو سداسي عشري). في الأساس 16 ، يحتوي كل رقم على 16 قيمة. بالنسبة للقيم من صفر إلى تسعة ، يتم استخدام نفس الأحرف كما في الأساس 10 ، ولكن بالنسبة للقيم الست المتبقية ، يتم استخدام البدائل في شكل الأحرف الستة الأولى من الأبجدية ، من A إلى F.

يتم تنفيذ الحساب في النظام الست عشري على النحو التالي: 0 1 2 3 4 5 6 7 8 9 ABCDEF 10 11 12 ، إلخ. في الشكل السداسي العشري ، لا يمتلك العشرات والمئات وما إلى ذلك المعنى نفسه كما في العلامة العشرية ، لذلك ينطق الأشخاص الأرقام بشكل منفصل. على سبيل المثال ، يتم نطق $ A57 بصوت عالٍ على أنه "A-five-seven". للتوضيح ، يمكنك أيضًا إضافة سداسي عشري ، على سبيل المثال ، "A-five-سبعة-hex". في نظام الأرقام السداسية العشرية ، يعادل الرقم العشري 20000 دولارًا أمريكيًا 4E20 - وهو شكل مضغوط أكثر بكثير مقارنة بـ 16 بتًا من النظام الثنائي.

أعتقد أنه تم اختيار النظام السداسي العشري بسبب التحويل الطبيعي جدًا من الثنائي إلى السداسي والعكس. يقابل كل رقم سداسي عشري 4 بتات (4 بتات) لرقم ثنائي مشابه. يتكون الرقمان السداسيان من بايت واحد (8 بت). يمكن تسمية رقم سداسي عشري واحد nibble ، وحتى بعض الناس يكتبونه من خلال y كـ "nybble".

كل رقم سداسي عشري هو 4 أرقام ثنائية
عرافةأ57
ثنائي101001010111

عند كتابة كود C ، يعتقد أن الرقم عشري (الأساس 10) ، ما لم يتم وضع علامة على خلاف ذلك. لإخبار المترجم C أن الرقم ثنائي ، نضيف الرقم صفر والحرف b بأحرف صغيرة ، مثل هذا: 0b1101101 . يمكن كتابة الرقم السداسي العشري في رمز C عن طريق الإضافة في بداية الصفر و x 0xA57 : 0xA57 . تستخدم بعض لغات التجميع علامة الدولار $: $A57 للإشارة إلى رقم ست عشري.

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

هل فهمت كل هذا؟ عظيم.

مقدمة موجزة للمعالج


إذا كنت تعرف هذا بالفعل ، يمكنك تخطي القسم بأمان.

وحدة المعالجة المركزية (CPU) هي آلة مصممة لتشغيل البرامج. الكتل الأساسية لوحدة المعالجة المركزية هي السجلات والتعليمات. بصفتك مطور برامج ، يمكنك التعامل مع هذه السجلات كمتغيرات. في معالج 8080 الخاص بنا ، من بين السجلات الأخرى ، هناك تسجيلات 8 بت تسمى A و B و C و D و E. يمكن تفسير هذه السجلات على أنها رمز C التالي:

 unsigned char A, B, C, D, E; 

تحتوي جميع المعالجات أيضًا على عداد برامج (عداد البرامج ، الكمبيوتر). يمكنك أن تأخذها كمؤشر.

 unsigned char* pc; 

بالنسبة لوحدة المعالجة المركزية ، البرنامج عبارة عن سلسلة من الأرقام السداسية العشرية. يقابل كل تعليم لغة التجميع في 8080 1-3 بايت في البرنامج. من أجل معرفة أي أمر يتوافق مع أي رقم ، فإن دليل المعالج (أو أي معلومات أخرى حول المعالج 8080 من الإنترنت) مفيد.

غالبًا ما تكون أسماء الأوامر (التعليمات) عبارة عن استذكار من العمليات التي تنفذها هذه الأوامر. يعد التذكير للتحميل في 8080 هو MOV (نقل) ، ويتم استخدام ADD لإجراء الإضافة.

أمثلة


قيمة الذاكرة الحالية التي يشير إليها عداد التعليمات هي 0x79. يتوافق هذا مع تعليمات MOV A,C للمعالج 8080. يبدو رمز التجميع هذا في رمز C مثل A=C; .

إذا كانت القيمة في جهاز الكمبيوتر بدلاً من ذلك هي 0x80 ، فسيقوم المعالج بتنفيذ ADD B في C ، هذا يتوافق مع السلسلة A = A + B; .

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

توقيت


في وحدة المعالجة المركزية ، يتطلب تنفيذ كل تعليمة مقدارًا معينًا من الوقت (التوقيت) ، يتم قياسه بالدورات. في المعالجات الحديثة ، قد يكون من الصعب الحصول على هذه المعلومات ، لأن التوقيت يعتمد على العديد من الجوانب المختلفة. ولكن في المعالجات القديمة مثل 8080 ، تكون التوقيتات ثابتة وغالبًا ما يتم توفير هذه المعلومات من قبل الشركة المصنعة للمعالج. على سبيل المثال ، تستغرق تعليمات النقل من التسجيل للتسجيل MOV دورة واحدة.

معلومات التوقيت مفيدة لكتابة كود فعال في المعالج. قد يسعى المبرمج إلى تجنب التعليمات التي تستغرق العديد من الدورات لإكمالها.

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

العمليات المنطقية


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

و العملية


فيما يلي جميع النتائج المحتملة لعملية AND (AND) (جدول الحقيقة) بين رقمين أحادي البت.

سذالنتيجة
000
010
100
111

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

ثنائيعرافة
مصدر العاشر011010116 مليار دولار
مصدر ذ11010010D2 دولار
س و ص0100001042 دولارًا

في C ، تكون العملية المنطقية AND هي علامة الضم بسيطة "&".

العملية أو (أو)


تعمل عملية OR بطريقة مماثلة. والفرق الوحيد هو أن النتيجة ستكون مساوية لواحدة إذا كانت قيمة واحدة على الأقل من قيم x أو y تساوي واحدة.

سذالنتيجة
000
011
101
111

ثنائيعرافة
مصدر العاشر011010116 مليار دولار
مصدر ذ11010010D2 دولار
س أو ص11111011$ Fb

في لغة C ، يشار إلى عملية OR المنطقية بشريط عمودي "|".

لماذا هذا مهم؟


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

  /*  1:     */ char *buttons_ptr = (char *)0x2043; char buttons = *buttons_ptr; if (buttons & 0x4) HandleLeftButton(); /*  2:  LED-    */ char * LED_pointer = (char *) 0x2089; char led = *LED_pointer; led = led | 0x40; //,  LED   6 *LED_pointer = led; /*  3:   LED- */ char * LED_pointer = (char *) 0x2089; char led = *LED_pointer; led = led & 0xBF; //  6 *LED_pointer = led; 

في المثال 1 ، العنوان 2043 دولار المخصص في الذاكرة هو عنوان الأزرار الموجودة على لوحة التحكم. يقرأ هذا الرمز ويستجيب للزر المضغوط. (بالطبع ، في غزاة الفضاء سيكون هذا الرمز بلغة التجميع!)

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

في المثال 3 ، تحتاج إلى إيقاف تشغيل المؤشر من المثال 2 ، لذا يجب أن يعيد الرمز تعيين البت 6 من العنوان 2089 دولارًا. يمكن القيام بذلك عن طريق تنفيذ العملية AND لبايت التحكم في المؤشر بقيمة لا تزيد عن البتة 6 فيها. لذا سنؤثر على 6 فقط ، مع ترك البتات المتبقية دون تغيير.

وهذا ما يسمى عادة "القناع". في لغة C ، عادة ما يتم كتابة القناع باستخدام عامل NOT ، والمشار إليه بالتلدة ("~"). لذلك ، بدلاً من كتابة 0xBF ، فقط أكتب ~0x40 وأحصل على نفس الرقم ، ولكن دون بذل الكثير من الجهد.

مقدمة في لغة التجميع


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

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

أهم شيء تحتاج إلى معرفته عن لغة التجميع هو أن كل سطر يُترجم إلى أمر معالج واحد.

تأمل مثل هذا البناء من لغة C:

 int a = b + 100; 

في لغة التجميع ، يجب تنفيذ هذه المهمة بالتسلسل التالي:

  1. قم بتحميل عنوان المتغير B في السجل 1
  2. قم بتحميل محتويات عنوان الذاكرة هذا في السجل 2
  3. أضف القيمة المباشرة 0x64 للتسجيل 2
  4. قم بتحميل عنوان المتغير A في السجل 1
  5. اكتب محتويات السجل 2 على العنوان المخزن في السجل 1

في الكود ، سيبدو شيئًا مثل هذا:

  lea a1, #$1000 ;   a lea a2, #$1008 ;   b move.l d0,(a2) add.l d0, #$64 mov (a1),d0 

تجدر الإشارة إلى ما يلي:

  • في لغة عالية المستوى ، يقرر المترجم مكان وضع المتغيرات في الذاكرة. عند كتابة التعليمات البرمجية في المجمع ، أنت بنفسك مسؤول عن كل عنوان ذاكرة ستستخدمه.
  • في معظم لغات التجميع ، تعني الأقواس "الذاكرة في هذا العنوان".
  • في معظم لغات التجميع ، تشير # إلى رقم جبرية ، وتسمى أيضًا القيمة الفورية. على سبيل المثال ، في السطر 1 من المثال أعلاه ، يكتب الرمز بالفعل القيمة # 0x1000 لتسجيل a1. إذا كان الرمز يشبه move.l a1, ($1000) ، move.l a1, ($1000) على محتويات الذاكرة على العنوان 0x1000.
  • لكل معالج لغة التجميع الخاصة به ، وقد يكون رمز النقل من معالج إلى آخر أمرًا صعبًا.
  • هذه ليست لغة تجميع معالج حقيقية ، لقد توصلت إليها كمثال.

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

الأكوام


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

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

لنفترض أن برنامجي يحتاج إلى استدعاء روتين فرعي. يمكنني كتابة رمز مشابه:

  0x1000 move.l (sp), d0 ;  d0   0x1004 add.l sp, #4 ;     0x1008 move.l (sp), d1 ;  d1   0x1010 add.l sp, #4 ;  .. 0x1014 move.l (sp), a0 0x1018 add.l sp, #4 0x101C move.l (sp), a1 0x1020 add.l sp, #4 0x1024 move.l (sp), #0x1030 ;   0x1028 add.l sp, #4 0x102C jmp #0x2040 ;   - 0x2040 0x1030 move.l a1, (sp) ;    0x1034 sub.l sp, #4 ;    0x1038 move.l a0, (sp) ;    0x103c sub.l sp, #4  .. 

يدفع الرمز الموضح أعلاه القيم d0 و d1 و a0 و a1 إلى المكدس. تستخدم معظم المعالجات مؤشر مكدس. يمكن أن يكون هذا سجلاً عاديًا ، حسب الاصطلاح المستخدم كمؤشر مكدس ، أو سجل خاص بوظائف لتعليمات معينة.

على المعالجات في سلسلة 68K ، يتم تحديد مؤشر المكدس فقط من خلال الاتفاقية ؛ وإلا فهو سجل منتظم. في معالج 8080 الخاص بنا ، يعد سجل SP سجلًا خاصًا. يحتوي على أوامر PUSH و POP التي تكتب وتنبثق من المكدس في أمر واحد فقط.

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

لغات عالية المستوى


عند كتابة برنامج بلغة عالية المستوى ، يتم تنفيذ جميع عمليات حفظ السجلات واستعادتها مع كل استدعاء دالة. نحن لا نفكر بهم ، لأن المترجم يتعامل معهم. يمكن أن تستهلك مكالمات الوظائف بلغة عالية المستوى الكثير من الذاكرة ووقت المعالج.

هل سبق لك أن واجهت تعطل برنامج عند استدعاء روتين فرعي في حلقة لا نهائية؟ يمكن أن يحدث هذا لأن كل استدعاء دالة دفع قيم التسجيل إلى المكدس ، وفي مرحلة ما نفدت الذاكرة من المكدس. (في حالة تكدس المكدس كبيرًا جدًا ، يسمى ذلك تجاوز سعة المكدس ، أو تجاوز سعة المكدس.)

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

اصطلاحات استدعاء


عند كتابة برنامج تجميع يستدعي رمزك فقط ، يمكنك أن تقرر بنفسك كيف ستتواصل الروتينات مع بعضها البعض. على سبيل المثال ، كيف أعود إلى وظيفة الاتصال بعد اكتمال الروتين؟ إحدى الطرق هي كتابة عنوان المرسل إلى سجل معين. والآخر في وضع عنوان المرسل فوق المكدس. في كثير من الأحيان ، يعتمد القرار على ما يدعمه المعالج. يحتوي 8080 على أمر CALL يدفع عنوان الإرجاع للدالة على المكدس. ربما ستستخدم هذا الأمر 8080 لتنفيذ مكالمات روتين.

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

قد تكون المفاضلة نهج مختلط. لنفترض أننا اخترنا سياسة يمكن للروتين من خلالها استخدام تسجيلات r10-r32 دون حفظ محتوياتها ، ولكن لا يمكنه تدمير r1-r9. في حالة مماثلة ، تعرف وظيفة الاستدعاء ما يلي:

  • عند العودة من دالة ، ستبقى محتويات r1-r9 دون تغيير
  • لا يمكنني الاعتماد على محتويات r10-r32
  • إذا كنت بحاجة إلى قيمة في r10-r32 بعد استدعاء روتين فرعي ، ثم قبل الاتصال به أحتاج إلى حفظه في مكان ما

وبالمثل ، يعرف كل روتين ما يلي:

  • يمكنني تدمير r10-r32
  • إذا كنت أرغب في استخدام r1-r9 ، فأنا بحاجة إلى حفظ المحتويات واستعادتها قبل العودة إلى الوظيفة التي اتصلت بي

أبي


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

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

العودة إلى المحاكي


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

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

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


All Articles