Technical Article

Портиране на вграден асемблер _ftol от 32-битова Delphi към DCC64

Оригиналният израз _ftol в 32-битова Delphi изглежда като умно едноредово решение: обвивка на Pascal функция, която преминава във вграден асемблер, за да манипулира контролната дума на x87 FPU, да отреже стойността в FPU стека и да извлече резултата. Той се компилираше успешно под DCC32 дълго време, което е причината да се озове в толкова много по-стари графични и PDF модули, без никой да го подлага на съмнение.

Превключете целта на компилация към 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 на Pascal е точно това, което DCC64 отхвърля. Двата компилатора имат различни правила за това къде е разрешен асемблерният код, и тази граница е важна.

Защо DCC64 поставя границата по различен начин

DCC32 позволява вграден асемблер в обикновени Pascal рутини. Компилаторът познава 32-битовата конвенция за извикване и може да определи къде се намират локалните променливи и параметрите, така че допуска асемблерни фрагменти, които имат достъп до рамката на стека по име. DCC64 заема по-строга позиция: асемблерният код трябва да бъде в отделна асемблерна функция, където цялото тяло е асемблерен код и конвенцията за извикване се управлява изрично. Смесеният Pascal-плюс-asm код изобщо не се поддържа.

Основната причина е архитектурна. В 64-битовата конвенция за извикване в Windows (Microsoft ABI), първите четири параметъра пристигат в RCX, RDX, R8 и R9 за целочислени типове, или в XMM0 до XMM3 за плаваща запетая. Няма участие на x87 FPU при нормалното предаване на параметри; x87 е технически наличен, но ABI не го използва за транспорт на аргументи. Асемблерният код, който предполага, че стойността е "в стека на FPU", разсъждава за състояние, което 64-битовият ABI никога не създава.

Така че старият фрагмент няма просто синтактичен проблем. Дори ако DCC64 го приемаше, предположенията за регистрите щяха да бъдат грешни.

Написване на правилна 64-битова асемблерна версия

Когато наистина трябва да експортирате символ _ftol с конвенция cdecl за двоична съвместимост, функцията трябва да бъде написана като чиста асемблерна рутина. Под 64-битовия ABI параметърът от тип Double пристига в XMM0, а целочисленият резултат трябва да бъде в RAX при връщане. Директивата .NOFRAME указва на DCC64, че рутината управлява собствения си стек, което е подходящо за толкова кратка крайна (leaf) функция:

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 инструкцията за конвертиране на число с плаваща запетая с двойна точност в целочислено число със знак с отрязване към нулата, което е точно това, което трябва да прави _ftol. Това е една инструкция, която взема параметъра директно от мястото, където ABI го е оставил, и поставя резултата там, където ABI го очаква. Не е необходимо никакво манипулиране на контролната дума на FPU.

Имайте предвид, че ако входната стойност надхвърля диапазона на 32-битово целочислено число със знак, CVTTSD2SI връща неопределената целочислена стойност ($80000000). Това е същото поведение като на x87 fistp при стойности извън диапазона. Добре е да потвърдите дали вашите извиквания могат да генерират такива стойности, преди да считате миграцията за завършена.

Кога Trunc е по-доброто решение

Асемблерната версия по-горе си струва да се пише само когато имате действително изискване за двоична съвместимост: някой външен код очаква символа _ftol със специфична конвенция за извикване и не можете да промените тези извиквания. Тази ситуация е рядка. През повечето време _ftol е била частна помощна функция, използвана само в рамките на съциия модул, и изобщо няма външна зависимост от нейното име или конвенция.

За този случай я заменете с чист Pascal:

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

Trunc отрязва стойността към нулата, което съвпада с това, което правеше _ftol с контролната дума на x87, настроена в режим на отрязване. Тя се компилира на DCC32 и DCC64 без модификация. Компилаторът генерира подходящата инструкция за всяка цел: на x64 той обикновено излъчва CVTTSD2SI така или иначе, същата инструкция като ръчно написаната версия. Получавате идентично поведение, липса на условна компилация за платформи и никакъв асемблерен код за поддръжка.

Единствената семантична разлика, която си струва да се провери: Trunc предизвиква изключение EInvalidOp в конфигурацията по подразбиране на Delphi, когато входната стойност е NaN или безкрайност. x87 fistp в оригиналния код просто записваше битов модел, без да предизвиква нищо. Ако вашият код подава необичайни стойности с плаваща запетая към тази функция и старото поведение е било тихо, използвайте IsNaN и IsInfinite от Math, преди да извикате Trunc.

Условна компилация при запазване на активността и на двете цели

Някои проекти трябва да продължат да доставят както 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;

Това е минималната механична корекция и си струва да се третира като временна. Кодова база, която носи специфичен за архитектурата асемблерен код в помощна функция, чиято единствена цел е отрязване от плаваща запетая до цяло число, носи излишен технически дълг. 32-битовият клон може да изчезне изцяло, след като потвърдите, че нищо не зависи от страничните ефекти на FPU от старото изпълнение.

Ако функцията се появява в компонент, използван в множество модули, потърсете в цялата кодова база за _ftol, преди да решите как да мигрирате. Символ с това име може да бъде деклариран на повече от едно място; свързващият редактор (linker) избира единия и мълчаливо игнорира останалите, което означава, че можете да коригирате едно копие, но все пак да се свържете с друго, което не е било докоснато.