RISC-V من الصفر

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

أولاً ، دعنا نتحدث قليلاً عن RISC-V وأهمية هذه البنية ، وتكوين سلسلة أدوات RISC-V ، وتشغيل برنامج C بسيط على أجهزة RISC-V تمت مضاهاتها.

محتوى


  1. ما هو RISC-V؟
  2. تكوين QEMU وأدوات RISC-V
  3. مرحبا RISC-V!
  4. نهج ساذج
  5. رفع الستار الخامس
  6. بحث المكدس لدينا
  7. ترتيب
  8. توقف عن ذلك! Hammertime! وقت التشغيل!
  9. تصحيح ولكن الآن الحقيقي
  10. ما التالي؟
  11. بالإضافة إلى ذلك

ما هو RISC-V؟


RISC-V هي بنية مجموعة التعليمات المجانية. نشأ المشروع في جامعة كاليفورنيا في بيركلي في عام 2010. لعبت دورًا مهمًا في نجاحها من خلال انفتاح الشفرة وحرية الاستخدام ، والتي كانت مختلفة تمامًا عن العديد من الهياكل الأخرى. استخدم ARM: لإنشاء معالج متوافق ، يتعين عليك دفع رسوم مقدمة تتراوح ما بين مليون دولار إلى 10 ملايين دولار ، بالإضافة إلى دفع إتاوات تتراوح بين 0.5٪ و 2٪ على المبيعات . يجعل النموذج المجاني والمفتوح RISC-V خيارًا جذابًا للكثيرين ، بما في ذلك للشركات الناشئة التي لا يمكنها دفع ترخيص للحصول على ARM أو معالج آخر ، وللباحثين الأكاديميين (ومن الواضح) لمجتمع المصادر المفتوحة.

النمو السريع في شعبية RISC-V لم تمر مرور الكرام. أطلق ARM موقعًا حاول (بدلاً من أن ينجح) إبراز الفوائد المزعومة لـ ARM على RISC-V (الموقع مغلق بالفعل). يتم دعم مشروع RISC-V بواسطة العديد من الشركات الكبيرة ، بما في ذلك Google و Nvidia و Western Digital.

تكوين QEMU وأدوات RISC-V


لا يمكننا تشغيل الكود على معالج RISC-V حتى نهيئ البيئة. لحسن الحظ ، هذا لا يتطلب معالج RISC-V فعلي ؛ بدلاً من ذلك ، نأخذ qemu . اتبع التعليمات لنظام التشغيل الخاص بك لتثبيت. لدي ماك ، لذلك فقط أدخل أمر واحد:

# also available via MacPorts - `sudo port install qemu` brew install qemu 

qemu ، يأتي qemu مع العديد من الآلات الجاهزة (راجع خيار الجهاز qemu-system-riscv32 -machine ).

بعد ذلك ، قم بتثبيت OpenOCD لأدوات RISC-V و RISC-V.

قم بتنزيل مجموعات جاهزة من أدوات RISC-V OpenOCD و RISC-V هنا .
نحن استخراج الملفات إلى أي دليل ، لدي ~/usys/riscv . تذكرها للاستخدام في المستقبل.

 mkdir -p ~/usys/riscv cd ~/Downloads cp openocd-<date>-<platform>.tar.gz ~/usys/riscv cp riscv64-unknown-elf-gcc-<date>-<platform>.tar.gz ~/usys/riscv cd ~/usys/riscv tar -xvf openocd-<date>-<platform>.tar.gz tar -xvf riscv64-unknown-elf-gcc-<date>-<platform>.tar.gz 

RISCV_OPENOCD_PATH متغيرات البيئة RISCV_OPENOCD_PATH و RISCV_PATH حتى تتمكن البرامج الأخرى من العثور على سلسلة أدواتنا. قد يبدو هذا مختلفًا اعتمادًا على نظام التشغيل و shell: لقد أضفت المسارات إلى ~/.zshenv .

 # I put these two exports directly in my ~/.zshenv file - you may have to do something else. export RISCV_OPENOCD_PATH="$HOME/usys/riscv/openocd-<date>-<version>" export RISCV_PATH="$HOME/usys/riscv/riscv64-unknown-elf-gcc-<date>-<version>" # Reload .zshenv with our new environment variables. Restarting your shell will have a similar effect. source ~/.zshenv 

قم بإنشاء رابط رمزي في /usr/local/bin لهذا الملف القابل للتنفيذ بحيث يمكنك تشغيله في أي وقت دون تحديد المسار الكامل إلى ~/usys/riscv/riscv64-unknown-elf-gcc-<date>-<version>/bin/riscv64-unknown-elf-gcc .

 # Symbolically link our gcc executable into /usr/local/bin. Repeat this process for any other executables you want to quickly access. ln -s ~/usys/riscv/riscv64-unknown-elf-gcc-8.2.0-<date>-<version>/bin/riscv64-unknown-elf-gcc /usr/local/bin 

وفويلا ، لدينا مجموعة أدوات RISC-V تعمل! جميع أدواتنا التنفيذية ، مثل riscv64-unknown-elf-gcc ، و riscv64-unknown-elf-gdb riscv64-unknown-elf-ld ، و riscv64-unknown-elf-ld وغيرها ، في ~/usys/riscv/riscv64-unknown-elf-gcc-<date>-<version>/bin/ .

مرحبا RISC-V!


26 مايو 2019 تصحيح:

لسوء الحظ ، نظرًا لوجود خطأ في RISC-V QEMU ، لم يعد برنامج "hello world" الخاص بالحرية الإلكترونية في QEMU يعمل. تم إصدار تصحيح لحل هذه المشكلة ، ولكن الآن ، تخطي هذا القسم. لن تكون هناك حاجة لهذا البرنامج في أقسام لاحقة من هذه المادة. أنا تتبع الموقف وتحديث المقال بعد إصلاح الخلل.

انظر هذا التعليق لمزيد من المعلومات.

من خلال إعداد الأدوات ، لنشغل برنامج RISC-V البسيط. لنبدأ باستنساخ مستودع SiFive freedom-e-sdk :

 cd ~/wherever/you/want/to/clone/this git clone --recursive https://github.com/sifive/freedom-e-sdk.git cd freedom-e-sdk 

حسب التقاليد ، لنبدأ ببرنامج "Hello، world" من مستودع freedom-e-sdk . نستخدم Makefile الجاهزة التي توفرها ترجمة هذا البرنامج في وضع التصحيح:

 make PROGRAM=hello TARGET=sifive-hifive1 CONFIGURATION=debug software 

وتشغيل في QEMU:

 qemu-system-riscv32 -nographic -machine sifive_e -kernel software/hello/debug/hello.elf Hello, World! 

هذه بداية رائعة. يمكنك تشغيل أمثلة أخرى من freedom-e-sdk . بعد ذلك ، سنكتب ونحاول تصحيح برنامجنا في C.

نهج ساذج


لنبدأ ببرنامج بسيط يضيف رقمين بلا حدود.

 cat add.c int main() { int a = 4; int b = 12; while (1) { int c = a + b; } return 0; } 

نريد تشغيل هذا البرنامج ، وأول شيء نحتاج إلى تجميعه لمعالج RISC-V.

 # -O0 to disable all optimizations. Without this, GCC might optimize # away our infinite addition since the result 'c' is never used. # -g to tell GCC to preserve debug info in our executable. riscv64-unknown-elf-gcc add.c -O0 -g 

يؤدي هذا إلى إنشاء ملف a.out ، والذي يتم تعيين gcc الافتراضية فيه على الملفات القابلة للتنفيذ. الآن قم بتشغيل هذا الملف في qemu :

 # -machine tells QEMU which among our list of available machines we want to # run our executable against. Run qemu-system-riscv64 -machine help to list # all available machines. # -m is the amount of memory to allocate to our virtual machine. # -gdb tcp::1234 tells QEMU to also start a GDB server on localhost:1234 where # TCP is the means of communication. # -kernel tells QEMU what we're looking to run, even if our executable isn't # exactly a "kernel". qemu-system-riscv64 -machine virt -m 128M -gdb tcp::1234 -kernel a.out 

لقد اخترنا الجهاز الذي جاء riscv-qemu الأصل .

الآن بعد أن تم تشغيل برنامجنا داخل QEMU مع خادم GDB على localhost:1234 ، نحن نتصل به مع عميل RISC-V GDB من محطة منفصلة:

 # --tui gives us a (t)extual (ui) for our GDB session. # While we can start GDB without any arguments, specifying 'a.out' tells GDB # to load debug symbols from that file for the newly created session. riscv64-unknown-elf-gdb --tui a.out 

ونحن داخل GDB!

  تم تكوين GDB هذا كـ "- المضيف = x86_64-apple-darwin17.7.0 - الهدف = riscv64-unknown-elf".  │
 اكتب "إظهار التكوين" للحصول على تفاصيل التكوين.  │
 للحصول على تعليمات الإبلاغ عن الأخطاء ، يرجى الاطلاع على: │
 <Http://www.gnu.org/software/gdb/bugs/>.  │
 ابحث عن دليل GDB ومصادر الوثائق الأخرى عبر الإنترنت على: │
     <Http://www.gnu.org/software/gdb/documentation/>.  │
                                                                                                       │
 للحصول على مساعدة ، اكتب "مساعدة".  │
 اكتب "كلمة apropos" للبحث عن الأوامر المتعلقة بـ "word" ... │
 قراءة الرموز من a.out ... │
 (جدب) 

يمكننا محاولة تشغيل أوامر run أو start للملف القابل للتنفيذ a.out في GDB ، لكن في الوقت الحالي لن يعمل هذا لسبب واضح. قمنا بتجميع البرنامج كـ riscv64-unknown-elf-gcc ، لذلك يجب أن يعمل المضيف على هندسة riscv64 .

ولكن هناك طريقة للخروج! هذا الموقف هو أحد الأسباب الرئيسية لوجود طراز عميل خادم GDB. يمكن أن نأخذ الملف القابل للتنفيذ riscv64-unknown-elf-gdb وبدلاً من تشغيله على المضيف ، riscv64-unknown-elf-gdb بعض الهدف البعيد (خادم GDB). كما تتذكر ، لقد بدأنا للتو riscv-qemu وأخبرنا أن نبدأ خادم GDB على localhost:1234 riscv-qemu localhost:1234 . فقط اتصل بهذا الخادم:

  (gdb) الهدف البعيد: 1234 │
 تصحيح الأخطاء عن بُعد باستخدام: 1234 

يمكنك الآن تعيين بعض نقاط التوقف:

 (gdb) b main Breakpoint 1 at 0x1018e: file add.c, line 2. (gdb) b 5 # this is the line within the forever-while loop. int c = a + b; Breakpoint 2 at 0x1019a: file add.c, line 5. 

وأخيرًا ، حدد GDB continue (الأمر المختصر c ) حتى نصل إلى نقطة الإيقاف:

 (gdb) c Continuing. 

ستلاحظ بسرعة أن العملية لا تنتهي بأي شكل من الأشكال. هذا غريب ... ألا يجب أن نصل على الفور إلى نقطة التوقف b 5 ؟ ماذا حدث



هنا يمكنك رؤية العديد من المشاكل:

  1. لا يمكن العثور على واجهة المستخدم النص المصدر. يجب أن تعرض الواجهة الرمز الخاص بنا وأي نقاط توقف قريبة.
  2. لا يرى GDB سطر التنفيذ الحالي ( L?? ) ويعرض العداد 0x0 ( PC: 0x0 ).
  3. بعض النص في سطر الإدخال ، والذي يبدو في مجمله كما يلي: 0x0000000000000000 in ?? () 0x0000000000000000 in ?? ()

إلى جانب حقيقة أننا لا نستطيع الوصول إلى نقطة النهاية ، تشير هذه المؤشرات إلى: لقد فعلنا شيئًا خاطئًا. لكن ماذا؟

رفع الستار الخامس


لفهم ما يحدث ، تحتاج إلى التراجع والتحدث عن كيفية عمل برنامجنا البسيط C تحت الغطاء. main المهمة main في إضافة بسيطة ، ولكن ما هو حقا؟ لماذا ينبغي أن يطلق عليه main ، وليس origin أو begin ؟ وفقًا للاتفاقية ، تبدأ جميع الملفات القابلة للتنفيذ في تنفيذ الوظيفة main ، ولكن ما هو السحر الذي يوفر هذا السلوك؟

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

 riscv64-unknown-elf-gcc add.c -O0 -g -v 

الإخراج كبير ، لذلك لن نشاهد القائمة بأكملها. من المهم الإشارة إلى أنه على الرغم من أن GCC عبارة عن مترجم رسميًا ، إلا أنه يستخدم أيضًا التحويل البرمجي (لتقييد نفسه بالتجميع والتجميع ، يجب تحديد العلامة -c ). لماذا هذا مهم؟ حسنًا ، ألقِ نظرة على المقتطف من الإخراج التفصيلي لـ gcc :

  # الأمر الفعلي لـ "gcc -v" يخرج مسارات كاملة ، لكن هذه هي تماما
 # طويلة ، لذلك ندعي وجود هذه المتغيرات.
 # $ RV_GCC_BIN_PATH = / المستخدمون / twilcock / usys / riscv / riscv64-unknown-elf-gcc- <date> - <version> / bin /
 # $ RV_GCC_LIB_PATH = $ RV_GCC_BIN_PATH /../ lib / gcc / riscv64-unknown-elf / 8.2.0

 $ RV_GCC_BIN_PATH /../ libexec / gcc / riscv64-unknown-elf / 8.2.0 / collect2 \
   ... مقطوع ... 
   $ RV_GCC_LIB_PATH /../../../../ riscv64-unknown-elf / lib / rv64imafdc / lp64d / crt0.o \ 
   $ RV_GCC_LIB_PATH / riscv64-unknown-elf / 8.2.0 / rv64imafdc / lp64d / crtbegin.o \
   -lgcc - بداية المجموعة -lc -lgloss - نهاية المجموعة -lgcc \ 
   $ RV_GCC_LIB_PATH / rv64imafdc / lp64d / crtend.o
   ... مقطوع ...
 COLLECT_GCC_OPTIONS = '- O0' '-g' '-v' '-march = rv64imafdc' '-mabi = lp64d' 

أفهم أنه حتى في الصيغة المختصرة ، هذا كثير ، لذلك اسمحوا لي أن أشرح. في السطر الأول ، تدير gcc برنامج crt0.o ، crt0.o الوسائط crt0.o ، و crtend.o ، و crtend.o ، -lgcc و --start-group . يمكن العثور على وصف collect2 هنا : باختصار ، collect2 تنظم وظائف التهيئة المختلفة عند بدء التشغيل ، مما يجعل التخطيط في تمريرة واحدة أو أكثر.

وبالتالي ، يقوم مجلس التعاون الخليجي بتجميع العديد من ملفات crt باستخدام الكود الخاص بنا. كما يمكنك تخمين ، crt يعني "وقت التشغيل C". هنا يتم وصفه بالتفصيل ما المقصود به كل crt ، لكننا مهتمون بـ crt0 ، والذي يفعل شيئًا مهمًا واحدًا:

"من المتوقع أن يحتوي هذا الكائن [crt0] على الحرف _start ، الذي يشير إلى تمهيد البرنامج."

يعتمد جوهر "bootstrap" على النظام الأساسي ، لكنه عادة ما يتضمن مهام مهمة مثل إعداد إطار مكدس ، وتمرير وسيطات سطر الأوامر ، والاتصال main . نعم ، لقد وجدنا أخيرًا إجابة السؤال: إنه _start يستدعي _start الرئيسية!

بحث المكدس لدينا


لقد حللنا لغزًا واحدًا ، لكن كيف يقربنا ذلك من الهدف الأصلي - تشغيل برنامج C بسيط في gdb ؟ يبقى حل العديد من المشكلات: الأولى تتعلق بكيفية تكوين crt0 لمجموعتنا.

كما رأينا أعلاه ، gcc الافتراضية crt0 . يتم تحديد المعلمات الافتراضية بناءً على عدة عوامل:

  • الهدف الثلاثي المطابق لهيكل machine-vendor-operatingsystem . لدينا riscv64-unknown-elf
  • الهدف العمارة ، rv64imafdc
  • الهدف ABI ، lp64d

عادة ما يعمل كل شيء بشكل جيد ، ولكن ليس لكل معالج RISC-V. كما ذكر سابقًا ، تتمثل إحدى مهام crt0 في تكوين المكدس. لكنه لا يعرف بالضبط أين ينبغي أن يكون المكدس لوحدة المعالجة المركزية لدينا ( -machine )؟ لا يستطيع أن يفعل ذلك دون مساعدتنا.

في الأمر qemu-system-riscv64 -machine virt -m 128M -gdb tcp::1234 -kernel a.out استخدمنا الجهاز virt . لحسن الحظ ، تسهّل qemu تفريغ معلومات الماكينة في dtb تفريغ dtb (blob tree device).

 # Go to the ~/usys/riscv folder we created before and create a new dir # for our machine information. cd ~/usys/riscv && mkdir machines cd machines # Use qemu to dump info about the 'virt' machine in dtb (device tree blob) # format. # The data in this file represents hardware components of a given # machine / device / board. qemu-system-riscv64 -machine virt -machine dumpdtb=riscv64-virt.dtb 

يصعب قراءة بيانات Dtb نظرًا لأنها في الأساس تنسيق ثنائي ، ولكن توجد أداة مساعدة لسطر الأوامر dtc (برنامج التحويل البرمجي لجهاز شجرة) يمكنها تحويل الملف إلى شيء أكثر قابلية للقراءة.

 # I'm running MacOS, so I use Homebrew to install this. If you're # running another OS you may need to do something else. brew install dtc # Convert our .dtb into a human-readable .dts (device tree source) file. dtc -I dtb -O dts -o riscv64-virt.dts riscv64-virt.dtb 

ملف الإخراج هو riscv64-virt.dts ، حيث نرى الكثير من المعلومات المثيرة للاهتمام حول: عدد النوى المعالج المتاحة ، وموقع الذاكرة من الأجهزة الطرفية المختلفة ، مثل UART ، وموقع الذاكرة الداخلية (RAM). يجب أن تكون المجموعة في هذه الذاكرة ، لذا ابحث عنها باستخدام grep :

 grep memory riscv64-virt.dts -A 3 memory@80000000 { device_type = "memory"; reg = <0x00 0x80000000 0x00 0x8000000>; }; 

كما ترون ، تحتوي هذه العقدة على "ذاكرة" محددة كـ device_type . على ما يبدو ، وجدنا ما كنا نبحث عنه. حسب القيم داخل reg = <...> ; يمكنك تحديد المكان الذي يبدأ فيه بنك الذاكرة وما طوله.

في مواصفات devicetree ، نرى أن بناء جملة reg عبارة عن عدد اعتباطي من الأزواج (base_address, length) . ومع ذلك ، هناك أربعة معاني داخل reg . غريب ، ليس هناك قيمتان كافيتان لبنك ذاكرة واحد؟

مرة أخرى ، من خلال مواصفات devicetree (البحث عن خاصية reg ) ، نكتشف أن عدد <u32> خلايا لتحديد العنوان والطول يتم تحديدهما بواسطة #address-cells #size-cells وخصائص #size-cells في العقدة الأصل (أو في العقدة نفسها). لم يتم تحديد هذه القيم في عقدة الذاكرة الخاصة بنا ، وعقدة الذاكرة الأصل هي ببساطة جذر الملف. دعنا ننظر فيها لهذه القيم:

 head -n8 riscv64-virt.dts /dts-v1/; / { #address-cells = <0x02>; #size-cells = <0x02>; compatible = "riscv-virtio"; model = "riscv-virtio,qemu"; 

اتضح أن العنوان والطول يتطلبان قيمتين 32 بت. هذا يعني أنه مع reg = <0x00 0x80000000 0x00 0x8000000>; تبدأ ذاكرتنا 0x00 + 0x80000000 (0x80000000) وتحتل 0x00 + 0x8000000 (0x8000000) بايت ، أي تنتهي عند 0x88000000 ، والتي تتوافق مع 128 ميجابايت.

ترتيب


باستخدام qemu و dtc وجدنا عناوين RAM في الجهاز الظاهري virt. نحن نعلم أيضًا أن gcc يؤلف crt0 افتراضيًا ، دون تكوين مكدس حسب حاجتنا. ولكن كيف تستخدم هذه المعلومات لتشغيل البرنامج وتصحيحه في النهاية؟

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

لحسن الحظ ، لسنا أول من طرح هذا السؤال ، والحل الجيد موجود بالفعل. يتيح لك رابط GNU ld تحديد الشخصية المتاحة من خلال crt0 . يمكننا تحديد رمز __stack_top المناسب للمعالجات المختلفة.

بدلاً من كتابة ملف الرابط الخاص بك من البداية ، فمن المنطقي أن تأخذ النص الافتراضي مع ld وتعديله قليلاً لدعم أحرف إضافية. ما هو البرنامج النصي رابط؟ هنا وصف جيد :

الغرض الرئيسي من البرنامج النصي للرابط هو وصف كيفية مطابقة أقسام الملفات في المدخلات والمخرجات ، والتحكم في تخطيط ذاكرة ملف الإخراج.

مع العلم بذلك ، riscv64-unknown-elf-ld البرنامج النصي riscv64-unknown-elf-ld الافتراضي riscv64-unknown-elf-ld إلى ملف جديد:

 cd ~/usys/riscv # Make a new dir for custom linker scripts out RISC-V CPUs may require. mkdir ld && cd ld # Copy the default linker script into riscv64-virt.ld riscv64-unknown-elf-ld --verbose > riscv64-virt.ld 

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

  vim riscv64-virt.ld

 # إزالة كل شيء أعلاه بما في ذلك السطر =============
 GNU ld (GNU Binutils) 2.32
   مضاهاة المدعومة:
    elf64lriscv
    elf32lriscv
 باستخدام البرنامج النصي رابط داخلي:
 ==================================================
 / * Script for -z combreloc: ضم وفرز أقسام إعادة التوطين * /
 / * حقوق الطبع والنشر (C) 2014-2019 ، مؤسسة البرمجيات الحرة
    نسخ وتوزيع هذا البرنامج النصي ، مع أو بدون تعديل ،
    مسموح في أي وسيط دون حقوق الملكية
    لاحظ ويتم الحفاظ على هذا الإشعار.  * /
 OUTPUT_FORMAT ("elf64-littleriscv"، "elf64-littleriscv"،
	       "Elf64-littleriscv")
 ... بقية البرنامج النصي رابط ... 

بعد ذلك ، قم بتشغيل الأمر MEMORY لتحديد مكان __stack_top يدويًا. حدد موقع السطر الذي يبدأ بـ OUTPUT_ARCH(riscv) ، ويجب أن يكون في الجزء العلوي من الملف ، وإضافة أمر MEMORY تحته:

 OUTPUT_ARCH(riscv) /* >>> Our addition. <<< */ MEMORY { /* qemu-system-risc64 virt machine */ RAM (rwx) : ORIGIN = 0x80000000, LENGTH = 128M } /* >>> End of our addition. <<< */ ENTRY(_start) 

أنشأنا كتلة ذاكرة تسمى RAM ( RAM ، والتي يُسمح بالقراءة ( r ) ، والكتابة ( w ) ، وتخزين الكود القابل للتنفيذ ( x ).

رائع ، لقد حددنا تخطيط الذاكرة الذي يتوافق مع مواصفات الجهاز RISC-V الخاص بنا. الآن يمكنك استخدامه. نريد أن نضع مكدسنا في الذاكرة.

تحتاج إلى تحديد حرف __stack_top . افتح البرنامج النصي الخاص riscv64-virt.ld ( riscv64-virt.ld ) في محرر نصوص وأضف بضعة أسطر:

 SECTIONS { /* Read-only sections, merged into text segment: */ PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x10000)); . = SEGMENT_START("text-segment", 0x10000) + SIZEOF_HEADERS; /* >>> Our addition. <<< */ PROVIDE(__stack_top = ORIGIN(RAM) + LENGTH(RAM)); /* >>> End of our addition. <<< */ .interp : { *(.interp) } .note.gnu.build-id : { *(.note.gnu.build-id) } 

كما ترى ، نحدد __stack_top باستخدام الأمر PROVIDE . سيتم الوصول إلى الرمز من أي برنامج مرتبط بهذا البرنامج النصي (على افتراض أن البرنامج نفسه لن يحدد شيئًا يحمل الاسم __stack_top ). اضبط __stack_top على ORIGIN(RAM) . نعلم أن هذه القيمة هي 0x80000000 بالإضافة إلى LENGTH(RAM) ، والتي تبلغ 128 ميجابايت ( 0x8000000 بايت). هذا يعني أنه __stack_top ضبط 0x88000000 على 0x88000000 .

للإيجاز ، لن أدرج ملف رابط كامل هنا ؛ يمكنك مشاهدته هنا .

توقف عن ذلك! Hammertime! وقت التشغيل!


الآن لدينا كل ما نحتاجه لإنشاء وقت تشغيل C لدينا ، في الواقع ، هذه مهمة بسيطة إلى حد ما ، crt0.s ملف crt0.s بأكمله:

 .section .init, "ax" .global _start _start: .cfi_startproc .cfi_undefined ra .option push .option norelax la gp, __global_pointer$ .option pop la sp, __stack_top add s0, sp, zero jal zero, main .cfi_endproc .end 

يجذب على الفور عددًا كبيرًا من الخطوط التي تبدأ بنقطة. هذا هو ملف لتجميع as . تسمى الخطوط التي تحتوي على نقاط بتوجيهات المجمّع : فهي توفر معلومات للتجميع. هذا ليس رمزًا قابلاً للتنفيذ ، مثل تعليمات المجمّع RISC-V مثل jal add .

دعنا نذهب من خلال سطر ملف سطرا. سنعمل مع مختلف سجلات RISC-V القياسية ، لذلك تحقق من هذا الجدول ، الذي يغطي جميع السجلات والغرض منها.

 .section .init, "ax" 

كما هو مذكور في دليل مجمّع جنو "باسم" ، فإن هذا السطر يخبر المجمّع بإدراج الكود التالي في قسم .init ، والذي تم تخصيصه ( a ) .init للتنفيذ ( x ). هذا القسم هو اصطلاح آخر شائع لتشغيل التعليمات البرمجية داخل نظام التشغيل. نحن نعمل على أجهزة نقية بدون نظام تشغيل ، لذلك في حالتنا قد لا تكون مثل هذه التعليمات ضرورية للغاية ، ولكن على أي حال هذه ممارسة جيدة.

 .global _start _start: 

.global يجعل الحرف التالي متاحًا لـ ld . بدون هذا ، لن يعمل الارتباط ، لأن الأمر ENTRY(_start) في البرنامج النصي رابط يشير إلى رمز _start كنقطة إدخال إلى الملف القابل للتنفيذ. يخبر السطر التالي المجمّع أننا بدأنا تعريف الحرف _start .

 _start: .cfi_startproc .cfi_undefined ra ...other stuff... .cfi_endproc 

.cfi توجيهات .cfi هذه عن هيكل الإطار وكيفية التعامل معه. .cfi_endproc و .cfi_endproc إلى بداية ونهاية الوظيفة ، .cfi_undefined ra المجمّع بأنه يجب عدم استعادة سجل ra إلى أي قيمة يحتوي عليها قبل _start .

 .option push .option norelax la gp, __global_pointer$ .option pop 

تغير توجيهات .option سلوك المجمّع وفقًا للرمز عندما تحتاج إلى تطبيق مجموعة محددة من الخيارات. فيما يلي وصف مفصل عن أهمية استخدام .option في هذا الجزء:

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

 .option push .option norelax la gp, __global_pointer$ .option pop 

بحيث تحصل بعد الاسترخاء على الكود التالي:

 auipc gp, %pcrel_hi(__global_pointer$) addi gp, gp, %pcrel_lo(__global_pointer$) 

بدلا من البساطة:

 addi gp, gp, 0 

والآن الجزء الأخير من crt0.s لدينا:

 _start: ...other stuff... la sp, __stack_top add s0, sp, zero jal zero, main .cfi_endproc .end 

هنا يمكننا أخيرًا استخدام رمز __stack_top ، والذي عملنا جاهدين لإنشائه. يقوم التعليمة الزائفة la (عنوان التحميل) بتحميل قيمة __stack_top في سجل sp (مؤشر المكدس) ، مع __stack_top للاستخدام في بقية البرنامج.

ثم add s0, sp, zero يضيف قيم السجلات sp zero (وهو في الواقع سجل x0 مع إشارة ثابتة إلى 0) ويضع النتيجة في السجل s0 . هذا سجل خاص غير عادي من عدة جوانب. أولاً ، إنه "سجل مستمر" ، أي ، يتم حفظه عند استدعاء الوظائف. ثانياً ، تعمل s0 أحيانًا كمؤشر إطار ، والذي يعطي كل وظيفة استدعاء مساحة صغيرة في المكدس لتخزين المعلمات التي تم تمريرها إلى هذه الوظيفة. كيفية عمل المكالمات باستخدام مؤشرات المكدس والإطار هو موضوع مثير للاهتمام للغاية يمكنك تكريسه بسهولة لمقال منفصل ، ولكن في الوقت الحالي ، تعرف فقط على أنه في وقت التشغيل ، من المهم تهيئة مؤشر الإطار s0 .

التالي نرى jal zero, main البيان jal zero, main . هنا jal تعني القفز jal . تتوقع التعليمة المعاملات في صورة jal rd (destination register), offset_address . من الناحية الوظيفية ، تكتب jal قيمة التعليمة التالية (تسجيل pc زائد أربعة) إلى rd ، ثم تقوم بتعيين تسجيل pc قيمة pc الحالية مضافًا إليها عنوان الإزاحة ، مع "استدعاء" هذا العنوان بشكل فعال.

كما ذكر أعلاه ، يرتبط x0 بإحكام بالقيمة الحرفية 0 ، والكتابة إليها عديمة الفائدة. , zero , RISC-V x0 . offset_address . , ?

jal zero, offset_address . , , . ISA, . , jal unconditional jump , RISC-V jal , jal zero, main .

لدى RISC-V العديد من هذه التحسينات ، والتي يتخذ معظمها شكل ما يسمى بالتعليمات الزائفة . يعرف المجمعون كيفية ترجمتها إلى إرشادات الأجهزة الحقيقية. على سبيل المثال ، تقوم j offset_addressأداة تجميع RISC-V بترجمة الإرشادات الزائفة للقفزات غير المشروطة إلى jal zero, offset_address. للحصول على قائمة كاملة بالتعليمات الزائفة المدعومة رسميًا ، انظر مواصفات RISC-V (الإصدار 2.2) .

 _start: ...other stuff... jal zero, main .cfi_endproc .end 

السطر الأخير لدينا هو توجيه المجمّع .end، والذي يمثل ببساطة نهاية الملف.

تصحيح ولكن الآن الحقيقي


C RISC-V, . qemu dtc virt RISC-V. riscv64-unknown-elf-ld , __stack_top . crt0.s , , , main . GDB.

, C:

 cat add.c int main() { int a = 4; int b = 12; while (1) { int c = a + b; } return 0; } 

:

 riscv64-unknown-elf-gcc -g -ffreestanding -O0 -Wl,--gc-sections -nostartfiles -nostdlib -nodefaultlibs -Wl,-T,riscv64-virt.ld crt0.s add.c 

, , , .

-ffreestanding , , . ( ), , .

-Wl — ( ld ). --gc-sections « », ld . -nostartfiles , -nostdlib -nodefaultlibs (, crt0 ), stdlib . crt0 , , .

-T , riscv64-virt.ld . , , , : crt0.s add.c . , a.out .

qemu :

 # -S freezes execution of our executable (-kernel) until we explicitly tell # it to start with a 'continue' or 'c' from our gdb client qemu-system-riscv64 -machine virt -m 128M -gdb tcp::1234 -S -kernel a.out 

gdb , a.out , :

 riscv64-unknown-elf-gdb --tui a.out GNU gdb (GDB) 8.2.90.20190228-git Copyright (C) 2019 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "--host=x86_64-apple-darwin17.7.0 --target=riscv64-unknown-elf". Type "show configuration" for configuration details. For bug reporting instructions, please see: <http://www.gnu.org/software/gdb/bugs/>. Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help, type "help". Type "apropos word" to search for commands related to "word"... Reading symbols from a.out... (gdb) 

gdb gdb , qemu :

 (gdb) target remote :1234 │ Remote debugging using :1234 

main:

 (gdb) b main Breakpoint 1 at 0x8000001e: file add.c, line 2. 

:

 (gdb) c Continuing. Breakpoint 1, main () at add.c:2 

, 2! , - L , PC: L2 , PC:0x8000001e . , :



gdb : -s , info all-registers . . … , , !

ما التالي؟


اليوم حققنا الكثير ، وآمل أن تكون قد تعلمت الكثير! لم يكن لدي مطلقًا خطة رسمية لهذا المقال والمقالات اللاحقة ، لقد تابعت ما كان أكثر إثارة للاهتمام بالنسبة لي في كل لحظة. لذلك ، لست متأكدًا مما سيحدث بعد ذلك. أعجبني بشكل خاص الانغماس العميق في التعليمات jal، لذلك ربما في المقالة التالية سنأخذ المعرفة المكتسبة هنا كأساس ، ولكن استبدلها add.cببعض البرامج في مجمّع RISC-V الخالص. إذا كان لديك شيء محدد ترغب في رؤيته أو لديك أي أسئلة ، فافتح التذاكر .

شكرا للقراءة! آمل أن ألتقي في المقال القادم!

بالإضافة إلى ذلك


إذا أعجبك المقال وتريد معرفة المزيد ، تحقق من عرض مات جودبولت بعنوان "البتات بين البتات: كيف نصل إلى الرئيسية ()" من مؤتمر CppCon2018. إنها تتعامل مع الموضوع بشكل مختلف قليلاً عما نحن هنا. محاضرة جيدة حقا ، انظر لنفسك!

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


All Articles