기술 문서

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

32비트 델파이의 원래 _ftol 관용구는 C 오브젝트 파일 때문에 존재합니다. 유닛이 32비트 C 컴파일러가 만든 .obj 파일을 링크하면, 그 오브젝트들은 실수를 정수로 바꾸는 캐스트 때마다 런타임 헬퍼 _ftol을 호출하고, 값은 매개변수로 전달되지 않고 x87 스택 꼭대기 ST(0)에 남습니다. 그래서 델파이 쪽 대행 루틴은 더미 Double 매개변수를 선언하고, 두 줄짜리 인라인 어셈블리 블록으로 ST(0)를 여기에 팝한 뒤, 파스칼에서 잘라냅니다. 이 코드는 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가 이를 수용하더라도 레지스터 가정이 잘못되었을 것입니다

Delphi 다이어그램: Pascal 루틴 안의 인라인 어셈블리를 컴파일하는 DCC32와, E1025로 중단되고 Win64 ABI 아래 순수 어셈블러 함수만 허용하는 DCC64 비교
DCC32는 Pascal 루틴 안의 asm 블록을 허용하지만 DCC64는 E1025로 응답합니다. Win64 ABI는 x87 스택 대신 레지스터로 인자를 전달하기 때문입니다

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

먼저 64비트 _ftol이 애초에 필요한지부터 확인하십시오. Win64 C 컴파일러는 MSVC든 Clang 기반 C++Builder 툴체인이든 SSE2 명령으로 실수를 정수로 인라인 변환하고 _ftol 호출을 만들어 내지 않으므로, 64비트 오브젝트 파일이 이 심볼을 참조하는 일은 드뭅니다. 바이너리 호환성을 위해 정말로 cdecl 규칙으로 _ftol 심볼을 내보내야 할 때는 함수를 순수 어셈블리 루틴으로 작성해야 합니다. 64비트 ABI에서 Double 매개변수는 XMM0으로 도착하고 Integer 결과는 RAX의 하위 절반인 EAX로 돌아옵니다. .NOFRAME 지시문은 루틴이 자체 스택을 관리한다고 DCC64에 알리는 것으로, 이렇게 짧은 리프 함수에는 적절합니다:

function _ftol: Integer; cdecl;
// 64비트 ABI에 따라 XMM0의 Double 값을 기대
asm
  .NOFRAME
  cvttsd2si  eax, xmm0   // 32비트 정수로 잘라내고 결과는 eax에
end;

CVTTSD2SI는 배정밀도 부동 소수점을 0을 향해 자르는(truncate) 부호 있는 정수로 변환하기 위한 SSE2 명령어이며, 이는 정확히 _ftol이 수행해야 할 작업입니다. 이는 하나의 명령어로서 매개변수를 ABI가 둔 곳에서 직접 가져오고 ABI가 기대하는 곳에 결과를 배치합니다. FPU 제어 단어를 이리저리 저글링할 필요가 없습니다

목적지가 32비트라는 점이 중요합니다. 입력이 32비트 부호 있는 정수 범위를 넘으면 EAX를 목적지로 하는 CVTTSD2SI는 정수 부정확 값($80000000)을 반환합니다. 대신 RAX에 쓰면 64비트 변환이 되어, 231에서 263 사이의 입력은 하위 32비트에 해당하는 값으로 돌아옵니다. 이는 범위를 벗어난 입력에 대한 x87 fistp와 같은 동작입니다. 마이그레이션이 끝났다고 선언하기 전에, 호출자가 그런 값을 만들어 낼 수 있는지 확인해 볼 가치가 있습니다

Delphi: Double 매개변수가 XMM0에 도착해 CVTTSD2SI 명령 하나로 잘린 정수로 변환되어 EAX로 반환되는 데이터 경로 다이어그램, 범위를 벗어난 값은 integer indefinite 패턴을 냄
DCC64에서 double은 XMM0으로 들어오고 CVTTSD2SI가 잘린 32비트 결과를 단일 SSE2 명령으로 EAX에 놓습니다

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)일 때 델파이의 기본 구성에서 Trunc가 EInvalidOp 예외를 발생시킨다는 것입니다. 원본 코드의 x87 fistp는 아무것도 발생시키지 않고 단지 비트 패턴을 썼을 뿐입니다. 코드에서 이 함수에 특이한 부동 소수점 값을 입력하고 이전 동작이 조용했다면, Trunc를 호출하기 전에 Math 유닛의 IsNaN 및 IsInfinite로 보호(guard)하세요

_ftol 포팅 Delphi 결정 다이어그램: 외부 호출자가 cdecl 심볼을 필요로 하지 않으면 단순 Trunc로 교체하고, 아니면 순수 어셈블러 익스포트를 유지하며, NaN 또는 무한대 입력은 EInvalidOp에 대비해 가드
외부 호출자가 익스포트된 심볼에 의존하지 않는 한 일반 Trunc로 이전하고, 어느 쪽이든 NaN이나 무한대 입력은 EInvalidOp에 대비해 가드합니다

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

일부 프로젝트는 32비트 및 64비트 바이너리를 계속 함께 배송해야 합니다. 32비트용 원래 어셈블러 버전을 유지하고 64비트용 새 구현을 제공해야 하는 경우 CPUX64 조건문을 사용하세요:

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

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

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