موضوع مقاله: قراردادهای فراخوانی اکس۸۶ | مخاطب: عمومی | لحن: دانشنامه‌ای | ساختار: عنوان، مقدمه، 3 بخش، جمع‌بندی | مقاله: | # قراردادهای فراخوانی اکس۸۶ | مقدمه: قراردادهای فراخوانی، رابط کاربری کد فراخوانی شده را توصیف می‌کند: ترتیبی که به پارامترهای اتمی (اسکالر) یا بخش‌های فردی از یک پارامتر پیچیده، اختصاص داده می‌شود پارامترها چگونه منتقل می‌شوند (گذاشته شدن بر روی پشته (به انگلیسی: stack)، قرار گرفتن در ثبّات (رجیستر)ها یا ترکیبی از هر دو) کدام ثبّات‌های تابع فراخوانی شده باید برای فراخوانی‌کننده حفظ شود (همچنین شناخته شدن به عنوان: ثبّات‌های ذخیره شده تابع صدا شده(callee-saved) یا غیر فرار (non-volatile) چگونه وظیفه آماده‌سازی پشته و بازگرداندن بعد از یکبار صدا زدنِ تابع، بین فراخوانی کننده(caller) و فراخوانی شده(callee) تقسیم می‌شود این دقیقاً با تخصیص دادنِ اندازه‌ها و فرمت‌ها به انواع زبان برنامه‌نویسی مرتبط است. | ## بخش 1: قراردادهای فراخوانی اکس۸۶ | یکی دیگر از موضوع‌های خیلی مرتبط برانگیختگی نام (Name mangling) است، که تعیین می‌کند که چگونه نمادها در کد به نماد مورد استفاده در لینکر مرتبط و تبدیل شوند. قراردادهای فراخوانی، نمایندگی‌های نوع، و برانگیختگی نام، همه بخشی از آنچه که به عنوان یک رابط دودویی نرم‌افزار شناخته می‌شود هستند. اغلب تفاوت‌های ظریفی در نحوه پیاده‌سازی این قراردادها در کامپایلر‌های مختلف وجود دارد، بنابراین اغلب سخت است که کدی را که توسط کامپایلرهای مختلف کامپایل شده‌است نمایش دهیم. از سوی دیگر، قراردادهایی که به عنوان استاندارد API (مانند stdcall) مورد استفاده قرار می‌گیرند، به‌طور خیلی یکسان پیاده‌سازی می‌شوند. پیشینه تاریخی قبل از میکرو رایانه‌ها، تولیدکننده دستگاه، به‌طور کلی یک سیستم عامل و کامپایلر‌هایی برای چند زبان برنامه‌نویسی ارائه می‌داد. قرارداد (های) فراخوانی برای هر پلت فرم، توسط ابزار برنامه نویسیِ سازنده آن تعریف می‌شود. رایانه شخصی آی‌بی‌ام با سیستم عامل دیسک (DOS) (پیشگام مایکروسافت برای ویندوز) عرضه شد، اما بدون کامپایلر. تنها سخت‌افزار استاندارد برای ماشین‌های سازگار با رایانه شخصی IBM توسط پردازنده‌های اینتل (۸۰۸۶، ۸۰۳۸۶) تعریف شد که سخت‌افزار تحت اللفظی IBM نامیده می‌شد. سپس فرمت‌های سخت‌افزاری و تمامی استانداردهای نرم‌افزاری (ذخیره برای قراردادهای فراخوانی BIOS) برای رقابت در بازار عرضه شد. بسیاری از شرکت‌های نرم‌افزارهای مستقل، سیستم عامل‌ها و کامپایلرهایی برای بسیاری از زبان‌های برنامه‌نویسی و برنامه‌های کاربردی ارائه می‌دهند. بسیاری از طرح‌های مختلف فراخوانی کردن توسط شرکت‌ها پیاده‌سازی شده‌است که اغلب متقابلاً منحصر به فرد، بر اساس الزامات مختلف، شیوه‌های تاریخی و خلاقیت برنامه‌نویس هستند. پس از انعطاف‌پذیری بازار IBM، سیستم عامل‌های و ابزارهای برنامه‌نویسی مایکروسافت (با قراردادهای متفاوت) غالب بودند، در حالی که شرکت‌های رده دوم مثل بورلند و نوول و پروژه‌های منبع بازمانند GCC هنوز استانداردهای خود را حفظ کردند. در نهایت مقررات مربوط به قابلیت همکاری بین فروشندگان و محصولات به تصویب رسید، ساده کردن مشکل انتخاب یک قرارداد قابل قبول بود. پاک شدن توسط فراخوانی کننده در این قراردادها، فراخوانی کننده، آرگومان‌ها را از پشته پاک می‌کند. مقادیر اعداد صحیح و آدرس‌های حافظه به وسیلهٔ ثبات (رجیستر) EAX و مقادیر اعشاری در ثبّات ST0 x87 بازگردانده می‌شوند. ثبّات‌های اعشاری x87 T ST0 تا ST7، هنگام فراخوانی یک تابع جدید باید خالی (برداشته شده یا آزاد) باشند و ST1 تا ST7 باید در هنگام خروج از یک تابع خالی باشد. در چارچوب زبان برنامه‌نویسی C، آرگومان‌های تابع بر روی پشته از راست به چپ قرار می‌گیرند، یعنی اول آخرین آرگومان قرار داده می‌شود. در نتیجه، برنامه‌های کامپایل شده توسط x86 با سیستم عامل‌های مختلف یا کامپایلرهای مختلف می‌تواند ناسازگار باشد، حتی اگر آن‌ها هر دو از قراردادهای "cdecl" استفاده کنند و به محیط زیر وابسته نباشند. بعضی از کامپایلرها ساختار داده‌های ساده را با طول ۲ ثبّات یا کمتر در جفت ثبّات EAX: EDX برمی‌گردانند، و ساختارهای بزرگتر و اشیاء کلاس‌ها که نیاز به رفتار ویژه توسط رسیدگی کنندهٔ استثنا دارند (مانند سازندهٔ تعریف شده، تخریب‌کننده یا اختصاص دادن) به حافظه برگرداننده می‌شوند. برای انتقال "در حافظه"، فراخوانی‌کننده حافظه را اختصاص می‌دهد و یک اشاره گر به آن را به عنوان پارامتر پنهان می‌دهد؛ فراخوانی شده حافظه را پر می‌کند و اشاره گر را برمی‌گرداند (هنگام بازگشت اشاره گر پنهان برداشته می‌شود). قراردادهای فراخوانی cdecl معمولاً قرارداد فراخوانی پیش فرض برای کامپایلر‌های x86 C است، اگر چه بسیاری از کامپایلرها مجهز به گزینه‌هایی برای تغییر خودکار قراردادهای فراخوانیِ استفاده شده هستند. سه آرگومان اول (چپ‌ترین) در EAX, EDX و ECX منتقل می‌شود و بیشتر از چهار آرگومان اعشاری در ST0 از طریق ST3 منتقل می‌شود، اگرچه فضای آن‌ها در لیست آرگومان‌ها بر پشته رزرو می‌شود. پاک کردن توسط فراخوانی شده در این قراردادها، تابع فراخوانی شده، آرگومان‌ها را از پشته پاک می‌کند. | ## بخش 2: قراردادهای فراخوانی اکس۸۶ | تشخیص توابعی ک این قراردادها را استفاده می‌کنند در کد ASM آسان است زیرا آن‌ها پس از بازگشت، پشته را باز می‌کنند. دستور x86 ret به پارامتر ۱۶ بیتی اختیاری اجازه می‌دهد که بعد از بازگشت به فراخوانی کننده، تعداد بایت‌های مشخصی از پشته را آزاد کند. نمونه ای از این کد به صورت زیر است: ret 12 قراردادهایی به نام fastcall یا ثبّات استاندارد نشده‌اند و پیاده‌سازی آن بسته به نوع فروشنده کامپایلر متفاوت است. به‌طور معمول قراردادهای مبتنی بر ثبّات، یک یا چند آرگومان را به ثبّات‌هایی که تعداد دسترسی‌های حافظه مورد نیاز برای فراخوانی را کاهش می‌دهد و به این ترتیب آن‌ها را سریع تر می‌کند، ارسال می‌کنند پاسکال بر اساس قرارداد فراخوانی زبان برنامه‌نویسی Borland Pascal، پارامترها بر روی پشته از چپ به راست (مخالف cdecl) قرار می‌گیرند و فراخوانی شده مسئول حذف آن‌ها از پشته است. بازگشت نتیجه به شرح زیر است: مقادیر عادی در AL (مقادیر ۸ بیتی)، AX (مقادیر ۱۶ بیتی)، EAX (مقادیر ۳۲ بیتی) یا DX: AX (مقادیر ۳۲ بیتی در سیستم‌های ۱۶ بیتی) بازگرداننده می‌شوند. اشاره گرها در EAX در سیستم‌های ۳۲ بیتی و در AX در سیستم‌های ۱۶ بیتی بازگرداننده می‌شوند.. نسخه‌های مدرن ویندوز API از stdcall استفاده می‌کنند، که هنوز هم فراخوانی شده پشته را طبق قرارداد پاسکال را ذخیره می‌کند، اما اکنون پارامترها از راست به چپ قرارداده می‌شوند. قرارداد stdcall قرارداد فراخوانی stdcall تغییری در قرارداد فراخوانی پاسکال است که در آن فراخوانی شده مسئول پاک کردن پشته است، اما پارامترها در پشته به ترتیب از راست به سمت چپ، همان‌طور که در قرارداد فراخوانی _cdecl قرار دارد، قرار می‌گیرند. قرارداد فراخوانیِ برداریِ مایکروسافت در ویژوال استودیو ۲۰۱۳، مایکروسافت قرارداد فراخوانی vectorcall__ پاسخگو به نگرانی‌های توسعه دهندگان کد در مورد کارآییِ بازی‌ها، گرافیک، ویدئو / صدا است. به‌طور مشابه برای IA-32، حداکثر تا ۶ ثبّاتها XMM / YMM به صورت تکراری برای آرگومان‌ها از نوع بردار از چپ به راست بدون در نظر گرفتن موقعیت قرار می‌گیرد. علاوه بر این، vectorcall__ پشتیبانی کردن از انتقال مجموعه ای از مقادیر همگن بردار (HVA) را اضافه کرد، که از نوع ترکیبی است که شامل بیش از چهار نوع بردار یکسان است، با استفاده از شش ثبّات یکسان. هنگامی که ثبّاتها برای آرگومان‌های برداری اختصاص داده می‌شوند، ثبّاتهای استفاده نشده به آرگومان‌های HVA از چپ به راست بدون توجه به موقعیت اختصاص داده می‌شوند. این قرارداد فراخوانیِ پیش فرضِ کامپایلر ۳۲ بیتی دلفی است، جایی که به عنوان ثبّات شناخته می‌شود. این به وسیلهٔ هسته لینوکس روی i386 از نسخه ۲٫۶٫۲۰ (منتشر شده در فوریه ۲۰۰۷) استفاده می‌شود. اگر هر آرگومان نتواند به یک ثبّات تخصیص داده شود (می‌گویند خیلی بزرگ است)، آن و تمام آرگومان‌های بعدی به پشته اختصاص داده می‌شوند. همان‌طور که در کتابچه راهنمای آن آمده‌است: «تعداد کمی از کاربران احتمالاً به این روش نیاز دارند، اما اگر لازم باشد، می‌تواند یک نجات دهنده باشد». قرارداد فراخوانی امن با قرارداد فراخوانی stdcall یکسان است، به جز اینکه آن استثناها به فراخوانی‌کننده در EAX به عنوان یک HResult (به جای در [FS:[0 ) منتقل می‌شوند، در حالی که نتیجه تابع توسط مرجع در پشته( هرچند آن پارامتر نهایی «خارج» باشد ) منتقل می‌شود. هنگام فراخوانی یک تابع دلفی از دلفی، این فراخوانی درست مثل هر قرارداد فراخوانی دیگر ظاهر می‌شود، زیرا هرچند استثنائات در EAX منتقل می‌شوند، آن‌ها به‌طور خودکار به وسیلهٔ فراخوانی‌کننده به حالت استثناء مناسب تبدیل می‌شوند. هنگام استفاده از اشیاء COM که در زبان‌های دیگر ایجاد شده‌است، HResults به صورت خودکار به عنوان استثنا مطرح می‌شود و نتیجه برای دریافت توابع در آن نتیجه است نه یک پارامتر. دو نسخه اصلی از thiscall بسته به نوع کامپایلر استفاده می‌شود و اینکه آیا این تابع از یک متغیر عددی از آرگومان‌ها، استفاده می‌کند یا خیر. وقتی توابع از یک متغیر عددی از آرگومان‌ها استفاده می‌کنند، فراخوانی‌کننده است که پشته را پاک می‌کند ( cf. ثبّاتهای ذخیره شده توسط فراخوانی کننده (فرار) با توجه به اینتل ABI که اکثریت کامپایلرها با آن مطابقت دارد، EAX, EDX و ECX برای استفاده در یک فرایند یا تابع آزاد هستند و لازم نیست که حفظ شوند همان‌طور که از نامش بر می‌آید، این ثبّاتهای همه منظوره معمولاً اطلاعات موقت (بی‌ثبات) را نگه می‌دارد که می‌تواند توسط هر تابع رونویسی شود. بنابراین، این مسئولیت تماس گیرنده است که هر یک از این ثبّاتها را بر روی پشته قرار دهد، اگر مایل به بازگرداندن مقادیر خود پس از فراخوانی فرعی باشد. | ## بخش 3: قراردادهای فراخوانی اکس۸۶ | ثبّات‌های ذخیره شده توسط فراخوانی شده (غیرقابل تغییر) دیگر ثبّات‌ها برای نگهداری مقادیر طولانی مدت (غیرقابل تغییر) استفاده می‌شود که باید در فراخوانی‌ها حفظ شود. به عبارت دیگر، هنگامی که فراخوانی‌کننده روند فراخوانی را اجرا می‌کند، می‌تواند انتظار داشته باشد که آن ثبّاتها پس از بازگشت از فراخوانی شده، همان مقدار را نگه داشته‌اند. بنابراین، فراخوانی شده مسئول هر دو کارِ ذخیره (قراردادن در پشته در ابتدا) و بازیابی (برداشتن از پشته به ترتیب) آنها، قبل از بازگشت به تماس گیرنده، می‌شود. همان‌طور که در مورد قبلی، این تمرین فقط باید در ثبّاتی انجام شود که فراخوانی شده آن را تغییر می‌دهد. قراردادهای فراخوانی x86-64 قراردادهای فراخوانی x86-64 از فضای اضافی ثبّات برای انتقال آرگومان‌های بیشتر در ثبّات‌ها بهره می‌برد. مقادیر بازگشتی از نوع عدد صحیح (شبیه به x86) در RAX اگر ۶۴ بیت یا کمتر برگرداننده می‌شوند. پارامترهایی که کمتر از ۶۴ بیت طول دارند، با صفر بسط داده نشده‌اند؛ بیت‌های بالا صفر نیستند. در قراداد فراخوانی مایکروسافت x64، مسئولیت تماس گیرنده است که دقیقاً قبل از فراخوانی تابع (صرف نظر از تعداد واقعی پارامترهای مورد استفاده) ۳۲ بایت از فضای سایه(shadow space) را در پشته اختصاص داده و از پشته را بعد از فراخوانی بردارد. فضای سایه برای ریختن RCX, RDX, R8، و R9، استفاده می‌شود اما باید تمام توابع، حتی آن‌هایی که با کمتر از چهار پارامتر هستند، را در دسترس بسازد. به عنوان مثال، یک تابع که ۵ آرگومان عدد صحیح می‌گیرد، اولین تا چهارم را در ثبّاتها می‌گیرد، و پنجم در بالای فضای سایه قرار می‌دهد؛ بنابراین هنگامی که تابع فراخوانی شده وارد می‌شود، پشته از (به ترتیب صعودی) آدرس برگشتی، و سپس فضای سایه (۳۲ بایت) و پارامتر پنجم تشکیل می‌شود. به این ترتیب، برای x86-64، تابع زبان اسمبلیِ نوشته شده توسط کاربر باید XMM6 و XMM7 را حفظ کند (در مقایسه با x86 که در آن نیازی نیست تابع زبان اسمبلیِ نوشته شده توسط کاربر XMM6 و XMM7 را حفظ کند). به عبارت دیگر، تابع زبان اسمبلیِ نوشته شده توسط کاربر باید برای ذخیره / بازگرداندن XMM6 و XMM7 قبل / بعد از تابع در هنگام حمل از x86 به x86-64 به روز شود. همان‌طور که در قرارداد فراخوانی مایکروسافت x64، آرگومان‌های اضافی به پشته منتقل می‌شوند . مقادیر بازگشتی صحیح تا ۶۴ بیت در اندازه در RAX ذخیره می‌شوند در حالی که مقادیری تا ۱۲۸ بیت در RAX و RDX ذخیره می‌شوند. اگر فراخوانی شده مایل به استفاده از ثبّاتهای RBX, RBP، و R12-R15 باشد، باید قبل از بازگشت کنترل به دست تماس گیرنده، مقادیر اصلی خود را بازگردانی کند. در صورت تمایل فراخوانی‌کننده به حفظ مقادیر خود، تمام ثبّاتهای دیگر باید توسط تماس گیرنده ذخیره شوند. برای توابع گره برگ (توابع که هیچ تابع دیگری را فراخوانی نمی‌کند)، یک فضای ۱۲۸ بایت فقط در زیر اشاره گر پشته تابع ذخیره می‌شود. کامپایلرها ممکن است برخی از دستورالعمل‌ها را در شروع عملیات (تنظیم RSP, RBP) با استفاده از این منطقه حذف کنند. با این حال، سایر توابع می‌توانند این منطقه را خراب کنند؛ بنابراین، این منطقه فقط باید برای توابع گره برگ استفاده شود. اگر فراخوانی شده یک تابع varadic باشد، سپس تعداد آرگومان‌های اعشاری که به تابع در ثبّاتهای برداری منتقل می‌شود، باید توسط فراخوانی‌کننده در ثبّات AL ارائه شود. برخلاف قرارداد فراخوانی مایکروسافت، فضای سایه ارائه نشد؛ در ورودی تابع، آدرس بازگشتی مجاورِ عدد صحیح هفتم در پشته است. این قراردادها عمدتاً برای کامپایلرهای C / C ++ (به ویژه بخش ۶۴ بیتی در زیر) تعریف شده‌اند و در نتیجه موارد بسیار خاصی در نظر گرفته شده‌است. | ## جمع‌بندی | زبان‌های دیگر ممکن است از فرمت‌ها و قراردادهای دیگر در پیاده‌سازی خود استفاده کنند.