Teknik Makale

32 bit Delphi'den DCC64'e _ftol Inline Assembly Taşıma

32 bit Delphi'deki orijinal _ftol deyiminin var olma nedeni C nesne dosyalarıdır. Bir unit, 32 bitlik bir C derleyicisi tarafından üretilen .obj dosyalarını bağladığında, bu nesneler her float-to-integer dönüşümü için çalışma zamanı yardımcısı _ftol'ü çağırır; değer ise parametre olarak aktarılmak yerine x87 yığınının tepesinde ST(0) içinde bırakılır. Delphi'nin yerine konan işlev bu yüzden sahte (dummy) bir Double parametresi tanımlar, iki satırlık bir satır içi (inline) assembly bloğuyla ST(0)'u bunun içine çeker ve sonra Pascal tarafında kırpar. DCC32 altında uzun süre sorunsuz derlendi; tam da bu yüzden kimse sorgulamadan bu kadar çok eski grafik ve PDF birimlerinde son buldu

Derleme hedefini 64 bite değiştirdiğinizde, derleyici E1025 Unsupported language feature: 'ASM' ile sonlanır. Bu hata bir uyumluluk uyarısı değildir. Bu, montajın daha önce ne kadar iyi çalıştığından bağımsız olarak DCC64'ün rutini hiçbir şekilde derlemeyeceği anlamına gelir

32 bitlik orijinal genellikle şuna benzerdi:

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

Pascal'ın begin...end gövdesi içindeki o asm bloğu, tam olarak DCC64'ün reddettiği şeydir. İki derleyicinin montaja (assembly) nerede izin verildiğine dair farklı kuralları vardır ve bu sınır önemlidir

DCC64 sınırı neden farklı çiziyor?

DCC32, sıradan Pascal rutinleri içinde satır içi (inline) assembly'ye izin verir. Derleyici, 32 bitlik çağrı kuralını (calling convention) bilir ve yerel değişkenlerin ve parametrelerin nerede yaşadığı hakkında mantık yürütebilir, bu nedenle yığın çerçevesine (stack frame) adıyla ulaşan assembly parçalarına tolerans gösterir. DCC64 ise daha katı bir tutum benimser: assembly, tüm gövdenin assembly olduğu ve çağrı kuralının açıkça ele alındığı ayrılmış bir assembler işlevi içinde olmalıdır. Karma Pascal-artı-asm (Pascal-plus-asm) kesinlikle desteklenmez

Altta yatan neden mimaridir. 64 bit Windows çağrı kuralında (Microsoft ABI), ilk dört parametre tam sayı türleri için RCX, RDX, R8 ve R9'a veya kayan nokta (floating-point) türleri için XMM0'dan XMM3'e ulaşır. Normal parametre iletiminde x87 FPU katılımı yoktur; x87 teknik olarak mevcuttur ancak ABI bunu argüman taşıma için kullanmaz. Bir değerin "FPU yığınında" olduğunu varsayan bir assembly, 64 bit ABI'nin asla yaratmadığı bir durum hakkında mantık yürütüyor demektir

Yani eski parçanın (fragment) sadece bir sözdizimi sorunu yoktur. DCC64 bunu kabul etse bile, yazmaç (register) varsayımları yanlış olacaktır

Delphi şeması: Pascal rutinleri içindeki satır içi asm'yi derleyen DCC32 ile E1025 ile duran ve Win64 ABI'sı altında yalnızca saf assembler işlevlerine izin veren DCC64'ü karşılaştırır
DCC32 bir Pascal yordamı içindeki asm bloğuna tahammül eder; DCC64 ise E1025 ile yanıt verir, çünkü Win64 ABI argümanları x87 yığını yerine yazmaçlarda taşır

Uygun bir 64 bit assembler sürümü yazma

Önce bir 64 bit _ftol'e gerçekten ihtiyaç olup olmadığını kontrol edin. Win64 C derleyicileri -MSVC ve Clang tabanlı C++Builder araç zinciri de dahil- kayan noktalı değerleri SSE2 talimatlarıyla satır içinde tam sayıya çevirir ve _ftol çağrıları üretmez; bu yüzden 64 bitlik nesne dosyaları bu sembole nadiren başvurur. İkili (binary) uyumluluk için bir _ftol sembolünü cdecl kuralı ile gerçekten dışa aktarmanız gerektiğinde, işlev saf bir assembler rutini olarak yazılmalıdır. 64 bit ABI altında bir Double parametresi XMM0'a gelir ve Integer sonuç, RAX'ın alt yarısı olan EAX'te geri döner. .NOFRAME yönergesi, DCC64'e rutinin kendi yığınını (stack) yönettiğini söyler; bu, bu kadar kısa bir yaprak işlevi (leaf function) için uygundur:

function _ftol: Integer; cdecl;
// 64 bit ABI'ye göre XMM0'da Double değeri beklenir
asm
  .NOFRAME
  cvttsd2si  eax, xmm0   // 32 bitlik bir tamsayıya kes, sonuç eax'te
end;

CVTTSD2SI, çift hassasiyetli bir kayan noktayı sıfıra doğru kırpma (truncation) ile işaretli bir tam sayıya dönüştürmek için kullanılan bir SSE2 talimatıdır ve _ftol'ün yapması beklenen şey de tam olarak budur. Tek bir talimattır, parametreyi doğrudan ABI'nin bıraktığı yerden alır ve sonucu ABI'nin beklediği yere koyar. FPU kontrol kelimesi (control-word) hokkabazlığına gerek yoktur

32 bitlik hedef önemlidir. Girdi 32 bitlik işaretli bir tam sayının aralığını aşarsa, hedef olarak EAX alan CVTTSD2SI belirsiz tam sayı (indefinite integer) değerini (0000000) döndürür. Bunun yerine RAX yazmak 64 bitlik bir dönüşüm üretir ve 231 ile 263 arasındaki girdiler, düşük 32 bitleri ne ise o olarak geri döner. Bu, sınır dışı (out-of-range) girdideki x87 fistp ile aynı davranıştır. Taşıma işlemini tamamlandı ilan etmeden önce, çağıranlarınızın (callers) bu tür değerler üretip üretemeyeceğini doğrulamaya değer

Delphi: Veri yolu şeması; XMM0'da gelen bir Double parametresinin tek bir CVTTSD2SI komutuyla EAX'ta döndürülen kesilmiş bir tamsayıya dönüştürüldüğünü, aralık dışı değerlerin tamsayı belirsiz desenini verdiğini gösterir
DCC64 altında double XMM0'a gelir ve CVTTSD2SI kesilmiş 32 bitlik sonucu tek bir SSE2 komutuyla EAX'a yerleştirir

Trunc'ın (Kırpmanın) daha iyi bir yanıt olduğu durumlar

Yukarıdaki assembler sürümü yalnızca gerçek bir ikili uyumluluk gereksiniminiz olduğunda yazmaya değerdir: harici bir çağıran, _ftol sembolünü belirli bir çağrı kuralı ile bekler ve o çağıranları değiştiremezsiniz. Bu durum nadir görülür. Çoğu zaman, _ftol yalnızca aynı birim içinde kullanılan özel (private) bir yardımcı idi ve adına veya kuralına yönelik herhangi bir dış bağımlılık (external dependency) yoktur

Bu durum için, onu düz (plain) Pascal ile değiştirin:

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

Trunc sıfıra doğru kırpar; bu da _ftol'ün x87 kontrol kelimesi kırpma moduna ayarlanmış haldeyken yaptığı şeyle eşleşir. Hem DCC32 hem de DCC64'te değişiklik yapılmadan derlenir. Derleyici her hedef için uygun talimatı üretir: x64'te zaten el ile yazılmış sürümle aynı talimat olan CVTTSD2SI'yi yayacaktır (emit). Aynı davranışı, platform koşulları (conditionals) ve bakımını yapacağınız bir assembly olmadan elde edersiniz

Kontrol etmeye değer tek semantik fark şudur: Trunc, girdi bir NaN (Bir Sayı Değil) veya sonsuz olduğunda Delphi'nin varsayılan yapılandırmasında bir EInvalidOp istisnası (exception) fırlatır. Orijinal koddaki x87 fistp ise hiçbir şey fırlatmadan sadece bir bit kalıbı (bit pattern) yazıyordu. Eğer kodunuz bu işleve olağandışı kayan nokta değerleri besliyorsa ve eski davranış sessiz idiyse, Trunc'ı çağırmadan önce Math'den IsNaN ve IsInfinite ile koruma sağlayın

_ftol taşıma için Delphi karar şeması: harici hiçbir çağıran cdecl sembolünü gerektirmiyorsa düz Trunc ile değiştirin, aksi hâlde saf bir assembler dışa aktarımını koruyun ve NaN veya sonsuz girdileri EInvalidOp'ye karşı koruyun
Dış çağıranlar dışa aktarılan simgeye bağlı olmadıkça sade Trunc'a geçin; her iki durumda da NaN ya da sonsuz girdileri EInvalidOp'ye karşı koruyun

Her iki hedef de aktif kaldığında koşullu derleme

Bazı projeler hem 32 bit hem de 64 bitlik ikili dosyaları (binaries) sağlamaya devam etmelidir. Eğer orijinal assembler sürümü 32 bit için saklanmalı ve 64 bit için yeni bir uygulama sağlanmalıysa, CPUX64 koşullusunu (conditional) kullanın:

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

Bu en az seviyedeki mekanik düzeltmedir ve geçici olarak ele alınmaya değer. Tek amacı kayan noktadan tam sayıya kırpma olan bir yardımcıda (helper) mimariye özgü assembly taşıyan bir kod tabanı, gereksiz bir borç taşımaktadır. Eski uygulamanın FPU yan etkilerine (side-effects) hiçbir şeyin bağlı olmadığını onayladığınızda 32 bit dalı tamamen ortadan kalkabilir

Eğer işlev birden fazla birimde kullanılan bir bileşende görünüyorsa, nasıl geçiş (migrate) yapacağınıza karar vermeden önce tüm kod tabanında _ftol araması yapın. Bu ada sahip bir sembol birden fazla yerde bildirilebilir (declared); bağlayıcı (linker) birini seçer ve diğerlerini sessizce yoksayar, bu da bir kopyayı düzeltebileceğiniz ve yine de dokunulmamış farklı bir kopyaya bağlanabileceğiniz anlamına gelir