Skip to content

أوّلُ نواةٍ تُقلِعُ في QEMU

اقرأْ هذا أوّلًا — صدقُ هذه الصفحةِ يعتمدُ عليه

هذا الدرسُ يقودُك إلى كائنِ ELF٣٢ مُنتَجٍ فعلًا — وقد أُنتِجَ وفُحِصَت ترويستُه أثناءَ كتابةِ الصفحة. أمّا الربطُ والإقلاعُ فلم يُنفَّذا هنا: لا ld.lld ولا nasm ولا qemu-system-i386 في بيئةِ الكتابة —

bash
$ qemu-system-x86_64 --version
qemu-system-x86_64: command not found

فالخطوتانِ ٤ و٥ غيرُ مُتحقَّقٍ منهما ومُعلَّمتانِ كذلك في موضعِهما. والمستودعُ لا يحوي نواةً ولا مُحمِّلَ إقلاع، فالنواةُ ههنا تُشحَنُ في الصفحةِ نفسِها.

ما ستملكُه في نهايةِ الخطوةِ الثالثة: ملفُّ نواة.o — كائنُ ELF٣٢ لـi686 يُصدِّرُ الرمزَ kmain، يكتبُ على شاشةِ VGA النصّيّةِ ثمّ يُسبِتُ المعالج. ما عليكَ أن تُحضِرَه أنت: كعبَ multiboot وسكربتَ الرابط.

الخطوةُ ١ · اكتُبِ النواة

ملفٌّ واحد، نواة.ص:

sad
# نواةٌ صغيرةٌ تكتبُ على شاشةِ VGA النصّيّةِ ثمّ تُسبِتُ المعالج.

@رمز("kernel_ticks") متغير متطاير نبضات = 0

دالة اكتب_خانة(رقم فهرس، رقم حرف، رقم لون)
    متغير خليّة = حرف + (لون * 256)
    اكتب_ذاكرة16(0xB8000 + (فهرس * 2)، خليّة)
نهاية

@رمز("kmain") دالة لا_ترجع نواة()
    اكتب_خانة(0، 83، 0x0A)
    اكتب_خانة(1، 65، 0x0A)
    اكتب_خانة(2، 68، 0x0A)
    نبضات = نبضات + 1
    بينما (صحيح)
        أسبت_المعالج()
    نهاية
نهاية

أربعُ لبناتٍ تستحقُّ الوقوفَ عندها:

  • @رمز("kmain") يمنحُ الدالّةَ رمزَ رابطٍ ASCII مُصدَّرًا بدلَ الاسمِ الداخليِّ المُشوَّه. بدونَه لا يجدُ كعبُ الإقلاعِ ما ينادي.
  • دالة لا_ترجع — المُعدِّلُ يلي «دالة» قبلَ الاسم. ⚠️ والصيغةُ اللاحقةُ دالة نواة() لا_ترجع تُترجَمُ بلا شكوى ولا تُنتِجُ noreturn (مقيسٌ بمقارنةِ الـIR) — فالتزِمِ الترتيبَ المُعلَن.
  • متطاير لصيقةٌ لاحقةٌ تجعلُ كلَّ قراءةٍ وكتابةٍ للمخزنِ volatile.
  • أسبت_المعالج() تُخفَضُ إلى hlt — لا نداءَ مكتبةٍ ولا حلقةَ انتظار.

الخطوةُ ٢ · ترجِمْ وتحقّقْ من الـIR

bash
$ sad-build نواة.ص --حرّ --بلا-رئيسية \
      --هدف=i686-unknown-elf --أظهر-llvm -o نواة.ll

لا مخرَجَ — والرمزُ صفر. وفي الـIR:

llvm
@kernel_ticks = global i64 0

; Function Attrs: noreturn
define void @kmain() #0 {
entry:
  ...
  store volatile i64 %addtmp, ptr @kernel_ticks, align 4
  br label %while_body_1

while_body_1:
  call void asm sideeffect "hlt", ""()
  br label %while_body_1
}

kmain بالاسمِ المطلوب · noreturn حاضرة · store volatile · وhlt تجميعًا مضمَّنًا. وكتابةُ الشاشةِ تظهرُ في الدالّةِ المساعِدة:

llvm
  %addtmp5 = add i64 753664, %shltmp4     ; 753664 = 0xB8000
  %mem.ptr = inttoptr i64 %addtmp5 to ptr
  store volatile i16 %0, ptr %mem.ptr, align 2

--بلا-رئيسية قِيسَ بلا أثرٍ مرصود

العَلَمُ مُعلَنٌ في language-truth/cli_flags.yaml (flag.no_main) ويقبلُه المترجّم (العَلَمُ المجهولُ يُرفَض). لكنّ ترجمةَ هذا البرنامجِ بالعلَمِ وبدونِه أعطت ملفَّي IR متطابقَينِ بصمةً (sha256 نفسُه، ١٤٨٢ سطرًا لكلٍّ)، و@main و@__sad_main باقيانِ في الحالتَين. فلا تعتمدْ عليه لإسقاطِ نقطةِ الدخولِ الافتراضيّة؛ اعتمِدْ على سكربتِ الرابطِ في تعيينِ ENTRY. وهذا مقيسٌ على هذا البرنامجِ بعينِه، لا دعوى عامّةً عن العَلَم.

الخطوةُ ٣ · أنتِجْ كائنَ ELF٣٢

bash
$ sad-build نواة.ص --حرّ --بلا-رئيسية --هدف=i686-unknown-elf -c -o نواة.o

والتحقّقُ من الترويسةِ مباشرةً (بلا objdump، فقد لا يكونُ عندك):

bash
$ python -c "d=open('نواة.o','rb').read(20); print(d[:4], d[4], d[16], d[18])"
b'\x7fELF' 1 1 3

الحقولُ بالترتيب: التوقيعُ · class=1 ⇒ ELF٣٢ · type=1 ⇒ كائنٌ قابلٌ للنقل · machine=3EM_386. هذا هو الناتجُ الذي يُبرهِنُ عليه هذا الدرس.

وللمعدنِ ٦٤-بت

--هدف=x86_64-unknown-none-elf يُنتِجُ ELF٦٤/EM_X86_64 (class 2، machine 62) — وقد قِيسَ كذلك. وهو الهدفُ الذي يقبلُ معالجاتِ المقاطعات، بخلافِ i686.

الخطوةُ ٤ · اربِطْ — ⚠️ غيرُ مُتحقَّقٍ منها هنا

نواةُ multiboot تلزمُها ترويسةٌ سحريّةٌ ومكدَّسٌ ابتدائيّ، وكلاهما خارجَ ما تُعبِّرُ عنه ص اليومَ (لا وسمَ مقطعٍ ولا بيانٌ ثنائيٌّ خام). فأحضِرْ كعبًا:

asm
; إقلاع.s — كعبُ multiboot1، يُجمَّعُ بـ`nasm -f elf32`
section .multiboot
    dd 0x1BADB002
    dd 0x00000000
    dd -(0x1BADB002 + 0x00000000)

section .bss
مكدّس_قاع: resb 16384
مكدّس_رأس:

section .text
global _start
extern kmain
_start:
    mov esp, مكدّس_رأس
    call kmain
.توقّف:
    hlt
    jmp .توقّف
ld
/* رابط.ld */
ENTRY(_start)
SECTIONS {
    . = 1M;
    .text   : { *(.multiboot) *(.text) }
    .rodata : { *(.rodata) }
    .data   : { *(.data) }
    .bss    : { *(.bss) *(COMMON) }
    _kernel_end = .;
}
bash
nasm -f elf32 إقلاع.s -o إقلاع.o
ld.lld -m elf_i386 -T رابط.ld إقلاع.o نواة.o -o نواة.elf

⚠️ لم أُنفِّذْ هذا الأمر. لا مُجمِّعَ ولا رابطَ في بيئةِ الكتابة. الكعبُ والسكربتُ نمطانِ قياسيّانِ لـmultiboot1، والذي قِيسَ أنّ نواة.o كائنُ EM_386 يُصدِّرُ kmain — أي أنّه يوافقُ extern kmain أعلاه. ويقبلُ المترجّمُ --سكربت-الرابط=رابط.ld علَمًا صالحًا إن آثرتَ أن يتولّى هو الربط.

الخطوةُ ٥ · أقلِعْ — ⚠️ غيرُ مُتحقَّقٍ منها هنا

bash
qemu-system-i386 -kernel نواة.elf

المتوقَّعُ: SAD بلونٍ أخضرَ في أعلى يسارِ الشاشةِ ثمّ إسباتٌ دائم.

⚠️ لم أُشغِّلْ QEMU — غيرُ مثبَّتٍ في هذه البيئة، والأمرُ في أعلى الصفحة يُثبِتُ ذلك. فإن لم تظهرِ الحروفُ عندَك فابدأْ بكعبِ الإقلاعِ لا بالنواة: نواة.o نفسُه مقيسٌ سليمَ الترويسةِ والرمز.

إذا تعثَّرتَ

العَرَضالسبب المرجَّح
[SEM019] … غير متاحة في الوضع الحرّمدمَجةٌ مستضافة — اقرأْ لماذا
warning: الوضع الحرّ بلا «--هدف»نسيتَ --هدف فوقعتَ على ثالوثِ المضيف
[SEM038] … تخصّ عائلة x86اكتب_منفذ أو نظيرتُها على هدفٍ غيرِ x86
رابطٌ يشكو من رمزٍ عربيّنسيتَ @رمز("...") على الدالّةِ المُصدَّرة
النواةُ صامتةٌ ولا خطأراجِعْ ملاحظةَ BSP: جسرا الإخراجِ والوقتِ كعبانِ خارجَ x86

ثمّ ماذا

اقرأْ ما الذي يمنعُه الوضعُ الحرُّ ولماذا لتفهمَ ما جرى، ثمّ MMIO والمنافذ لتُخرِجَ على المنفذِ التسلسليّ، ثمّ معالجاتِ المقاطعات لتُدخِلَ المؤقّتَ في الحلقة.

مُرخَّص بموجب رخصة MIT