لغة التجميع المكتوبة يدويًا مليئة بالتكرار. تظهر المجموعة القصيرة نفسها من التعليمات مرارًا وتكرارًا، مع اختلاف ضئيل، ولا توجد سوى قليل من التجريدات لتقليل الكود المتكرر. وليس هذا فقط، فالكود بلغة التجميع يتطلب إدارة قيم بلا دلالة واضحة. فلا توجد معاملات مُسمّاة، ويجب على المبرمج أن يتعامل يدويًا مع معظم تفاصيل الآلة الكامنة.
يوفّر NASM، وهو المُجمِّع المستخدم في المسار، بعض الأدوات التي تساعد المبرمج في كلتا المشكلتين. تعمل هذه الأدوات في وقت التجميع، قبل أن تصل أي تعليمة إلى المعالج. وهي ترشد المُجمِّع إلى كيفية إعادة كتابة النص المصدري قبل تحويله حتى إلى الكود الآلي.
وأهم هذه الأدوات هي الماكرو، الذي يمكن التفكير فيه كوصفة للمُجمِّع. يُوسَّع الماكرو في الموضع الذي يُستخدم فيه، وكأن الكود قد كُتب هناك يدويًا.
يمكن للماكرو أن يأخذ عددًا من الرموز الحرفية كمعاملات، ما يجعله قابلًا لإعادة الاستخدام في سياقات مختلفة.
لكنه ليس دالة: فلا يوجد call ولا ret ولا نقل للتحكم إلى نقطة أخرى في الكود.
أبسط تعريف يمنح اسمًا لقيمة أو تعبير قصير باستخدام %define:
%define WIDTH 8
%define BUFFER_SIZE (WIDTH * 16)
كل ظهور لاحق لـ WIDTH يُستبدل بـ 8 قبل التجميع.
الاستبدال نصي، لذا يُستبدل الاسم في أي موضع يظهر فيه، سواء في معامل العملية أو داخل تعريف آخر.
بالنسبة إلى الثوابت الرقمية، فضّل equ على %define.
الماكرو أحادي السطر المُعلَن بـ %define لا يُوسَّع إلا عند استخدامه، ويمكن إعادة إسناده أو إلغاء تعريفه.
أما ثابت equ، فيُقيَّم عند نقطة التعريف ولا يُعدَّل أبدًا.
يمكن أن يأخذ %define أيضًا معاملات مُسمّاة، فيتصرف كدالة صغيرة مضمّنة:
%define double(x) ((x) + (x))
mov eax, double(WIDTH) ; assembled as mov eax, ((8) + (8))
ضع الجسم بأكمله وكل معامل بين الأقواس الهلالية.
مع %define square(x) x*x، يُوسَّع الاستدعاء square(1+1) إلى 1+1*1+1، وهو 3.
ومكتوبًا بالصيغة %define square(x) ((x)*(x))، يُوسَّع الاستدعاء نفسه إلى ((1+1)*(1+1))، وهو 4.
الأسماء المُعرَّفة بـ %define حساسة لحالة الأحرف.
البديل %idefine يجعل الاسم غير حساس لحالة الأحرف، إن أُريد ذلك.
الماكرو أحادي السطر مفيد لإعطاء معنى لأجزاء مختلفة من الكود. وإذا استُخدم بعناية، فإنه يحسّن قابلية القراءة ويقلل احتمال الأخطاء المطبعية. على سبيل المثال، يمكن تعريف سجل له غرض مخصص داخل دالة باسم غرضه:
%define COUNTER rcx
...
.loop:
...
dec COUNTER
jnz .loop
...
يمكن التقاط تتابع كامل من الكود، بأي عدد من الأسطر، باستخدام %macro، ويُغلق بـ %endmacro.
ويشير الرقم بعد الاسم إلى عدد الوسائط التي يأخذها الماكرو، ويمكن أن يكون صفرًا.
وعند تمرير الوسائط، تُفصل بفاصلة (,):
%macro example_macro 0
...
%endmacro
الوسائط في الماكرو متعدد الأسطر ليست مُسمّاة.
ويُشار إليها بـ %1 و%2 وهكذا، بالترتيب الذي تُمرَّر به:
; Multiplies two values, returning the result in the first argument
%macro mult_two 2 ; %1 = accumulator, %2 = value
imul %1, %2
%endmacro
mult_two eax, ecx ; multiplies eax and ecx and returns the result into eax
mult_two r8, r9 ; multiplies r8 and r9 and returns the result into r8
لاحظ أن الوسائط يجب أن تكون صياغتها صحيحة بالنسبة إلى العمليات المنفَّذة في جسم الماكرو.
هنا يتم تجميع كلٍّ من mult_two eax, ecx وmult_two r8, r9، لكن mult_two eax, cl لن يُجمّع، لأن imul eax, cl غير معرّفة.
الماكرو الذي يعرّف تسمية عادية يتعطل لحظة استخدامه مرتين، لأن التسمية ستُعرَّف حينها مرتين أيضًا.
لاستخدام تسميات داخل ماكرو قد يُستدعى أكثر من مرة، من الضروري أن تسبق هذه التسمية بـ %%.
وهذا يوجّه المُجمِّع إلى إعطاء هذه التسمية اسمًا مختلفًا وفريدًا في كل توسيع:
; clobbers rcx
%macro clear_n 2 ; %1 = base address, %2 = count of dwords to clear
xor ecx, ecx
%%loop:
mov dword [%1 + rcx*4], 0
inc ecx
cmp ecx, %2 ; %2 is compared with ecx: it is an immediate or a 32-bit operand
jb %%loop
%endmacro
clear_n rdi, 4 ; gets its own %%loop
clear_n rsi, 8 ; a different %%loop, no clash
يمكن تكرار كتلة كود من سطر واحد باستخدام times.
وهذا مفيد بشكل خاص عند تعريف قيم متكررة في الذاكرة:
section .data
negative_block: times 16 db -1
; this assembles to 16 db -1 in sequence, i.e., an array of 16 bytes, all equal to -1
لتكرار الكود متعدد الأسطر، يمكننا استخدام %rep بدلًا من ذلك:
%rep 2 ; number of repetitions: 2
add rcx, 32
mov dword [rsi + rcx], 0
%endrep
لاحظ أن عدد التكرارات يُحدَّد مباشرة بعد %rep، وهناك %endrep لإغلاق الكتلة المتكررة.
من الشائع استخدام عدّاد في وقت التجميع داخل كتلة %rep، تمامًا كما تستخدم حلقة وقت التشغيل عدّادًا.
وبينما يمكن تعريف ثابت صحيح في وقت التجميع بـ equ، فإننا نستخدم %assign للمتغيرات الصحيحة في وقت التجميع.
وعلى عكس %define، يُقيَّم %assign فورًا إلى عدد ويمكن إعادة إسناده:
xor eax, eax
%assign i 0
%rep 4
add eax, i ; add 0, then 1, then 2, then 3
%assign i i + 1
%endrep
بعد تشغيل المعالج القبلي، تختفي الحلقة وتبقى أربع تعليمات add، مع تضمين القيم الفورية 0 و1 و2 و3.
هذا هو فك الحلقات الذي يقوم به المُجمِّع: لا سجل عدّاد ولا تفريع، مجرد كود خطّي.
تتوفر للمعالج القبلي شروط متنوعة. وهي تختار بين عدد من المسارات المختلفة، فتوسّع جسمها في موضعه، لكن فقط عندما يتحقق شرطها.
%if ... %elif ... %else ... %endif تختار فرعًا من اختبار رقمي:
%if WIDTH > 4
mov eax, 2
%elif WIDTH > 2
mov eax, 1
%else
mov eax, 0
%endif
غالبًا يُحتفظ بالماكرو في ملف خاص به، ويُضم إلى كل مصدر يحتاجه باستخدام %include:
%include "patterns.mac"
يُدرج الملف المسمّى حرفيًا في ذلك الموضع، قبل التجميع، تمامًا وكأن محتوياته قد كُتبت في مكانها. وهذا يُبقي التعريفات في موضع واحد ويتيح لعدة ملفات مصدرية مشاركتها.
إذا كان ملف الماكرو قد يُجمَّع مستقلًا بذاته، فينبغي أن تكون امتداده .asm.
لاحظ أن الماكرو ليس له ارتباط.
فحتى إذا جُمّع ملف الماكرو، فإن ماكروه لا يكون مرئيًا إلا لملف آخر يضمّه بـ %include.
وإلا، فيمكن لملف مخصص لـ %include فقط أن يحمل أي امتداد.
واستخدام .asm أو .mac عُرف شائع.
أنت تتولّى صيانة مكتبة الماكرو التي بُني عليها محرّك تنضيد المجلة. المحرّك نفسه، أي الدوال التي تنسّق كل صفحة، ثابت ولا تكتبه أنت. ما تكتبه أنت هو الآليات التي يُبنى منها المحرّك في وقت التجميع.
لا يطلب منك هذا التمرين كتابة أي دالة، بل الماكرو فقط. ثم تُضمَّن هذه الماكروات وتُستدعى في دوال معرّفة مسبقًا في ملف آخر.
لديك أربع مهام.
يُستدعى كل ماكرو بالاسم نفسه وترتيب الوسائط نفسه الذي تحدده مهمته، فاكتبها تمامًا كما هي. وإلا فلن يُجمَّع الملف.
تنسيق المجلة بضعة أعداد ثابتة، وبقية التخطيط يشير إليها بالاسم فقط. عليك أن تمنح كل واحد منها اسمًا مقروءًا في وقت التجميع:
COLUMN_COUNT يساوي 4
COLUMN_STRIDE يساوي 18
SPACE يساوي 0x20، وهو البايت الفارغRULE يساوي 0x3D، وهو البايت =
RULE_WIDTH يساوي 4
PLAIN يساوي 0x2E، وهو البايت .
RULED يساوي 0x2D، وهو البايت -
BOXED يساوي 0x23، وهو البايت #
يُبلّغ المحرّك عن هذه القيم بالترتيب أعلاه.
out = {4, 18, 32, 61, 4, 46, 45, 35} // the eight named values, in order
fill_run
يُفرّغ المحرّك مساحة التنضيد ويرسم خطوط الترويسة عن طريق طبع متتالية من بايت واحد مكرّر. التقط هذه الخطوة في ماكرو متعدّد الأسطر يمكن للمحرّك استدعاؤه كلما احتاج إلى ملء متتالية.
أولًا، عرّف اسمًا مقروءًا COUNTER للسجل rcx.
ثم عرّف الماكرو fill_run الذي ينبغي أن يملأ مصفوفة بـ N نسخة من بايت.
يأخذ ثلاثة وسائط بهذا الترتيب:
N) المطلوب كتابتها، ويُمرَّر في سجل عام بطول 64 بت أو كقيمة فورية.fill_run rdi, rax, SPACE
fill_run rdx, RULE_WIDTH, RULE
يجوز للماكرو fill_run أن يطمس COUNTER بحرية، ولكن ليس أي سجل آخر.
لاحظ أن عدد البايتات قد يُمرَّر في سجل عام بطول 64 بت، مثل rax أو rsi، أو كقيمة فورية، مثل 10 أو 4.
ويمكنك اعتبار أن هذا العدد ليس سالبًا أبدًا.
يُستدعى هذا الماكرو ثلاث مرات، في دالتين مختلفتين.
length = 8
page = {32, 32, 32, 32, 32, 32, 32, 32}
width = 8
page = {61, 61, 61, 61, 32, 32, 32, 32} // '====' then spaces
lay_columns
الشبكة ثابتة بالنسبة إلى التنسيق، لذا يمكن حساب بدايات الأعمدة في وقت التجميع.
عرّف الماكرو lay_columns، الذي يكتب عددًا من الإزاحات مقداره COLUMN_COUNT، كل واحدة عدد صحيح بطول 64 بت.
يأخذ وسيطين بهذا الترتيب:
lay_columns rsi, rax
عدد الإزاحات التي ستُكتب في عنوان الوجهة ثابت ويساوي COLUMN_COUNT.
كل إزاحة قيمة بطول 64 بت تُحسب بالصيغة offset[i] = margin + i * COLUMN_STRIDE، حيث i هو فهرس الإزاحة بدءًا من 0، وmargin هو الوسيط الثاني.
يجوز لهذا الماكرو أن يطمس وسيطيه من السجلات، ولا غيرهما. يُستدعى مرة واحدة.
margin = 3
out = {3, 21, 39, 57}
set_header
لكل قسم من الأقسام الثابتة الثلاثة نمط ترويسة يحدده التنسيق، لذا فالبايت معروف في وقت التجميع.
عرّف set_header، وهو يأخذ وسيطين بهذا الترتيب:
0 النمط البسيط، و1 المسطّر، و2 المؤطّر.عنوان الوجهة هو تعبير عن عنوان ذاكرة فعّال.
وهو يتألف من سجل أساس يمكن جمعه مع سجل فهرس مضروب في 1 أو 2 أو 4 أو 8، وقيمة فورية:
set_header rdx+8*rcx+2, 1
اختر بايت الترويسة (PLAIN أو RULED أو BOXED) عبر التجميع الشرطي (%if / %elif / %else) بدلًا من تفرّع أثناء التشغيل.
ينبغي ألا يطمس هذا الماكرو أي سجل. يُستدعى ثلاث مرات في الدالة نفسها.
pitch = 4
out = {46, 0, 0, 0, 45, 0, 0, 0, 35} // '.', '-', '#'
سجّل في Exercism لتتعلّم وتتقن x86-64 Assembly عبر 22 مفهومًا130 تمرينًا، وإرشاد بشري حقيقي، وكل ذلك مجانًا.