في هذا التمرين، ستُحدِّد الصيغة ونموذج التنفيذ للغة صغيرة مستوحاة من لغة التجميع x86-64.
الهدف هو إضفاء صيغة رسمية على كيفية كتابة الكود وتحليله وتنفيذه داخل بيئة مقيَّدة.
سيُكتب كود التجميع الخاص بك داخل تركيب خاص:
let program := assemble!(
-- assembly code here
)
يجب أن يُنتج هذا التركيب برنامجًا يحاكي دالةً تنفِّذ كود التجميع وتُرجع قيمة.
ثم يُستدعى هذا البرنامج بالصيغة التالية، حيث a وb وc وغيرها وسائط:
let result := program(a, b, c, ...)
جميع الوسائط والقيمة المُرجَعة هي أعداد صحيحة موقَّعة بطول 64 بت. ويُسمح بـ 6 وسائط كحدٍّ أقصى.
تُمرَّر القيمة المُرجَعة وجميع الوسائط عبر السجلات. يجب أن تدعم لغتك السجلات التالية:
| السجل | الدور |
|---|---|
rdi |
الوسيط الأول |
rsi |
الوسيط الثاني |
rdx |
الوسيط الثالث |
rcx |
الوسيط الرابع |
r8 |
الوسيط الخامس |
r9 |
الوسيط السادس |
rax |
القيمة المُرجَعة |
تبدأ السجلات بقيمة افتراضية هي 0، إلا إذا كانت تُستخدم لتمرير الوسائط إلى الدالة.
على سبيل المثال، في البرنامج التالي، تكون قيمة جميع السجلات 0 باستثناء rdi، الذي يُهيَّأ بالقيمة 10، وrsi، الذي يُهيَّأ بالقيمة -20:
program(10, -20)
تُخزَّن القيمة المُرجَعة للبرنامج دائمًا في rax.
أسماء السجلات لا تُميّز بين حالة الأحرف.
هذا يعني أن rax وRAX وrAx تشير كلها إلى السجل نفسه.
يمكن أن يأخذ كل سطر في كود التجميع أحد شكلين:
ما لم تُعدِّلها تعليمة ما، يسير تدفّق تنفيذ البرنامج بشكل خطي. هذا يعني أن السطر السابق يُنفَّذ بالكامل قبل تنفيذ السطر الذي يليه.
للوسوم الصيغة التالية:
<label>:
لا تُغيِّر قيمة أي سجل، ولا يكون لها أي تأثير على البرنامج غير تحديد مواضع معيّنة في الكود لتتمكن التعليمات من استخدامها.
الوسوم تُميّز بين حالة الأحرف.
هذا يعني أن Start وstart وsTart وسوم مختلفة.
لمعظم التعليمات الصيغة التالية:
<opcode> <destination>, <source>
يشير opcode إلى العملية الجاري تنفيذها، باستخدام قيم طرفَي العملية: الهدف والمصدر.
وتُخزَّن نتيجة العملية في طرف العملية الهدف.
على سبيل المثال، تجمع التعليمة add القيمتين في طرفَي العملية الهدف والمصدر وتخزّن هذه النتيجة في طرف العملية الهدف.
طرف العملية الهدف دائمًا سجل، بينما قد يكون طرف العملية المصدر سجلًا أو عددًا حرفيًا.
هذه قائمة بالتعليمات التي يجب أن يدعمها برنامجك:
| رمز العملية | العملية المنفَّذة |
|---|---|
mov |
الهدف := المصدر |
add |
الهدف := الهدف + المصدر |
sub |
الهدف := الهدف - المصدر |
mul |
الهدف := الهدف * المصدر |
div |
الهدف := الهدف / المصدر |
xor |
الهدف := الهدف ^^^ المصدر |
and |
الهدف := الهدف &&& المصدر |
or |
الهدف := الهدف ||| المصدر |
shl |
الهدف := الهدف <<< المصدر |
shr |
الهدف := الهدف >>> المصدر |
وإلى جانب تلك، يجب أن يدعم برنامجك التعليمة cmp ذات طرفَي العملية.
لا تُعدِّل هذه التعليمة أي سجل، بل تقارن طرفَي العملية الهدف والمصدر، وتضبط حالة داخلية في البرنامج على أحد ثلاثة خيارات:
destination > source
destination == source
destination < source
وكيفية تتبّعك لهذه الحالة داخليًا متروكة لك.
هناك بعض التعليمات التي تُغيِّر تدفّق البرنامج، فتَنقُل التنفيذ إلى موضع آخر في الكود. وتُسمّى تعليمات القفز.
تأخذ جميعها طرف عملية واحدًا فقط، وهو وسم يشير إلى هدف القفز، أي الموضع في الكود الذي سيستمر التنفيذ منه:
<opcode> <label>
تُجري التعليمة jmp القفز إلى الوسم الهدف دائمًا.
أما تعليمات القفز الأخرى فلا تُجري القفز إلا إذا كانت الحالة الداخلية للبرنامج مضبوطة على قيمة معيّنة.
هذا يعني أنها تُسبق عادةً بتعليمة cmp.
هذه هي تعليمات القفز التي يجب أن يدعمها برنامجك:
| التعليمة | العملية المنفَّذة |
|---|---|
jmp |
قفز غير مشروط. يُنفَّذ القفز دائمًا |
je |
يقفز إذا كانت الحالة الداخلية يساوي |
jl |
يقفز إذا كانت الحالة الداخلية أصغر من |
jg |
يقفز إذا كانت الحالة الداخلية أكبر من |
رموز التعليمات، سواء كانت ذات طرفَي عملية أو طرف عملية واحد، لا تُميّز بين حالة الأحرف.
هذا يعني أن add وADD وaDd تشير كلها إلى التعليمة نفسها.