خطای Stack Too Deep در سالیدیتی

در این مطلب می‌خواهیم به بررسی علت به وجود آمدن خطای Stack Too Deep یا «پشته‌ بیش از حد عمیق» بپردازیم. سعی ما این است که ضمن ارزیابی جوانب مختلف این قضیه، راهکارهای احتمالی را به شما معرفی کنیم تا بتوانید از پس این چالش آزاردهنده‌ سالیدیتی (Solidity) بربیایید.

0 89

وقتی شروع به کدنویسی در سالیدیتی می‌کنید، دیر یا زود به مانعی بسیار آزاردهنده بر می‌خورید. این مانع همان خطای Stack Too Deep یا «پشته‌ بیش از حد عمیق» است. هر کسی ممکن است به راحتی در چنین تله‌ای گیر بیفتند، و وقتی چنین اتفاقی افتاد، دیگر به سختی می‌توان راهی برای خروج از این تله پیدا کرد. اگر منصفانه نگاه کنیم، این مشکل از خود سالیدیتی نشأت نمی‌گیرد، بلکه مربوط به ماشین مجازی اتریوم (EVM) است و به همین دلیل بر سایر زبان‌هایی که در EVM کامپایل می‌شوند اثر می‌گذارد. ولی به هر روی، این اتفاق چالشی است که پیش روی همه‌ی برنامه‌نویسان قراردادهای هوشمند قرار دارد.

عجیب این است که علی‌رغم آن که خطای مذکور بسیار آزاردهنده است، منابع خوبی برای مقابله با این مشکل وجود ندارد، بنابراین ما می‌خواهیم در این پست به این موضوع بپردازیم تا اگر به مشکل مذکور برخورده‌اید بتوانید از محتوای این مطلب استفاده کنید.

این خطا به طور کلی زمانی ایجاد می‌شود که کد باید به لایه‌ای از پشته دسترسی یابد که از عنصر شانزدهم خودش (با شمارش از بالا به پایین) عمیق‌تر باشد. با این حال، به روش‌های مختلفی می‌توانیم به این لایه دسترسی پیدا کنیم. در این پست نمی‌خواهیم به تشریح علل تئوری به وجود آمدن این خطا بپردازیم. اما اگر در این باره آگاهی داشته باشید، درک بهتری از نحوه‌ی مدیریت پشته توسط EVM خواهید داشت. حتی شاید بتوانیم این منطق را به سایر موقعیت‌هایی که این خطا در آن‌ها رخ می‌دهد تعمیم داده و به دنبال روشی برای حل آن باشیم.

در سالیدیتی، اکثر انواع داده‌ها (مثل انواع ابتدایی داده نظیر اعداد، آدرس‌ها و نوع بولی، و نه آرایه‌ها، ساختارها یا نگاشت‌ها) از طریق مقدار (ارزش) به تابع منتقل می‌شوند: وقتی تابع فراخوانی شد، بخشی از پشته (یعنی فریمی از پشته) به جایگاه بازگشتی اختصاص می‌یابد که برنامه در هنگام برگشت تابع باید به آن‌جا (آدرس بازگشت) برود؛ بخشی نیز به نسخه‌ای از آرگومان‌های ورودی و خروجی نوع-مقدار تابع اختصاص داده می‌شود. هر آرگومان به طور عادی لایه‌ای از پشته را در اختیار دارد و هر لایه ۲۵۶ بیت است.

یکی از رایج‌ترین نمونه‌های برخورد با خطای Stack Too Deep زمانی رخ می‌دهد که مجموع آرگومان‌های ورودی و خروجی بیش از ۱۶ عدد باشد. اما در واقعیت، اگر می‌خواهیم تابع‌مان عملکرد مفیدی داشته باشد، باید مراقب باشیم و احتمالا تعداد آرگومان‌ها را کاهش دهیم.

برای آزمایش این روش، در Remix قرارداد کوچکی مثل نمونه زیر ساختیم:

Remix ابزار مناسبی برای این مدل بررسی‌هاست چون در آن به سرعت می‌توانیم قرارداد بسازیم و آن را مورد پرس‌وجو قرار دهیم، ولی علاوه بر این، Remix دیباگری قدرتمند است که از قابلیت پیاده‌سازی کد اجرایی و فهرست‌بندی پشته، حافظه و فضای ذخیره‌سازی استفاده می‌کند. همچنین، با توجه به این که Remix یکی از بهترین ابزارهایی است که تا به حال با آن کار کرده‌ایم، عقب و جلو رفتن در کد با کمک آن آسان است.

این قرارداد بسیار ساده است، چون هیچ متغیر وضعیتی ندارد و فقط از یک تابع استفاده می‌کند که به هیچ عنوان پیچیده نیست. این تابع تنها یک آرگومان دریافت کرده و از همان لاگ می‌سازد.

ما این قرارداد را در فایل جدیدی در Remix کپی، کامپایل و پیاده‌سازی می‌کنیم. در این مرحله نباید هیچ خطا یا هشداری دریافت کنیم. پس به زبانه‌ی Run می‌رویم و دکمه‌ی Deploy را انتخاب می‌کنیم.

سپس فهرست قرارداد SimpleFunction را بسط داده و مقداری را در فیلد جلوی logArg وارد می‌کنیم. حالا دکمه‌ی مربوطه را فشار داده و خروجی را از کنسول نگاه می‌کنیم:

stack too deep

همان طور که می‌بینید، ما مقدار 7 را وارد و همین عدد را به عنوان تنها عنصر لاگ دریافت کردیم. اگرچه مبحث لاگ‌ها خودش به تنهایی مستلزم یک پست جداگانه است، اما در این مطلب به چند نکته درباره‌ی لاگ‌ها اشاره می‌کنیم.

این لاگ در قالب JSON به شکل زیر است:

  • لاگ‌ها در سالیدیتی با کلیدواژه‌ی انتشار ساخته می‌شوند. در این فرآیند رویدادی از سالیدیتی به وجود آمده و به کد اجرایی LOGn اتلاق می‌شود.
  • لاگ‌ها را می‌توان با اپلیکیشن‌های طرف کلاینت که در بیرون از زنجیره اجرا می‌شوند فیلتر کرد. فیلتر وضعیت مربوط به یکی از تاپیک‌هایی است که در لاگ وجود دارد.
  • لاگ‌ها همیشه دارای تاپیک صفر هستند، عنصری که حالت رمزنگاری ‌شده‌ امضای رویداد است.
  • تاپیک‌های بعدی را می‌توان با شاخص‌گذاری آرگومان‌ها ایجاد کرد. تعداد آرگومان‌های شاخص‌گذاری‌شده می‌تواند حداکثر ۳ عدد باشد. آرگومان‌های باقی‌مانده همان آرگومان‌هایی هستند که به عنوان اطلاعات رویداد در نظر گرفته می‌شوند.

در این مثال، به راحتی می‌توانیم تشخیص دهیم که فقط یک تاپیک (“0xfcf771399d75a67a6d0e730ae98d34c40b6bfe6ebf8053b98ddf4da8c2706250”) وجود دارد و اطلاعات به عنوان بخشی از عدد آرگومان‌های شئ لاگ نمایش می‌یابد. علاوه بر این، می‌توانیم تایید کنیم که کد مطابق انتظار ما عمل می‌کند.

اکنون بگذارید محدودیت‌های این قرارداد را آزمایش کرده و تابع را تغییر دهیم تا بتوانیم حداکثر تعداد آرگومان‌ها را دریافت کنیم.

ما ۱۶ متغیر ورودی و صفر متغیر خروجی داریم، بنابراین فقط باید از ۱۶ لایه‌ی پشته استفاده کنیم. ابتدا تابعی را فعال می‌کنیم که مقادیر ۱ تا ۱۶ را منتقل و مقدار آخر را منتشر می‌کند. اگر لاگ را بررسی کنیم می‌توانیم مقدار ۱۶ را ببینیم. پس برنامه کار می‌کند.

حالا تغییر بسیار کوچکی در قراردادمان ایجاد و اولین آرگومان را لاگ می‌کنیم:

stack too deep

چه اتفاقی افتاد؟ لاگ گرفتن از آرگومانی متفاوت باعث شد قرارداد کاملا سالم ما دچار خطای Stack Too Deep شود. اما این یعنی چه؟

این مشکل چیزی است که سالیدیتی نمی‌تواند آن را به سادگی توضیح دهد. در این سطح، تغییر مذکور کاملا بی‌آزار به نظر می‌رسد. برای این که بفهمیم چه اتفاقی دارد می‌افتد باید وارد بایت‌کد EVM شویم. ولی قبل از انجام این کار، ابتدا می‌خواهیم آزمایش دیگری انجام دهیم تا سرنخ‌های بیشتری به دست بیاوریم. پس نسخه‌ی سومی از قرارداد می‌سازیم ولی این بار a2 را لاگ می‌کنیم:

این کد به درستی کار کرده و مقدار صحیح را لاگ می‌کند. حتی اگر مقدار a3 را لاگ کنیم نیز مشکلی پیش نمی‌آید. پس فرض می‌کنیم که می‌توانیم از همه‌ آرگومان‌های بین a2 تا a16 به درستی لاگ بگیریم.

کدهای اجرایی نهایی در این سه فایل قرار دارد:

  • (log(a2
  • (log(a3)
  • (log(a16

این ۳ لاگ را با خودشان مقایسه کردیم، و اولین چیزی که جالب توجه بود این بود که اندازه‌ی این سه (از نظر تعداد خط) با یکدیگر متفاوت است. نکته بعدی این است که هر سه لاگ تا خط ۲۳۷ کاملا مشابه یکدیگرند و تنها یک تفاوت وجود دارد. کدی که بعد از این خط وجود دارد کاملا فرق می‌کند و ظاهرا غیرقابل پیش‌بینی است. اما از آن‌جایی که به نظر می‌رسد این بخش مربوط به زمانی باشد که تابع برگشته، آن را نادیده می‌گیریم.

سپس روی تفاوتی که در خط ۱۹۸ رخ می‌دهد تمرکز می‌کنیم. حالا می‌توانیم روی این ایده مانور بدهیم که در بعضی از بخش‌های کد به لحاظ منطقی باید برخی از کدهای اجرایی DUP یا SWAP را فراخوانی کنیم، کدهایی که وجود خارجی ندارند. به نظر می‌رسد که مشکل از همین جا به وجود آمده است: هر ۳ نسخه تا خط ۲۳۷ مثل یکدیگرند، به جز تنها تفاوتی که در خط ۱۹۸ اتفاق می‌افتد:

  • log(a2): DUP16
  • log(a3): DUP15
  • log(a16): DUP2

کدهای اجرایی DUPn مقدار مزبور را در سطح n اُم پشته تکرار می‌کند. فقط ۱۶ نمونه از این کد اجرایی وجود دارد: از DUP1 تا DUP16. DUP1 نسخه‌ای از مقداری را که فعلا در بالا قرار دارد به پشته می‌فرستد، و DUP16 شانزدهمین مقدار بزرگ پشته را کپی می‌کند. میان محل متغیر فهرست آرگومان و مقدار DUPn این خط رابطه‌ای مشهود وجود دارد، و اگر این مدرک را به مورد (log(a1 تعمیم دهیم، قاعده‌ مذکور حاکی از آن است که ما به کد اجرایی DUP17 نیاز داریم. اما چنین کدی وجود خارجی ندارد و به مقداری اشاره می‌کند که از سطح دسترسی ما پایین‌تر است؛ در نتیجه خطای Stack Too Deep به وجود می‌آید.

سوالی که حالا پیش می‌آید این است که این کد اجرایی DUP دقیقا این‌جا چه کار می‌کند؟ هدف آن چیست؟

بایت‌کد چیز ترسناکی است. کمتر کسی در چنین سطحی به بررسی کد اسمبلی می‌پردازد تا مشکل‌یابی کند. من هم تا به حال با EVM کار نکرده‌ام، پس طبیعتا قصد ندارم ۲۰۰ خط کد را تجزیه و تحلیل کنم. ولی Remix در این زمینه نیز ابزارهای خوبی فراهم کرده است. در زبانه‌ی دیباگ، می‌توانیم کد اجرایی تراکنش را به صورت بخش به بخش اجرا کنیم، و در یک نگاه محتوای پشته، حافظه و فضای ذخیره‌سازی را ببینیم.

این تابع نکته‌ خاصی ندارد و اکثر بخش‌های بایت‌کد صرفا تکرار بخش‌های قبلی است. ۱۷ عدد از رخدادهای این کد CALLDATALOAD است. اولی در بلاک نخست کد و قبل از شروع کار تابع ظاهر می‌شود. این رخداد بررسی می‌کند تا مطمئن شود که calldata بیش از حد کوتاه نیست، در غیر این صورت تابع برگردانده می‌شود. سپس انتخاب‌گر تابع را با متدهای شناخته‌شده‌ قرارداد مقایسه می‌کند و اگر نمونه‌ی مشابه‌ای پیدا شد، برنامه را به سوی آدرسی هدایت می‌کند که تابع را پیاده‌سازی می‌نماید. در غیر این صورت، تابع برگردانده می‌شود.

در این مثال، کد ما تنها تابع موجود را فراخوانی کرده و برنامه به آدرس ۷۰ پرش می‌کند.

۱۶ نمونه‌ی باقی‌مانده از CALLDATALOAD دقیقا همان اعداد یا آرگومان‌های ماست، آن‌ها دقیقا در ۹ فاصله‌ی خطی ظاهر شده و احتمالا مسئول پردازش هر آرگومان تابع هستند. اگر با دیباگر Remix این خطوط را بررسی کنیم، درمی‌یابیم که آن‌ها همه‌ی آرگومان متوالی را در پشته بارگذاری می‌کنند. این عملیات با دستورالعمل‌های 3 POP ادامه می‌یابد که بخشی از پشته را پاک می‌کند که دیگر به آن نیازی نداریم (و قبلا برای محاسبه‌ی جایگاه فراخوانی داده در آرگومان بعدی استفاده می‌شد). در این مرحله، بالای پشته حاوی شانزدهمین آرگومان است، عنصر دوم پانزدهمین آرگومان را در بر دارد و بقیه نیز به همین ترتیب هستند. شانزدهمین عنصر پشته، در این مرحله، اولین آرگومان است. این عنصر در کنار آدرس برگشتی و انتخاب‌گر تابع قرار می‌گیرد.

این کد سپس تاپیک شناسه‌ ۳۲ بایتی 0 fcf771399d75a67a6d0e730ae98d34c40b6bfe6ebf8053b98ddf4da8c2706250 را به پشته می‌فرستد، همان عنصری که اولین ورودی را از ۱۶ عنصر برتر پشته خارج کرده و این عمل را با کد اجرایی DUP ادامه می‌دهد و آرگومان مربوط به رویداد لاگ را در بالای پشته می‌گذارد.

۲۰ خط بعدی حافظه را برای دریافت آرگومان رویداد لاگ در موقعیت 0x80 حافظه آماده کرده و تضمین می‌کند که پشته در دو جایگاه برتر خود حاوی این آدرس و طول اطلاعات مربوطه است. سپس کد اجرایی LOGI فراخوانی می‌شود که با استفاده از اطلاعات ۳ جایگاه برتر پشته رویداد لاگی با یک آرگومان و یک تاپیک منتشر می‌کند:

  • 0: 0x0000000000000000000000000000000000000000000000000000000000000080
  •  1: 0x0000000000000000000000000000000000000000000000000000000000000020
  •  2: 0xfcf771399d75a67a6d0e730ae98d34c40b6bfe6ebf8053b98ddf4da8c2706250

در مجموع پنج کد اجرایی LOGn وجود دارد: LOG0 تا LOG4، که n در آن عدد تاپیک‌های لاگ را نشان می‌دهد. topic0 همیشه شناسه‌ی نوع رویداد است که با هش امضای آن تعریف می‌شود، اما می‌تواند با استفاده از LOG0 نادیده گرفته شود، چون LOG0 رویداد نامعلومی را مشخص می‌کند. هر تاپیک اضافه نیازمند لایه‌ی دیگری در پشته است، به همین خاطر بسیاری از آرگومان‌های دیگر از فهرست آرگومان‌های قابل دسترسی خارج می‌شود.

این تحلیل نشان می‌دهد که رویدادی با یک آرگومان از بکارگیری متغیر جلوگیری می‌کند، زیرا topic0 پیش از اطلاعات رویداد در پشته قرار گرفته است. در نتیجه چند سوال پیش می‌آید:

  • اگر تاپیک‌های بیشتری داشته باشیم چه؟ آیا آن‌ها هم در پشته پیش از اطلاعات قرار می‌گیرند؟
  • و تاثیر آرگومان‌های بیشتر رویداد چیست؟ آیا آن‌ها قبل از تاپیک قرار می‌گیرند یا بعد از آن؟

برای آزمایش این مورد قرارداد را دوباره تغییر داده و متوجه شدیم که رویدادها می‌توانند بی‌نهایت آرگومان داشته باشند ولی فقط سه آرگومان شاخص‌گذاری می‌شود. آرگومان‌های شاخص‌گذاری‌شده به تاپیک تبدیل می‌شوند در حالی که سایر شاخص‌ها به بخش اطلاعات می‌روند. در این مرحله فرض ما این است که هر تاپیک (آرگومان شاخص‌گذاری‌شده) پیش از اطلاعات در پشته قرار گرفته، و از دسترسی به بیشتر متغیرهای ابتدایی جلوگیری می‌کند.

ما در این آزمایشات چندین سناریوی مختلف را بررسی کردیم، اما همگی به یک نتیجه منجر شد، بنابراین در این بخش از بررسی تک‌تک آزمایش‌ها خودداری کرده و مستقیما بعد از بررسی یکی از این موارد، به سراغ نتیجه‌ کار می‌رویم.

اول بیایید به سراغ این نسخه از قرارداد برویم که در آن یک مقدار شاخص‌گذاری‌شده و دو مقدار شاخص‌گذاری‌نشده وجود دارد:

بایت‌کد این تابع (پس از شروع به کار آن) تا زمانی که رویداد منتشر شود از قرار زیر است:

کدی که رویداد را منتشر می‌کند LOG2 است. این یعنی دو تاپیک داریم که یکی تاپیک پیش‌فرض topic0 است (یعنی امضای رویداد) و دیگری تنها آرگومان شاخص‌گذاری‌شده در امضای رویداد. دو مقدار باقی‌مانده در حافظه گروه‌بندی می‌شود.

اگر برای این کد اجرایی Ethervm را چک کنیم، می‌بینیم که مقدار آخر از پشته خوانده شده، و اولین تاپیکی که به آن وارد می‌شود topic1 است. این تاپیک در ابتدا در موقعیت ۱۵ پشته قرار می‌گیرد. کد اجرایی DUP15 نسخه‌ای از این مقدار را در بالای پشته قرار داده و بعد سایر آرگومان‌ها را پایین می‌آورد. از این جا به بعد، برای مثال، a2 در موقعیت ۱۶ و a1 در موقعیت ۱۷ قرار دارد. دستورالعمل‌های بعدی دو کد اجرایی DUP16 است. اولی مقدار موجود در موقعیت ۱۶ را که در حال حاضر سومین آرگومان (a3) است کپی می‌کند. ولی از آن‌جایی که این کار باعث انتقال عنصر جدید به پشته می‌شود، وقتی کد اجرایی فراخوانی شود، DUP16 چهارمین آرگومان (a4) را به تابع کپی می‌کند. در این مرحله، اطلاعات رویداد، یعنی همان آرگومان شاخص‌گذاری‌شده و شناسه‌ یکتای رویداد، را در بالای پشته در اختیار داریم.

خطوط بعدی دو مقدار نخست حافظه را کپی می‌کند:

  • ۳۰۲ تا ۳۰۵: محتوای حافظه 0x40 را دو بار در بالای پوشه قرار می‌دهد. این همان موقعیتی است که اطلاعات رویداد در آن قرار می‌گیرد (و در مثال ما 0x80 است).
  • ۳۰۶ تا ۳۰۸: اولین کلمه‌ اطلاعات را در موقعیت اولین جایگاه خالی حافظه قرار می‌دهد (یعنی a3 را در 0x80 می‌گذارد).
  • ۳۰۹ تا ۳۱۱: جایگاه خالی بعدی را در بالای پشته‌ حافظه قرار می‌دهد.
  • ۳۱۲ تا ۳۱۴: دومین کلمه‌ اطلاعات را در جایگاه خالی بعدی حافظه قرار می‌دهد (یعنی a4 را در 0xa0 می‌گذارد).
  • ۳۱۵ تا ۳۲۱: جایگاه خالی بعدی حافظه را محاسبه کرده و بعد از حذف مقادیری که دیگر به درد ما نمی‌خورد، آن را در بالای پشته رها می‌کند.
  • ۳۲۲ تا ۳۲۷: با تفریق آدرس اولیه‌ی جایگاه خالی بعدی حافظه از مقدار فعلی آن جایگاه، طول اطلاعاتی که در رویداد ثبت شده را پیدا می‌کند.
  • ۳۲۸: دو عنصر نخست پشته را ثبت می‌کند تا اولی عنصر شروع اطلاعات رویداد و دومی طول این اطلاعات باشد.
  • ۳۲۹: در نهایت تابع لاگ کد اجرایی را فرا می‌خواند.

اطلاعات بالا را از این جهت به شما گفتیم تا، اگر خواستید، درک صحیحی از نحوه‌ی عملکرد این پروسه داشته باشید. حالا شاید بتوانید توضیح دهید که در مرحله‌ بعدی چه اتفاقی می‌افتد. کافی است امضای رویداد را به صورت زیر عوض کنیم:

stack too deep

بله، همان طور که می‌بینید دوباره خطای Stack Too Deep به وجود آمده است. اما می‌توانید بگویید چرا؟

بایت‌کد تغییر چندانی نمی‌کند. ما همچنان همان تعداد تاپیک را در اختیار داریم، بنابراین کد اجرایی در انتها باز هم LOG2 خواهد بود. و هنوز انتظار می‌رود که کد اجرایی آرگومان‌های خود را با ترتیب مشابه‌ای دریافت کند، یعنی اول تاپیک‌ها و بعد اطلاعات را بگیرد.

حالا ابتدا باید تاپیک دوم را بارگذاری کنیم تا a3 اولین مقداری باشد که با DUP14 به پشته منتقل می‌شود. سپس topic0 ارسال می‌گردد. در این مرحله، EVM دو آرگومانی را که باید در حافظه ذخیره شود، یعنی a2 و a4، را در بالای پشته قرار می‌دهد. این آرگومان‌ها در ابتدا در موقعیت ۱۵ و ۱۳ قرار داشتند. اما EVM تاکنون دو جابجایی انجام داده، بنابراین حالا موقعیت آن‌ها ۱۷ و ۱۵ است. با توجه به این که امکان ندارد بتوانیم مقدار نخست را در پشته قرار دهیم (چون DUP17 وجود ندارد)، پس برنامه خطا می‌دهد.

حالا که این موضوع را فهمیدیم، تابع لاگ را به صورت زیر تغییر می‌دهیم:

این کد به درستی کار می‌کند، زیرا این برنامه پیش از آن که ترتیب آرگومان‌های شاخص‌گذاری‌شده را تغییر دهیم، رابطه‌ مناسبی با بلاک آخر دارد. در این کد، مقدار شاخص‌گذاری‌شده‌ رویداد a2 نام داشت. در این نسخه، نام این مقدار هنوز همان a2 است که به موقعیت مذکور انتقال داده شده، و بقیه نیز به همان صورت باقی می‌مانند. توضیح بایت‌کد هم عملا به شکلی مشابه انجام می‌گیرد.

جمع‌بندی

اگر تا انتهای این مطلب با ما همراه بودید، در این جا می‌خواهیم نگاهی اجمالی به همه‌ اتفاقاتی بیندازیم که در حال رخ دادن است، تا بتوانید به برنامه‌ی خودتان برگردید و ببینید که آیا خطای Stack Too Deep برای شما نیز به همین دلایل رخ می‌دهد یا نه. البته ما در این پست فقط مبحث رویدادهای انتشاردهنده را پوشش دادیم و سایر توابع مستلزم کدهای اجرایی متفاوتی است، اما منطق پشت قضیه تغییری نمی‌کند، چون همه جا باید آرگومان‌های تابع (یا مقادیر آنی) را، در هنگام نیاز به محاسبه، به پشته کپی کنید.

پس موارد زیر را در نظر داشته باشید:

  • وقتی یک تابع فراخوانی می‌شود، فریمی برای پشته به وجود می‌آید. این فریم از پایین به سمت بالا شامل موارد زیر است:
    • انتخاب‌گر تابع
    • آدرس بازگشت
    • چپ‌ترین آرگومان نوع-مقدار تابع
    • راست‌ترین آرگومان نوع-مقدار تابع
  • خطاهای Stack Too Deep به کد اجرایی مرکزی عملیات ما (مثلاً محاسبه، هش کردن، فراخوانی سایر توابع، انتشار رویدادها و غیره) وابسته است.
  • اگر این عملیات‌های مرکزی در آرگومان‌های توابع خالص اجرا شود، ترتیب انتقال آن‌ها به تابع می‌تواند باعث به وجود آمدن خطای Stack Too Deep شود. (از لایه‌های پشته هم می‌توان برای محاسبات آنی و متغیرهای محلی استفاده کرد.)
  • آگاهی از تعداد و ترتیب آرگومان‌های کد اجرایی ضروری است. این آرگومان‌ها معمولا از پشته خوانده می‌شوند (تنها استثنا کد اجرایی PUSH است).
  • آرگومان‌های کد اجرایی باید پیش از اجرای کد به پشته برگردانده شوند. هر PUSH آرگومان‌های تابع را حداقل یک لایه پایین‌تر می‌برد. آرگومان‌های تابعی که در پشته عمیق‌تر هستند همان آرگومان‌هایی‌اند که ابتدا پردازش می‌شوند، به همین خاطر چپ‌ترین آرگومان‌های موجود در امضای تابع هستند.
  • اگر آرگومان‌های تابع در عملیات کد اجرایی استفاده نشوند، در صورتی که می‌خواهیم احتمال از دسترس خارج بودن آرگومان‌های کد اجرایی کاهش یابد، باید آن‌ها را در ابتدای امضای تابع قرار دهیم.
  • کدهای اجرایی در پشته از سطوح مختلف آرگومان‌ها استفاده می‌کنند. اول سطوح عمیق‌تر جابجا می‌شوند. اگر آرگومانی پس از یک آرگومان دیگر جابجا شود، نمایش آن در امضای تابع نیز باید پس از آرگومان اول صورت بگیرد، در غیر این صورت، ممکن است آرگومان دیگری پیش از به کار گرفته شدن در پشته پایین برود. مثلا:

۱. رویدادی با دو آرگومان شاخص‌گذاری‌شده‌ی t1 و t2 را در نظر بگیرید که همراه با چند آرگومان درون یک تابع فراخوانی می‌شوند. در این میان آرگومان a1 جلوتر از a2 قرار می‌گیرد.

۲. اگر رویداد با t1 = a1 و t2 = a2 منتشر شود، کد اجرایی LOG3 فراخوانی می‌شود.

۳. پیش از فراخوانی این کد، t2 = a2 به ابتدای پشت فرستاده می‌شود.

۴. این کار باعث می‌شود a1 یکی پایین بیاید و این احتمال به وجود بیاید که در هنگام Push شدن مقدار t1 = a1 این آرگومان از دسترس خارج شود.

۵. اگر a1 پس از a2 به امضای تابع بیاید، می‌توانیم از این مشکل جلوگیری کنیم، چون جایگاه این آرگومان از a2 بالاتر خواهد بود. با فرض آن که a2 به هنگام جابجا شدن در دسترس باشد، پس a1 هم در دسترس خواهد بود.

  • پست بالا صرفا با تمرکز بر کد اجرایی LOGn نوشته شده، خصوصا با تمرکز بر نسخه‌هایی که در پشته به ۳ یا ۴ آرگومان نیاز دارند. در نمونه‌های دشوارتر، باید توابع را در سایر قراردادها یا کتابخانه‌ها فراخوانی کنیم، چون کدهای اجرایی CALL و DELEGATECALL هر کدام ۶ یا ۷ ورودی می‌گیرند، و احتمال بیشتری برای برقراری تعامل میان کد اجرایی و آرگومان‌های تابع وجود دارد.

امیدواریم این مطلب به شما کمک کرده باشد تا اطلاعات مفیدی درباره‌ی مشکل‌یابی و برطرف‌سازی خطای Stack Too Deep به دست آورده باشید.

شاید از این مطالب هم خوشتان بیاید.

ارسال پاسخ

آدرس ایمیل شما منتشر نخواهد شد.