
در گذار اپل به تراشههای Apple Silicon، هر اپلیکیشن یا باید برای Arm کامپایل و بهصورت بومی اجرا میشد یا از طریق Rosetta بهعنوان برنامه x86_64 ترجمه میشد؛ اما این یک انتخاب دوتایی بود و ترکیب هر دو در یک فرایند ممکن نبود. ویندوز روی Arm نیز برای اجرای برنامههای x64 از شبیهسازی استفاده میکند، با این تفاوت که مایکروسافت یک گام جلوتر رفته است: یک فرایند ویندوزی میتواند کد بومی Arm را در کنار کد x64 شبیهسازیشده اجرا کند. این توانایی مدیون رابط دودویی برنامه (Application Binary Interface یا ABI) موسوم به Arm64EC است.
Arm64EC چیست و چه تفاوتی ایجاد میکند؟
Arm64EC یک ABI است که مایکروسافت در سال ۲۰۲۱ همراه با ویندوز ۱۱ روی Arm معرفی کرد. حروف EC به معنای Emulation Compatible است. این رابط اجازه میدهد افزونههای قدیمی x64 در کنار Office روی Arm اجرا شوند و ویندوز بتواند بخشی از کد خود را که یک برنامه x64 فراخوانی میکند، بهصورت بومی اجرا کند. در دستگاه HP OmniBook Ultra 16 که آزمون شده، بسته درایور انویدیا برای RTX Spark بیشتر اجزای حالت کاربر (User-Mode) خود را دو بار بستهبندی کرده است: یک بار بهصورت Arm64 خالص و یک بار بهصورت Arm64EC.
Arm64EC؛ کد Arm با قواعد x64
در نگاه اول، Arm64EC کد Arm64 واقعی است که بهصورت بومی روی پردازنده اجرا میشود، اما در قراردادهای نرمافزاری مانند چیدمان دادهها و پشته (Stack Layout) از قواعد سازگار با x64 پیروی میکند و بخش زیادی از قراردادهای فراخوانی Arm64 را نیز حفظ میکند. نتیجه این است که کد کامپایلشده بهصورت Arm64EC در بررسیهای معماری، خود را x64 معرفی میکند. کامپایلر برای چنین برنامهای ماکرو _M_AMD64 را تعریف میکند و فایل اجرایی Arm64EC در سرآیند (Header) خود نوع ماشین x64 را حمل میکند. با این حال سیستم میداند که با چه چیزی روبهروست و مدیر وظیفه (Task Manager) این برنامهها را با عنوان ARM64 (x64 compatible) نشان میدهد. این سازوکار فقط برای x64 است؛ برنامههای ۳۲بیتی x86 همچنان از لایه شبیهسازی قدیمیتر WOW64 استفاده میکنند.
انگیزه اصلی: پشتیبانی از افزونهها
مهمترین دلیل وجود Arm64EC، پشتیبانی از افزونهها (Plugins) است. پیش از معرفی این ABI، انتقال یک برنامه به Arm به تعبیر مایکروسافت حالتی همهیاهیچ داشت. اگر برنامهای DLLهای شخص ثالث را در همان فرایند خود بارگذاری میکرد که کنترلشان در دست توسعهدهنده نبود، مانند یک افزونه Office یا یک افزونه صوتی برای ایستگاه کار صوتی دیجیتال (DAW)، باید منتظر میماند تا توسعهدهنده اصلی هم کد خود را به Arm منتقل کند. Arm64EC این کار را آسانتر میکند: توسعهدهنده میتواند کد خود را به Arm بومی بازکامپایل کند و همزمان افزونههای x64 را زیر شبیهسازی Prism در همان فرایند بارگذاری کند. مایکروسافت ۶۴بیتی Office برای Arm را نیز به همین شکل ساخته است تا افزونههای x64 موجود بهصورت یکپارچه کار کنند. به گفته این شرکت، بیشتر کد سیستمعاملی که یک برنامه x64 روی Arm فراخوانی میکند بهصورت Arm64EC کامپایل شده است؛ یعنی بخش بزرگی از کد ویندوز که برنامه شبیهسازیشده به آن وارد میشود، بهصورت بومی اجرا میشود. ریشه این ایده به فناوری قدیمیتر Compiled Hybrid PE یا CHPE بازمیگردد که نخستین بار در Build 2017 نمایش داده شد و بهطور مشابه کد Arm64 را با سازگاری x86 ترکیب میکرد.
هزینه فنی: ثبتها و ساختار CONTEXT
Arm64EC بدون هزینه نیست. Arm64 دارای ۳۱ ثبت عمومی (General-Purpose Register) و ۳۲ ثبت برداری (Vector Register) است، در حالی که x64 فقط ۱۶ ثبت عمومی و بدون AVX-512 شانزده ثبت برداری دارد. این موضوع برای ویندوز اهمیت ویژه دارد، چون وضعیت پردازنده یک رشته (Thread) در ساختاری به نام CONTEXT نگهداری میشود که مدیریت استثناها، اشکالزدایها و APIهایی مانند GetThreadContext به آن وابستهاند. چون ویندوز بر پیشفرضهای x86_64 بنا شده است، کد x64 انتظار دارد این ساختار چیدمان x64 داشته باشد. Arm64EC با حفظ ساختار x64 این مسئله را دور میزند؛ ثبتهای Arm که استفاده میشوند در همان ساختار CONTEXT جای میگیرند. برای نمونه X0 به RCX، X1 به RDX، اشارهگر پشته به RSP و شمارنده برنامه به RIP نگاشت میشود. برخی نگاشتها عجیبترند؛ چند ثبت Arm مانند X6، X7، X9 تا X12، X15 و ثبت پیوند (Link Register) در جایگاه ثبتهای ممیز شناور x87 ذخیره میشوند. هر چیزی که جا نشود کاملاً ممنوع میشود؛ از جمله X13، X14، X23، X24 و X28 و همچنین ثبتهای برداری V16 تا V31. ریموند چن، مهندس باسابقه مایکروسافت، توضیح داده است که مقادیر این ثبتها در جابهجاییهای زمینه (Context Switch) یا استثناهای ناهمگام (Asynchronous Exception) حفظ نمیشوند، چون جایی برای ذخیره آنها وجود ندارد. مایکروسافت نگاشت را طوری انتخاب کرده که همه ثبتهای مورد استفاده در قراردادهای فراخوانی معمول Arm64 همچنان در دسترس باشند تا توابع Arm64EC بتوانند مانند کد بومی Arm64 یکدیگر را فراخوانی کنند. استثناهایی هم وجود دارد؛ توابع متغیرپذیر (Variadic) مانند printf از قواعدی شبیه x64 برای انتقال آرگومان استفاده میکنند و فقط چهار موقعیت نخست آرگومان میتوانند در ثبتها قرار بگیرند. این محدودیت تعداد ثبتهای در دسترس کامپایلر را کم میکند و میتواند سربار (Overhead) ایجاد کند.
Thunkها و بررسیکننده فراخوانی
حتی وقتی چیدمان دادهها یکسان باشد، Arm64 و x64 بر سر محل قرارگیری آرگومانهای یک تابع اختلاف دارند. مثال خود مایکروسافت تابعی است که یک int، یک double، یک int دیگر و یک double دیگر میگیرد. در Arm64 این آرگومانها به X0، D0، X1 و D1 میروند، اما در x64 به RCX، XMM1، R8 و XMM3. یک قطعه کد چسبناک کوچک به نام Thunk بسته به نوع و موقعیت هر آرگومان تبدیل را انجام میدهد. یک Entry Thunk کد x64 را که به Arm64EC فراخوانی میکند مدیریت میکند و آرگومانها را از چیدمان x64 به چیدمان Arm بازمیچیند. یک Exit Thunk نیز همین کار را در جهت عکس انجام میدهد، وقتی کد Arm64EC چیزی را فراخوانی میکند که ممکن است x64 باشد. مشکل اینجاست که کامپایلر اغلب نمیداند در حال فراخوانی چه معماریای است، چون یک اشارهگر تابع میتواند به کد بومی Arm یا کد شبیهسازیشده x64 اشاره کند و این موضوع ممکن است فقط در زمان اجرا روشن شود. مایکروسافت این را با عبور فراخوانیهای غیرمستقیم Arm64EC از یک Call Checker که ویندوز فراهم میکند حل کرده است. اگر مقصد Arm64EC باشد، فراخوانی بهصورت بومی ادامه مییابد. اگر x64 باشد، بررسیکننده آن را از Exit Thunk عبور میدهد و به شبیهساز میفرستد. برای عملی شدن این سازوکار، فایلهای دودویی Arm64EC جدولی در خود دارند که مشخص میکند کدام صفحههای کد شامل دستورهای Arm64EC و کدام شامل دستورهای x64 هستند. توابع Arm64EC همچنین چهار بایت بلافاصله پیش از نخستین دستور خود رزرو میکنند تا یک آفست نسبی به Entry Thunk خود را ذخیره کنند؛ وقتی شبیهساز با کد x64 روبهرو میشود که یک تابع Arm64EC را فراخوانی میکند، از این آفست برای یافتن Thunk درست استفاده میکند. کدی که در زمان اجرا توسط کامپایلر JIT تولید میشود نیز بهطور پیشفرض x64 در نظر گرفته میشود تا .NET، Java و سایر زمانهای اجرای x64 بدون تغییر کار کنند. برای برنامههای ویندوزی که با بازنویسی چند دستور نخست، توابع سیستمی را Hook میکنند و انتظار دستورهای x64 دارند، DLLهای Arm64EC بهطور پیشفرض یک Stub کوچک x64 برای هر تابع صادرشده فراهم میکنند که مایکروسافت آن را Fast-Forward Sequence مینامد. این Stub اجرا را به کد واقعی Arm64EC هدایت میکند و اگر چیزی آن را تغییر نداده باشد، ویندوز کاملاً از Stub صرفنظر میکند. اگر Hook شده باشد، شبیهساز نسخه تغییریافته را اجرا میکند تا Hook دستنخورده بماند. همینجاست که سربار Arm64EC معنا پیدا میکند: هر فراخوانی غیرمستقیم Arm64EC به یک بررسی نیاز دارد، حتی وقتی هر دو طرف بومی باشند، و فراخوانیهایی که به کد x64 میرسند به Thunk و ورود به شبیهساز نیاز دارند. فراخوانیهای مستقیم میان توابع Arm64EC اما صرفاً فراخوانیهای معمول Arm هستند.
Arm64X؛ بستهبندی دو جهان در یک فایل
Arm64X قطعه دیگری از راهحل مایکروسافت است؛ قالبی فایلی که میتواند کد Arm64 معمولی و کد Arm64EC را در یک فایل جای دهد. مایکروسافت آن را به تصاویر سهبعدی قدیمی قرمز و آبی تشبیه میکند: یک برنامه x64 همان DLL را مانند یک DLL x64 میبیند، در حالی که یک برنامه Arm64 همان DLL را بهعنوان یک DLL Arm64 میبیند. ویندوز از Arm64X برای DLLهای سیستمی استفاده میکند که هم برنامههای x64 و هم برنامههای Arm64 به آنها از System32 نیاز دارند؛ این کار به شرکت اجازه میدهد بهجای پوشه SysWOW64 که برای برنامههای ۳۲بیتی x86 به کار میرفت، از یک پوشه واحد از فایلهای سیستمی x64 استفاده کند. Arm64X برای توسعهدهندگانی هم مفید است که باید هر دو را پشتیبانی کنند، چون فرایندهای Arm64 معمولی و Arm64EC نمیتوانند DLLهای یکدیگر را مستقیماً بارگذاری کنند.
اپل: ترکیب Intel و Arm در یک فرایند ممنوع است
رویکرد اپل به Rosetta و Apple Silicon کاملاً متفاوت است. مستندات رسمی این شرکت میگوید سیستم اجازه نمیدهد کد arm64 و کد x86_64 در یک فرایند با هم ترکیب شوند و ترجمه Rosetta به کل فرایند اعمال میشود، شامل همه ماژولهای کدی که فرایند بهصورت پویا بارگذاری میکند. اگر یک برنامه بومی Arm بخواهد افزونهای صرفاً Intel را در همان فرایند بارگذاری کند، سیستم خطا میدهد. این به معنای آن نیست که کد Intel و Arm روی مک نمیتوانند با هم کار کنند؛ فقط نمیتوانند یک فرایند مشترک داشته باشند. پیشنهاد خود اپل اجرای افزونهها در فرایندهای جداگانه و ارتباط آنها از طریق XPC است؛ یعنی یک سرویس برای افزونههای arm64 و سرویس دیگر برای افزونههای x86_64. Audio Units از شناختهشدهترین نمونههای این رویکردند. macOS یک AUHostingCompatibilityService داخلی دارد که افزونههای AU صرفاً Intel را بیرون از میزبان اجرا میکند و برنامههایی مانند Ableton از آن برای پشتیبانی از افزونههای Intel استفاده میکنند. با این حال افزونههای VST2 و VST3 صرفاً Intel به این شکل کار نمیکنند و تنها راه Ableton اجرای کل DAW از طریق Rosetta است. اپل هرگز نمیخواست کد Intel را برای همیشه نگه دارد؛ نشانه روشن آن انتخاب ABI زبان C++ در Apple Silicon است که با ABI دستگاههای iOS و نه ABI مکهای مبتنی بر Intel هماهنگ شد. Rosetta نیز عمر محدودی دارد و بهعنوان ابزار عمومی فقط تا macOS 27 ادامه خواهد داشت. در مقابل، مایکروسافت چنین محدودیت زمانی معادلی را برای شبیهسازی x64 در ویندوز اعلام نکرده و Arm64EC دقیقاً محصول پلتفرمی است که انتظار دارد کد قدیمی x64 همچنان اجرا شود.
پذیرش Arm64EC بیرون از مایکروسافت
Arm64EC یک ABI استاندارد رسمی نیست. مایکروسافت میگوید ABI معمول Arm64 در ویندوز معمولاً از EABI استاندارد AArch64 شرکت Arm پیروی میکند و مشخصات ABI خود Arm برای AArch64 هیچ اشارهای به Arm64EC ندارد. این طراحی مخصوص ویندوز و حول سیستم شبیهسازی x64 مایکروسافت ساخته شده و مستندسازی آن هم بر عهده مایکروسافت است. پشتیبانی از آن برای دیگران کار سنگینی است. در سال ۲۰۲۴، مارتین استورشو، نگهدارنده زنجیره ابزار llvm-mingw، در فهرست پستی GCC گفته بود Arm64EC چیز کاملاً متفاوتی است و در تقریباً هر سطح یک ABI جداگانه محسوب میشود. در آن زمان GCC تازه در حال دریافت پشتیبانی از ویندوز روی Arm معمولی بود و ظاهراً تا امروز هم Arm64EC را پشتیبانی نمیکند. LLVM از نسخه 18.1 آن را پشتیبانی میکند و Rust آن را بهعنوان هدف Tier 2 ارائه میدهد، هرچند نگهدارنده هدف Arm64EC در Rust یک توسعهدهنده مایکروسافت است. Wine یکی از معدود پیادهسازیهای مستقل را دارد و پشتیبانی از آن در Wine 10.0 در ژانویه ۲۰۲۵ اضافه شد. این یکی از نکات برجسته آن انتشار بود و همان نسخه یک رابط شبیهسازی x86-64 هم افزود که به گفته توسعهدهندگان از پشتیبانی Arm64EC بهره میبرد تا همه کد Wine بهصورت بومی اجرا شود و فقط کد x86-64 خود برنامه به شبیهسازی نیاز داشته باشد. Wine شبیهساز x86-64 اختصاصی ندارد، اما FEX، شبیهساز x86 برای Arm Linux، وقتی بهصورت Arm64EC ساخته شود این رابط را پیاده میکند. برنامههای Arm میتوانند روی Linux با همان تکنیکهای ویندوز اجرا شوند، با یک استثنا: Wine 10.0 به اندازه صفحه حافظه ۴ کیلوبایتی نیاز دارد، چون ABI ویندوز همین را مشخص میکند.
جمعبندی
در نهایت، اپل رویکردی بهمراتب سادهتر دارد، اما این سادگی از آنجا میآید که کل اکوسیستم macOS را در اختیار دارد. ویندوز دههها فرض سازگاری را با خود حمل میکند و Arm64EC نتیجهای است که وقتی نرمافزار قدیمی هنوز باید کار کند به دست میآید. این راهحل ممکن است بینقص نباشد، اما رویکرد مایکروسافت به Arm در مقایسه با چند سال پیش جهشی چشمگیر است.