Deep EVM #6: إدارة ذاكرة Yul — mstore وmload ومؤشر الذاكرة الحرة
Engineering Team
مراجعة نموذج الذاكرة
ذاكرة EVM هي مصفوفة بايت خطية تتوسع عند الطلب. تبدأ فارغة وتنمو في أجزاء من 32 بايت. في Yul لديك كودا تشغيل أساسيان:
- mstore(offset, value) — كتابة 32 بايت في الذاكرة عند الإزاحة
- mload(offset) — قراءة 32 بايت من الذاكرة عند الإزاحة
- mstore8(offset, value) — كتابة بايت واحد
التكلفة: 3 غاز لـ mload/mstore، بالإضافة إلى تكلفة التوسيع إذا كتبت أبعد من الحد الحالي.
مؤشر الذاكرة الحرة (0x40)
Solidity يحتفظ بمؤشر عند الموضع 0x40 يشير إلى أول بايت ذاكرة غير مُخصص. عند العمل في Yul داخل Solidity، يجب عليك احترام هذا الاتفاق:
assembly {
// تخصيص 64 بايت
let ptr := mload(0x40)
// استخدام الذاكرة المخصصة
mstore(ptr, value1)
mstore(add(ptr, 0x20), value2)
// تحديث المؤشر
mstore(0x40, add(ptr, 0x40))
}
إذا نسيت تحديث المؤشر، فإن Solidity قد يكتب فوق بياناتك عند التخصيص التالي.
منطقة الصفر (Scratch Space)
أول 64 بايت (0x00-0x3f) محجوزة كمنطقة صفر في اتفاقية Solidity. يمكنك استخدامها بحرية في كتل assembly دون تحديث مؤشر الذاكرة الحرة:
assembly {
// منطقة الصفر — آمنة للاستخدام المؤقت
mstore(0x00, key)
mstore(0x20, slot)
let hash := keccak256(0x00, 0x40)
}
هذا أرخص من تخصيص ذاكرة جديدة لأنه لا يوسع الذاكرة ولا يحتاج لتحديث المؤشر.
تخطيط الذاكرة اليدوي
في عقود Yul المستقلة (بدون Solidity)، أنت تتحكم بالكامل في تخطيط الذاكرة:
// تخطيط مخصص:
// 0x00-0x1f: منطقة الصفر 1
// 0x20-0x3f: منطقة الصفر 2
// 0x40-0x5f: مخزن مؤقت للإرجاع
// 0x60+: ذاكرة ديناميكية
let returnBuffer := 0x40
mstore(returnBuffer, result)
return(returnBuffer, 0x20)
ترميز ABI الفعال
Solidity يولد ترميز ABI آمن لكنه مكلف. في Yul يمكنك ترميز البيانات يدوياً:
assembly {
// ترميز استدعاء transfer(address,uint256)
let ptr := mload(0x40)
mstore(ptr, 0xa9059cbb00000000000000000000000000000000000000000000000000000000)
mstore(add(ptr, 0x04), recipient)
mstore(add(ptr, 0x24), amount)
let success := call(gas(), token, 0, ptr, 0x44, 0, 0)
}
هذا يوفر الغاز الذي ينفقه Solidity على:
- التحقق من الأنواع في وقت التشغيل
- الحشو والمحاذاة التلقائية
- التحقق من returndata
أنماط كتابة الأحداث
كتابة الأحداث (LOG) في Yul:
assembly {
// emit Transfer(from, to, amount)
let sig := 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
mstore(0x00, amount)
log3(0x00, 0x20, sig, from, to)
}
مقارنة: Solidity مقابل Yul
// Solidity: ~800 غاز
function transfer(address to, uint256 amount) external {
balances[msg.sender] -= amount;
balances[to] += amount;
emit Transfer(msg.sender, to, amount);
}
// Yul: ~500 غاز
function transfer(address to, uint256 amount) external {
assembly {
// حساب فتحة المرسل
mstore(0x00, caller())
mstore(0x20, balances.slot)
let senderSlot := keccak256(0x00, 0x40)
let senderBal := sload(senderSlot)
if lt(senderBal, amount) { revert(0, 0) }
sstore(senderSlot, sub(senderBal, amount))
// حساب فتحة المستلم
mstore(0x00, to)
let recipientSlot := keccak256(0x00, 0x40)
sstore(recipientSlot, add(sload(recipientSlot), amount))
// الحدث
mstore(0x00, amount)
log3(0x00, 0x20,
0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef,
caller(), to)
}
}
أخطاء شائعة
- الكتابة فوق مؤشر الذاكرة الحرة — نسيان تحديث 0x40 بعد التخصيص
- اختلاط الإزاحات — mstore يكتب 32 بايت، لذا الإزاحة التالية هي +0x20 وليس +0x01
- تجاهل توسيع الذاكرة — الكتابة في إزاحات عالية جداً تكلف غازاً تربيعياً
- عدم تصفير الذاكرة — الذاكرة المخصصة حديثاً مصفرة، لكن منطقة الصفر قد تحتوي بقايا
الخلاصة
إدارة الذاكرة في Yul هي المهارة الأساسية لكتابة كود EVM فعال. فهم مؤشر الذاكرة الحرة ومنطقة الصفر وتخطيط الذاكرة اليدوي يتيح لك كتابة عقود أسرع وأرخص بكثير من مخرجات Solidity الافتراضية.