مقاله فنی

لینک آبجکت‌های OMF به COFF در PDFlibPas روی Win32

PDFlibPas زیر Free Pascal برای Win32 بیلد می‌شود، و بخش سختِ کار هرگز پاسکال نبود. آبجکت فایل‌ها بودند: آبجکت‌های AES و OpenJPEG که بیلد Delphi لینک می‌کند از نوع OMFاند، لینکر داخلی Free Pascal تنها COFF را می‌فهمد، و تبدیل بین این دو نام سکشن‌ها و سمبل‌های تعریف سکشنی تحویل می‌دهد که لینکر را نه با یک diagnosis بلکه با internal error زمین می‌زنند

هر کسی که آبجکت C را در یک کتابخانه پاسکال لینک کرده باشد این خاک را می‌شناسد. Win64 نسبتاً متمدن است: یک فرمت آبجکت، یک calling convention و هیچ name decorationای نه. Win32 همه لایه‌های تاریخی که این پلتفرم جمع کرده را نگه داشته، و کتابخانه‌ای که کد C ثالث را استاتیک لینک می‌کند با همه‌شان یک‌جا روبرو می‌شود

دایرکتوری کامپایلر به شما تارگت را نمی‌گوید

از نقطه ورود بیلد شروع کنید، چون اشتباه‌گرفتنش ساعت‌ها را پیش از آن‌که پای هیچ آبجکت فایلی به میان بیاید هدر می‌دهد. نام دایرکتوری نصب Free Pascal می‌گوید کامپایلر اصلی کجا زندگی می‌کند، نه چه چیزی تولید می‌کند. یک کامپایلر میزبان ۳۲بیتی می‌تواند cross-compiler کنار دستش را صدا بزند و کد ۶۴بیتی تحویل بگیرد، به شرطی که سوئیچ‌های تارگت درست را پاس بدهید؛ پس استنتاج تارگت از روی مسیر حدس‌زدنی است که تا وقتی کسی toolchain‌اش را جابه‌جا نکرده اتفاقاً جواب می‌دهد

روش قابل‌اتکا پرسیدن از خود کامپایلر است. پردازنده و سیستم‌عامل تارگت واقعی را از طریق سوئیچ‌های اطلاعاتی خود کامپایلر کوئری بگیرید، و هر دو چیدمان رایج نصب را بپذیرید، هم دایرکتوری باینری تخت و هم حالت تودرتوی نسخه‌دار، چون installerهای مختلف و toolchain managerهای مختلف شکل‌های متفاوتی تحویل می‌دهند. اسکریپت بیلدی که هر کدام از این چیدمان‌ها را hard-code کند دقیقاً روی یک ماشین کار می‌کند

چرا یک آبجکت فایل تبدیل‌شده لینکر داخلی را می‌شکند؟

چون تبدیل، قرارداد نام‌گذاری سکشن OMF را حفظ می‌کند و سمبل‌های تعریف سکشنی می‌سازد که با آنچه لینکر COFF انتظار دارد جور درنمی‌آیند. تبدیل آبجکت‌های OMF به COFF لازم است ولی کافی نیست: فایل‌های حاصله هنوز نام‌های سکشن کلاسیک _TEXT و _DATA و _BSS را حمل می‌کنند، به‌علاوه نام سمبل‌های تعریف سکشنی مشتق‌شده از آن‌ها، و دادن همین به لینکر داخلی Free Pascal به‌جای پیامی درباره نام‌گذاری سکشن‌ها internal compiler error تحویل می‌دهد

internal error بدترین حالت شکست برای یک مشکل بیلد است، چون هیچ نمی‌گوید ورودی چه ایرادی داشته. fix یک پاس نرمال‌سازی پس از تبدیل روی فایل COFF است: نام سکشن‌ها را به شکل مورد انتظار بازنویسی کنید و سمبل‌های تعریف سکشنی متناظر را برای هم‌خوانی بازنویسی کنید، بی‌آنکه شاخص سمبل‌ها، بایت‌های کد و relocationها دست بخورد. همان قید آخر تمامِ سختی است. بازنویسی‌ای که سمبل‌ها را دوباره شماره‌گذاری کند یا آفست‌ها را جابه‌جا کند آبجکتی تحویل می‌دهد که لینک می‌شود و بعد crash می‌کند

برای یکی از آن دو مجموعه آبجکت یک گام مقدماتی هم لازم است. آبجکت‌های OpenJPEG که کامپایلر کلاسیک ۳۲بیتی ++C ساخته به روتین‌های helper عددی ۶۴بیتی خصوصی Delphi وابسته‌اند، که Free Pascal فراهمشان نمی‌کند، پس هیچ مقدار تبدیل فرمتی آن‌ها را قابل استفاده نمی‌کند. این‌ها اول با کامپایلر مبتنی بر Clang بازساخت می‌شوند، که آن وابستگی‌ها را تولید نمی‌کند، و بعد تبدیل می‌شوند

خط لوله‌ای که آبجکت‌های استاتیک C کتابخانه PDFlibPas را از OMF دلفی در Lib\thirdparty\Win32 به COFF قابل‌لینک FPC در Lib\thirdparty\Win32f روی Win32 می‌برد: تبدیل OMF به COFF، یک پاس نرمال‌سازی که نام سکشن‌ها و سمبل‌های تعریف سکشنی را بازنویسی می‌کند بی‌آنکه شاخص سمبل‌ها، بایت‌های کد و relocationها را دست بزند، و بازساخت Clang برای آبجکت‌های OpenJPEG که helperهای ۶۴بیتی دلفی را صدا می‌زنند
تبدیل لازم است ولی کافی نیست: COFF تغییرنام‌شده ولی نرمال‌نشده را به لینکر داخلی Free Pascal بدهید و جوابش internal error است، پس یک پاس پس از تبدیل نام‌ها و سمبل‌ها را fix می‌کند بی‌آنکه آفست‌ها را دست بزند
// آبجکت‌های تارگت FPC در دایرکتوری خودشان زندگی می‌کنند و
// جانشین مجموعه آبجکت Delphi نمی‌شوند، چون هر دو toolchain از یک
// درخت سورس بیلد می‌کنند و هرکدام ورودی لینک خودش را می‌خواهد
//
//   Lib\thirdparty\Win32   آبجکت‌های OMF دلفی، دست‌نخورده
//   Lib\thirdparty\Win32f  آبجکت‌های COFF قابل‌لینک FPC، تبدیل و نرمال شده
//
// نقاط ورود بیلد:
//   build-Win32-Lib-FPC.cmd
//   build-Win64-Lib-FPC.cmd

helperهای خصوصی کامپایلر پرتابل نیستند، و قراردادهایشان هم نیست

runtime خود Delphi برای عملیات عددی ۶۴بیتی روی x86 سی‌ودوبیتی trampoline اسمبلی فراهم می‌کند، و آبجکت‌های C از پیش کامپایل‌شده‌ای که برای Delphi ساخته شده‌اند داخل آن‌ها را صدا می‌زنند. Free Pascal چیدمان خودش را دارد، پس این ارجاع‌ها باید جور دیگری برآورده شوند نه اینکه redirect شوند. جزئیاتی که redirect را ناممکن می‌کند calling convention است: helper زمان‌سنجی که کد imaging استفاده می‌کند آرگومان چهاربایتی‌اش را callee پاک می‌کند، در حالی که helper تقسیم ۶۴بیتی شانزده بایت را پاک می‌کند و نتیجه‌اش را در جفت رجیستر کلاسیک برمی‌گرداند. دو helper، دو قرارداد، و trampolineای که برای یکی نوشته شده برای دیگری بی‌سروصدا stack را خراب می‌کند

name decoration نیمه دوم مسئله را اضافه می‌کند. روی Win32، Free Pascal ایمپورت‌های خارجی C را خودکار با آندرلاین پیشوند می‌گذارد در حالی که اعلان‌های public name را عیناً اکسپورت می‌کند، پس سمت ایمپورت و سمت اکسپورت یک پل واحد از دو قانون مختلف پیروی می‌کنند. پل runtime C که OpenJPEG لازم دارد پس باید دقیقاً همان نام سمبل‌های C را اکسپورت کند، و نقاط ورود variadic به یک jump غیرمستقیم ۳۲بیتی نیاز دارند نه jump مستقیم. هیچ‌کدام از این‌ها وقتی گفته شد عجیب نیست. همه‌اش به شکل link errorای تمام می‌شود که نام سمبلی را می‌گوید که هیچ‌کس ننوشته

چه چیزی یک فایل اجرایی Win32 را پیش از main کشت؟

یک DLL شصت‌وچهاربیتی روی search path، که به آن می‌رسیم چون یونیت zlib فری پاسکال به‌جای لینک استاتیک، bind دینامیک می‌شود. علامت، خروج فوری با کد وضعیت invalid-image بود، پیش از آنکه حتی یک خط کد پاسکال برنامه اجرا شده باشد؛ چیزی که چشم‌تان را به برنامه‌ای می‌دوزد که همین حالا بیلد کرده‌اید، در حالی که مقصر لودری است که دارد یک ایمپورت را با معماری غلط resolve می‌کند

درس این داستان درباره فرض‌هاست نه zlib. یونیتی که اسم یک کتابخانه فشرده‌سازی را یدک می‌کشد لزوماً آن را درون خودش ندارد؛ ممکن است یک binding باشد که در زمان اجرا منتظر یک shared library می‌ماند، و وابستگی دینامیکی ناخواسته حتی وقتی اتفاقی resolve می‌شود یک بدهی استقرار است. رفتن سراغ پیاده‌سازی stream پاسکال خالص به هر دو تارگت یک مسیر فشرده‌سازی استاتیک بدون هیچ وابستگی خارجی می‌دهد، که دقیقاً همان چیزی است که کتابخانه‌ای که قرار است در برنامه یکی دیگر جاسازی شود از اول باید داشته باشد

همان غریزه درباره بک‌اند انکودر خارجی JBIG2 هم صادق است. روی تارگت ۳۲بیتی انکودر خارجی لینک نمی‌شود، پس درخواست‌ها به انکودر پاسکال داخلی fallback می‌کنند، و تستی که این را راستی‌آزمایی می‌کند باید وضعیت ثبتِ تارگت جاری را چک کند نه اینکه یک انکود موفق را مدرک حضور بک‌اند خارجی بگیرد. fallbackای که کار می‌کند دقیقاً همان چیزی است که یک وابستگی غایب را قایم می‌کند، که الگوی شکست بررسی‌شده در تشخیص شکست‌های بی‌صدای stub است. کار لینک استاتیک ۶۴بیتی در لینک استاتیک jbig2enc زیر FPC پوشش داده شده

جریان تشخیص برای یک فایل اجرایی Win32 که زیر Free Pascal پیش از main خارج می‌شود: لودر ایمپورت‌ها را در حالی که initialization یونیت در جریان است resolve می‌کند، binding زامبی یونیت zlib یک DLL ۶۴بیتی را روی search path پیدا می‌کند، و پروسه با وضعیت invalid-image پیش از هر statement پاسکالی می‌میرد، و PDFlibPas را به سمت مسیر فشرده‌سازی پاسکال خالصِ استاتیک‌ضم‌شده هل می‌دهد
مقصر هرگز برنامه‌ای که همین حالا بیلد شده نبود: یونیتی به اسم zlib یک binding زمان اجرا بود که با معماری غلط resolve می‌شد، و fallbackای که کار می‌کند، مثل انکودر داخلی JBIG2، وابستگی غایب را قایم می‌کند

حساب ۳۲بیتی روی یک memory stream

کدی که اندازه بافرها را با حساب بدون‌علامت به پهنای اشاره‌گر دستکاری می‌کند روی Win64 درست است و روی Win32 یک تصویر بزرگ با overflow فاصله دارد. stream درون‌حافظه‌ای که کدک JPEG 2000 را تغذیه می‌کند با دوبرابرکردن رشد می‌کند و با جمع جلو می‌رود، و روی تارگت ۳۲بیتی هر دو عملیات می‌توانند روی ورودی‌هایی که بزرگ‌اند ولی کاملاً مشروع wrap کنند

پس هر write و skip و seek و تخصیص اولیه پیش از محاسبه چک می‌کند، و سقف ظرفیت بیشترین مقدار علامت‌دار به پهنای اشاره‌گر است، انتخاب‌شده تا با چیزی که روتین جابه‌جایی بلاک و مقادیر برگشتی callback می‌توانند بیان کنند جور دربیاید. الزام رفتاری وقتی یک درخواست رد می‌شود به‌راحتی از دست می‌رود: ردکردن نباید موقعیت stream یا طولش را تغییر دهد. یک جهش جزئی که دنبالش خطا بیاید stream را در حالتی رها می‌کند که caller نمی‌تواند درباره‌اش استدلال کند، و عملیات بعدی آن را بدتر می‌کند

// پیش از محاسبه چک کنید. روی Win32 هر دوی این‌ها روی ورودی‌هایی
// که یک تصویر بزرگ JPEG 2000 مشروعانه تولید می‌کند wrap می‌کنند
if Needed > NativeUInt(High(NativeInt)) - FPosition then
  Exit(False);                    // رد کن، موقعیت و اندازه را دست نزن

NewCapacity := FCapacity;
while (NewCapacity < FPosition + Needed) do
begin
  if NewCapacity > NativeUInt(High(NativeInt)) shr 1 then
    Exit(False);                  // دوبرابرکردن overflow می‌دهد
  NewCapacity := NewCapacity shl 1;
end;

دو تله خروجی بیلد که از خود پورت طولانی‌عمرترند

جداکردن فایل‌های اجرایی تست و نمونه بر اساس معماری تارگت در دایرکتوری‌های خروجی به‌ازای هر تارگت واضحاً کار درستی است و بی‌درنگ هر چیزی را می‌شکند که دیتای تستش را با شمردن سطح‌های دایرکتوری به بالا پیدا می‌کرد. fix این است که به‌جای فرض عمق ثابت، به بالا دنبال دایرکتوری asset بگردید، با یک محدودیت عمدی: نمونه signing فقط از دایرکتوری پروژه خودش fallback گواهی قبول می‌کند، هرگز از یک ancestor دلخواه نه، چون گواهی هم‌نامی که بالاتر در درخت پیدا شود یک سورپرایز امنیتی است نه یک آسان‌گیری

تله دوم از هر پورتی جان به در می‌برد و ارزش دارد به هر پروژه FPC برده شود. بعد از ارتقای کامپایلر، اینکه کامپایلر فایل‌های PPU کهنه را رد کند کافی نیست، چون لینکر هنوز آبجکت فایل‌های باقی‌مانده در unit search path را ترجیح می‌دهد حتی وقتی PPUای که لود کرده از دایرکتوری درست آمده، و اضافه‌کردن یک مسیر خروجی آبجکت صریح هم آن ترجیح را باطل نمی‌کند. تنها جواب قابل‌اتکا یک دایرکتوری یونیت موقت تازه به‌ازای هر دور بیلد است. هر چیز کمتر باینری‌ای تحویل می‌دهد که از دو نسخه کامپایلر لینک شده و به شکلی fail می‌شود که شبیه باگ سورس است

conditionalهای پلتفرم تکه آخرند، و انتخاب محور درست مهم‌تر از آن است که به نظر می‌آید. سؤال درست معمولاً این است که آیا کد Windows-specific است، نه اینکه آیا یک کتابخانه widget خاصی حاضر است، همان‌طور که کار تبدیل متافایل در واردات برداری EMF و conditionalهای پلتفرم نشان داد: عوض‌کردن آن guard از شرط کتابخانه کنترل به شرط پلتفرم یک بازنویسی فرض‌شده را به تغییر یک directive تبدیل کرد. پشتیبانی Free Pascal و Lazarus برای هر دو تارگت ویندوز همراه PDFlibPas Delphi PDF library عرضه می‌شود، بیلدشده از همان سورس‌های بسته‌های Delphi و C++Builder