Deep EVM #10: Manajemen Stack Huff — takes(), returns(), dan Seni dup/swap
Engineering Team
Model Mental Mesin Stack
EVM adalah mesin stack. Tidak ada register, tidak ada variabel bernama — hanya stack last-in-first-out dari word 32-byte, 1024 slot dalamnya. Setiap opcode mendorong, mengambil, atau mengatur ulang item di stack ini. Jika Anda tidak bisa memegang state stack saat ini di kepala Anda, Anda akan menghasilkan bytecode yang bermasalah. Artikel ini tentang membangun model mental itu.
Konvensi Notasi
Di seluruh artikel ini (dan dalam komentar Huff), kami merepresentasikan state stack dengan tanda kurung di mana item paling kiri adalah atas stack:
// [atas, kedua, ketiga, ..., bawah]
0x01 // [1]
0x02 // [2, 1]
add // [3]
Setiap makro Huff harus memiliki komentar stack setelah setiap opcode. Ini bukan opsional — ini satu-satunya cara untuk mengaudit kebenaran.
DUP: Menduplikasi Item Stack
EVM menyediakan DUP1 hingga DUP16. DUPn menyalin item ke-n dari atas dan mendorongnya ke stack. Stack tumbuh sebanyak 1.
// Stack: [a, b, c, d]
dup1 // [a, a, b, c, d] — salin atas
dup3 // [c, a, a, b, c, d] — salin ke-3 dari atas
Biaya gas: 3 gas untuk DUPn manapun. Ini adalah salah satu operasi termurah di EVM.
Kapan Menggunakan DUP
DUP adalah alat Anda untuk pembacaan non-destruktif. Banyak opcode mengkonsumsi argumennya (ADD mengambil dua, mendorong satu), jadi jika Anda memerlukan nilai lagi nanti, DUP sebelum menyerahkannya ke opcode yang mengkonsumsi.
#define macro SAFE_SUB() = takes(2) returns(1) {
// takes: [a, b] — hitung a - b, revert jika b > a
dup2 dup2 // [a, b, a, b]
lt // [a < b?, a, b]
revert_underflow jumpi // [a, b]
sub // [a - b]
done jump
revert_underflow:
0x00 0x00 revert
done:
}
Perhatikan dup2 dup2 — kita menduplikasi baik a maupun b karena lt akan mengkonsumsinya, tetapi kita masih memerlukan aslinya untuk sub.
SWAP: Mengatur Ulang Stack
EVM menyediakan SWAP1 hingga SWAP16. SWAPn menukar item teratas dengan item ke-(n+1). Ukuran stack tetap sama.
// Stack: [a, b, c, d]
swap1 // [b, a, c, d] — tukar atas dengan ke-2
swap3 // [d, a, c, b] — tukar atas dengan ke-4
Biaya gas: 3 gas untuk SWAPn manapun.
Kapan Menggunakan SWAP
SWAP mengurutkan ulang argumen untuk opcode yang mengharapkan urutan tertentu. Misalnya, SUB menghitung stack[0] - stack[1]. Jika nilai Anda dalam urutan yang salah:
// Stack: [b, a] — tetapi kita ingin a - b
swap1 // [a, b]
sub // [a - b]
Batasan Kedalaman-16
DUP dan SWAP hanya menjangkau 16 level. Jika sebuah nilai ada di posisi 17 atau lebih dalam, Anda tidak bisa mengaksesnya dengan satu opcode. Ini adalah kendala keras EVM.
Strategi untuk stack dalam:
- Restrukturisasi logika Anda untuk menjaga nilai yang dibutuhkan dekat atas. Ini pendekatan terbaik.
- Gunakan memory sebagai scratch space. Simpan nilai dengan
MSTORE, ambil nanti denganMLOAD. Biaya 3+3=6 gas vs 3 gas untuk DUP, tetapi menembus batasan kedalaman. - Pecah makro menjadi makro yang lebih kecil yang masing-masing beroperasi pada item stack yang lebih sedikit.
#define macro STASH_TO_MEMORY() = takes(1) returns(0) {
// takes: [value]
0x80 mstore // [] — simpan di 0x80 (scratch space)
}
#define macro RECALL_FROM_MEMORY() = takes(0) returns(1) {
0x80 mload // [value]
}
Dalam kontrak MEV kita sering memesan 0x80..0xc0 sebagai area scratch untuk nilai yang sebaliknya akan mendorong stack melewati 16.
Pola Umum
Pola 1: Menyimpan Nilai Melalui Operasi yang Mengkonsumsi
Anda memiliki [x] dan perlu memanggil opcode yang mengkonsumsi x tetapi Anda masih memerlukan x setelahnya.
// Ingin: hitung hash dari x, tetapi pertahankan x
// Stack: [x]
dup1 // [x, x]
0x00 mstore // [x] — memory[0] = x
0x20 0x00 // [0, 32, x]
keccak256 // [hash, x]
Pola 2: Merotasi Tiga Item
Anda memiliki [a, b, c] dan memerlukan [c, a, b]:
swap2 // [c, b, a]
swap1 // [c, a, b]
2 opcode, 6 gas. Tidak ada rotasi opcode tunggal di EVM.
Anda memiliki [a, b, c] dan memerlukan [b, c, a]:
swap1 // [b, a, c]
swap2 // [b, c, a]
Pola 3: Membersihkan Item Stack yang Tidak Diinginkan
Setelah komputasi Anda mungkin memiliki item ekstra. Gunakan pop (2 gas) untuk membuang:
// Stack: [result, sampah1, sampah2]
swap1 pop // [result, sampah2]
swap1 pop // [result]
Pola 4: Menduplikasi Sepasang
Anda perlu menyalin dua item teratas:
// Stack: [a, b]
dup2 // [b, a, b]
dup2 // [a, b, a, b]
Perhatikan Anda DUP dalam urutan terbalik. dup2 pertama menyalin b (yang ada di posisi 2), kemudian dup2 menyalin a (sekarang di posisi 2 karena kita menumbuhkan stack). Pola ini muncul terus-menerus dalam kode perbandingan-sebelum-aritmatika.
Disiplin Visualisasi Stack
Ketika menulis Huff, adopsi disiplin ini:
- Komentari setiap baris dengan state stack setelah eksekusi.
- Verifikasi takes/returns — hitung item stack di masuk dan keluar.
- Telusuri setiap cabang — di setiap JUMPI, baik jalur yang diambil maupun tidak diambil harus meninggalkan stack dalam state yang valid.
- Awasi drift stack — jika badan loop tidak menyeimbangkan push dan pop secara sempurna, stack akan tumbuh atau menyusut di setiap iterasi.
#define macro TRANSFER() = takes(3) returns(0) {
// takes: [amount, from, to]
// Muat saldo pengirim
dup2 // [from, amount, from, to]
sload // [bal_from, amount, from, to]
// Periksa saldo cukup
dup1 dup3 // [amount, bal_from, bal_from, amount, from, to]
gt // [amount > bal_from?, bal_from, amount, from, to]
insufficient jumpi // [bal_from, amount, from, to]
// Kurangi dari pengirim
dup2 // [amount, bal_from, amount, from, to]
swap1 sub // [bal_from - amount, amount, from, to]
dup3 // [from, new_bal, amount, from, to]
sstore // [amount, from, to]
// Tambahkan ke penerima
dup3 // [to, amount, from, to]
sload // [bal_to, amount, from, to]
add // [new_bal_to, from, to]
swap2 // [to, from, new_bal_to]
sstore // [from]
pop // []
done jump
insufficient:
0x00 0x00 revert
done:
}
Setiap baris memiliki komentar stack. Setiap cabang berakhir dengan bersih. Ini satu-satunya cara untuk menulis Huff yang benar.
Debugging Error Stack
Bug paling umum di Huff:
- Stack underflow — Mengambil dari stack kosong. EVM revert saat runtime. Penyebab: salah menghitung
takesatau lupa DUP. - Ketidakseimbangan stack di JUMP — JUMPDEST yang dicapai dari dua jalur berbeda mengharapkan state stack yang berbeda. Compiler tidak akan menangkap ini.
- Off-by-one di DUP/SWAP —
dup3vsdup4ketika Anda menambahkan push ekstra sebelumnya. Inilah mengapa komentar stack wajib.
huffc memiliki flag --stack-check yang melakukan analisis stack dasar:
huffc src/Contract.huff -r --stack-check
Ia menangkap underflow yang jelas tetapi tidak bisa menelusuri semua jalur jump dinamis. Untuk kontrak kompleks, telusuri eksekusi secara manual dengan forge debug atau evm-trace.
Lanjutan: Stack sebagai Register File
Pengembang Huff berpengalaman memikirkan ~8 posisi stack teratas sebagai register file:
Posisi 1 (atas): Register kerja — komputasi saat ini
Posisi 2-3: Register argumen — input ke operasi berikutnya
Posisi 4-6: Register variabel lokal — nilai yang segera dibutuhkan
Posisi 7-8: Register konteks — counter loop, base pointer
Posisi 9+: Area spill — jarang diakses, pertimbangkan memory
Model mental ini membantu Anda memutuskan kapan SWAP nilai ke atas vs kapan DUP, dan kapan spill ke memory.
Ringkasan
Manajemen stack adalah keterampilan inti untuk pengembangan Huff. DUP untuk pembacaan non-destruktif, SWAP untuk pengurutan ulang, dan memory untuk nilai melewati kedalaman 16. Komentari setiap baris dengan state stack. Verifikasi setiap cabang. Di artikel berikutnya, kita akan menggunakan keterampilan ini untuk membangun dispatcher fungsi O(1) dengan jump table yang dipadatkan — di mana manajemen stack yang tepat langsung diterjemahkan menjadi penghematan gas.