기술 문서

32비트 델파이에서 DCC64로 _ftol 인라인 어셈블리 포팅

32비트 델파이의 원래 _ftol 관용구는 훌륭한 한 줄 코드처럼 보입니다: x87 FPU 제어 단어를 조작하고, FPU 스택의 값을 자르고, 결과를 팝오프(pop)하기 위해 인라인 어셈블리로 들어가는 파스칼 함수 래퍼입니다. 이 코드는 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;

파스칼 begin...end 본문 내부의 asm 블록은 바로 DCC64가 거부하는 것입니다. 두 컴파일러는 어셈블리가 허용되는 위치에 대해 서로 다른 규칙을 가지며 그 경계가 중요합니다

DCC64가 선을 다르게 긋는 이유

DCC32는 일반 파스칼 루틴 내에서 인라인 어셈블리를 허용합니다. 컴파일러는 32비트 호출 규칙(calling convention)을 알고 로컬 변수와 매개변수가 있는 위치를 추론할 수 있으므로, 이름으로 스택 프레임에 도달하는 어셈블리 조각을 허용합니다. DCC64는 더 엄격한 입장을 취합니다: 어셈블리는 전체 본문이 어셈블리이고 호출 규칙이 명시적으로 처리되는 전용 어셈블러 함수에 있어야 합니다. 파스칼과 asm이 혼합된 방식은 전혀 지원되지 않습니다

근본적인 이유는 아키텍처에 있습니다. 64비트 Windows 호출 규칙(Microsoft ABI)에서 처음 4개의 매개변수는 정수형의 경우 RCX, RDX, R8, R9에 도착하거나 부동 소수점의 경우 XMM0부터 XMM3까지 도달합니다. 일반적인 매개변수 전달에는 x87 FPU가 관여하지 않습니다. 기술적으로 x87을 사용할 수 있지만 ABI는 인자 전송에 사용하지 않습니다. 값이 "FPU 스택에" 있다고 가정하는 어셈블리는 64비트 ABI가 결코 생성하지 않는 상태에 대해 추론하는 것입니다

따라서 이전 조각(fragment)은 단순히 구문 문제만 있는 것이 아닙니다. DCC64가 이를 수용하더라도 레지스터 가정이 잘못되었을 것입니다

적절한 64비트 어셈블러 버전 작성하기

이진 호환성을 위해 cdecl 규칙과 함께 _ftol 심볼을 명시적으로 내보내야(export) 할 때 함수는 순수 어셈블러 루틴으로 작성되어야 합니다. 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는 배정밀도 부동 소수점을 0을 향해 자르는(truncate) 부호 있는 정수로 변환하기 위한 SSE2 명령어이며, 이는 정확히 _ftol이 수행해야 할 작업입니다. 이는 하나의 명령어로서 매개변수를 ABI가 둔 곳에서 직접 가져오고 ABI가 기대하는 곳에 결과를 배치합니다. FPU 제어 단어를 이리저리 저글링할 필요가 없습니다

입력이 32비트 부호 있는 정수의 범위를 초과하는 경우 CVTTSD2SI는 정수 무한대 값($80000000)을 반환한다는 점에 유의하세요. 이는 범위를 벗어난 입력에 대한 x87 fistp와 동일한 동작입니다. 마이그레이션이 완료되었다고 선언하기 전에 호출자가 이러한 값을 생성할 수 있는지 확인해 볼 가치가 있습니다

Trunc가 더 나은 해결책인 경우

위의 어셈블러 버전은 실제 이진 호환성 요구 사항이 있는 경우에만 작성할 가치가 있습니다. 일부 외부 호출자는 특정 호출 규칙이 있는 기호 _ftol을 기대하며 여러분은 이러한 호출자를 변경할 수 없습니다. 그러한 상황은 흔하지 않습니다. 대부분의 경우 _ftol은 동일한 유닛 내에서만 사용되는 비공개 도우미(private helper)였으며 이름이나 규칙에 대한 외부 의존성이 전혀 없습니다

그런 경우에는 일반 파스칼로 대체하세요:

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

Trunc는 0을 향해 자르며, 이는 _ftol이 x87 제어 단어를 자르기(truncation) 모드로 설정하여 수행하던 것과 일치합니다. 변경 없이 DCC32 및 DCC64에서 컴파일됩니다. 컴파일러는 각 대상에 대해 적절한 명령을 생성합니다. x64에서는 어쨌든 수작업으로 작성된 버전과 동일한 명령인 CVTTSD2SI를 일반적으로 방출(emit)합니다. 동일한 동작을 얻을 수 있고, 플랫폼 조건문이 없으며, 유지 관리할 어셈블리가 없습니다

확인해 볼 가치가 있는 한 가지 의미론적 차이점은 입력이 NaN 또는 무한대(infinity)일 때 델파이의 기본 구성에서 TruncEInvalidOp 예외를 발생시킨다는 것입니다. 원본 코드의 x87 fistp는 아무것도 발생시키지 않고 단지 비트 패턴을 썼을 뿐입니다. 코드에서 이 함수에 특이한 부동 소수점 값을 입력하고 이전 동작이 조용했다면, Trunc를 호출하기 전에 Math 유닛의 IsNaNIsInfinite로 보호(guard)하세요

두 대상이 모두 활성 상태를 유지할 때의 조건부 컴파일

일부 프로젝트는 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;

이것이 최소한의 기계적인 수정이며 임시방편으로 취급할 가치가 있습니다. 오로지 부동 소수점을 정수로 자르는 것만을 목적으로 하는 도우미에 아키텍처별 어셈블리를 포함하는 코드베이스는 불필요한 부채를 짊어지는 것입니다. 이전 구현의 FPU 부작용에 의존하는 것이 없음을 확인하면 32비트 분기를 완전히 없앨 수 있습니다

여러 유닛에 걸쳐 사용되는 컴포넌트에 이 함수가 나타나면 전체 코드베이스에서 _ftol을 검색하여 마이그레이션 방법을 결정하세요. 그 이름의 심볼은 두 군데 이상에서 선언될 수 있습니다. 링커가 하나를 선택하고 다른 것은 조용히 무시하기 때문에 하나의 복사본을 고쳤음에도 여전히 건드리지 않은 다른 복사본에 링크될 수도 있습니다