
لذلك ، الحق في هذه النقطة. سنكتب تحت Linux ، على NASM وباستخدام QEMU. هذا سهل التثبيت ، لذلك تخطي هذه الخطوة.
من المفهوم أن القارئ على دراية بجملة NASM على الأقل في المستوى الأساسي (ومع ذلك ، لن يكون هناك أي شيء معقد بشكل خاص هنا) ويفهم ماهية السجلات.
النظرية الأساسية
أول شيء يبدأ تشغيل المعالج عند تشغيل الكمبيوتر هو رمز BIOS (أو UEFI ، لكن هنا سأتحدث فقط عن BIOS) ، وهو "سلكي" في ذاكرة اللوحة الأم (على وجه التحديد ، عند 0xFFFFFFF0).
بعد تشغيل BIOS مباشرة ، يبدأ الاختبار الذاتي لبدء التشغيل (POST) - الاختبار الذاتي بعد التشغيل. يقوم نظام BIOS بالتحقق من صحة الذاكرة ، والكشف عن الأجهزة المتصلة وتهيئتها ، والتحقق من السجلات ، وتحديد حجم الذاكرة ، وما إلى ذلك.
الخطوة التالية هي تحديد قرص التمهيد الذي يمكنك منه تشغيل نظام التشغيل. قرص التمهيد هو قرص (أو أي محرك أقراص آخر) يحتوي على آخر 2 بايت من القطاع الأول (القطاع الأول يعني أول 512 بايت من محرك الأقراص ، لأن قطاع واحد = 512 بايت) هو 55 و AA (بتنسيق سداسي عشري). بمجرد العثور على قرص تمهيد ، سيقوم BIOS بتحميل أول 512 بايت له في ذاكرة الوصول العشوائي على العنوان 0x7c00 ونقل التحكم إلى المعالج على هذا العنوان.
بالطبع ، في هذه 512 بايت لن تعمل لتناسب نظام التشغيل كامل. لذلك ، عادةً ما يتم في هذا القطاع وضع المُحمل الأساسي ، الذي يقوم بتحميل رمز نظام التشغيل الرئيسي في ذاكرة الوصول العشوائي وينقل التحكم فيه.
منذ البداية ، كان المعالج يعمل في الوضع الحقيقي (= وضع 16 بت). هذا يعني أنه لا يمكنه العمل إلا مع البيانات ذات 16 بت ويستخدم عنونة الذاكرة المقسمة ، ويمكنه فقط معالجة 1 ميغابايت من الذاكرة. لكننا لن نستخدم الثانية هنا. توضح الصورة أدناه حالة ذاكرة الوصول العشوائي (RAM) عند نقل التحكم إلى الرمز الخاص بنا (يتم التقاط الصورة من هنا ).

آخر ما أقوله قبل الجزء العملي هو الانقطاعات. المقاطعة هي إشارة خاصة (على سبيل المثال ، من جهاز إدخال ، مثل لوحة المفاتيح أو الماوس) إلى معالج يقول إنه من الضروري مقاطعة تنفيذ التعليمات البرمجية الحالية على الفور وتنفيذ رمز معالج المقاطعة. توجد جميع عناوين معالجات المقاطعة في جدول واصف المقاطعة (IDT) في الذاكرة الرئيسية. كل مقاطعة لها معالج المقاطعة الخاصة بها. على سبيل المثال ، عند الضغط على مفتاح لوحة المفاتيح ، يتم استدعاء المقاطعة ، ويتوقف المعالج ، ويتذكر عنوان التعليمة التي تمت مقاطعتها ، ويحفظ جميع قيم سجلاته (في الحزمة) ، ويستمر في تنفيذ معالج المقاطعة. بمجرد انتهاء التنفيذ ، يستعيد المعالج قيم السجلات ويعود إلى التعليمات التي تمت مقاطعتها ويستمر في التنفيذ.
على سبيل المثال ، من أجل عرض شيء ما على الشاشة ، يستخدم BIOS المقاطعة 0x10 (التنسيق السداسي العشري) ، والمقاطعة 0x16 لانتظار الضغط على المفتاح. في الواقع ، كل هذه المقاطعات التي سنحتاجها هنا.
أيضًا ، لكل مقاطعة وظيفتها الفرعية التي تحدد خصوصية سلوكها. لعرض شيء ما في تنسيق النص (!) ، تحتاج إلى إدخال القيمة 0x0e في سجل AH. بالإضافة إلى ذلك ، المقاطعات لها المعلمات الخاصة بها. يأخذ 0x10 القيم من ah (يحدد وظيفة فرعية محددة) و al (الحرف الذي سيتم طباعته). بهذه الطريقة
mov ah, 0x0e mov al, 'x' int 0x10
يعرض الحرف "س". تأخذ 0x16 القيمة من ah (دالة فرعية محددة) وتحمل قيمة المفتاح الذي تم إدخاله في التسجيل al. سوف نستخدم الدالة 0x0.
الجزء العملي
لنبدأ برمز المساعد. سنحتاج إلى وظيفة مقارنة سطرين ووظيفة عرض خط على الشاشة. حاولت وصف تشغيل هذه الوظائف في التعليقات بشكل واضح قدر الإمكان.
str_compare.asm:
compare_strs_si_bx: push si ; push bx push ax comp: mov ah, [bx] ; , cmp [si], ah ; ah jne not_equal ; , cmp byte [si], 0 ; , je first_zero ; inc si ; bx si inc bx jmp comp ; first_zero: cmp byte [bx], 0 ; bx != 0, , jne not_equal ; , not_equal mov cx, 1 ; , cx = 1 pop si ; pop bx pop ax ret ; not_equal: mov cx, 0 ; , cx = 0 pop si ; pop bx pop ax ret ;
تقبل الدالة سجلات SI و BX كمعلمات. إذا كانت الخطوط متساوية ، فسيتم تعيين CX على 1 ، وإلا 0.
تجدر الإشارة أيضًا إلى أن السجلات AX و BX و CX و DX مقسمة إلى جزأين أحادي البايتتين: AH و BH و CH و DH للبايت العالية و AL و BL و CL و DL للبايتات المنخفضة.
في البداية ، من المفترض أن في bx و si هناك مؤشرات (!) (أي ، يخزن العنوان في الذاكرة) إلى بعض العناوين في الذاكرة التي توجد فيها بداية السطر. ستأخذ العملية [bx] مؤشرًا من bx ، وستنتقل إلى هذا العنوان وستأخذ بعض القيمة من هناك. يعني inc bx أن المؤشر الآن سوف يشير إلى العنوان مباشرة بعد العنوان الأصلي.
print_string.asm:
print_string_si: push ax ; ax mov ah, 0x0e ; ah 0x0e, call print_next_char ; pop ax ; ax ret ; print_next_char: mov al, [si] ; cmp al, 0 ; si jz if_zero ; int 0x10 ; al inc si ; jmp print_next_char ; ... if_zero: ret
كمعلمة ، تأخذ الدالة تسجيل SI وتطبع البايت بايت سلسلة.
الآن دعنا ننتقل إلى الرمز الرئيسي. أولاً ، دعونا نحدد جميع المتغيرات (سيكون هذا الرمز في نهاية الملف):
; 0x0d - , 0xa - wrong_command: db "Wrong command!", 0x0d, 0xa, 0 greetings: db "The OS is on. Type 'help' for commands", 0x0d, 0xa, 0xa, 0 help_desc: db "Here's nothing to show yet. But soon...", 0x0d, 0xa, 0 goodbye: db 0x0d, 0xa, "Goodbye!", 0x0d, 0xa, 0 prompt: db ">", 0 new_line: db 0x0d, 0xa, 0 help_command: db "help", 0 input: times 64 db 0 ; - 64 times 510 - ($-$$) db 0 dw 0xaa55
تنقل حرف الإرجاع حرف النقل إلى الحافة اليسرى من الشاشة ، أي إلى بداية السطر.
input: times 64 db 0
يعني أننا نخصص 64 بايت تحت المخزن المؤقت للإدخال ونملأها بالأصفار.
هناك حاجة لبقية المتغيرات لعرض بعض المعلومات ، وفهم الكود الذي ستفهمه جميعًا.
times 510 - ($-$$) db 0 dw 0xaa55
يعني أننا حددنا حجم ملف الإخراج (مع الامتداد .bin) بشكل صريح إلى 512 بايت ، وملء أول 510 بايت بالأصفار (وبطبيعة الحال ، تمتلئ قبل تنفيذ الشفرة بأكملها) ، واثنين من البايتات الأخيرتين بنفس البايت "السحري" 55 و AA . $ تعني عنوان التعليمة الحالية ، و $ هو عنوان التعليمة الأولى في الكود.
دعنا ننتقل إلى الرمز الفعلي:
org 0x7c00 ; (1) bits 16 ; (2) jmp start ; start %include "print_string.asm" ; %include "str_compare.asm" ; ==================================================== start: mov ah, 0x00 ; (3) mov al, 0x03 int 0x10 mov sp, 0x7c00 ; (4) mov si, greetings ; call print_string_si ; mainloop
(1). يوضح هذا الأمر لـ NASM أننا ننفذ التعليمات البرمجية بدءًا من 0x7c00. هذا يسمح لها بالتحيز التلقائي لجميع العناوين المتعلقة بهذا العنوان حتى لا نقوم بذلك صراحة.
(2). يرشد هذا الأمر NASM بأننا نعمل في وضع 16 بت.
(3). عند إطلاقها ، تقوم QEMU بطباعة الكثير من المعلومات التي لا نحتاج إليها. للقيام بذلك ، قم بتعيين ah 0x00 ، على al 0x03 واتصل بالرقم 0x10 لمسح شاشة كل شيء.
(4). لحفظ السجلات على المكدس ، يجب عليك تحديد العنوان الذي سيتم تحديد موقع رأسه باستخدام مؤشر مكدس SP. سوف يشير SP إلى المساحة في الذاكرة التي سيتم فيها كتابة القيمة التالية. أضف القيمة إلى المكدس - تنخفض SP الذاكرة بمقدار 2 بايت (نظرًا لأننا في الوضع الفعلي ، حيث تكون جميع معاملات التسجيل 16 بت ، أي بايت مزدوج ،). حددنا 0x7c00 ، وبالتالي سيتم تخزين القيم الموجودة في المجموعة بجوار رمزنا في الذاكرة. مرة أخرى - المكدس ينمو (!). هذا يعني أنه كلما زادت القيم الموجودة على المكدس ، قل مؤشر الذاكرة في مكدس SP.
mainloop: mov si, prompt ; call print_string_si call get_input ; jmp mainloop ; mainloop...
الحلقة الرئيسية. هنا ، مع كل تكرار ، نطبع الحرف ">" ، وبعد ذلك نسميها الدالة get_input ، والتي تنفذ العمل مع مقاطعة لوحة المفاتيح.
get_input: mov bx, 0 ; bx input_processing: mov ah, 0x0 ; 0x16 int 0x16 ; ASCII cmp al, 0x0d ; enter je check_the_input ; , , ; cmp al, 0x8 ; backspace je backspace_pressed cmp al, 0x3 ; ctrl+c je stop_cpu mov ah, 0x0e ; - ; int 0x10 mov [input+bx], al ; inc bx ; cmp bx, 64 ; input je check_the_input ; , enter jmp input_processing ;
(1) يعني [input + bx] أننا نأخذ عنوان بداية إدخال مخزن الإدخال المؤقت ونضيفه إلى bx ، أي أننا نصل إلى bx + العنصر الأول من المخزن المؤقت.
stop_cpu: mov si, goodbye ; call print_string_si jmp $ ; ; $
كل شيء بسيط هنا - إذا ضغطت على Ctrl + C ، فإن الكمبيوتر يؤدي وظيفة jmp $ إلى ما لا نهاية.
backspace_pressed: cmp bx, 0 ; backspace , input , je input_processing ; mov ah, 0x0e ; backspace. , int 0x10 ; , mov al, ' ' ; , int 0x10 ; mov al, 0x8 ; int 0x10 ; backspace dec bx mov byte [input+bx], 0 ; input jmp input_processing ;
حتى لا تمحو الحرف ">" عند الضغط على مسافة للخلف ، نتحقق مما إذا كان الإدخال فارغًا. إذا لم يكن كذلك ، فلا تفعل شيئًا.
check_the_input: inc bx mov byte [input+bx], 0 ; , ; ( '\0' ) mov si, new_line ; call print_string_si mov si, help_command ; si help mov bx, input ; bx - call compare_strs_si_bx ; si bx ( help) cmp cx, 1 ; compare_strs_si_bx cx 1, ; je equal_help ; => ; help jmp equal_to_nothing ; , "Wrong command!"
هنا ، أعتقد أن كل شيء واضح من التعليقات.
equal_help: mov si, help_desc call print_string_si jmp done equal_to_nothing: mov si, wrong_command call print_string_si jmp done
بناءً على ما تم إدخاله ، نعرض إما نص المتغير help_desc أو نص المتغير wrong_command.
; done input done: cmp bx, 0 ; input je exit ; , mainloop dec bx ; , mov byte [input+bx], 0 jmp done ; exit: ret
في الواقع ، الكود كله هو:
الفوري.
org 0x7c00 bits 16 jmp start ; start %include "print_string.asm" %include "str_compare.asm" ; ==================================================== start: cli ; , ; mov ah, 0x00 ; mov al, 0x03 int 0x10 mov sp, 0x7c00 ; mov si, greetings ; call print_string_si ; mainloop mainloop: mov si, prompt ; call print_string_si call get_input ; jmp mainloop ; mainloop... get_input: mov bx, 0 ; bx input_processing: mov ah, 0x0 ; 0x16 int 0x16 ; ASCII cmp al, 0x0d ; enter je check_the_input ; , , ; cmp al, 0x8 ; backspace je backspace_pressed cmp al, 0x3 ; ctrl+c je stop_cpu mov ah, 0x0e ; - ; int 0x10 mov [input+bx], al ; inc bx ; cmp bx, 64 ; input je check_the_input ; , enter jmp input_processing ; stop_cpu: mov si, goodbye ; call print_string_si jmp $ ; ; $ backspace_pressed: cmp bx, 0 ; backspace , input , je input_processing ; mov ah, 0x0e ; backspace. , int 0x10 ; , mov al, ' ' ; , int 0x10 ; mov al, 0x8 ; int 0x10 ; backspace dec bx mov byte [input+bx], 0 ; input jmp input_processing ; check_the_input: inc bx mov byte [input+bx], 0 ; , ; ( '\0' ) mov si, new_line ; call print_string_si mov si, help_command ; si help mov bx, input ; bx - call compare_strs_si_bx ; si bx ( help) cmp cx, 1 ; compare_strs_si_bx cx 1, ; je equal_help ; => ; help jmp equal_to_nothing ; , "Wrong command!" equal_help: mov si, help_desc call print_string_si jmp done equal_to_nothing: mov si, wrong_command call print_string_si jmp done ; done input done: cmp bx, 0 ; input je exit ; , mainloop dec bx ; , mov byte [input+bx], 0 jmp done ; exit: ret ; 0x0d - , 0xa - wrong_command: db "Wrong command!", 0x0d, 0xa, 0 greetings: db "The OS is on. Type 'help' for commands", 0x0d, 0xa, 0xa, 0 help_desc: db "Here's nothing to show yet. But soon...", 0x0d, 0xa, 0 goodbye: db 0x0d, 0xa, "Goodbye!", 0x0d, 0xa, 0 prompt: db ">", 0 new_line: db 0x0d, 0xa, 0 help_command: db "help", 0 input: times 64 db 0 ; - 64 times 510 - ($-$$) db 0 dw 0xaa55
لتجميع كل هذا ، أدخل الأمر:
nasm -f bin prompt.asm -o bootloader.bin
ونحن نحصل على ثنائي مع رمز لدينا في الإخراج. الآن قم بتشغيل محاكي QEMU باستخدام هذا الملف (- يسمح لك stdio -monitor بعرض قيمة التسجيل في أي وقت باستخدام أمر print $ reg):
qemu-system-i386 bootloader.bin -monitor stdio
ونحن نحصل على الإخراج:
