في هذه المقالة ، نستكشف مختلف مفاهيم المستوى المنخفض (التجميع والتخطيط ، أوقات التشغيل البدائية ، المجمّع ، وغير ذلك) من خلال منظور بنية RISC-V ونظامها البيئي. أنا مطور ويب ، وأنا لا أفعل أي شيء في العمل ، لكن هذا مثير جدًا بالنسبة لي ، حيث جاء المقال! انضموا إلي في هذه الرحلة المحمومة إلى أعماق الفوضى المنخفضة المستوى.
أولاً ، دعنا نتحدث قليلاً عن RISC-V وأهمية هذه البنية ، وتكوين سلسلة أدوات RISC-V ، وتشغيل برنامج C بسيط على أجهزة RISC-V تمت مضاهاتها.
محتوى
- ما هو RISC-V؟
 
- تكوين QEMU وأدوات RISC-V
 
- مرحبا RISC-V!
 
- نهج ساذج
 
- رفع الستار الخامس
 
- بحث المكدس لدينا
 
- ترتيب
 
- توقف عن ذلك! Hammertime!وقت التشغيل!
 
- تصحيح ولكن الآن الحقيقي
 
- ما التالي؟
 
- بالإضافة إلى ذلك
ما هو 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 . اتبع 
التعليمات لنظام التشغيل الخاص بك لتثبيت. لدي ماك ، لذلك فقط أدخل أمر واحد:
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 .
 
قم بإنشاء رابط رمزي في 
/usr/local/bin لهذا الملف القابل للتنفيذ بحيث يمكنك تشغيله في أي وقت دون تحديد المسار الكامل إلى 
~/usys/riscv/riscv64-unknown-elf-gcc-<date>-<version>/bin/riscv64-unknown-elf-gcc .
 
وفويلا ، لدينا مجموعة أدوات 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.
 
يؤدي هذا إلى إنشاء ملف 
a.out ، والذي يتم تعيين 
gcc الافتراضية فيه على الملفات القابلة للتنفيذ. الآن قم بتشغيل هذا الملف في 
qemu :
 
لقد اخترنا الجهاز الذي 
جاء riscv-qemu الأصل .
الآن بعد أن تم تشغيل برنامجنا داخل QEMU مع خادم GDB على 
localhost:1234 ، نحن نتصل به مع عميل RISC-V GDB من محطة منفصلة:
 
ونحن داخل 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  
وأخيرًا ، حدد GDB 
continue (الأمر المختصر 
c ) حتى نصل إلى نقطة الإيقاف:
 (gdb) c Continuing. 
ستلاحظ بسرعة أن العملية لا تنتهي بأي شكل من الأشكال. هذا غريب ... ألا يجب أن نصل على الفور إلى نقطة التوقف 
b 5 ؟ ماذا حدث

هنا يمكنك رؤية العديد من المشاكل:
- لا يمكن العثور على واجهة المستخدم النص المصدر. يجب أن تعرض الواجهة الرمز الخاص بنا وأي نقاط توقف قريبة.
 
- لا يرى GDB سطر التنفيذ الحالي ( L??) ويعرض العداد 0x0 (PC: 0x0).
 
- بعض النص في سطر الإدخال ، والذي يبدو في مجمله كما يلي: 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).
 
يصعب قراءة بيانات Dtb نظرًا لأنها في الأساس تنسيق ثنائي ، ولكن توجد أداة مساعدة لسطر الأوامر 
dtc (برنامج التحويل البرمجي لجهاز شجرة) يمكنها تحويل الملف إلى شيء أكثر قابلية للقراءة.
 
ملف الإخراج هو 
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  
يحتوي هذا الملف على 
الكثير من المعلومات المثيرة للاهتمام ، أكثر بكثير مما يمكننا مناقشته في هذه المقالة. يتضمن الإخراج المفصل باستخدام 
--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 :
 
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. إنها تتعامل مع الموضوع بشكل مختلف قليلاً عما نحن هنا. محاضرة جيدة حقا ، انظر لنفسك!