مقاله فنی

پورت کردن اسمبلی درون‌خطی _ftol از دلفی 32 بیتی به DCC64

اصطلاح اصلی _ftol در دلفی 32 بیتی شبیه یک راه‌حل هوشمندانه یک‌خطی به نظر می‌رسد: یک پوشش‌دهنده تابع پاسکال که وارد کد اسمبلی درون‌خطی (inline assembly) می‌شود تا کلمه کنترلی x87 FPU را تغییر دهد، مقدار موجود روی پشته FPU را کوتاه (truncate) کند، و نتیجه را خارج سازد (pop). این روش برای مدت طولانی تحت DCC32 بدون مشکل کامپایل می‌شد، که دقیقاً به همین دلیل است که بدون اینکه کسی آن را زیر سؤال ببرد، در بسیاری از واحدهای گرافیکی و PDF قدیمی جا خوش کرد

با تغییر هدف ساخت (build target) به 64 بیت، کامپایلر با خطای E1025 Unsupported language feature: 'ASM' متوقف می‌شود. این خطا یک هشدار سازگاری نیست. این بدان معناست که DCC64 اصلاً روتین را کامپایل نخواهد کرد، صرف نظر از اینکه کد اسمبلی قبلاً چقدر خوب کار می‌کرده است

نسخه اصلی 32 بیتی معمولاً چیزی شبیه به این به نظر می‌رسید:

function _ftol(f: Double): Integer; cdecl;
begin
  asm
    lea   eax, f
    fstp  qword ptr [eax]
  end;
  Result := Trunc(f);
end;

آن بلوک asm در داخل بدنه begin...end پاسکال دقیقاً همان چیزی است که DCC64 آن را نمی‌پذیرد. این دو کامپایلر قوانین متفاوتی در مورد مکان مجاز برای کدهای اسمبلی دارند و این مرز اهمیت دارد

چرا DCC64 خط قرمز را متفاوت رسم می‌کند

DCC32 کدهای اسمبلی درون‌خطی در داخل روتین‌های معمولی پاسکال را مجاز می‌داند. کامپایلر، قرارداد فراخوانی 32 بیتی را می‌شناسد و می‌تواند درک کند که متغیرهای محلی و پارامترها در کجا قرار دارند، بنابراین قطعات اسمبلی که با استفاده از نام به فریم پشته (stack frame) دسترسی پیدا می‌کنند را تحمل می‌کند. DCC64 موضع سخت‌گیرانه‌تری اتخاذ می‌کند: کد اسمبلی باید در یک تابع اختصاصی اسمبلی باشد، جایی که کل بدنه از اسمبلی تشکیل شده و قرارداد فراخوانی به صراحت مدیریت می‌شود. استفاده ترکیبی پاسکال-به-علاوه-اسمبلی به هیچ وجه پشتیبانی نمی‌شود

دلیل اصلی این امر، معماری سیستم است. در قرارداد فراخوانی ویندوز 64 بیتی (Microsoft ABI)، چهار پارامتر اول برای انواع عدد صحیح به ترتیب در RCX، RDX، R8 و R9 قرار می‌گیرند، یا برای مقادیر ممیز شناور (floating-point) در XMM0 تا XMM3. در ارسال عادی پارامترها، هیچ دخالتی از سوی x87 FPU وجود ندارد؛ x87 از نظر فنی در دسترس است اما ABI از آن برای انتقال آرگومان‌ها استفاده نمی‌کند. کد اسمبلی که فرض می‌کند مقداری "روی پشته FPU" قرار دارد، در حال استدلال در مورد وضعیتی است که ABI 64 بیتی هرگز آن را ایجاد نمی‌کند

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

نوشتن یک نسخه درست اسمبلی 64 بیتی

زمانی که واقعاً نیاز دارید نماد _ftol را با قرارداد cdecl به منظور سازگاری باینری (binary compatibility) خروجی دهید، تابع باید به عنوان یک روتین اسمبلی خالص نوشته شود. تحت 64-bit ABI یک پارامتر Double در XMM0 دریافت می‌شود، و نتیجه عدد صحیح باید در هنگام بازگشت در RAX قرار داشته باشد. دستور .NOFRAME به DCC64 می‌گوید که روتین، پشته مربوط به خود را مدیریت می‌کند، که این کار برای یک تابع برگ (leaf function) به این کوتاهی مناسب است:

function _ftol: Integer; cdecl;
// Double value expected in XMM0 per 64-bit ABI
asm
  .NOFRAME
  cvttsd2si  rax, xmm0   // truncate-to-integer, result in rax
end;

دستور CVTTSD2SI دستور SSE2 برای تبدیل یک ممیز شناور با دقت مضاعف (double-precision float) به عدد صحیح علامت‌دار با کوتاه کردن به سمت صفر است، که دقیقاً همان کاری است که _ftol باید انجام دهد. این فقط یک دستورالعمل است، پارامتر را مستقیماً از جایی که ABI آن را رها کرده می‌گیرد، و نتیجه را در جایی که ABI انتظار دارد قرار می‌دهد. نیازی به شعبده‌بازی با کلمه کنترلی (control-word) در FPU نیست

توجه داشته باشید که اگر ورودی از محدوده یک عدد صحیح 32 بیتی علامت‌دار فراتر رود، CVTTSD2SI مقدار نامعین عدد صحیح ($80000000) را بازمی‌گرداند. این همان رفتار دستور fistp در x87 روی ورودی خارج از محدوده است. این که آیا فراخوان‌کننده‌های شما می‌توانند چنین مقادیری تولید کنند، پیش از اعلام پایان مهاجرت، ارزش تایید کردن دارد

چه زمانی Trunc راه‌حل بهتری است

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

در آن صورت، آن را با پاسکال ساده جایگزین کنید:

function _ftol(f: Double): Integer; cdecl;
begin
  Result := Trunc(f);
end;

دستور Trunc به سمت صفر کوتاه می‌کند، که با کاری که _ftol با تنظیم کلمه کنترل x87 به حالت کوتاه کردن (truncation mode) انجام می‌داد، مطابقت دارد. این کد بدون هیچ تغییری در DCC32 و DCC64 کامپایل می‌شود. کامپایلر دستورالعمل مناسب را برای هر هدف تولید می‌کند: روی پلتفرم x64 در هر صورت معمولاً دستور CVTTSD2SI را صادر می‌کند، یعنی همان دستورالعملی که در نسخه دست‌نویس استفاده شد. شما رفتاری دقیقاً مشابه، بدون دستورات شرطی پلتفرم و بدون نیاز به نگهداری اسمبلی دریافت می‌کنید

تنها تفاوت معنایی که ارزش بررسی دارد: اگر ورودی NaN یا بی‌نهایت باشد، Trunc در تنظیمات پیش‌فرض دلفی خطای استثنای EInvalidOp را ایجاد می‌کند. در حالی که دستور fistp متعلق به معماری x87 در کد اصلی، فقط یک الگوی بیتی می‌نوشت بدون اینکه هیچ خطایی ایجاد کند. اگر کد شما مقادیر ممیز شناور غیرمعمولی را به این تابع می‌دهد و رفتار قدیمی روی آنها در سکوت بود، پیش از فراخوانی Trunc با استفاده از IsNaN و IsInfinite از کتابخانه Math کد را محافظت کنید

کامپایل شرطی وقتی هر دو هدف فعال می‌مانند

برخی از پروژه‌ها باید به ارائه باینری‌های 32 بیتی و 64 بیتی ادامه دهند. اگر نیاز است نسخه اصلی اسمبلی برای هدف 32 بیتی حفظ شود و یک پیاده‌سازی جدید برای سیستم‌های 64 بیتی ارائه گردد، از دستور شرطی CPUX64 استفاده کنید:

function _ftol(f: Double): Integer; cdecl;
begin
{$IFDEF CPUX64}
  Result := Trunc(f);
{$ELSE}
  // 32-bit path: DCC32 accepts inline asm
  asm
    lea   eax, f
    fstp  qword ptr [eax]
  end;
  Result := Trunc(f);
{$ENDIF}
end;

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

اگر تابع در کامپوننتی ظاهر می‌شود که در بین یونیت‌های متعدد استفاده می‌گردد، کل پایگاه‌کد را پیش از تصمیم‌گیری درباره نحوه مهاجرت به دنبال _ftol بگردید. یک نماد با آن نام می‌تواند در بیش از یک مکان اعلان شود؛ پیونددهنده (linker) یکی را انتخاب کرده و بقیه را بدون هیچ هشداری نادیده می‌گیرد، که این یعنی ممکن است شما یک کپی را اصلاح کنید و باز هم به یک کپی متفاوت که به آن دست نزده‌اید، پیوند بخورید