مسك الدفاتر

مسك الدفاتر

تمرين تعلّمي

مقدمة

الثنكات

ذُكر في مفهوم سابق أن التسميات المحلية والدوال ليست في الحقيقة سوى عناوين داخل قسم يحوي كودًا قابلًا للتنفيذ، مثل section .text.

بل يمكن التعامل مع الدوال بالطريقة نفسها التي تُعامَل بها أي عناوين ذاكرة، أي يمكن تحميلها في السجلات وتمريرها وتخزينها في الذاكرة. كما يمكن استخدام call أو jmp لنقل التنفيذ إلى دالة مخزَّنة في سجل أو في الذاكرة:

section .text
sum_op:
    lea rax, [rdi + rsi] ; loads the sum rdi + rsi into rax
    ret

apply_sum:
    lea rax, [rel sum_op]
    jmp rax   ; tail call

ويُطلق على عنوان الدالة الذي يُمرَّر كقيمة اسم الثنك. والثنكات حجر أساس في البرمجة عالية الرتبة بلغة التجميع: كود يعمل على كود آخر.

الكود كبيانات

يمكن أيضًا تخزين عناوين الدوال في الذاكرة واسترجاعها لاحقًا:

section .bss
    cached_fn resq 1

section .text
save_op:
    mov qword [rel cached_fn], rdi
    ret

apply_op:
    ; arguments are already set up according to the ABI
    jmp qword [rel cached_fn] ; tail call

تكتب الدالة save_op عنوان الدالة الذي تستقبله في cached_fn. وتبقى القيمة موجودة بعد أن تُرجع الدالة save_op، لذا فإن أي استدعاء لاحق لـ apply_op يقفز قفزة ذيلية إلى العنوان الذي خُزِّن أخيرًا. وهذا يجعل من الممكن تغيير الدالة التي تستدعيها apply_op أثناء التشغيل.

جداول الإرسال

يتيح تخزين عناوين الدوال في مصفوفة اختيار دوال مختلفة وفق فهرس معيّن، قد يعتمد على شرط أثناء التشغيل. وهذا ما يُسمّى جدول إرسال:

section .data
    dispatch_table dq add_op, sub_op, mul_op

section .text
dispatch:
    ; this function takes two arguments in rdi and rsi, and an index in rdx
    ; it then applies the function corresponding to the index in rdx to the arguments
    lea rax, [rel dispatch_table]
    jmp qword [rax + 8*rdx]   ; tail-call the function address for the index

الثنكات ذات الحالة

قد يتصرف الثنك الذي يقرأ أو يحدّث ذاكرة مستمرة بين الاستدعاءات بشكل مختلف تبعًا لما جرى قبله. وقد تعتمد نتيجته على أكثر من وسائطه وحدها.

على سبيل المثال، عدّاد يأخذ دالة ويستدعيها بالعدّ الحالي، مقدِّمًا العدّ في كل مرة:

section .data
    count dq 0

section .text
tick:
    mov rax, rdi               ; saves the function address
    mov rdi, [rel count]       ; loads the current count as the function's argument
    inc qword [rel count]      ; advances the count
    jmp rax                    ; tail-calls the function

تستدعي الدالة tick الدالة المعطاة بالعدّ الحالي كوسيط، ثم تقدّم العدّ. لذا فإن أول استدعاء tick(square) يستدعي square(0)، والاستدعاء التالي tick(square) يستدعي square(1)، والذي يليه square(2)، وهكذا.

ومن الأمثلة الأخرى الحساب المؤجَّل:

section .bss
    captured_fn resq 1
    argument resq 1

section .text
delay:
    mov qword [rel captured_fn], rdi ; saves the function
    mov qword [rel argument], rsi    ; saves the argument
    lea rax, [rel invoke]            ; returns the `invoke` function
    ret

invoke:
    mov rdi, qword [rel argument]    ; loads the saved argument into `rdi`
    jmp qword [rel captured_fn]      ; tail-calls the saved function

تأخذ الدالة delay دالةً وقيمةً، وتخزّنهما، وتُرجع الدالة invoke. وعند استدعاء invoke، تُشغّل الدالة المحتجَزة بالوسيط المخزَّن.

تستند كثير من الأنماط الشائعة في اللغات عالية المستوى، مثل الاستدعاءات الرجعية، والطرق الافتراضية، والمولّدات، والكاريّة، وتركيب الدوال، وغيرها الكثير، إلى الثنكات المقترنة بحالة مستمرة.

التعليمات

أنت أمين الحسابات في بنك صغير في قرية. لكل عميل حساب، وأنت تحتفظ برصيده في دفتر حساباتك. وعلى مدار السنة، تُطبَّق المعاملات على هذه الأرصدة: تُضاف الفوائد، وتُخصم الرسوم، وتُدفع المكافآت، وتُفرض الغرامات. كل معاملة تأخذ رصيدًا وتنتج رصيدًا جديدًا.

أمامك أربع مهام.

Note

يمكنك افتراض أن كل thunk (أي المعاملات والحرّاس) في هذا التمرين هو دالة:

  1. تأخذ عددًا صحيحًا غير سالب بطول 64 بت كوسيط
  2. وتُرجع أيضًا عددًا صحيحًا غير سالب بطول 64 بت.

1. حفظ معاملة

يتعلّم أمين الصندوق معاملة جديدة في بداية اليوم ويدوّنها حتى يمكن تطبيقها لاحقًا عندما يصل عميل.

عرّف دالتين:

  • تأخذ remember_transaction معاملة وتخزّنها في الذاكرة.
  • تأخذ apply_remembered رصيدًا وتطبّق عليه المعاملة المخزّنة سابقًا.

مثال، بافتراض أن add_interest معاملة تضيف خمس وحدات من الفائدة:

remember_transaction(add_interest);
apply_remembered(100);
// => 105

remember_transaction(service_fee);
apply_remembered(100);
// => 98   (assuming service_fee deducts 2)

بالنسبة إلى remember_transaction:

  • الوسيط معاملة تُحفظ لاستخدامها لاحقًا.
  • لا توجد قيمة مُرجَعة.

بالنسبة إلى apply_remembered:

  • الوسيط عدد صحيح غير سالب بطول 64 بت.
  • القيمة المُرجَعة عدد صحيح غير سالب بطول 64 بت.

2. دليل البنك

يحتوي دليل البنك على قائمة بالمعاملات المتكررة مخزّنة في جدول توجيه. يحتفظ كل فرع بنسخته الخاصة من القائمة، ويمكنه تسجيل معاملات مختلفة حسب السياسة المحلية.

عرّف دالتين تعملان على جدول توجيه يمرّره المستدعي:

  • تأخذ register_transaction عنوان ذاكرة جدول توجيه، وفهرسًا، ومعاملة. وهي تخزّن هذه المعاملة عند الفهرس المعطى في الجدول.
  • تأخذ select_transaction عنوان ذاكرة جدول توجيه، وفهرسًا، ورصيدًا. وهي تبحث عن المعاملة عند الفهرس المعطى وتطبّقها على الرصيد، وتُرجع الرصيد الجديد.

ينبغي أن تصل select_transaction إلى المعاملة التي بحثت عنها باستدعاء ذيلي غير مباشر واحد.

مثال، بافتراض أن manual عنوان ذاكرة جدول توجيه فيه أربع خانات فارغة:

register_transaction(manual, 0, monthly_interest);
register_transaction(manual, 1, service_fee);

select_transaction(manual, 0, 100);
// applies monthly_interest to 100

select_transaction(manual, 1, 100);
// applies service_fee to 100

بالنسبة إلى register_transaction:

  • الوسيط الأول عنوان ذاكرة جدول توجيه.
  • الوسيط الثاني عدد صحيح غير سالب بطول 64 بت (الفهرس).
  • الوسيط الثالث معاملة.
  • لا توجد قيمة مُرجَعة.

بالنسبة إلى select_transaction:

  • الوسيط الأول عنوان ذاكرة جدول توجيه.
  • الوسيط الثاني عدد صحيح غير سالب بطول 64 بت (الفهرس).
  • الوسيط الثالث عدد صحيح غير سالب بطول 64 بت (الرصيد).
  • القيمة المُرجَعة عدد صحيح غير سالب بطول 64 بت.

3. معالجة كشف حساب شهري

في نهاية الشهر، يُسوّى حساب العميل. تُطبَّق كل معاملة جرت خلال الشهر على الرصيد الابتدائي، واحدة تلو الأخرى، وتكون النتيجة هي الرصيد الجديد.

عرّف دالة process_statement تأخذ رصيدًا ابتدائيًا، وعنوان ذاكرة مصفوفة من المعاملات، وعدد المعاملات في المصفوفة. لكل معاملة بالتتابع، ينبغي أن تطبّق المعاملة على الرصيد الجاري، ثم تستخدم النتيجة رصيدًا للمعاملة التالية. وتُرجَع قيمة الرصيد النهائي.

في الكود الزائف، تحسب process_statement(balance, transactions, n) ما يلي:

for each transaction in transactions:
    balance = transaction(balance)
return balance

مثال، بافتراض أن transactions عنوان ذاكرة مصفوفة تحتوي على المعاملات add_interest وservice_fee وadd_interest بهذا الترتيب، حيث تضيف add_interest العدد 5 وتخصم service_fee العدد 2:

process_statement(100, transactions, 3);
// add_interest(100) = 105
// service_fee(105)  = 103
// add_interest(103) = 108
// => 108

الوسيط الأول عدد صحيح غير سالب بطول 64 بت. الوسيط الثاني عنوان ذاكرة مصفوفة من المعاملات. الوسيط الثالث عدد صحيح غير سالب بطول 64 بت (طول المصفوفة). القيمة المُرجَعة عدد صحيح غير سالب بطول 64 بت.

4. المعالجة باستخدام حارس

تتطلب سياسة البنك فحص بعض المعاملات قبل اعتمادها. الحارس دالة تفحص رصيدًا مقترحًا وتقرر ما إذا كان مقبولًا. تُرجع دالة الحارس هذه قيمة غير صفرية للموافقة، أو صفرًا للرفض.

عرّف process_with_guard التي تأخذ رصيدًا ابتدائيًا، وعنوان ذاكرة مصفوفة من المعاملات، وعدد المعاملات في المصفوفة، ودالة حارس. لكل معاملة بالتتابع:

  1. طبّق المعاملة على الرصيد الجاري لحساب رصيد جديد مؤقت.
  2. استدعِ الحارس مع الرصيد المؤقت.
  3. إذا أرجع الحارس قيمة غير صفرية، فاعتمد المعاملة: يصبح الرصيد الجاري هو الرصيد المؤقت.
  4. إذا أرجع الحارس صفرًا، يبقى الرصيد الجاري دون تغيير وتُتخطّى المعاملة.

بعد معالجة كل المعاملات، أرجع الرصيد النهائي إلى جانب عدد المعاملات المعتمدة.

في الكود الزائف، تحسب process_with_guard(balance, transactions, n, guard) ما يلي:

approved = 0
for each transaction in transactions:
    tentative = transaction(balance)
    if guard(tentative) is non-zero:
        balance = tentative
        approved = approved + 1
return balance, approved

على سبيل المثال، افترض أن:

  1. add_interest معاملة تضيف 5، وservice_fee معاملة أخرى تخصم 2
  2. at_least_10 حارس يُرجع قيمة غير صفرية عندما يكون الرصيد أكبر من أو يساوي 10

إذن:

process_with_guard(5, {add_interest, service_fee, add_interest}, 3, at_least_10);
// add_interest(5) = 10; at_least_10(10) != 0;
// => balance = 10, approved = 1
//
// service_fee(10) = 8; at_least_10(8) = 0;
// => balance = 10, approved = 1
//
// add_interest(10) = 15; at_least_10(15) != 0;
// => balance = 15, approved = 2
//
// final balance (15) is returned in rax
// number of approved transactions (2) is returned in rdx

بالنسبة إلى process_with_guard:

  • الوسيط الأول عدد صحيح غير سالب بطول 64 بت (الرصيد الابتدائي).
  • الوسيط الثاني عنوان ذاكرة مصفوفة من المعاملات.
  • الوسيط الثالث عدد صحيح غير سالب بطول 64 بت (طول المصفوفة).
  • الوسيط الرابع دالة حارس تأخذ عددًا صحيحًا غير سالب بطول 64 بت وتُرجع عددًا صحيحًا غير سالب بطول 64 بت.
  • القيم المُرجَعة عددان صحيحان غير سالبان بطول 64 بت: الرصيد النهائي في rax، وعدد المعاملات المعتمدة في rdx.
تعديل عبر GitHub يفتح الرابط في نافذة أو علامة تبويب جديدة
x86-64 Assembly Exercism

مستعد لبدء مسك الدفاتر؟

سجّل في Exercism لتتعلّم وتتقن x86-64 Assembly عبر 22 مفهومًا130 تمرينًا، وإرشاد بشري حقيقي، وكل ذلك مجانًا.