٣- محمل الإقلاع bootloader

  28 Jul 2015


ستتعلم في هذا الفصل كيف تكتب أول قطاع إقلاع خاص بك، وهو قطاع إقلاع بسيط يقوم بتحميل رقم مميز 0x1234 إلى المسجل AX ثم يتوقف باستخدام التعليمة HLT:

MOV AX, 0x1234   # load 0x1234 into AX.
CLI              # disable Interrupts.
HLT              # halt the processor.

التعليمةCLI تقوم بتعطيل المقاطعات. سنتحدث عن طلبات المقاطعة بالتفصيل في مقال لاحق. أما التعليمةHLT فهي تقوم بتوقيف المعالج حتى يأتي طلب مقاطعة. ولأن طلبات المقاطعة قد تم تعطيلها، فإن التعليمة HLT هنا ستتسبب في توقف المعالج نهائياً.

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

.intel_syntax noprefix
.code16
.text

السطر الأول .intel_syntax noprefix يحدد الـ syntax الذي نستخدمه في كتابة البرنامج. هناك أكثر من syntax للغة التجميع الخاصة بمعالجات intel. الـ syntax الذي نستخدمه هنا هو نفسه المستخدم في المستندات الخاصة بإنتل. أما .code16 فكأننا نقول للمجمع أن هذا البرنامج هو برنامج ١٦-بت، أي أنه برنامج للمعالج 8088 (أو 80386 عندما يعمل في الوضع الحقيقي كما ذكرنا سابقا). السطر الأخير يوضح للمترجم أن ما سيتم كتابته لاحقا هو متن البرنامج text، وليس بيانات data.

عند تحميل قطاع الإقلاع إلى العنوان 0x7C00، تقوم البايوس بفحص آخر ٢ بايت في القطاع، أي البايت رقم ٥١٠ ورقم ٥١١. يجب أن يحتويا على 0x55 و0xAA بالترتيب. تسمى تلك الأرقام بـ BIOS Boot Signature، ويجب أن يحتوي القطاع عليهما في آخر ٢ بايت وإلا فإنه يعتبر قطاع تالف وسترفض البايوس الإقلاع منه.

وبالتالي نحتاج الآن لكي نقول للمجمع أن البرنامج يجب أن يخرج في ملف حجمه ٥١٢ بايت (حجم القطاع)، وأن آخر ٢ بايت هما 0x55 و0xAA. يمكن استخدام التوجيه .org (يعد السطر الخاص بالتعليمة HLT) لعمل ذلك:

.org 510, 0x00   # move to 510 and fill the spaces with 0x00.
.word 0xAA55     # BIOS boot signature.

وهذا يعني أن المجمع سيقوم بملأ البايتات بعد التعليمة HLT بالرقم 0x00، حتى يصل عدد البايتات الإجمالية إلى ٥١٠، ثم بعد ذلك يقوم بوضع الكلمة 0xAA55. وبالتالي يصبح المجموع ٥١٢.

هذا هو شكل البرنامج كاملاً:

.intel_syntax noprefix
.code16
.text

MOV AX, 0x1234   # load 0x1234 into AX.
CLI              # disable Interrupts.
HLT              # halt the processor.

.org 510, 0x00   # move to 510 and fill the spaces with 0x00.
.word 0xAA55     # BIOS boot signature.

قم بحفظ البرنامج في ملف نصي باسم bootloader.s. ثم افتح الطرفية وانتقل إلى الدليل الموجود فيه bootloader.s. يستحسن إنشاء دليل جديد ووضع bootloader.s فيه. من الطرفية قم بكتابة الأمر التالي:

$ as -o bootloader.o bootloader.s

يقوم ذلك الأمر بتجميع البرنامج bootloader.s وإخراج الـ object file المسمى bootloader.o. الخطوة التالية هي إدخال bootloader.o إلى الرابط linker. الرابط هو عبارة عن برنامج يقوم بتجميع مخرجات المترجم والمكتبات وما إلى ذلك وربطهم سويا. في حالتنا هذه سنطلب من الرابط أيضاً إخراج الملف المخرج كـbinary file خالص بدلاً من تنسيق ELF، ويتم ذلك بكتابة هذا الأمر في الطرفية:

$ ld --oformat=binary -o bootloader.bin -e 0 bootloader.o

يقوم هذا الأمر بتشغيل الرابط، والذي سيقوم بربط الملف bootloader.o وتحويله من تنسيق ELF إلى تنسيق binary. أما -e فهي تستخدم لتعريف الرابط بعنوان أول تعليمة سيتم تنفيذها داخل الملف (كإزاحة بالنسبة لبداية الملف). الآن يمكنك استخدام ls من أجل رؤية التطورات:

$ ls -l

قام الرابط بإخراج bootloader.bin، وحجمه ٥١٢ بايت كما وصفنا للأسمبلر. نريد الآن رؤية محتويات هذا الملف، يمكننا استخدام الأمر hexedit من أجل ذلك:

$ hexedit bootloader.bin

الأرقام الموجودة في أول ٥ بايت هما في الحقيقة كود الهدف أو الأوبكود opcode الذي أخرجه المعالج assembler للتعليمات MOV وCLI وHLT. الرقم 0xB8 في أول بايت هو الأوبكود الخاص بالتعليمة MOV AX, some value، ويتبعه الـ value التي نريد وضعها في AX، وهي 0x1234 حيث يكون 0x34 هو البايت الأول (الأقل قيمة مكانية) والبايت 0x12 هو البايت الثاني (اﻷعلى قيمة مكانية). أما 0xFA فهي التعليمة CLI، والرقم 0xF4 هي التعليمة HLT. بعد ذلك قام الأسمبلر بملء الملف بالأصفار 0x00 حتى البايت 0x1FE (٥١٠) حيث قام بوضع 0x55 في البايت 0x1FE و0xAA في البايت 0x1FF وهو البايت الأخير.

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

$ qemu-system-i386 -hda bootloader.bin

يقوم هذا الأمر بتشغيل qemu وقراءة محمل الإقلاع من bootloader.bin. في الحقيقة فإن -hda تستخدم من أجل تحديد صورة قرص صلب hard disk image كي يستخدمها qemu كقرص صلب للماكينة الوهمية virtual machine. في حالتنا هذه يمكن اعتبار bootloader.bin بإنه قرص صلب يحتوي على قطاع واحد فقط، وهو قطاع الإقلاع. بعد تشغيل qemu تظهر الشاشة الآتية:

لقد قامت البايوس من الإقلاع من الهارد ديسك، فتم تحميل البرنامج الخاص بنا إلى العنوان 0x7C00 وتشغيله، فقام البرنامج بوضع 0x1234 في AX ثم قام بعد ذلك بتوقيف المعالج تماما. نريد الآن أن نشاهد محتويات المسجل AX. قم بالضغط على Alt+Ctrl+2 لفتح Qemu Console، ثم اكتب الأمر info registers. يقوم qemu بطباعة كل المسجلات على الشاشة. قم بالضغط على Ctrl+Up لتمرير الشاشة إلى الأعلى، حتى تتمكن من رؤية محتويات AX.

مبروك. لقد انتهيت الآن من أول محمل إقلاع في سلسلتنا.


محمل إقلاع Hello World:

تعلمنا في الفصل السابق كيف نكتب محمل إقلاع للكمبيوتر الشخصي. الخطوة التالية هي جعل محمل الإقلاع يقوم بكتابة Hello World على شاشة العرض.

في الكمبيوتر IBM PC/XT يمكن لبطاقة العرض CGA أو (Color graphics adapter) أن تعمل في عدد مختلف من اﻷوضاع. عند بدء الكمبيوتر تكون بطاقة العرض في وضع النصوص text mode، أي انها تعمل بشكل أساسي لإخراج نصوص على الشاشة. الوضع البديل هو وضع الرسوم graphics mode والذي تقوم فيه بإخراج رسومات بدلاً من نصوص. عند بدء الكمبيوتر تقوم البايوس بإجراء تمهيد initialization لبطاقة العرض CGA وتحويلها إلى وضع النصوص.

في وضع النصوص تنقسم الشاشة إلى ٨٠ عموداً و٢٥ صفاً. أي ان عدد الخلايا يساوي ٢٠٠٠ خلية، وكل خلية تمثل محرفاً. الهدف هو كتابة !Hello World في الصف الأول، بدءاً من العمود الأول وانتهاءاً عند العمود الثاني عشر:

</td> 0</td> 1</td> 2</td> 3</td> 4</td> 5</td> 6</td> 7</td> 8</td> 9</td> 10</td> 11</td> 12</td> ...</td> 79</td> </tr>
0</td> H</td> e</td> l</td> l</td> o</td> </td> W</td> o</td> r</td> l</td> d</td> !</td> </td> </td> </td> </tr>
1</td> </td> </td> </td> </td> </td> </td> </td> </td> </td> </td> </td> </td> </td> </td> </td> </tr>
.
.
.</td>
</td> </td> </td> </td> </td> </td> </td> </td> </td> </td> </td> </td> </td> </td> </td> </tr>
24</td> </td> </td> </td> </td> </td> </td> </td> </td> </td> </td> </td> </td> </td> </td> </td> </tr> </table> </center>
</div> ذكرنا مسبقاً أن ذاكرة العرض Video RAM تظهر من العنوان 0xB8000 إلى العنوان 0xBFFFF. كل محرف على الشاشة يتم تمثيله بـ ٢ بايت، بدءاً من 0xB8000. أي أن الكلمة عند 0xB8000-0xB8001 هي تمثيل للمحرف الأول في الصف الأول، والكلمة عند 0xB8002-0xB8003 هي تمثل للمحرف الثاني في الصف الأول، وهكذا. وبالتالي مجموع البايتات المستخدم لوصف الشاشة هما ٤٠٠٠ بايت، بدءاً من 0xB8000. كل محرف يتمثل بـ ٢ بايت، البايت الأول هو الـ ASCII code الخاص بالمحرف، والبايت التاني هو صفة المحرف attribute (أي لونه). يصف البايت التاني لون المحرف ولون الخلفية التي وراءه. يتم تمثيل لون المحرف بـ ٤ بت، ولون الخلفية بـ ٣ بت، أما البت المتبقى فيستخدم لتفعيل الوميض blinking. ولأن لون المحرف يتم تمثيله بـ ٤ بت فإن عدد الألوان الممكنة هو ١٦ لون، ولذلك يسمى هذا الوضع (وضع ١٦ لون). ترقم الألوان من صفر إلى ١٥. لون المحرف ممكن أن يكون واحدا من الألوان الموجودة في الجدول أدناه. أما لون الخلفية يمكن فقط أن يكون واحد من أول ثمانية ألوان في الجدول.
التسلسل اللون التسلسل اللون التسلسل اللون التسلسل اللون
0 أسود 4 أحمر 8 رصاصي 12 أحمر
فاتح
1 أزرق 5 بنفسجي 9 أزرق
فاتح
13 بنفسجي
فاتح
2 أخضر 6 بني 10 أخضر
فاتح
14 أصفر
3 سماوي 7 رصاصي
فاتح
11 سماوي
فاتح
15 أبيض

Attributes Byte:
Bits 0..3 : Foreground Color
Bits 4..6 : Background Color
Bit 7: 0 = No Blinking, 1 = Enable Blinking
نريد رسم الحروف بلون خلفية أزرق (١) ولون الحروف أبيض (١٥)، وبالتالي تكون الـ bits كلها واحد ماعدا bit5 وbit6 وbit7، أي أن الـ attribute سيكون مساويا لـ 0x1F. يوضح الجدول أدناه شكل الـ Video RAM بعد الكتابة:
العنوان القيمة المعنى الصف المقابل العمود المقابل
0xB8000 'H' الحرف 'H' الصف الأول العمود الأول
0xB8001 0x1F خلفية زرقاء وأمامية بيضاء
0xB8002 'e' الحرف 'e' الصف الأول العمود الثاني
0xB8003 0x1F خلفية زرقاء وأمامية بيضاء
0xB8004 'l' الحرف 'l' الصف الأول العمود الثالث
0xB8005 0x1F خلفية زرقاء وأمامية بيضاء
0xB8006 'l' الحرف 'l' الصف الأول العمود الرابع
0xB8007 0x1F خلفية زرقاء وأمامية بيضاء
0xB8008 'o' الحرف 'o' الصف الأول العمود الخامس
0xB8009 0x1F خلفية زرقاء وأمامية بيضاء
0xB800A ' ' مسافة الصف الأول العمود السادس
0xB800B 0x1F خلفية زرقاء وأمامية بيضاء
0xB800C 'W' الحرف 'W' الصف الأول العمود السابع
0xB800D 0x1F خلفية زرقاء وأمامية بيضاء
0xB800E 'o' الحرف 'o' الصف الأول العمود الثامن
0xB800F 0x1F خلفية زرقاء وأمامية بيضاء
0xB8010 'r' الحرف 'r' الصف الأول العمود التاسع
0xB8011 0x1F خلفية زرقاء وأمامية بيضاء
0xB8012 'l' الحرف 'l' الصف الأول العمود العاشر
0xB8013 0x1F خلفية زرقاء وأمامية بيضاء
0xB8014 'd' الحرف 'd' الصف الأول العمود الحادي عشر
0xB8015 0x1F خلفية زرقاء وأمامية بيضاء
0xB8016 '!' الحرف '!' الصف الأول العمود الثاني عشر
0xB8017 0x1F خلفية زرقاء وأمامية بيضاء

إذن فإن مهمة محرك الإقلاع بوضوح هي نقل البايتات الموضحة في العمود الثاني من الجدول أعلاه، إلى العناوين الموضحة بالعمود الأول. ولعمل ذلك يجب أولا تحميل المسجل DS بالقيمة 0xB800 لكي يتسنى لنا إرسال البيانات إلى الـ memory segment الذي يبدأ من 0xB8000:
MOV AX, 0xB800              # move 0xB800 to AX
MOV DS, AX                  # set data segment to 0xB800
بعد ذلك يمكن نقل البايتات مباشرة:
MOV BYTE PTR [0x0000], 'H'  # move 'H'  to 0xB800:0x0000 (0xB8000)
MOV BYTE PTR [0x0001], 0x1F # move 0x1F to 0xB800:0x0001 (0xB8001)
MOV BYTE PTR [0x0002], 'e'  # move 'e'  to 0xB800:0x0002 (0xB8002)
MOV BYTE PTR [0x0003], 0x1F # move 0x1F to 0xB800:0x0003 (0xB8003)
MOV BYTE PTR [0x0004], 'l'  # move 'l'  to 0xB800:0x0004 (0xB8004)
MOV BYTE PTR [0x0005], 0x1F # move 0x1F to 0xB800:0x0005 (0xB8005)
MOV BYTE PTR [0x0006], 'l'  # move 'l'  to 0xB800:0x0006 (0xB8006)
MOV BYTE PTR [0x0007], 0x1F # move 0x1F to 0xB800:0x0007 (0xB8007)
MOV BYTE PTR [0x0008], 'o'  # move 'o'  to 0xB800:0x0008 (0xB8008)
MOV BYTE PTR [0x0009], 0x1F # move 0x1F to 0xB800:0x0009 (0xB8009)
MOV BYTE PTR [0x000A], ' '  # move ' '  to 0xB800:0x000A (0xB800A)
MOV BYTE PTR [0x000B], 0x1F # move 0x1F to 0xB800:0x000B (0xB800B)
MOV BYTE PTR [0x000C], 'W'  # move 'W'  to 0xB800:0x000C (0xB800C)
MOV BYTE PTR [0x000D], 0x1F # move 0x1F to 0xB800:0x000D (0xB800D)
MOV BYTE PTR [0x000E], 'o'  # move 'o'  to 0xB800:0x000E (0xB800E)
MOV BYTE PTR [0x000F], 0x1F # move 0x1F to 0xB800:0x000F (0xB800F)
MOV BYTE PTR [0x0010], 'r'  # move 'r'  to 0xB800:0x0010 (0xB8010)
MOV BYTE PTR [0x0011], 0x1F # move 0x1F to 0xB800:0x0011 (0xB8011)
MOV BYTE PTR [0x0012], 'l'  # move 'l'  to 0xB800:0x0012 (0xB8012)
MOV BYTE PTR [0x0013], 0x1F # move 0x1F to 0xB800:0x0013 (0xB8013)
MOV BYTE PTR [0x0014], 'd'  # move 'd'  to 0xB800:0x0014 (0xB8014)
MOV BYTE PTR [0x0015], 0x1F # move 0x1F to 0xB800:0x0015 (0xB8015)
MOV BYTE PTR [0x0016], '!'  # move '!'  to 0xB800:0x0016 (0xB8016)
MOV BYTE PTR [0x0017], 0x1F # move 0x1F to 0xB800:0x0017 (0xB8017)
وبالتالي يصبح الشكل العام للبرنامج هكذا:
.intel_syntax noprefix
.code16
.text

MOV AX, 0xB800              # move 0xB800 to AX
MOV DS, AX                  # set data segment to 0xB800

MOV BYTE PTR [0x0000], 'H'  # move 'H'  to 0xB800:0x0000 (0xB8000)
MOV BYTE PTR [0x0001], 0x1F # move 0x1F to 0xB800:0x0001 (0xB8001)
MOV BYTE PTR [0x0002], 'e'  # move 'e'  to 0xB800:0x0002 (0xB8002)
MOV BYTE PTR [0x0003], 0x1F # move 0x1F to 0xB800:0x0003 (0xB8003)
MOV BYTE PTR [0x0004], 'l'  # move 'l'  to 0xB800:0x0004 (0xB8004)
MOV BYTE PTR [0x0005], 0x1F # move 0x1F to 0xB800:0x0005 (0xB8005)
MOV BYTE PTR [0x0006], 'l'  # move 'l'  to 0xB800:0x0006 (0xB8006)
MOV BYTE PTR [0x0007], 0x1F # move 0x1F to 0xB800:0x0007 (0xB8007)
MOV BYTE PTR [0x0008], 'o'  # move 'o'  to 0xB800:0x0008 (0xB8008)
MOV BYTE PTR [0x0009], 0x1F # move 0x1F to 0xB800:0x0009 (0xB8009)
MOV BYTE PTR [0x000A], ' '  # move ' '  to 0xB800:0x000A (0xB800A)
MOV BYTE PTR [0x000B], 0x1F # move 0x1F to 0xB800:0x000B (0xB800B)
MOV BYTE PTR [0x000C], 'W'  # move 'W'  to 0xB800:0x000C (0xB800C)
MOV BYTE PTR [0x000D], 0x1F # move 0x1F to 0xB800:0x000D (0xB800D)
MOV BYTE PTR [0x000E], 'o'  # move 'o'  to 0xB800:0x000E (0xB800E)
MOV BYTE PTR [0x000F], 0x1F # move 0x1F to 0xB800:0x000F (0xB800F)
MOV BYTE PTR [0x0010], 'r'  # move 'r'  to 0xB800:0x0010 (0xB8010)
MOV BYTE PTR [0x0011], 0x1F # move 0x1F to 0xB800:0x0011 (0xB8011)
MOV BYTE PTR [0x0012], 'l'  # move 'l'  to 0xB800:0x0012 (0xB8012)
MOV BYTE PTR [0x0013], 0x1F # move 0x1F to 0xB800:0x0013 (0xB8013)
MOV BYTE PTR [0x0014], 'd'  # move 'd'  to 0xB800:0x0014 (0xB8014)
MOV BYTE PTR [0x0015], 0x1F # move 0x1F to 0xB800:0x0015 (0xB8015)
MOV BYTE PTR [0x0016], '!'  # move '!'  to 0xB800:0x0016 (0xB8016)
MOV BYTE PTR [0x0017], 0x1F # move 0x1F to 0xB800:0x0017 (0xB8017)

CLI                         # disable interrupts.
HLT                         # halt the processor.

.org 510, 0x00              # move to 510 and fill spaces with 0x00.
.word 0xAA55                # BIOS boot signature.
قم بإنشاء دليل فارغ واحفظ فيه الملف باسم bootloader.s. الآن نحن جاهزون لتجميع البرنامج وربطه كما فعلنا في الفصل السابق. بدلا من كتابة الأوامر التي كتبناها في الفصل السابق كل مرة، يمكننا عمل Makefile بالأوامر التي نريد تنفيذها:
all:
	as -o bootloader.o bootloader.s
	ld --oformat=binary -o bootloader.bin -e 0 bootloader.o
	qemu-system-i386 -hda bootloader.bin
قم بحفظ الملف باسم Makefile في نفس الدليل. قم بتشغيل الطرفية وانتقل إلى الدليل الذي قمت بإنشائه، ثم قم بكتابة الأمر التالي في الطرفية والذي يقوم بتنفيذ الـ Makefile، أي تجميع الملف bootloader.s وإخراج bootloader.bin ثم تشغيل المحاكي qemu.
$ make
بعد أن يبدأ qemu في التشغيل ستقوم البايوس بتحميل قطاع الإقلاع الذي كتبناه في التو، ليقوم بطباعة Hello World على الشاشة في الصف الأول.

comments powered by Disqus