التمهيد نواة لينكس. الجزء الأول

من محمل الإقلاع إلى النواة

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

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

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

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

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

المعرفة المطلوبة

  • فهم كود C
  • فهم كود المجمع (بناء جملة AT&T)

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

لقد بدأت في كتابة هذا الكتاب في أيام Linux 3.18 kernel ، وقد تغير الكثير منذ ذلك الحين. إذا كانت هناك تغييرات ، فسأقوم بتحديث المقالات وفقًا لذلك.

زر الطاقة السحري ، ما هي الخطوة التالية؟


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

يجب أن تحتوي المعالجات 80386 والإصدارات الأحدث على القيم التالية في تسجيلات CPU بعد إعادة التشغيل:

  IP 0xfff0
 محدد CS 0xf000
 قاعدة CS 0xffff0000 

يبدأ المعالج في العمل في الوضع الحقيقي . دعنا نعود قليلاً ونحاول فهم تقسيم الذاكرة في هذا الوضع. الوضع الحقيقي مدعوم على جميع المعالجات المتوافقة مع x86: من 8086 إلى معالجات Intel 64 بت الحديثة. يستخدم المعالج 8086 ناقل عنوان 20 بت ، أي أنه يمكن أن يعمل مع مساحة عنوان 0-0xFFFFF أو 1 . ولكن لديها سجلات 16 بت فقط مع عنوان أقصى 2^16-1 أو 0xffff (64 كيلوبايت).

يلزم تقسيم الذاكرة لاستخدام مساحة العنوان المتاحة بالكامل. تنقسم الذاكرة بالكامل إلى أجزاء صغيرة بحجم ثابت يبلغ 65536 بايت (64 كيلوبايت). نظرًا لأنه باستخدام سجلات 16 بت ، لا يمكننا الوصول إلى الذاكرة التي تزيد عن 64 كيلوبايت ، فقد تم تطوير طريقة بديلة.

يتكون العنوان من جزأين: 1) محدد قطعة بعنوان أساسي. 2) الإزاحة من العنوان الأساسي. في الوضع الحقيقي ، يكون العنوان الأساسي * 16 المقطع * 16 . وبالتالي ، للحصول على العنوان الفعلي في الذاكرة ، تحتاج إلى ضرب جزء من محدد المقطع في 16 وإضافة الإزاحة إليه:

   =   * 16 +  

على سبيل المثال ، إذا كان سجل CS:IP يحتوي على القيمة 0x2000:0x0010 ، فسيكون العنوان الفعلي المقابل على النحو التالي:

 >>> hex((0x2000 << 4) + 0x0010) '0x20010' 

ولكن إذا أخذت محدد الجزء الأكبر والإزاحة 0xffff:0xffff ، فستحصل على العنوان:

 >>> hex((0xffff << 4) + 0xffff) '0x10ffef' 

أي 65520 بايت بعد أول ميغا بايت. نظرًا لأن ميغابايت واحد فقط متاح في الوضع الحقيقي ، يصبح 0x00ffef مع تعطيل خط A20 .

حسنًا ، نحن الآن نعرف القليل عن الوضع الحقيقي والذاكرة التي تعالج في هذا الوضع. دعنا نعود إلى مناقشة قيم التسجيل بعد إعادة التعيين.

يتكون سجل CS من جزأين: محدد مقطع مرئي وعنوان أساسي مخفي. على الرغم من أن العنوان الأساسي يتكون عادة بضرب قيمة محدد المقطع في 16 ، أثناء إعادة تعيين الجهاز ، فإن محدد المقطع في سجل CS هو 0xf000 ، والعنوان الأساسي هو 0xffff0000 . يستخدم المعالج هذا العنوان الأساسي الخاص حتى تتغير CS.

يتكون عنوان البداية بإضافة العنوان الأساسي إلى القيمة في سجل EIP:

 >>> 0xffff0000 + 0xfff0 '0xfffffff0' 

نحصل على 0xfffffff0 ، وهو 16 بايت أقل من 4 غيغابايت. تسمى هذه النقطة متجه إعادة التعيين . هذا هو الموقع في الذاكرة حيث تنتظر وحدة المعالجة المركزية تنفيذ التعليمات الأولى بعد إعادة التعيين: عملية انتقال ( jmp ) ، والتي تشير عادةً إلى نقطة إدخال BIOS. على سبيل المثال ، إذا src/cpu/x86/16bit/reset16.inc نظرة على شفرة المصدر لـ coreboot ( src/cpu/x86/16bit/reset16.inc ) ، فسوف نرى:

  .section ".reset", "ax", %progbits .code16 .globl _start _start: .byte 0xe9 .int _start16bit - ( . + 2 ) ... 

هنا نرى كود jmp (العملية opcode ) ، أي 0xe9 ، وعنوان الوجهة _start16bit - ( . + 2) .

نرى أيضًا أن قسم reset هو 16 بايت ، ويتم 0xfffff0 للتشغيل من العنوان 0xfffff0 ( src/cpu/x86/16bit/reset16.ld ):

 SECTIONS { /* Trigger an error if I have an unuseable start address */ _bogus = ASSERT(_start16bit >= 0xffff0000, "_start16bit too low. Please report."); _ROMTOP = 0xfffffff0; . = _ROMTOP; .reset . : { *(.reset); . = 15; BYTE(0x00); } } 

يبدأ BIOS الآن ؛ بعد تهيئة جهاز BIOS والتحقق منه ، تحتاج إلى العثور على جهاز التمهيد. يتم حفظ ترتيب التمهيد في تكوين BIOS. عند محاولة التمهيد من القرص الصلب ، يحاول BIOS العثور على قطاع التمهيد. على الأقراص المقسمة MBR ، يتم تخزين قطاع التمهيد في أول 446 بايت من القطاع الأول ، حيث يكون كل قطاع 512 بايت. 0x55 من القطاع الأول هما 0x55 و 0xaa . يظهرون BIOS أنه جهاز تمهيد.

على سبيل المثال:

 ; ; :       Intel x86 ; [BITS 16] boot: mov al, '!' mov ah, 0x0e mov bh, 0x00 mov bl, 0x07 int 0x10 jmp $ times 510-($-$$) db 0 db 0x55 db 0xaa 

نجمع وندير:

nasm -f bin boot.nasm && qemu-system-x86_64 boot

يتلقى QEMU أمرًا لاستخدام ثنائي boot الذي أنشأناه للتو كصورة قرص. نظرًا لأن الملف الثنائي الذي تم إنشاؤه أعلاه يلبي متطلبات قطاع التمهيد (بدءًا من 0x7c00 وينتهي بالتسلسل السحري) ، سيأخذ QEMU في الاعتبار الثنائي باعتباره سجل التمهيد الرئيسي (MBR) لصورة القرص.

سترى:



في هذا المثال ، نرى أن الكود يعمل في الوضع الحقيقي 16 بت ويبدأ من العنوان 0x7c00 في الذاكرة. بعد البدء ، يتسبب في مقاطعة 0x10 ، والتي ببساطة طباعة حرف ! ؛ يملأ 510 بايت المتبقية 0xaa و 0x55 .

يمكنك مشاهدة ملف التفريغ الثنائي باستخدام الأداة objdump :

nasm -f bin boot.nasm
objdump -D -b binary -mi386 -Maddr16,data16,intel boot


بالطبع ، في قطاع التمهيد الحقيقي ، هناك رمز لمواصلة عملية التمهيد وجدول التقسيم بدلاً من مجموعة من الأصفار وعلامة تعجب :). من هذه اللحظة ، ينقل BIOS التحكم إلى أداة تحميل التشغيل.

ملاحظة : كما هو موضح أعلاه ، فإن وحدة المعالجة المركزية في الوضع الحقيقي ؛ حيث يكون حساب العنوان الفعلي في الذاكرة كما يلي:

   =   * 16 +  

لدينا فقط تسجيلات للأغراض العامة ذات 16 بت ، والقيمة القصوى لسجل 16 بت هي 0xffff ، لذلك عند القيم الأكبر ستكون النتيجة:

 >>> hex((0xffff * 16) + 0xffff) '0x10ffef' 

حيث 0x10ffef هو 1 + 64 - 16 . يحتوي المعالج 8086 ( المعالج الأول مع الوضع الحقيقي) على سطر عنوان 20 بت. نظرًا لأن 2^20 = 1048576 ، فإن الذاكرة الفعلية المتوفرة هي 1 ميجابايت.

بشكل عام ، عنونة الذاكرة في الوضع الحقيقي هي كما يلي:

  0x00000000 - 0x000003FF - جدول ناقلات المقاطعة في الوضع الحقيقي
 0x00000400 - 0x000004FF - منطقة بيانات BIOS
 0x00000500 - 0x00007BFF - غير مستخدم
 0x00007C00 - 0x00007DFF - أداة تحميل التشغيل الخاصة بنا
 0x00007E00 - 0x0009FFFF - غير مستخدم
 0x000A0000 - 0x000BFFFF - ذاكرة الوصول العشوائي للفيديو (VRAM) 
 0x000B0000 - 0x000B7777 - ذاكرة فيديو أحادية اللون
 0x000B8000 - 0x000BFFFF - ذاكرة الفيديو في وضع الألوان
 0x000C0000 - 0x000C7FFF - BIOS ROM للفيديو
 0x000C8000 - 0x000EFFFF - منطقة الظل (ظل BIOS)
 0x000F0000 - 0x000FFFFF - BIOS النظام 

في بداية المقال ، كتب أن التعليمات الأولى للمعالج تقع في 0xFFFFFFF0 ، وهو أكثر بكثير من 0xFFFFF (1 ميجابايت). كيف يمكن لوحدة المعالجة المركزية الوصول إلى هذا العنوان في الوضع الحقيقي؟ الإجابة في وثائق لب القالب :

0xFFFE_0000 - 0xFFFF_FFFF: 128 ROM

في بداية التنفيذ ، BIOS ليس في ذاكرة الوصول العشوائي ، ولكن في ROM.

محمل الإقلاع


يمكن تحميل نواة لينكس مع محمل إقلاع مختلف ، مثل GRUB 2 و syslinux . يحتوي kernel على بروتوكول تمهيد يحدد متطلبات أداة تحميل التشغيل لتنفيذ دعم Linux. في هذا المثال ، نعمل مع GRUB 2.

استمرارًا لعملية التمهيد ، حدد BIOS جهاز التمهيد ونقل التحكم إلى قطاع التمهيد ، ويبدأ التنفيذ بـ boot.img . نظرًا لصغر حجمها ، يعد هذا رمزًا بسيطًا للغاية. يحتوي على مؤشر للانتقال إلى الصورة الرئيسية لـ GRUB 2. يبدأ بـ diskboot.img وعادة ما يتم تخزينه مباشرة بعد القطاع الأول في مساحة غير مستخدمة قبل القسم الأول. يحمّل الرمز أعلاه في الذاكرة بقية الصورة التي تحتوي على نواة GRUB 2 وبرامج تشغيل معالجة أنظمة الملفات. بعد ذلك ، يتم تنفيذ وظيفة grub_main .

تقوم وظيفة grub_main بتهيئة وحدة التحكم ، وإرجاع العنوان الأساسي للوحدات النمطية ، grub_main الجهاز الجذر ، وتحميل / توزيع ملف تكوين grub ، وتحميل الوحدات النمطية ، إلخ. في نهاية التنفيذ ، يضع اليرقة في الوضع العادي. وظيفة grub_normal_execute (من ملف المصدر grub-core/normal/main.c ) تكمل آخر الاستعدادات وتعرض قائمة لاختيار نظام التشغيل. عند تحديد أحد عناصر قائمة grub ، يتم grub_menu_execute_entry وظيفة grub_menu_execute_entry ، والتي تنفذ أمر boot grub وتحميل نظام التشغيل المحدد.

كما هو موضح في بروتوكول تمهيد kernel ، يجب أن يقوم برنامج تحميل التمهيد بقراءة وملء بعض حقول رأس تثبيت kernel ، والتي تبدأ عند الإزاحة 0x01f1 من رمز تثبيت kernel. يشار إلى هذا الإزاحة في البرنامج النصي linker . قوس رأس kernel / x86 / boot / header.S يبدأ بـ:

  .globl hdr hdr: setup_sects: .byte 0 root_flags: .word ROOT_RDONLY syssize: .long 0 ram_size: .word 0 vid_mode: .word SVGA_MODE root_dev: .word 0 boot_flag: .word 0xAA55 

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

كما ترى في بروتوكول تمهيد kernel ، سيتم عرض الذاكرة كما يلي:

  |  وضع النواة المحمي |
 100000 + ------------------------ +
          |  تعيين الإدخال / الإخراج |
 0A0000 + ------------------------ +
          |  الاحتياطي  لـ BIOS |  اترك أكبر قدر ممكن من الحرية
          ~ ~
          |  سطر الأوامر |  (قد يكون أيضًا أقل من X + 10000)
 X + 10000 + ------------------------ +
          |  تكديس / كومة |  لاستخدام كود وضع kernel الحقيقي
 X + 08000 + ------------------------ +
          |  تركيب نواة |  رمز الوضع الحقيقي لـ Kernel
          |  قطاع تمهيد النواة |  قطاع التمهيد kernel القديم
        X + ------------------------ +
          |  محمل |  <- نقطة إدخال 0x7C00 قطاع التمهيد
 001000 + ------------------------ +
          |  الاحتياطي  لـ MBR / BIOS |
 000800 + ------------------------ +
          |  تستخدم عادة  MBR |
 000600 + ------------------------ +
          |  تستخدم  BIOS فقط |
 000000 + ------------------------ +

لذلك ، عندما ينقل اللودر التحكم إلى النواة ، يبدأ بالعنوان:

 X + sizeof (KernelBootSector) + 1 

حيث X هو عنوان قطاع تمهيد kernel. في حالتنا ، X هو 0x10000 ، كما هو موضح في تفريغ الذاكرة:



قام برنامج bootloader بنقل نواة Linux إلى الذاكرة ، وملء حقول الرأس ، ثم نقلها إلى عنوان الذاكرة المطابق. يمكننا الآن الانتقال مباشرة إلى رمز تثبيت kernel.

بداية مرحلة تركيب النواة


أخيرا نحن في صميم! على الرغم من أنها ليست قيد التشغيل من الناحية الفنية. أولاً ، يحتاج جزء تثبيت kernel إلى تكوين شيء ما ، بما في ذلك برنامج فك الضغط وبعض الأشياء في إدارة الذاكرة. بعد كل هذا ، ستقوم بتفريغ النواة الحقيقية وتذهب إليها. يبدأ التثبيت في arch / x86 / boot / header.S بحرف _start .

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

qemu-system-x86_64 vmlinuz-3.18-generic

سترى:



في الواقع ، يبدأ ملف header.S السحري MZ (انظر لقطة شاشة التفريغ أعلاه) ونص رسالة الخطأ ورأس PE :

 #ifdef CONFIG_EFI_STUB # "MZ", MS-DOS header .byte 0x4d .byte 0x5a #endif ... ... ... pe_header: .ascii "PE" .word 0 

هناك حاجة لتحميل نظام تشغيل بدعم UEFI . سننظر في جهازها في الفصول التالية.

نقطة الدخول الفعلية لتثبيت النواة:

 // header.S line 292 .globl _start _start: 

يعرف محمل الإقلاع (grub2 وغيره) هذه النقطة (الإزاحة 0x200 من MZ ) ويذهب إليها مباشرة ، على الرغم من أن header.S يبدأ من قسم .bstext ، حيث يوجد نص رسالة الخطأ:

 // // arch/x86/boot/setup.ld // . = 0; // current position .bstext : { *(.bstext) } // put .bstext section to position 0 .bsdata : { *(.bsdata) } 

نقطة إدخال تثبيت Kernel:

  .globl _start _start: .byte 0xeb .byte start_of_setup-1f 1: // // rest of the header // 

هنا نرى رمز العملية jmp ( 0xeb ) ، الذي يذهب إلى النقطة start_of_setup-1f . في Nf ، على سبيل المثال ، يشير 2f إلى التسمية المحلية 2: في حالتنا ، هذه هي التسمية 1 ، والتي تكون موجودة مباشرة بعد النقل ، وتحتوي على ما تبقى من رأس الإعداد. مباشرة بعد رأس التثبيت ، نرى قسم .entrytext ، الذي يبدأ start_of_setup .

هذا هو الرمز الأول الذي تم تنفيذه بالفعل (بخلاف تعليمات القفزة السابقة ، بالطبع). بعد أن يتلقى جزء من تثبيت kernel التحكم من اللودر ، توجد تعليمات jmp الأولى عند الإزاحة 0x200 من بداية وضع kernel الحقيقي ، أي بعد 512 بايت الأولى. يمكن ملاحظة ذلك في كل من بروتوكول تمهيد kernel Linux وفي التعليمات البرمجية المصدر لـ grub2:

 segment = grub_linux_real_target >> 4; state.gs = state.fs = state.es = state.ds = state.ss = segment; state.cs = segment + 0x20; 

في حالتنا ، يتم تشغيل النواة على العنوان 0x10000 . هذا يعني أنه بعد بدء تثبيت kernel ، سيكون للسجلات المقطع القيم التالية:

gs = fs = es = ds = ss = 0x10000
cs = 0x10200


بعد الذهاب إلى start_of_setup يجب أن تقوم النواة بما يلي:

  • تأكد من أن جميع قيم تسجيل المقطع هي نفسها
  • إذا لزم الأمر ، قم بتكوين المكدس الصحيح
  • تكوين bss
  • انتقل إلى رمز C في arch / x86 / boot / main.c

دعونا نرى كيف يتم تنفيذ ذلك.

محاذاة حالة القطعة


بادئ ذي بدء ، تتحقق النواة من أن المقطع ds و es يشير إلى نفس العنوان. ثم يمسح علم الاتجاه باستخدام cld :

  movw %ds, %ax movw %ax, %es cld 

كما كتبت سابقًا ، يقوم grub2 افتراضيًا بتحميل رمز تثبيت kernel عند 0x10000 ، و cs عند 0x10200 ، لأن التنفيذ لا يبدأ من بداية الملف ، ولكن من الانتقال هنا:

 _start: .byte 0xeb .byte start_of_setup-1f 

هذا هو إزاحة 512 بايت من 4d 5a . من الضروري أيضًا محاذاة cs من 0x10200 إلى 0x10000 ، مثل جميع سجلات المقطع الأخرى. بعد ذلك قم بتثبيت المكدس:

  pushw %ds pushw $6f lretw 

تدفع هذه التعليمات قيمة ds إلى المكدس ، متبوعة بعنوان التسمية 6 وتعليمات lretw ، التي تقوم بتحميل عنوان التسمية 6 في سجل عداد الأوامر وتحميل cs بالقيمة ds . بعد ذلك ، سيكون للقيم ds و cs نفس القيم.

إعداد المكدس


كل هذا الرمز تقريبًا جزء من عملية إعداد بيئة C في الوضع الحقيقي. الخطوة التالية هي التحقق من قيمة تسجيل ss وإنشاء المكدس الصحيح إذا كانت قيمة ss غير صحيحة:

  movw %ss, %dx cmpw %ax, %dx movw %sp, %dx je 2f 

يمكن أن يؤدي هذا إلى ثلاثة سيناريوهات مختلفة:

  • ss قيمة صالحة 0x1000 (كما هو الحال مع جميع السجلات الأخرى باستثناء cs )
  • ss قيمة غير صالحة وتم CAN_USE_HEAP علامة CAN_USE_HEAP (انظر أدناه)
  • ss قيمة غير صالحة ولم CAN_USE_HEAP تعيين علامة CAN_USE_HEAP (انظر أدناه)

ضع في اعتبارك جميع السيناريوهات بالترتيب:

  • ss قيمة صالحة ( 0x1000 ). في هذه الحالة ، ننتقل إلى التسمية 2:

 2: andw $~3, %dx jnz 3f movw $0xfffc, %dx 3: movw %ax, %ss movzwl %dx, %esp sti 

نقوم هنا بضبط محاذاة تسجيل dx (الذي يحتوي على قيمة sp المحددة بواسطة المُحمل) إلى 4 بايت والتحقق من الصفر. إذا كان صفرًا ، فإننا نضع القيمة 0xfffc dx (عنوان محاذي 4 بايت قبل الحجم الأقصى 0xfffc 64 كيلوبايت). إذا لم يكن يساوي الصفر ، فإننا نواصل استخدام قيمة sp المحددة بواسطة أداة 0xf7f4 ( 0xf7f4 في حالتنا). ثم نضع قيمة ax في ss ، مما يحفظ عنوان المقطع الصحيح 0x1000 sp الصحيح. الآن لدينا المكدس الصحيح:



  • في السيناريو الثاني ، ss != ds . أولاً نضع القيمة _end (عنوان نهاية رمز التثبيت) في dx loadflags حقل الرأس ، باستخدام تعليمات testb للتحقق مما إذا كان يمكن استخدام الكومة. loadflags هو رأس قناع البت الذي يتم تعريفه على النحو التالي:

 #define LOADED_HIGH (1<<0) #define QUIET_FLAG (1<<5) #define KEEP_SEGMENTS (1<<6) #define CAN_USE_HEAP (1<<7) 

وكما هو موضح في بروتوكول التمهيد:

: loadflags

.

7 (): CAN_USE_HEAP
1, ,
heap_end_ptr . ,
.


إذا تم CAN_USE_HEAP بت CAN_USE_HEAP ، فإننا في dx نقوم بتعيين القيمة heap_end_ptr (التي تشير إلى _end ) ونضيف STACK_SIZE إليها (الحد الأدنى لحجم المكدس هو 1024 بايت). بعد ذلك ، انتقل إلى التسمية 2 (كما في الحالة السابقة) وقم بعمل المكدس الصحيح.



  • إذا لم CAN_USE_HEAP تعيين CAN_USE_HEAP سوى استخدام الحد الأدنى من المكدس من _end إلى _end + STACK_SIZE :



إعداد BSS


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

  cmpl $0x5a5aaa55, setup_sig jne setup_bad 

تقارن التعليمات ببساطة setup_sig بالرقم السحري 0x5a5aaa55. إذا لم تكن متساوية ، يتم الإبلاغ عن خطأ فادح.

إذا كان الرقم السحري مطابقًا ولدينا مجموعة من سجلات المقطع الصحيحة ومكدسًا ، فكل ما تبقى هو تكوين قسم BSS قبل المتابعة إلى رمز C.

يتم استخدام قسم BSS لتخزين البيانات غير المهيأة بشكل ثابت. يتحقق Linux بعناية من إعادة تعيين منطقة الذاكرة هذه:

  movw $__bss_start, %di movw $_end+3, %cx xorl %eax, %eax subw %di, %cx shrw $2, %cx rep; stosl 

أولاً ، يتم نقل عنوان بدء __bss_start إلى di . ثم يتم نقل العنوان _end + 3 (+3 للمحاذاة بـ 4 بايت) إلى cx . يتم مسح سجل eax (باستخدام تعليمات xor ) ، ويتم حساب حجم قسم bss ( cx-di ) ، ويتم وضعه في cx . ثم يتم تقسيم cx إلى أربعة (حجم "الكلمة") ويتم استخدام تعليمات stosl ، وتخزين القيمة (صفر) في العنوان مشيرًا إلى di ، وزيادة di تلقائيًا بمقدار أربعة وتكرار ذلك حتى يصل إلى الصفر). التأثير الصافي لهذا الرمز هو كتابة الأصفار لجميع الكلمات في الذاكرة من __bss_start إلى _end :



انتقل إلى الصفحة الرئيسية


هذا كل شيء: لدينا مكدس و BSS ، لذلك يمكنك الانتقال إلى الوظيفة main() C:

  calll main 

تقع الوظيفة main() في arch / x86 / boot / main.c. سنتحدث عنها في الجزء التالي.

الخلاصة


هذا هو نهاية الجزء الأول عن جهاز Linux kernel.إذا كان لديك أسئلة أو اقتراحات، يرجى الاتصال بي على تويتر ، على البريد الإلكتروني أو مجرد إنشاء تذكرة . في الجزء القادم سوف نرى رمز الأول في C، والتي تتم أثناء تثبيت نواة لينكس، وتنفيذ البرامج الفرعية الذاكرة، مثل memset، memcpy، earlyprintkوالتنفيذ المبكر وتهيئة وحدة، وأكثر من ذلك.

المراجع


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


All Articles