كيفية ترجمة ملف COM DOS من قبل المترجم GCC

نشرت المقالة 9 ديسمبر 2014
تحديث لعام 2018: قام RenéRebe بعمل فيديو مثير للاهتمام بناءً على هذه المقالة ( الجزء 2 )

شاركت في عطلة نهاية الأسبوع الماضي في Ludum Dare # 31 . ولكن حتى قبل الإعلان عن المؤتمر ، بسبب هوايتي الأخيرة ، أردت أن أصنع لعبة المدرسة القديمة تحت DOS. النظام الأساسي الهدف هو DOSBox. هذه هي الطريقة الأكثر عملية لتشغيل تطبيقات DOS ، على الرغم من حقيقة أن جميع معالجات x86 الحديثة متوافقة تمامًا مع الإصدارات القديمة ، حتى 1686 8086.

لقد نجحت في إنشاء لعبة DOS Defender وعرضها في المؤتمر. يعمل البرنامج في الوضع الحقيقي لـ 3238-bit 80386. يتم دمج جميع الموارد في ملف COM القابل للتنفيذ ، ولا توجد تبعيات خارجية ، لذلك يتم تعبئة اللعبة بأكملها في ثنائي ثنائي 10 كيلوبايت.



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

الجزء الأكثر إثارة للاهتمام تقنيًا هو أنه لم تكن هناك حاجة إلى أدوات تطوير DOS لإنشاء اللعبة ! استخدمت فقط المترجم العادي لينكس سي (دول مجلس التعاون الخليجي). في الواقع ، لا يمكنك حتى بناء DOS Defender لـ DOS. أرى DOS فقط كنظام أساسي مضمن ، وهو الشكل الوحيد الذي لا يزال DOS موجودًا فيه حتى اليوم . جنبا إلى جنب مع DOSBox و DOSEMU ، هذه مجموعة مريحة إلى حد ما من الأدوات.

إذا كنت مهتمًا فقط بالجزء العملي من التطوير ، فانتقل إلى قسم "Cheat on GCC" ، حيث سنكتب برنامج DOS COM "Hello، World" مع GCC Linux.

إيجاد الأدوات المناسبة


عندما بدأت هذا المشروع ، لم أفكر في دول مجلس التعاون الخليجي. في الواقع ، لقد ذهبت بهذه الطريقة عندما اكتشفت حزمة bcc (مترجم C لـ Bruce) لـ Debian ، والتي تجمع ثنائيات 16 بت لـ 8086. يتم الاحتفاظ بها لتجميع محمل التمهيد x86 وأشياء أخرى ، ولكن يمكن أيضًا استخدام bcc لتجميع ملفات DOS COM. يهمني.

كمرجع: تم إصدار معالج Intel 8086 16-bit في عام 1978. لم يكن لديها ميزات غريبة للمعالجات الحديثة: لا توجد حماية للذاكرة ، ولا تعليمات للفاصلة العائمة ، و 1 ميغابايت فقط من ذاكرة الوصول العشوائي القابلة للتحكم. لا يزال بإمكان جميع أجهزة الكمبيوتر المكتبية والمحمولة x86 الحديثة التظاهر بأنها معالج 16 بت 8086 قبل أربعين عامًا ، بنفس العنوان المحدود وكل ذلك. هذا التوافق مع الإصدارات السابقة. تسمى هذه الوظيفة الوضع الحقيقي . هذا هو الوضع الذي يتم فيه تمهيد كافة أجهزة الكمبيوتر x86. تتحول أنظمة التشغيل الحديثة على الفور إلى الوضع المحمي من خلال العنونة الافتراضية وتعدد المهام الآمن. DOS لم تفعل ذلك.

لسوء الحظ ، ليس bcc مترجمًا لـ ANSI C. وهو يدعم مجموعة فرعية من K&R C ، بالإضافة إلى كود المجمع x86 المدمج. على عكس المترجمات 8086 C الأخرى ، ليس لديها مفهوم المؤشرات "البعيدة" أو "الطويلة" ، لذلك يلزم وجود رمز مجمع مدمج للوصول إلى أجزاء الذاكرة الأخرى (VGA ، الساعات ، إلخ). ملاحظة: لا تزال بقايا 8086 هذه "المؤشرات الطويلة" محفوظة في Win32 API: LPSTR و LPWORD و LPDWORD وما إلى ذلك. هذا المجمّع المدمج لا يقارن بشكل وثيق مع المجمع المدمج في دول مجلس التعاون الخليجي. في المجمّع ، تحتاج إلى تحميل المتغيرات يدويًا من المكدس ، وبما أن نسخة مخفية الوجهة تدعم اصطلاحين مختلفين للاتصال ، فيجب أن تكون المتغيرات في الكود مشفرة بشكل ثابت وفقًا لمصطلح واحد أو آخر.

بالنظر إلى هذه القيود ، قررت البحث عن بدائل.

DJGPP


DJGPP - منفذ GCC تحت DOS. مشروع مثير للإعجاب للغاية ينقل POSIX بالكامل تقريبًا تحت DOS. يتم إجراء العديد من البرامج المستندة إلى DOS على DJGPP. لكنه يقوم فقط بإنشاء برامج 32 بت للوضع المحمي. إذا كنت في الوضع المحمي تحتاج إلى العمل مع الأجهزة (على سبيل المثال ، VGA) ، يقوم البرنامج بعمل طلبات لخدمة واجهة الوضع المحمي DOS (DPMI). إذا كنت قد أخذت DJGPP ، فلا يمكنني أن أقصر نفسي على ثنائي مستقل واحد ، لأنه كان يجب أن يكون لدي خادم DPMI. يعاني الأداء أيضًا من طلبات DPMI.

إن الحصول على الأدوات اللازمة لـ DJGPP أمر صعب ، على أقل تقدير. لحسن الحظ ، وجدت مشروعًا مفيدًا لبناء build-djgpp يقوم بتشغيل كل شيء ، على الأقل على Linux.

إما كان هناك خطأ جسيم ، أو أن ثنائيات DJGPP الرسمية أصيبت بالفيروس مرة أخرى ، ولكن عندما بدأت برامجي في DOSBox ، ظهر الخطأ "ليس COFF: التحقق من الفيروسات" باستمرار. للتحقق من أن الفيروسات ليست على الجهاز الخاص بي ، قمت بإعداد بيئة DJGPP على Raspberry Pi ، والتي تعمل كغرفة نظيفة. لا يمكن إصابة هذا الجهاز المستند إلى ARM بفيروس x86. ولا تزال نفس المشكلة تنشأ ، وكانت جميع التجزئة الثنائية هي نفسها بين الآلات ، لذلك ليس خطأي.

لذلك بالنظر إلى هذه المشكلة ومشكلة DPMI ، بدأت أبحث أكثر.

خداع مجلس التعاون الخليجي


ما استقرت عليه أخيرًا هو الحيلة الصعبة المتمثلة في "الغش" في دول مجلس التعاون الخليجي لإنشاء ملفات COM COM في الوضع الحقيقي. تعمل الخدعة حتى 80386 (وهو ما تحتاجه عادةً). تم إطلاق المعالج 80386 في عام 1985 وأصبح أول معالج دقيق 32 بت x86. لا يزال مجلس التعاون الخليجي يلتزم بمجموعة التعليمات هذه ، حتى في بيئات x86-64. لسوء الحظ ، لا يمكن لدول مجلس التعاون الخليجي إنتاج رمز 16 بت بأي شكل من الأشكال ، لذلك اضطررت إلى التخلي عن الهدف الأصلي المتمثل في إنشاء لعبة لـ 8086. ومع ذلك ، هذا لا يهم ، لأن منصة DOSBox المستهدفة هي في الأساس محاكي 80386.

من الناحية النظرية ، يجب أن تعمل الخدعة أيضًا في مترجم MinGW ، ولكن هناك خطأ طويل الأمد يمنعها من العمل بشكل صحيح ("لا يمكن تنفيذ عمليات PE على ملف الإخراج غير PE"). ومع ذلك ، يمكنك الالتفاف حولها ، ولقد فعلت ذلك بنفسي: يجب عليك إزالة الأمر OUTPUT_FORMAT وإضافة خطوة إضافية objcopy ( objcopy -O binary ).

مرحبًا بالعالم على DOS


للتوضيح ، سنقوم بإنشاء برنامج DOS COM "Hello، World" باستخدام GCC على Linux.

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

يحتوي DOS على تسعة مقاطعات: 0x20 ، 0x21 ، 0x22 ، 0x23 ، 0x24 ، 0x25 ، 0x26 ، 0x27 ، 0x2f. أهم شيء يهمنا هو 0x21 ، الوظيفة 0x09 (طباعة خط). بين DOS و BIOS ، هناك الآلاف من الوظائف المسماة بعد هذا النمط . لن أحاول شرح مجمع x86 ، ولكن باختصار ، فإن رقم الوظيفة عالق في سجل ah - ويشتعل انقطاع المقاطعة 0x21. تأخذ الدالة 0x09 أيضًا وسيطة - مؤشر إلى خط للطباعة ، والذي يتم تمريره في تسجيلات dx و ds .

فيما يلي وظيفة print() لمجمع المجمّع المضمّن في دول مجلس التعاون الخليجي. يجب أن تنتهي الخطوط التي تم تمريرها إلى هذه الدالة بالحرف $. لماذا؟ لأن DOS.

 static void print(char *string) { asm volatile ("mov $0x09, %%ah\n" "int $0x21\n" : /* no output */ : "d"(string) : "ah"); } 

تم التصريح عن كود volatile لأنه له تأثير جانبي (طباعة خط). بالنسبة إلى دول مجلس التعاون الخليجي ، فإن رمز المجمع غير شفاف ، ويعتمد المُحسِّن على قيود الإخراج / الإدخال / clobber (الأسطر الثلاثة الأخيرة). لمثل هذه البرامج DOS ، فإن أي مجمع مدمج سيكون له آثار جانبية. هذا لأنه مكتوب ليس للتحسين ، ولكن للوصول إلى موارد الأجهزة و DOS - الأشياء التي لا يمكن الوصول إليها بسهولة C.

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

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

 int dosmain(void) { print("Hello, World!\n$"); return 0; } 

يقتصر حجم ملفات COM على 65279 بايت. وذلك لأن مقطع ذاكرة x86 هو 64 كيلو بايت ، ويقوم DOS ببساطة بتنزيل ملفات COM إلى عنوان مقطع 0x0100 وينفذ. لا عناوين ، مجرد ثنائي نظيف. نظرًا لأن برنامج COM ، من حيث المبدأ ، لا يمكن أن يكون له حجم كبير ، فلا يجب أن يحدث تخطيط حقيقي (قائم بذاته) ، يتم تجميع كل شيء كوحدة ترجمة واحدة. ستكون هذه مكالمة خليجية واحدة مع مجموعة من المعلمات.

خيارات المترجم


فيما يلي خيارات المترجم الرئيسية.

-std=gnu99 -Os -nostdlib -m32 -march=i386 -ffreestanding

نظرًا لعدم استخدام المكتبات القياسية ، فإن الاختلاف الوحيد بين gnu99 و c99 هو المثلثات المعطلة (كما يجب أن تكون) ، ويمكن كتابة المجمع المدمج asm بدلاً من __asm__ . هذه ليست سلة نيوتن. سيكون المشروع وثيق الصلة بدول مجلس التعاون الخليجي لدرجة أنني ما زلت غير معني بتمديدات دول مجلس التعاون الخليجي.

يقلل الخيار -Os نتيجة التجميع قدر الإمكان. لذلك سوف يعمل البرنامج بشكل أسرع. هذا مهم بالنظر إلى DOSBox ، لأن المحاكي الافتراضي يعمل ببطء مثل جهاز 80s. أريد أن أنسجم مع هذا القيد. إذا كان المُحسِّن يتسبب في حدوث مشكلات ، -O0 مؤقتًا -O0 لتحديد ما إذا كان الخطأ أو المُحسّن -O0 هنا.

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

المعلمة التالية هي -nostdlib ، حيث لن نتمكن من الارتباط بأي مكتبات صالحة ، حتى بشكل ثابت.

المعلمات -m32-march=i386 المترجم بإصدار الرمز 80386. إذا كتبت محمل الإقلاع لجهاز كمبيوتر حديث ، فإن مشهد 80686 سيكون عاديًا أيضًا ، لكن DOSBox هو 80386.

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

خيارات الرابط


-Wl استخدام -Wl لتمرير الوسائط إلى الرابط ( ld ). نحن بحاجة إلى هذا لأننا نفعل كل شيء في مكالمة واحدة لمجلس التعاون الخليجي.

 -Wl,--nmagic,--script=com.ld 

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

تشير المعلمة --script إلى أننا نريد استخدام نص رابط خاص. هذا يسمح لك بوضع الأقسام ( text ، data ، bss ، rodata ) rodata من برنامجنا. هنا هو البرنامج النصي com.ld

 OUTPUT_FORMAT(binary) SECTIONS { . = 0x0100; .text : { *(.text); } .data : { *(.data); *(.bss); *(.rodata); } _heap = ALIGN(4); } 

OUTPUT_FORMAT(binary) بعدم وضع هذا في ملف ELF (أو PE ، إلخ.). يجب على الرابط إعادة تعيين الرمز النظيف فقط. ملف COM هو مجرد رمز نظيف ، أي نعطي الأمر للرابط لإنشاء ملف COM!

قلت أنه يتم تحميل ملفات COM إلى 0x0100 . ينقل السطر الرابع الثنائيات هناك. لا يزال البايت الأول من ملف COM هو البايت الأول من التعليمات البرمجية ، ولكن سيتم تشغيله من إزاحة الذاكرة هذه.

ثم تتبع جميع الأقسام: text (برنامج) ، data ( data ثابتة) ، bss (بيانات بدون تهيئة أولية) ، rodata (سلاسل). أخيرًا ، أقوم بتمييز نهاية الثنائي برمز _heap . سيكون هذا مفيدًا لاحقًا عند كتابة sbrk() عندما ننتهي من "Hello، World". أشرت إلى محاذاة _heap مع 4 بايت.

أوشكت على الانتهاء.

إطلاق البرنامج


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

هناك خيار STARTUP في برنامج نصي الرابط لمثل هذه الأشياء ، ولكن من أجل البساطة ، سنقوم بتطبيقه مباشرة في البرنامج. عادة ما تسمى هذه الأشياء crt0.o أو Boot.o ، في حالة Boot.o في مكان ما. يجب أن يبدأ الكود الخاص بنا بهذا المجمع المدمج ، قبل أي شوائب وما شابه. سيقوم DOS بمعظم التثبيت بالنسبة لنا ، نحتاج فقط للذهاب إلى نقطة الدخول.

 asm (".code16gcc\n" "call dosmain\n" "mov $0x4C, %ah\n" "int $0x21\n"); 

.code16gcc يخبر المجمّع بأننا سنعمل في الوضع الحقيقي ، بحيث يقوم بإجراء التكوين الصحيح. على الرغم من الاسم ، لن ينتج كود 16 بت! أولاً ، dosmain وظيفة dosmain ، التي dosmain سابقًا. ثم يخبر DOS باستخدام وظيفة 0x4C ("ينتهي برمز الإرجاع") أننا قد انتهينا بتمرير رمز الخروج إلى سجل 1 بايت (تم تعيينه بالفعل بواسطة وظيفة dosmain ). هذا المجمع المدمج volatile تلقائيًا لأنه لا يحتوي على مدخلات ومخرجات.

معًا


هنا البرنامج بأكمله في C.

 asm (".code16gcc\n" "call dosmain\n" "mov $0x4C,%ah\n" "int $0x21\n"); static void print(char *string) { asm volatile ("mov $0x09, %%ah\n" "int $0x21\n" : /* no output */ : "d"(string) : "ah"); } int dosmain(void) { print("Hello, World!\n$"); return 0; } 

لن أكرر com.ld هنا هو التحدي الخليجي.

 gcc -std=gnu99 -Os -nostdlib -m32 -march=i386 -ffreestanding \ -o hello.com -Wl,--nmagic,--script=com.ld hello.c 

واختباره في DOSBox:



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

تخصيص الذاكرة


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

كالعادة في x86 ، يكون البرنامج والأقسام في الذاكرة السفلية (0x0100 في هذه الحالة) ، والمكدس في الذاكرة العليا (في حالتنا ، في منطقة 0xffff). في الأنظمة الشبيهة بـ Unix ، تأتي الذاكرة التي يتم إرجاعها بواسطة malloc() من مكانين: sbrk() و mmap() . ما sbrk() هو تخصيص الذاكرة فقط فوق مقاطع البرنامج / البيانات ، مما sbrk() "لأعلى" نحو المكدس. sbrk() كل مكالمة إلى sbrk() هذه المساحة (أو تتركها تمامًا). ستتم إدارة هذه الذاكرة عن طريق malloc() وما شابه.

فيما يلي كيفية تطبيق sbrk() في برنامج COM. يرجى ملاحظة أنك تحتاج إلى تحديد size_t الخاص بك ، لأنه ليس لدينا مكتبة قياسية.

 typedef unsigned short size_t; extern char _heap; static char *hbreak = &_heap; static void *sbrk(size_t size) { char *ptr = hbreak; hbreak += size; return ptr; } 

يقوم ببساطة بتعيين المؤشر على _heap حسب الحاجة. سوف يكون sbrk() أكثر ذكاءً sbrk() أيضًا مع المحاذاة.

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

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


All Articles