عادةً ما يقوم نظام التشغيل (OS) بتخطيط الذاكرة لبرنامج ما وفق تخطيط عام:
| العنوان | منطقة الذاكرة |
|---|---|
| عالية | المكدس |
| ... | |
| الكومة | |
| قطاع القراءة والكتابة | |
| قطاع الكود/القراءة فقط | |
| منخفضة | محجوز |
تُنظَّم الذاكرة في قطاعات على هيئة أقسام، لكل منها أذونات مختلفة.
كانت جميع الدوال التي عرّفناها حتى الآن في section .text.
يحتوي هذا القسم على بيانات قابلة للتنفيذ، وهي للقراءة فقط.
وتُستخدم أقسام أخرى لتعريف متغيرات البيانات، وقد تكون للقراءة فقط أو للقراءة والكتابة، لكنها غير قابلة للتنفيذ.
.data
تُعرَّف البيانات المُهيّأة في section .data.
في NASM (المُجمِّع Netwide، وهو المُجمِّع المستخدم في هذا المسار)، يكون للمتغير المُهيّأ اسم، وتوجيه يحدد حجم البيانات، وقائمة قيم يفصل بينها فاصلة.
ويفصل بين كلٍّ من هذه العناصر مسافة، ويمكن اختياريًا أن تتبع التسمية علامة :.
أهم التوجيهات وأحجام البيانات المرتبطة بها هي:
| التوجيه | الحجم |
|---|---|
| db | 1 بايت |
| dw | 2 بايت |
| dd | 4 بايت |
| dq | 8 بايت |
على سبيل المثال، يعرّف هذا متغيرًا بحجم بايت اسمه space وقيمته 10:
section .data
space db 10
المتغيرات المُعرَّفة في section .data قابلة للتغيير، أي أنها للقراءة والكتابة.
كما أن لها مدة تخزين ساكنة، أي أنها توجد طوال مدة تشغيل البرنامج.
.rodata
يشبه section .rodata القسم section .data.
يحتوي القسمان على بيانات مُهيّأة تُعرَّف بالطريقة نفسها، ولهما مدة التخزين نفسها.
والفرق الرئيسي بينهما أن البيانات في section .rodata غير قابلة للتغيير، أي أنها للقراءة فقط.
الثوابت المُعرَّفة باستخدام equ تختلف عن تلك المُعرَّفة في section .rodata.
الثابت المُعرَّف باستخدام equ لا يشغل مساحة في الذاكرة، ويستبدله المُجمِّع بقيمته مباشرة.
بل هو في الحقيقة بديل عن تلك القيمة.
في المقابل، تُخزَّن الثوابت المُعرَّفة في section .rodata فعليًا في الذاكرة، ولها عنوان.
يجب أن يكون للبيانات المُعرَّفة اسم مرتبط بها. ويسمى هذا الاسم تسمية.
التسمية رمز يشفّر العنوان المحدد للبيانات في الذاكرة. العناوين في x86-64 قيم بطول 64 بت.
في NASM، لا تؤدي محاولة الوصول إلى البيانات مباشرةً عبر تسميتها إلى الحصول على الذاكرة المخصصة، بل إلى عنوانها:
section .data
example dq 27 ; this declares a 8-byte variable initialized with 27
section .text
fn:
mov rax, example ; this stores the address of the declared variable in rax, not its contents
...
للوصول إلى محتوى عنوان في الذاكرة، يجب فكّ إشارته. ويسمى هذا الإشارة غير المباشرة.
في NASM، يتم ذلك باستخدام []:
section .data
example dq -27 ; this declares a 8-byte variable initialized with -27
section .text
fn:
mov rax, [example] ; this dereferences example and access the value stored in memory (-27)
...
لكن هناك بعض الحالات التي قد يلتبس فيها حجم الذاكرة التي فُكّت إشارتها. وفي تلك الحالات، يجب استخدام بادئة تحدد هذا الحجم.
هذه أهم البادئات وأحجامها في برنامج نمطي على x86-64:
| البادئة | الحجم |
|---|---|
| byte | 1 بايت |
| word | 2 بايت |
| dword | 4 بايت |
| qword | 8 بايت |
يمكن كتابة التحميل نفسه مع ذكر الحجم صراحةً:
mov rax, qword [example] ; same dereference, size stated explicitly
من الممارسات الجيدة استخدام بادئة دائمًا عند فك إشارة الذاكرة.
تتم الكتابة إلى الذاكرة بالطريقة نفسها، عبر فك إشارة عنوان:
section .data
example1 db 10 ; example1 is a 1-byte memory location initialized with value 10
example2 dq -456 ; example2 is a 8-byte memory location initialized with value -456
example3 dd 54 ; example3 is a 4-byte memory location initialized with value 54
section .text
fn:
mov byte [example1], 20 ; example1 now has value 20
mov qword [example2], rdx ; example2 now has value equal to the contents in rdx
mov dword [example3], eax ; example3 now has value equal to the contents in eax
لاحظ أنه يمكنك استخدام معطيات الذاكرة في معظم التعليمات دون تحميل محتواها في سجل أولًا. لكن لا يمكن عادةً استخدامها في معطى المصدر ومعطى الوجهة معًا، بل في أحدهما فقط:
section .data
example4 dw 4
example5 dq -8
example6 dd 15
section .text
fn:
add word [example4], 5 ; example4 is now a 2-byte memory location with the value 4 + 5 = 9
imul rax, qword [example5] ; rax = rax * (-8)
; this is not possible -> sub dword [example6], dword [example6]
lea
رغم أنه يمكن استخدام mov لتخزين عنوان متغير في سجل، فإن هناك تعليمة مخصصة لهذا الغرض تحديدًا: lea.
تستخدم هذه التعليمة معطى على صيغة موضع ذاكرة، لكنها لا تقرأ الذاكرة. بل تحسب تعبير العنوان الفعّال وتكتب النتيجة في معطى الوجهة:
lea rax, [example] ; this stores the address of 'example' in rax
من الأسلوب المألوف أكثر استخدام lea لحساب عناوين الذاكرة وتخزينها في السجلات.
عند الوصول إلى مواضع الذاكرة، فإن السلوك الافتراضي في NASM هو توليد عناوين مطلقة، أي عناوين ذاكرة ثابتة.
لأسباب أمنية، غالبًا ما تُبنى الملفات التنفيذية كـ PIE (ملف تنفيذي مستقل عن الموضع)، حيث توضع مناطق الذاكرة في مواضع عشوائية.
وفي ملف PIE، لا يُعرف العنوان النهائي للمتغير وقت الربط.
لذلك يحسب الكود العناوين بدلًا من ذلك كإزاحة عن القيمة الموجودة في سجل خاص يسمى rip، وهو يشير إلى التعليمة التالية التي ستُنفَّذ.
ويسمى هذا عادةً العنونة النسبية إلى rip.
في NASM يمكنك طلب وصول نسبي إلى rip باستخدام العامل rel:
mov rax, qword [rel variable]
يمكن أيضًا جعل العنونة النسبية هي الافتراضية لملف مصدر ما باستخدام default rel في أوله.
تُجمَّع جميع التمارين في هذا المسار وتُربط كـ PIE، لذلك ينبغي استخدام rel لتوليد عناوين نسبية.
التسميات (الدوال والبيانات) المُعرَّفة في أي قسم (مثل .text و.data و.rodata) تكون مرئية داخل ملف المصدر نفسه.
وإذا عُرِّفت بـ global، تكون مرئية لملفات المصدر الأخرى أيضًا.
وبالعكس، تكون التسميات المُعرَّفة في ملفات مصدر أخرى مرئية لملف المصدر الحالي إذا عُرِّفت بـ extern.
وفي هذه الحالة، لا يوجد أي إشارة إلى حجم البيانات في لغة التجميع، ويجب معرفة ذلك مسبقًا.
default rel
section .data
global number1 ; 'number1' is a variable visible to other source files
number1 db 200
extern number2 ; 'number2' is a variable visible to the current source file, but defined in another
section .text
extern sum ; sum is a function visible to the current source file, but defined in another
fn:
mov dil, byte [number1]
mov sil, byte [number2]
call sum
...
صديقك خوسيه مدرّس في مدرسة محلية. راودته فكرة لبعض التجارب الممتعة التي تُظهر كيف يمكن دمج الألوان لإنتاج ألوان مختلفة.
وقد طلب مساعدتك في إجراء تلك التجارب.
يُمثَّل اللون في هذا التمرين بعدد مقداره 32 بت (4 بايت)، يرمّز قيمة RGB الخاصة به.
تتكوّن قيمة RGB من 3 قنوات، هي الأحمر و_الأخضر_ و_الأزرق_، تشغل كل واحدة منها 8 بتات (1 بايت).
تُخصَّص البايت الرابع عادةً لقناة ألفا، لكن قيمتها في هذا التمرين ستكون فارغة (0).
قيم كل لون مخزّنة بالفعل في جدول مُعرَّف في ملف مصدر آخر. ويُعرَّف اللون بعنوان فريد في هذا الجدول.
عرّف دالة get_color_value تُرجع قيمة 32 بت للون.
تأخذ هذه الدالة معاملًا هو عنوان صالح لهذا اللون في جدول الألوان.
get_color_value(black)
// => 0
تلميح - 32 بت تكافئ 4 بايت.
لكي يمزج خوسيه ألوانًا مختلفة، سيحدّد أولًا لونًا أساسيًا ويغيّر فقط اللون الثانوي المدمج معه.
عرّف دالة add_base_color تحفظ قيمة 32 بت للون في المتغير base_color، حتى يمكن استخدامها لاحقًا.
لا تُرجع هذه الدالة أي قيمة، وتأخذ معاملًا هو عنوان اللون في جدول الألوان.
المتغير base_color تعرّفه أنت، ويجب أن يكون متاحًا من ملفات مصدر أخرى.
لن يكون هناك أكثر من لون أساسي واحد في الوقت نفسه. وإذا أُضيف لون أساسي جديد، فسيُهمَل القديم.
افتراضيًا، عند بدء البرنامج، يجب تهيئة base_color بقيمة 32 بت للون الأبيض، وهي 0xFFFFFF00.
تلميح - يقبل NASM الأعداد المعرّفة بالنظام الست عشري باستخدام 0x في البداية، كما في 0xFFFFFF00.
يتوقع خوسيه إجراء تركيبات كثيرة باستخدام الألوان الأولية، لذا يريد فصلها لوصول سريع.
ولأنه يستخدم RGB لتمثيل الألوان، فإن الألوان الأولية هي:
RED، بقيمة 0xFF000000.GREEN، بقيمة 0x00FF0000.BLUE، بقيمة 0x0000FF00.عرّف ثابتًا واحدًا لكل لون من تلك الألوان. ويجب أن تكون هذه الثوابت متاحة من ملفات مصدر أخرى.
يجب دمج الألوان وفق combining_function معرّفة في ملف مصدر آخر.
تأخذ هذه الدالة معاملين هما قيمتا 32 بت لكل من base_color واللون الثانوي المراد مزجه معه.
وتُرجع قيمة 32 بت للون المدمج.
عرّف دالة make_color_combination تدمج لونين وتحفظ النتيجة في الذاكرة.
لا تُرجع هذه الدالة أي قيمة، وتأخذ معاملين بهذا الترتيب:
لاحظ أن combining_function قد تُعدّل القيم في السجلات التي تستخدمها.
تأكد من حفظ أي متغير تحتاجه في الذاكرة قبل استدعاء الدالة.
سجّل في Exercism لتتعلّم وتتقن x86-64 Assembly عبر 22 مفهومًا130 تمرينًا، وإرشاد بشري حقيقي، وكل ذلك مجانًا.