مقدمه ای بر Bytecode و Opcode در زبان Solidity

Photo by Patrick Lindenberg هرچقدر که در Smart Contract ها عمیق‌تر می‌شویم بیشتر با اصطلاحاتی چون PUSH1 ، SSTORE ، MSTORE و ... مواجه می‌شویم.این‌ها دقیقا چه هستند و آیا ما باید به آنها اهمیت دهیم ؟برای درک درست این اصطلاحات باید در Ethereum Virtual‌ Machine یا همان EVM عمیق‌تر شویم.زبان Solidity از نوع زبان‌های High Level Language است. ما آن را می‌فهمیم ولی ماشین آن را نمی‌فهمد. زمانی که ما یکی از کلاینت‌های اِتِریوم مانند geth را نصب می‌کنیم، EVM خودکار با آن نصب می‌شود. در واقع EVM یک سیستم عامل سبک می‌باشد که اختصاصا برای اجرای Smart Contract ها ساخته شده است.وقتی ما کدهای Solidity را کامپایل می‌کنیم، کدها به Bytecode تبدیل می‌شوند و این همان چیزیست که ماشین می‌فهمد.این یک نمونه Smart Contract بسیار ساده است :pragma solidity ^0.4.11;contract MyContract { uint i = (10 + 2) * 2; }اگر این Smart Contract را در Remix اجرا نمایید، با کلیک بر روی دکمه details اطلاعات زیر را می‌بینیم :Solidity Bytecode and Opcodesاین بخش کامپایل شده‌ی کدهای Solidity است : 60606040525b600080fd00a165627a7a7230582012c9bd00152fa1c480f6827f81515bb19c3e63bf7ed9ffbb5fda0265983ac7980029این چیزی نیست جز یک عدد بر مبنای شانزده (هگزا)، که از کدهایی که در Contract نوشتیم بدست آمده است و به آن Bytecode گفته می‌شود.در همین پنجره (details) در بخش Web3 Deploy چنین چیزی می‌بینیم :... { from: web3.eth.accounts[0], data: '0x606060405260186000553415601357600080fd5b5b60368060216000396000f30060606040525b600080fd00a165627a7a7230582012c9bd00152fa1c480f6827f81515bb19c3e63bf7ed9ffbb5fda0265983ac7980029', gas: '4300000' }, function (e, contract){ console.log(e, contract); if (typeof contract.address !== 'undefined') { console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash); } })در نهایت چیزی که از Contract ما بر روی شبکه اِتِریوم قرار می‌گیرد همین عدد هگزا ای است که در فیلد data می‌بینید و برای این عمل به پرداخت 4,300,000 gas نیاز دارد.در واقع EVM با همه چیز به عنوان یک مقدار هگزا برخورد می‌کند. اگر تابحال برایتان سوال پیش آمده که چرا در ابتدای آدرس اَکانت های اِتِریوم یا هَشِ تراکنش‌ها "0x" را می‌بینیم، دلیل آن همین است.در واقع "0x" به این معنی است که مقداری که در جلوی آن قرار می‌گیرد، یک عدد بر مبنای شانزده است.وجود "0x" الزامی نیست، چراکه همانطور که گفته شد EVM با همه مقادیر به صورت هگزا برخورد می کند.همینطور عباراتی به شکل زیر را می‌بینیم :PUSH1 0x60 PUSH1 0x40 MSTORE PUSH1 0x18 PUSH1 0x0 SSTORE CALLVALUE ISZERO PUSH1 0x13 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST JUMPDEST PUSH1 0x36 DUP1 PUSH1 0x21 PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN STOP PUSH1 0x60 PUSH1 0x40 MSTORE JUMPDEST PUSH1 0x0 DUP1 REVERT STOP LOG1 PUSH6 0x627A7A723058 KECCAK256 SLT 0xc9 0xbd STOP ISZERO 0x2f LOG1 0xc4 DUP1 0xf6 DUP3 PUSH32 0x81515BB19C3E63BF7ED9FFBB5FDA0265983AC798002900000000000000000000به این بخش Operation Code یا همان Opcode گفته می‌شود.این بخش برای انسان قابل خواندن است ولی درک آن بسیار مشکل می باشد. تمام این Operation ها در واقع معادلی در مبنای شانزده دارند. بطور مثال معادل MSTORE مقدار 0x52 می باشد و یا SSTORE معادل 0x55 می‌باشد. در ریپازیتوری گیت‌هاب اِتِریوم می‌توانید لیست کامل Opcode ها را مشاهده نمایید.ماشین EVM از نوع Stack Machine می‌باشد. برای درک ساده‌ی اِستَک‌ها تصور کنید قطعات نان را در فِر قرار می‌دهید. طبیعتا قطعه نان آخری که در فِر قرار داده‌اید را، در هنگام خارج کردن نان‌ها، اول از همه بیرون خواهید آورد. در علوم کامپیوتر به این فرآیند Last In First Out یا همان LIFO گفته می‌شود.در محاسبات ریاضیاتی، ما چنین عمل می‌کنیم :۱۰ + ۲ * ۲ ابتدا ۲ را در ۲ ضرب نموده و سپس با ۱۰ جمع می‌کنیمدر Stack Machine اصول LIFO به این شکل عمل می‌کنند :Stack Machine : 2 2 * 10 + به این معنی که : ابتدا ۲ را در اِستَک قرار بده، سپس عدد ۲ بعدی را قرار بده و بعد از آن عملیات ضرب را انجام بده. حاصل آن ۲ می‌شود که در راس اِستَک قرار می‌گیرد. سپس عدد ۱۰ را در اِستَک قرار بده (بعد از ۴) و این دو عدد را با هم جمع کن. مقدار نهایی استک ۱۴ خواهد بود. عمل وارد کردن داده در استک PUSH و عمل خارج کردن داده POP نام دارد. در Opcode ـی که بالا تر دیدم تعداد زیادی PUSH1 وجود دارد که به معنی قرار دادن ۱ بایت از داده ای مشخص در اِستَک می باشد. (در ادامه این مورد با مثال‌های متعدد بیشتر توضیح داده می‌شود)در نتیجه، این عبارت :PUSH1 0x60به معنی قرار دادن ۱ بایت با مقدار "0x60" در اِستَک می‌باشد. تصادفا مقدار هگزایِ عملیات PUSH برابر با "0x60" است که اگر عبارت غیر الزامی "0x" را حذف کنیم، Bytecode بدست آمده برابر با "6060" می‌شود.یک قدم پیش تر برویم :PUSH1 0x60 PUSH1 0x40 MSTOREبا نگاهی به لیست Opcode ها، می‌بینیم که MSTORE (هگزا : 0x52) دو ورودی می‌گیرد و خروجی‌ای ندارد. Opcode بالا بدین معنی است :PUSH1 0x60 : مقدار 0x60 را وارد اِستَک کن2. PUSH1 0x40 :مقدار 0x40 را وارد اِستَک کن3. MSTORE 0x52 :به مقدار 0x60 از فضای حافظه را اختصاص بده و به نقطه 0x40 برو(در ادامه توضیح ساده تری از این Opcode می‌بینید)نتیجه نهایی Bytecode اینچنین خواهد بود :Result : 6060604052در حقیقت ما همیشه مقدار "6060604052" را در ابتدای بایت کدهای Solidity می‌بینیم و دلیل این موضوع این است که Smart Contract ها با این Opcode راه اندازی (Bootstrap) می‌شوند.در Opcode بالا، مقدار 0x60 در مبنای ده برابر ۹۶ و مقدار 0x40 در مبنای ده برابر ۶۴ است. در نتیجه Opcode بالا به این معنی است که :۹۶ بایت از حافظه را تخصیص بده و مکان پوینتر را به ابتدای ۶۴ اُمین بایت تغییر بده.در EVM داده‌ها در سه بخش می‌توانند ذخیره شوند : اِستَک : که نمونه آن PUSH می‌باشد و بالا تر مثال آن را دیدیمحافظه تصادفی (RAM) : که نمونه آن MSTORE در Opcode ها می‌باشدحافظه دیسک : که نمونه Opcode آن SSTORE استدر اِتِریوم برای ذخیره داده در حافظه دیسک باید gas پرداخت شود و به همین دلیل ذخیره داده در دیسک گران‌ترین و ذخیره داده در اِستَک ارزان‌ترین است.زبان Assemblyنوشتن کامل Smart Contract با Opcode ها امکان پذیر است و برای این منظور زبان اسمبلی Solidity ساخته شده است. طبیعتا نوشتن در این حالت بسیار سخت تر است، ولی برای نوشتن Contract هایی که در آن‌ها باید در مصرف gas صرفه جویی شود و یا زمانی که کاری می‌خواهید انجام دهید که اجرای آن در Solidity امکان پذیر نیست، استفاده از زبان اسمبلی Solidity کارا خواهد بود.در انتهابرای شروع به نوشتن Smart Contract ها نیازی به دانستن Opcode ها نداریم. اما دانستن آنها علاوه بر اینکه دانش ما را در این زمینه بالا می‌برد، در هنگام Error Handling (که به خوبی در EVM انجام نمی‌شود) بسیار مفید می باشد. نویسنده : Bernard Peh