العدد الصحيح تجريد يمثّل الأعداد الصحيحة، مثل 4 و-2 و0 و64532.
ولتمثيل عدد صحيح كتسلسل من البايتات، يُستخدم الترميز الثنائي. في هذا الترميز، يمثّل كل بت في التسلسل أسًا مختلفًا من أسس العدد 2، وتزداد القيمة كلما زاد فهرس البت من اليمين إلى اليسار.
إذا كان العدد لا يمكن أن يكون إلا غير سالب، فإنه يُسمى عددًا غير مؤشَّر.
تُمثَّل الأعداد غير المؤشَّرة مباشرةً كمجموع قوى العدد 2 المقابلة لجميع البتات المضبوطة في تسلسلها.
يمتد نطاق الأعداد الصحيحة غير السالبة القابلة للتمثيل في سجل من 0 (دون أي بت مضبوط) إلى 2⁶⁴ - 1 (مجموع البتات الـ 64 كلها مضبوطة).
ويتم توسيع عدد غير مؤشَّر إلى حجم أكبر بملء جميع البتات العليا بالقيمة 0، بحيث لا يساهم أي بت جديد في القيمة.
وهذا ما يُسمى امتداد الصفر.
التعليمة movzx (حيث z تعني صفر) تمدّ طرف المصدر ذا 8 أو 16 بت بامتداد الصفر إلى طرف وجهة أكبر.
أما طرف المصدر ذو 32 بت فيُمَدّ دائمًا بامتداد الصفر إلى جميع بتات طرف الوجهة الـ 64 عبر تعليمة mov بسيطة.
إذا كان العدد الصحيح قادرًا على أخذ قيم موجبة أو سالبة، فإنه يُسمى عددًا مؤشَّرًا.
ولتمثيل الأعداد السالبة، تستخدم x86-64 تمثيل متمم الاثنين.
في متمم الاثنين، تُمثَّل الأعداد المؤشَّرة أيضًا كمجموع قوى العدد 2 المقابلة للبتات المضبوطة. غير أنه إذا كان البت الأعلى مضبوطًا، فإنه يُطرح بدلًا من أن يُضاف إلى الباقي.
وبما أن هذا البت يقابل قيمة أكبر من مجموع كل البتات الأخرى، فعمليًا يعني ذلك أن أي عدد يكون فيه هذا البت مضبوطًا هو عدد سالب دائمًا. ويُسمى هذا البت الخاص بت الإشارة.
أما توسيع عدد مؤشَّر إلى حجم أكبر فيعني ملء كل بت علوي جديد بنسخة من بت الإشارة، حتى تبقى القيمة محفوظة. وهذا ما يُسمى امتداد الإشارة.
التعليمة movsx (حيث s تعني إشارة) تمدّ طرف المصدر ذا 8 أو 16 بت بامتداد الإشارة إلى طرف وجهة أكبر.
وتفعل صيغة من movsx تُسمى movsxd الأمر نفسه من طرف مصدر ذي 32 بت إلى طرف وجهة ذي 64 بت.
ويمكن استخدام التعليمة neg لتغيير إشارة عدد.
في لغة التجميع، لا توجد طريقة لمعرفة ما إذا كان تسلسل من البايتات يمثل عددًا مؤشَّرًا أم غير مؤشَّر. ومسؤولية المبرمج أن يمنح هذه البايتات معنى.
ويمكن أن يساعد استخدام التعليقات كثيرًا في هذه المهمة.
ذُكر في مفهوم سابق أنه يمكن استخدام عدد ثابت، مثل 4 أو -15، كطرف مصدر لكثير من التعليمات.
وتُسمى هذه الأعداد القيم المباشرة.
القيمة المباشرة لا تُحفظ في سجل ولا في الذاكرة: بل تُضمَّن داخل التعليمة نفسها. وفي معظم التعليمات، لا تتجاوز المساحة المحجوزة لها 32 بت، مهما كان حجم طرف الوجهة.
وعندما يكون طرف الوجهة بعرض 64 بت، تُمَدّ تلك البتات الـ 32 بامتداد الإشارة لملئه. فيُملأ النصف العلوي من الطرف بالكامل بنسخ من البت الأعلى للقيمة المباشرة، لذا لا يمكن كتابة بهذه الطريقة إلا عدد يقع في نطاق عدد صحيح مؤشَّر ذي 32 بت:
add rax, -1 ; the immediate is sign-extended, so all 64 bits of rax are affected
add rax, 2147483647 ; the largest immediate an instruction like this accepts
ولا يمكن استخدام عدد خارج هذا النطاق كقيمة مباشرة.
والاستثناء من هذه القاعدة هو mov، الذي يمكنه أن يأخذ قيمة مباشرة كاملة من 64 بت عندما يكون طرف الوجهة سجلًا.
وإذا احتجت إلى قيمة مباشرة من 64 بت، فاستخدم mov أولًا لتحميلها في سجل، ثم استخدم ذلك السجل:
mov rax, 3435973837 ; this works, mov can take a 64-bit immediate
mov rdx, 18446744073709551615 ; the largest immediate mov accepts
sub rdx, rax
لاحظ أن القيمة المباشرة السالبة والعدد غير المؤشَّر ذا التمثيل البتي نفسه متكافئان، ويُجمَّعان إلى القيمة ذاتها تمامًا:
mov rax, -1 ; rax = 18446744073709551615
mov rax, 18446744073709551615 ; rax = -1
يمكن حساب جمع عددين باستخدام التعليمة add.
وهناك أيضًا التعليمة inc ذات الطرف الواحد، التي تضيف 1 إلى القيمة الموجودة في طرفها:
inc rax ; rax = rax + 1
وتجري عملية جمع عددين صحيحين بالطريقة نفسها لكل من الأعداد غير المؤشَّرة والمؤشَّرة.
تُنفَّذ عملية طرح عددين صحيحين باستخدام التعليمة sub.
وهناك أيضًا التعليمة dec ذات الطرف الواحد، التي تطرح 1 من القيمة الموجودة في طرفها:
dec rax ; rax = rax - 1
كما تجري عملية طرح عددين صحيحين أيضًا بالطريقة نفسها لكل من الأعداد غير المؤشَّرة والمؤشَّرة.
توجد تعليمتان مختلفتان لإجراء الضرب بين عددين في x86-64.
وكقاعدة عامة، يستخدم الضرب غير المؤشَّر التعليمة mul، بينما يستخدم الضرب المؤشَّر التعليمة imul.
تأخذ التعليمة mul الصيغة التالية ذات الطرف الواحد، حيث يكون src هو طرف المصدر:
mul src
ويمكن أن تأخذ التعليمة imul صيغة ذات طرف واحد، أو طرفين، أو ثلاثة أطراف:
imul src
imul dest, src
imul dest, src1, src2
يُستخدم سجلان ضمنيًا لإجراء الضرب بالصيغة ذات الطرف الواحد: rax وrdx.
وإذا كان الضرب يشمل عددين من 64 بت، فستكون البتات الـ 64 الدنيا من النتيجة في rax والبتات الـ 64 العليا في rdx.
ويُسمى هذا عادةً rdx:rax، للإشارة إلى أن السجلين يُستخدمان معًا:
mul rcx ; rax = lower 64 bits of rax * rcx
; rdx = upper 64 bits of rax * rcx
ويحدث الأمر نفسه مع أحجام الأطراف الأخرى.
فمثلًا، إذا جرى ضرب عددين من 32 بت، فسيُستخدم eax وedx.
والاستثناء هو الضرب بين بايتين.
في هذه الحالة، سيُستخدم ax بدلًا من dl:al.
وسيحصل الجزء الأدنى من ax (al) على البتات الثماني الدنيا من ناتج الضرب، بينما سيحصل الجزء الأعلى (ah) على البتات الثماني العليا.
السجلات المستخدمة ضمنيًا في الضرب، مثل rax وrdx، يُكتب فوق محتواها دائمًا.
لذا ينبغي حفظ القيم الموجودة في تلك السجلات قبل العملية إذا كنت ستحتاجها لاحقًا.
الصيغة ذات الطرفين من imul لها طرف وجهة صريح وتتبع الصيغة المعتادة.
ولا يُستخدم rdx.
بل يُقتطع الناتج ليتّسع في طرف الوجهة.
imul r8, r9 ; r8 = lower 64 bits of r8 * r9
الصيغة ذات الأطراف الثلاثة من imul لها طرفا مصدر، ثانيهما قيمة مباشرة دائمًا (عدد ثابت).
ويُضرب طرفا المصدر معًا، ثم يُقتطع الناتج ويوضع في طرف الوجهة:
imul r8, r9, 100 ; r8 = lower 64 bits of r9 * 100
لاحظ أن طرف الوجهة لا يُستخدم في الضرب. بل يستقبل الناتج فقط.
يقتطع الضرب ذو الطرفين والضرب ذو الأطراف الثلاثة الناتج ليتّسع في حجم طرف الوجهة.
أما الضرب ذو الطرف الواحد فيحفظ النطاق كاملًا، لكنه عادةً يُقسَّم بين سجلين، هما rdx وrax.
لذا يفيد أحيانًا توسيع الأطراف قبل الضرب لإفساح المجال لناتج الضرب كاملًا في سجل واحد. فيُمَدّ الطرف غير المؤشَّر بامتداد الصفر، بينما يُمَدّ الطرف المؤشَّر بامتداد الإشارة:
movzx eax, di ; di and si hold unsigned 16-bit numbers
movzx ecx, si
mul ecx ; the 32-bit product fits in eax, and edx is cleared
وكما هو الحال مع الضرب، توجد أيضًا تعليمتان لإجراء القسمة بين عددين.
يستخدم القسمة غير المؤشَّرة التعليمة div، بينما تستخدم القسمة المؤشَّرة التعليمة idiv.
وتعمل كلتا التعليمتين بطرف واحد فقط:
div src
idiv src
تستخدم القسمة ذات 16 بت و32 بت و64 بت كلا من dx:ax وedx:eax وrdx:rax على الترتيب كمقسوم.
وفي هذه الحالات، يعمل السجلان معًا لتكوين قيمة من 2N بت، حيث N هو حجم العملية (16 بت أو 32 بت أو 64 بت).
ثم تُقسَّم هذه القيمة على طرف المصدر.
فيُكتب خارج القسمة في ax أو eax أو rax، ويُكتب الباقي في dx أو edx أو rdx، وفقًا لحجم العملية.
والقسمة بين بايتين حالة خاصة: فبدلًا من استخدام dl:al، يُستخدم ax.
فتحصل البتات الثماني الدنيا من ax (al) على خارج القسمة، وتحصل البتات الثماني العليا (ah) على الباقي.
لاحظ أنه ينبغي ضبط جميع بتات المقسوم بشكل مناسب قبل القسمة.
فأي بت مضبوط في rdx (أو في ah عند القسمة ذات 8 بت) يساهم في القيمة التي تُقسَم.
في القسمة غير المؤشَّرة، عندما تتّسع القيمة المراد قسمتها في النصف الأدنى، ينبغي تصفير النصف الأعلى.
وأي تعليمة تصفّر تلك البتات تكفي.
فمثلًا، تصفّر mov edx, 0 البتات العليا في القسمة ذات 32 بت.
وفي القسمة المؤشَّرة، ينبغي بدلًا من ذلك مدّ القيمة بامتداد الإشارة.
وتوجد تعليمات تؤتمت هذه العملية: cbw وcwd وcdq وcqo.
تضبط الأولى البتات في ah وفقًا لإشارة al.
وتجري الأخرى امتداد الإشارة من ax إلى dx، ومن eax إلى edx، ومن rax إلى rdx، على الترتيب.
السجلات المستخدمة ضمنيًا في القسمة، مثل rax وrdx، يُكتب فوق محتواها دائمًا.
لذا ينبغي حفظ القيم الموجودة في تلك السجلات قبل القسمة إذا كنت ستحتاجها لاحقًا.
ينقل متجر محلي مخزونه إلى مستودع أكبر. تم توظيفك لتعبئة كل شيء ونقله.
لديك أربع مهام، وكلها متعلقة بإدارة عملية النقل.
These are the instructions mentioned in this concept:
| Instruction | Description |
|---|---|
| add a, b | a = a + b |
| inc a | a = a + 1 |
| sub a, b | a = a - b |
| dec a | a = a - 1 |
| imul a | rdx:rax = a * rax (signed) |
| imul a, b | a = a * b (signed, truncated) |
| imul a, b, c | a = b * c (signed, truncated) |
| mul a | rdx:rax = a * rax (unsigned) |
| div a | rax = quotient, rdx = remainder of rdx:rax / a (unsigned) |
| idiv a | rax = quotient, rdx = remainder of rdx:rax / a (signed) |
| movzx a, b | a = b, adding 0 to the extra bits |
| movsx a, b | a = b, adding 1 to the extra bits if b < 0 or 0 otherwise |
تذكّر أنه يمكنك الوصول إلى السجل نفسه بأحجام مختلفة عبر تغيير اسم المعامل.
على سبيل المثال: rax (64 بت)، وeax (32 بت)، وax (16 بت)، وal (8 بت).
يمكنك الرجوع إلى المفهوم السابق للاطلاع على الجدول الكامل.
تُعبَّأ العناصر في صناديق يجب أن تُوسَم بوزنها. لا يوجد ميزان في الجوار، لكن لحسن الحظ أنت تعرف متوسط وزن كل عنصر.
ولتنظيم الأمور على نحو أفضل، لا يحمل الصندوق إلا عناصر من منتجين مختلفين.
عرّف دالة get_box_weight تُرجع الوزن الإجمالي لصندوق، بوحدة g.
تأخذ هذه الدالة الوسائط التالية، بهذا الترتيب:
g
g
اعتبر أن الصندوق الفارغ يزن 500 غرام.
المُثبَّت WEIGHT_OF_EMPTY_BOX معرّف في أعلى ملف الحل.
مثال:
get_box_weight(30, 40, 50, 20);
// => 2700
جميع الوسائط أعداد صحيحة غير سالبة بطول 16 بت، والقيمة المُرجَعة عدد صحيح غير سالب بطول 32 بت.
تُكدَّس الصناديق وتُنقل إلى المستودع الجديد في شاحنة. لكن المساحة الرأسية داخل الشاحنة محدودة.
عرّف دالة max_number_of_boxes تُرجع عدد الصناديق ذات ارتفاع معيّن التي يمكن تكديسها عموديًا (فوق بعضها بعضًا) داخل الشاحنة.
تأخذ هذه الدالة معاملًا واحدًا هو ارتفاع الصندوق، بوحدة cm.
اعتبر أن الارتفاع الداخلي للشاحنة 300 سم.
المُثبَّت TRUCK_HEIGHT معرّف في أعلى ملف الحل.
مثال:
max_number_of_boxes(30);
// => 10
الوسيط والقيمة المُرجَعة عددان صحيحان غير سالبان بطول 8 بت.
ارتفاع الصندوق لا يقل عن 2 دائمًا، لذا فإن النتيجة تتّسع في 8 بت.
توجد قائمة تحقّق في المستودع الجديد تضم عدد العناصر التي لم تُحسَب بعد لكل منتج. لكل صندوق جديد يُنقل إلى هناك، عليك حساب القيمة الجديدة في قائمة التحقّق لكل منتج في الصندوق.
عرّف دالة items_to_be_moved تُرجع عدد العناصر المتبقية التي يجب نقلها إلى المستودع الجديد لمنتج معيّن.
تأخذ هذه الدالة الوسائط التالية، بهذا الترتيب:
مثال:
items_to_be_moved(76532, 120);
// => 76412
الوسائط أعداد صحيحة غير سالبة بطول 32 بت. القيمة المُرجَعة عدد صحيح بطول 32 بت. في حال حدوث خطأ في العملية، قد تكون النتيجة عددًا سالبًا.
يعتمد أجرك على عدد الصناديق التي نُقلت وعدد رحلات الشاحنة التي كانت ضرورية.
مقابل كل صندوق، ستحصل على 5 دولارات، ومقابل كل رحلة، ستحصل على 220 دولارًا.
المُثبَّتان PAY_PER_BOX وPAY_PER_TRUCK_TRIP معرّفان في أعلى ملف الحل.
لاحظ أنك ربما تكون قد تلقّيت جزءًا من هذا الأجر مقدمًا لتغطية التكاليف الأولية، وينبغي طرح هذه الدفعة المقدّمة من الأجر النهائي. بالإضافة إلى ذلك، بعض المنتجات غير مشمولة بالتأمين، وسيُخصم من أجرك أيضًا قيمة أي من تلك العناصر المكسورة أو المفقودة. ومن الممكن أن ينتهي بك الأمر مدينًا إن لم تكن حذرًا!
وهذا يعني أن المبلغ الصافي المستحق لك، أو الذي تدين به، هو:
net = boxes * PAY_PER_BOX + trips * PAY_PER_TRUCK_TRIP - up_front - broken_items * item_value
يُقسَّم هذا الأجر، أو الدين، بالتساوي بينك وبين عدد من العمال الذين وظّفتهم.
وأي مال متبقٍّ، أو دين متبقٍّ، فهو لك.
على سبيل المثال، إذا كان المبلغ الصافي 100 يُقسَّم على 6 أشخاص (أنت و5 عمال)، فستحصل على 20 (100/(5 + 1) = 16 زائد 4 المتبقية).
عرّف دالة calculate_payment تُرجع المبلغ الذي ينبغي أن تتقاضاه، أو تدفعه، في النهاية.
تأخذ هذه الدالة الوسائط التالية، بهذا الترتيب:
مثال:
calculate_payment(2000, 1000, 5, 21, 2, 1);
// => 2029
القيمة المُرجَعة عدد صحيح بطول 64 بت.
سجّل في Exercism لتتعلّم وتتقن x86-64 Assembly عبر 22 مفهومًا130 تمرينًا، وإرشاد بشري حقيقي، وكل ذلك مجانًا.