Deep EVM #1: كيف تنفذ EVM الكود الخاص بك — أكواد التشغيل والمكدس والغاز
Engineering Team
EVM هي آلة مكدسية
آلة إيثيريوم الافتراضية ليست مثل معالج x86 في حاسوبك المحمول. لا تحتوي على سجلات. بدلاً من ذلك، هي آلة مكدسية — كل عملية حسابية تدفع إلى أو تسحب من مكدس من 1024 عنصراً حيث كل عنصر هو كلمة 256 بت (32 بايت).
عندما تستدعي عقداً ذكياً، تستقبل EVM البايتكود الخاص بالعقد — سلسلة مسطحة من أكواد التشغيل أحادية البايت — وتبدأ التنفيذ من البايت 0. لا توجد جداول دوال، ولا رأس ELF، ولا خطوة ربط. البايتكود هو البرنامج.
// Solidity:
// uint256 result = 2 + 3;
// يُترجم إلى بايتكود:
// PUSH1 0x02 PUSH1 0x03 ADD
// تتبع المكدس:
// [] -> PUSH1 0x02 -> [2]
// [2] -> PUSH1 0x03 -> [2, 3]
// [2, 3] -> ADD -> [5]
كل كود تشغيل يستهلك معاملاته من أعلى المكدس ويدفع نتيجته للخلف. كود التشغيل ADD يسحب قيمتين ويجمعهما ويدفع المجموع. هذا يختلف جوهرياً عن البنى القائمة على السجلات حيث تحدد سجلات المصدر والوجهة.
فئات أكواد التشغيل
تحدد EVM حوالي 140 كود تشغيل، مجمعة في فئات وظيفية:
العمليات الحسابية والمقارنة
- ADD, SUB, MUL, DIV, MOD — حساب أعداد صحيحة أساسي 256 بت. تكلفة كل منها 3 غاز (مستوى G_verylow).
- SDIV, SMOD — القسمة والباقي مع إشارة باستخدام المتمم الثنائي.
- ADDMOD, MULMOD — حساب معياري:
(a + b) % Nو(a * b) % Nفي كود تشغيل واحد. حاسمة لعمليات المنحنى الإهليلجي، تكلفتها 8 غاز. - EXP — الرفع إلى قوة. تكلفته 10 غاز + 50 لكل بايت في الأس، مما يجعله من أغلى أكواد التشغيل الحسابية.
- LT, GT, SLT, SGT, EQ, ISZERO — أكواد مقارنة تدفع 1 (صحيح) أو 0 (خطأ).
العمليات البتية
- AND, OR, XOR, NOT — منطق بتي، 3 غاز لكل منها.
- SHL, SHR, SAR — إزاحة لليسار، إزاحة منطقية لليمين، إزاحة حسابية لليمين (أُضيفت في Constantinople، EIP-145).
- BYTE — استخراج بايت واحد من كلمة 32 بايت.
معالجة المكدس
- POP — تجاهل العنصر العلوي.
- PUSH1 إلى PUSH32 — دفع 1 إلى 32 بايت من البيانات الفورية إلى المكدس.
- DUP1 إلى DUP16 — تكرار العنصر N من المكدس إلى الأعلى.
- SWAP1 إلى SWAP16 — تبديل العنصر العلوي مع العنصر N أسفل منه.
معلومات البيئة والكتلة
- CALLER (msg.sender)، CALLVALUE (msg.value)، CALLDATALOAD، CALLDATASIZE، CALLDATACOPY — الوصول إلى سياق المعاملة.
- NUMBER، TIMESTAMP، BASEFEE، CHAINID — معلومات مستوى الكتلة.
- BALANCE، EXTCODESIZE، EXTCODECOPY — الاستعلام عن حسابات أخرى.
جدول الغاز
لكل كود تشغيل تكلفة غاز. يخدم الغاز غرضين: منع الحلقات اللانهائية (مشكلة التوقف) وتسعير الموارد الحسابية بشكل عادل.
| المستوى | الغاز | أمثلة |
|---|---|---|
| صفر | 0 | STOP, RETURN, REVERT |
| أساسي | 2 | ADDRESS, ORIGIN, CALLER |
| منخفض جداً | 3 | ADD, SUB, LT, GT, AND, OR, POP |
| منخفض | 5 | MUL, DIV, MOD |
| متوسط | 8 | ADDMOD, MULMOD, JUMP |
| مرتفع | 10 | JUMPI |
| خاص | متغير | SLOAD, SSTORE, CALL, CREATE |
الأكواد الأغلى هي التي تتعامل مع الحالة:
// تكاليف الغاز للوصول إلى الحالة (بعد EIP-2929):
// SLOAD (بارد): 2100 غاز
// SLOAD (دافئ): 100 غاز
// SSTORE (بارد، 0->غير صفري): 22100 غاز
// SSTORE (دافئ): 100 غاز (+ 20000 إذا 0->غير صفري)
// CALL (بارد): 2600 غاز
// CALL (دافئ): 100 غاز
الوصول البارد مقابل الدافئ (EIP-2929)
قدم EIP-2929 (ترقية Berlin، أبريل 2021) مفهوم قائمة الوصول — مجموعة لكل معاملة من العناوين وفتحات التخزين التي تم لمسها.
في المرة الأولى التي تصل فيها إلى فتحة تخزين أو عنوان خارجي ضمن معاملة، يكون “بارداً” ويكلف غازاً إضافياً. الوصولات اللاحقة “دافئة” ورخيصة. لهذا السبب يهم ترتيب قراءة فتحات التخزين لتحسين الغاز.
تدفق التنفيذ: ماذا يحدث في المعاملة
عندما ترسل معاملة تستدعي عقداً، إليك تسلسل التنفيذ الكامل:
- التحقق من المعاملة — فحص الرقم التسلسلي، الرصيد >= القيمة + الغاز × سعر الغاز، التحقق من التوقيع.
- خصم الغاز الجوهري — 21000 غاز للمعاملة نفسها، بالإضافة إلى 16 غاز لكل بايت غير صفري في calldata و4 لكل بايت صفري.
- إعداد السياق — تنشئ EVM سياق تنفيذ: الكود، calldata، المستدعي، القيمة، الغاز المتبقي.
- عداد البرنامج يبدأ من 0 — تقرأ EVM كود التشغيل في الموضع 0 وتنفذه.
- التنفيذ التسلسلي — يُنفَّذ كل كود تشغيل، يُخصم الغاز. JUMP وJUMPI يسمحان بتدفق تحكم غير خطي، لكن فقط إلى المواضع المعلمة بـ JUMPDEST.
- الإنهاء — ينتهي التنفيذ بـ STOP (نجاح)، RETURN (نجاح مع بيانات)، REVERT (فشل، يتم التراجع عن الحالة)، أو نفاد الغاز.
- تثبيت أو تراجع الحالة — عند النجاح، تُثبَّت جميع تغييرات الحالة. عند التراجع، تُلغى جميع التغييرات.
عداد البرنامج وJUMP
عداد البرنامج (PC) هو سجل ضمني يتتبع الموضع الحالي في البايتكود. يتقدم لأكواد تشغيل عادية بمقدار 1 (أو 1 + N لأكواد PUSH). كودان يعدلان PC مباشرة:
- JUMP — يسحب وجهة من المكدس ويضبط PC على تلك القيمة. يجب أن تحتوي الوجهة على كود JUMPDEST وإلا تتراجع المعاملة.
- JUMPI — قفز مشروط. يسحب الوجهة والشرط. إذا كان الشرط غير صفري، يقفز؛ وإلا يستمر تسلسلياً.
الاستدعاءات الفرعية: CALL، STATICCALL، DELEGATECALL
يمكن للعقود استدعاء عقود أخرى باستخدام ثلاثة أكواد استدعاء:
- CALL — استدعاء قياسي. ينشئ سياق تنفيذ جديد بمكدسه وذاكرته الخاصة.
- STATICCALL — استدعاء للقراءة فقط (EIP-214). أي كود تشغيل يعدل الحالة داخل المستدعى يسبب تراجعاً فورياً.
- DELEGATECALL — ينفذ كود المستدعى لكن في سياق تخزين المستدعي. يُحفظ msg.sender وmsg.value من الاستدعاء الأصلي. هكذا تعمل أنماط الوكيل والمكتبات.
التبعات العملية لـ MEV
إذا كنت تبني روبوتات MEV، فإن فهم EVM على مستوى أكواد التشغيل ليس خياراً — إنه متطلب تنافسي. كل وحدة غاز توفرها في تنفيذ روبوتك هي هامش ربح.
- المحاكاة قبل الإرسال — استخدم
eth_callأو EVM محلي (revm، EVMONE) لتتبع التنفيذ. - تقليل الوصول البارد — سخّن فتحات التخزين مسبقاً عبر قوائم الوصول (EIP-2930).
- استخدم STATICCALL للقراءة — أرخص قليلاً ويضمن عدم تغيير الحالة.
الخلاصة
EVM أنيقة في بساطتها: آلة مكدسية بكلمات 256 بت، وتنسيق بايتكود مسطح، ونظام قياس غاز يسعّر كل عملية. فهم هذا الأساس — أكواد التشغيل والمكدس والغاز — هو الشرط المسبق لكل ما يلي في هذه السلسلة.