다음을 통해 공유


ARM64 ABI 규칙 개요

64비트 모드(ARMv8 이상 아키텍처)로 ARM 프로세서에서 컴파일되고 실행되는 Windows용 ABI(기본 애플리케이션 이진 인터페이스)는 ARM의 표준 AArch64 EABI를 따릅니다. 이 문서에서는 몇 가지 주요 가정과 EABI에 설명된 항목의 변경 내용을 중점적으로 설명합니다. 32비트 ABI에 대한 자세한 내용은 ARM ABI 규칙 개요를 참조하세요. 표준 ARM EABI에 대한 자세한 내용은 ARM 아키텍처용 애플리케이션 이진 인터페이스(ABI)(외부 링크)를 참조하세요.

정의

ARM은 64비트 지원을 도입하여 다음과 같은 여러 용어를 정의했습니다.

  • AArch32 – Thumb 모드 실행을 포함한 ARM에서 정의된 레거시 32비트 명령 집합 아키텍처(ISA)입니다.
  • AArch64 – ARM에서 정의된 새 64비트 ISA(명령 집합 아키텍처)입니다.
  • ARMv7 - AArch32에 대한 지원도 포함하는 “7세대” ARM 하드웨어의 사양입니다. 이 버전의 ARM 하드웨어는 ARM용 Windows가 지원되는 첫 버전입니다.
  • ARMv8 - AArch32 및 AArch64 모두에 대한 지원을 포함하는 “8세대” ARM 하드웨어의 사양입니다.

Windows는 다음과 같은 용어를 사용하기도 합니다.

  • ARM – AArch32(32비트 ARM 아키텍처)를 나타내며 WoA(Windows on ARM)라고도 합니다.
  • ARM32 – 위의 ARM과 동일하며, 명확성을 위해 이 문서에서 사용됩니다.
  • ARM64 – 64비트 ARM 아키텍처(AArch64)를 말합니다. WoA64는 없습니다.

마지막으로 데이터 형식을 말할 때 ARM의 다음과 정의가 참조됩니다.

  • Short-Vector – 벡터에서 8바이트 또는 16바이트 분량의 요소로 직접 표현할 수 있는 데이터 형식입니다. 크기는 8바이트 또는 16바이트에 맞춰져 있으며 각 요소는 1, 2, 4 또는 8바이트가 될 수 있습니다.
  • HFA(동일 부동 소수점 집계) – 2개~4개의 동일한 부동 소수점 멤버(floats 또는 doubles)를 포함하는 데이터 형식입니다.
  • HVA(동일 Short-Vector 집계) – 2개~4개의 동일한 Short-Vector 멤버가 있는 데이터 형식입니다.

기본 요구 사항

ARM64 버전의 Windows는 항상 ARMv8 이상 아키텍처에서 실행된다고 가정합니다. 부동 소수점 및 NEON 지원이 모두 하드웨어에 존재하는 것으로 간주됩니다.

ARMv8 사양은 AArch32 및 AArch64에 대한 새로운 선택적인 암호화 및 CRC 도우미 opcode를 설명합니다. 이에 대한 지원은 현재 선택 사항이지만 권장됩니다. opcode를 활용하기 위해 앱은 먼저 런타임이 존재하는지 확인해야 합니다.

endian

Windows ARM32 버전의 경우와 마찬가지로 ARM64 Windows는 little-endian 모드로 실행됩니다. endian 전환은 AArch64에서 커널 모드를 지원하지 않으면 실현하기 어려워서 적용하기가 더 쉽습니다.

맞춤

ARM64에서 실행되는 Windows를 사용하면 CPU 하드웨어가 정렬되지 않은 액세스를 투명하게 처리할 수 있습니다. AArch32의 향상된 기능을 통해 이 지원 기능은 이제 모든 정수 액세스(다중 단어 액세스 포함)와 부동 소수점 액세스도 적용됩니다.

그러나 캐시되지 않은(디바이스) 메모리에 대한 액세스는 여전히 항상 정렬되어야 합니다. 코드에서 캐시되지 않은 메모리의 불일치 데이터를 읽거나 쓸 수 있는 경우 모든 액세스를 정렬해야 합니다.

지역에 대한 기본 레이아웃 맞춤:

크기(바이트) 맞춤(바이트)
1 1
2 2
3, 4 4
> 4 8

전역 및 정적의 기본 레이아웃 맞춤:

크기(바이트) 맞춤(바이트)
1 1
2 - 7 4
8 - 63 8
>= 64 16

정수 레지스터

AArch64 아키텍처는 32 정수 레지스터를 지원합니다.

등록 변동 역할
x0-x8 휘발성 매개 변수/결과 스크래치 레지스터
x9-x15 휘발성 스크래치 레지스터
x16-x17 휘발성 프로시저 호출 내 스크래치 레지스터
x18 해당 없음 예약된 플랫폼 레지스터: 커널 모드에서 현재 프로세서에 대한 KPCR을 가리킵니다. 사용자 모드에서 TEB를 가리킵니다.
x19-x28 비휘발성 스크래치 레지스터
x29/fp 비휘발성 프레임 포인터
x30/lr 모두 링크 레지스터: 호출 수신자 함수는 자체 반환을 위해 보존해야 하지만 호출자의 값은 손실됩니다.

각 레지스터는 전체 64비트 값(x0-x30을 통해) 또는 32비트 값(w0-w30를 통해)으로 액세스할 수 있습니다. 32비트 작업은 그 결과를 최대 64비트로 0 확장합니다.

매개 변수 레지스터 사용에 대한 자세한 내용은 매개 변수 전달 섹션을 참조하세요.

AArch32와 달리 PC(프로그램 카운터)와 SP(스택 포인터)는 인덱싱되지 않은 레지스터입니다. 액세스할 수 있는 방법에 제한을 받습니다. 또한 x31 레지스터가 없다는 점에 유의해야 합니다. 인코딩은 특수한 용도로 사용됩니다.

프레임 포인터(x29)는 ETW 및 기타 서비스에서 사용하는 빠른 스택 워크와의 호환성을 위해 필요합니다. 스택의 이전 {x29, x30} 쌍을 가리켜야 합니다.

부동 소수점/SIMD 레지스터

AArch64 아키텍처는 아래에 요약된 32 부동 소수점/SIMD 레지스터도 지원합니다.

등록 변동 역할
v0-v7 휘발성 매개 변수/결과 스크래치 레지스터
v8-v15 모두 낮은 64비트가 비휘발성 비트입니다. 높은 64비트가 휘발성입니다.
v16-v31 휘발성 스크래치 레지스터

각 레지스터는 전체 128비트 값(v0-v31 또는 q0-q31을 통해)으로 액세스할 수 있습니다. 64비트 값(d0-d31을 통해), 32비트 값(s0-s31을 통해), 16비트 값(h0-h31을 통해) 또는 8비트 값(b0-b31을 통해)으로 액세스할 수 있습니다. 128비트보다 작은 액세스는 전체 128비트 레지스터의 하위비트에만 액세스합니다. 달리 지정하지 않는 한 나머지 비트는 그대로 둡니다. (AArch64는 더 작은 레지스터가 더 큰 레지스터 위에 압축된 AArch32와는 다릅니다.)

FPCR(부동 소수점 제어 레지스터)에는 그 내부의 다양한 비트 필드에 대한 특정 요구 사항이 있습니다.

비트 의미 변동 역할
26 AHP 비휘발성 대체 반정밀도 컨트롤
25 DN 비휘발성 기본 NaN 모드 컨트롤
24 FZ 비휘발성 0으로 플러시 모드 컨트롤
23-22 RMode 비휘발성 반올림 모드 컨트롤
15,12-8 IDE/IXE/etc 비휘발성 예외 트랩 사용 비트, 항상 0이어야 함

시스템 레지스터

AArch32와 마찬가지로 AArch64 사양에서는 세 가지 시스템 제어 “스레드 ID” 레지스터를 제공합니다.

등록 역할
TPIDR_EL0 예약되었습니다.
TPIDRRO_EL0 현재 프로세서의 CPU 번호를 포함합니다.
TPIDR_EL1 현재 프로세서의 KPCR 구조를 가리킵니다.

부동 소수점 예외

IEEE 부동 소수점 예외에 대한 지원은 AArch64 시스템에서 선택 사항입니다. 하드웨어 부동 소수점 예외가 발생하지 않는 프로세서 변형의 경우 Windows 커널이 예외를 자동으로 catch하고 FPCR 레지스터에서 암시적으로 사용하지 않도록 설정합니다. 따라서 이 트랩은 프로세서 변형 전체에서 정규화된 동작을 보장합니다. 그러지 않으면, 예외 지원이 안 되는 플랫폼에서 개발한 코드는 지원되는 플랫폼에서 실행될 때 예기치 않은 예외가 발생할 수 있습니다.

매개 변수 전달

Variadic 함수가 아닌 경우 Windows ABI는 매개 변수 전달에 대해 ARM에 의해 지정된 규칙을 따릅니다. 관련 규칙은 AArch64 아키텍처에 대한 프로시저 호출 표준에서 직접 발췌한 것입니다.

단계 A - 초기화

이 단계는 인수 처리가 시작되기 전에 정확히 한 번만 수행됩니다.

  1. 다음 NGRN(범용 레지스터 번호)은 0으로 설정됩니다.

  2. NSRN(다음 SIMD 및 부동 소수점 레지스터 번호)은 0으로 설정됩니다.

  3. NSAA(다음 누적 인수 주소)는 현재 SP(스택 포인터) 값으로 설정됩니다.

단계 B - 인수 사전 채우기 및 확장명

목록의 각 인수에 대해 다음 목록에서 일치하는 첫 번째 규칙이 적용됩니다. 일치하는 규칙이 없는 경우 인수는 수정되지 않은 상태로 사용됩니다.

  1. 인수 형식이 복합 형식(호출자와 호출 수신자가 모두 해당 크기를 정적으로 확인할 수 없음)인 경우 인수는 메모리에 복사되며 복사본의 포인터로 대체됩니다. (C/C++에는 해당 형식이 없지만 다른 언어나 언어 확장에 존재함)

  2. 인수 형식이 HFA 또는 HVA인 경우 인수는 수정되지 않은 상태로 사용됩니다.

  3. 인수 형식이 16바이트보다 큰 복합 형식이면 인수는 호출자가 할당한 메모리에 복사되고 인수는 복사본에 대한 포인터로 대체됩니다.

  4. 인수 형식이 복합 형식이면 인수의 크기는 8바이트의 가장 가까운 배수로 반올림됩니다.

단계 C - 레지스터와 스택에 대한 인수 할당

목록의 각 인수에 대해 인수가 할당될 때까지 다음 규칙이 차례로 적용됩니다. 인수가 레지스터에 할당되면 레지스터에서 사용되지 않는 모든 비트가 지정되지 않은 값을 갖습니다. 인수가 스택 슬롯에 할당되면 사용되지 않는 패딩 바이트는 지정되지 않은 값을 갖습니다.

  1. 인수가 반, 단일, 이중 또는 사분면 부동 소수점 또는 짧은 벡터 형식이고 NSRN이 8보다 작은 경우 인수는 레지스터 v[NSRN]의 가장 중요한 비트에 할당됩니다. NSRN은 1씩 증가합니다. 이제 인수가 할당되었습니다.

  2. 인수가 HFA 또는 HVA고 할당되지 않은 SIMD와 부동 소수점 레지스터가 충분히 있는 경우(NSRN + 멤버 수 ≤ 8), 인수는 HFA 또는 HVA의 멤버당 하나의 레지스터인 SIMD 및 부동 소수점 레지스터에 할당됩니다. NSRN은 사용된 레지스터 수만큼 증분됩니다. 이제 인수가 할당되었습니다.

  3. 인수가 HFA 또는 HVA이면 NSRN이 8로 설정되고 인수의 크기가 가장 가까운 8바이트의 배수로 반올림됩니다.

  4. 인수가 HFA, HVA, 4 자릿수 부동 소수점 또는 Short Vector 형식인 경우 NSAA는 8 또는 인수 형식의 자연 맞춤 중에서 더 큰 값으로 반올림됩니다.

  5. 인수가 반정밀도 또는 단정밀도 부동 소수점 형식이면 인수의 크기가 8바이트로 설정됩니다. 그 결과는 인수가 64비트 레지스터 중 가장 중요하지 않은 비트에 복사되고 나머지 비트가 지정되지 않은 값으로 채워진 경우와 같습니다.

  6. 인수가 HFA, HVA, 1/2, 1, 2 또는 4 자릿수 부동 소수점 또는 Short Vector 형식인 경우 인수는 조정된 NSAA의 메모리에 복사됩니다. NSAA는 인수 크기만큼 증분됩니다. 이제 인수가 할당되었습니다.

  7. 인수가 정수 또는 포인터 형식이면 인수 크기가 8바이트보다 작거나 같고 NGRN이 8보다 작으면 인수가 x[NGRN]에서 가장 작은 비트로 복사됩니다. NGRN은 1씩 증가합니다. 이제 인수가 할당되었습니다.

  8. 인수에 맞춤이 16인 경우 NGRN은 다음 짝수로 반올림됩니다.

  9. 인수가 정수 형식이면 인수의 크기가 16이고 NGRN이 7보다 작으면 인수가 x[NGRN] 및 x[NGRN+1]에 복사됩니다. x[NGRN]은 인수의 메모리 표현에 대한 주소가 낮은 이중 단어를 포함해야 합니다. NGRN은 2씩 증가합니다. 이제 인수가 할당되었습니다.

  10. 인수가 복합 형식이고 인수의 두 단어 크기가 NGRN을 뺀 8보다 작으면 인수가 x[NGRN]부터 연속 범용 레지스터로 복사됩니다. 인수는 메모리에서 연속 레지스터를 로드하는 LDR 명령의 적절한 시퀀스를 사용해 이중 단어 맞춤 주소에서 레지스터로 로드된 것처럼 전달됩니다. 레지스터의 사용되지 않는 부분에 대한 콘텐츠는 이 표준에서 지정되지 않습니다. NGRN은 사용된 레지스터 수만큼 증분됩니다. 이제 인수가 할당되었습니다.

  11. NGRN은 8로 설정됩니다.

  12. NSAA는 8 또는 인수 형식의 자연 맞춤 중 큰 값으로 반올림됩니다.

  13. 인수가 복합 형식이면 인수는 조정된 NSAA 메모리에 복사됩니다. NSAA는 인수 크기만큼 증분됩니다. 이제 인수가 할당되었습니다.

  14. 인수의 크기가 8바이트보다 작은 경우 인수의 크기가 8바이트로 설정됩니다. 그 결과는 인수가 64비트 레지스터 중 가장 중요하지 않은 비트에 복사되고 나머지 비트가 지정되지 않은 값으로 채워진 경우와 같습니다.

  15. 인수가 조정된 NSAA 메모리에 복사됩니다. NSAA는 인수 크기만큼 증분됩니다. 이제 인수가 할당되었습니다.

부록: Variadic 함수

가변적인 개수의 인수를 사용하는 함수는 다음과 같이 위와는 다르게 다음과 같이 처리됩니다.

  1. 모든 복합은 동일하게 처리됩니다. HFA 또는 HVA에 대한 특별한 처리가 없습니다.

  2. SIMD 및 부동 소수점 레지스터가 사용되지 않습니다.

실질적으로 규칙 C.12-C.15에 따라 인수를 허수부 스택에 할당하는 것과 동일합니다. 이 경우 스택의 처음 64바이트는 x0-x7로 로드되고 나머지 스택 인수는 정상적으로 배치됩니다.

반환 값

정수 계열 값은 x0에서 반환됩니다.

부동 소수점 s0, d0 또는 v0에서 적절하게 반환됩니다.

다음이 모두 유지되는 경우 형식 HFA 또는 HVA로 간주됩니다.

  • 비어 있지 않습니다.
  • 트리비얼이 아닌 기본값이나 복사 생성자, 소멸자 또는 대입 연산자가 없습니다.
  • 모든 멤버가 동일한 HFA 또는 HVA 형식이거나 다른 멤버의 HFA 또는 HVA 형식과 일치하는 float, double 또는 neon 형식입니다.

요소가 4개 이하인 HVA 값은 s0-s3, d0-d3 또는 v0-v3에서 적절하게 반환됩니다.

값으로 반환되는 형식은 특정 속성이 있는지, 그리고 함수가 정적이 아닌 멤버 함수인지에 따라 다르게 처리됩니다. 해당 속성을 모두 포함하는 형식은 다음과 같습니다.

  • C++14 표준 정의에 의한 aggregate입니다. 즉, 사용자 제공 생성자가 없고, 프라이빗 또는 보호된 비정적 데이터 멤버가 없고, 기본 클래스가 없고, 가상 함수가 없는 배열 또는 클래스입니다.
  • 간단한 복사 대입 연산자가 있습니다.
  • 간단한 소멸자가 있습니다.

비멤버 함수 또는 정적 멤버 함수에서 반환하며, 다음 반환 스타일을 사용합니다.

  • 요소가 4개 이하인 HFA 형식은 s0-s3, d0-d3 또는 v0-v3에서 적절하게 반환됩니다.
  • 8바이트보다 작거나 같은 형식이 x0에 반환됩니다.
  • 16바이트보다 작거나 같은 형식은 x0 및 x1에서 반환되고 x0은 하위 8바이트를 포함합니다.
  • 다른 집계 형식의 경우 호출자는 결과를 보유하기에 충분한 크기와 맞춤의 메모리 블록을 예약해야 합니다. 메모리 블록의 주소는 x8의 함수에 추가 인수로 전달됩니다. 호출 수신자는 서브 루틴을 실행하는 동안 언제든지 결과 메모리 블록을 수정할 수 있습니다. 호출 수신자는 x8에 저장된 값을 유지하는 데 필요하지 않습니다.

다른 모든 형식은 다음 규칙을 사용합니다.

  • 호출자는 결과를 저장하기에 충분한 크기 및 맞춤의 메모리 블록을 예약해야 합니다. $메모리 블록의 주소는 x0에서 전달된 경우 x0 또는 x1의 함수에 추가 인수로 전달되어야 합니다. 호출 수신자는 서브 루틴을 실행하는 동안 언제든지 결과 메모리 블록을 수정할 수 있습니다. 호출 수신자는 x0에서 메모리 블록의 주소를 반환합니다.

Stack

ARM에 대한 ABI를 따라 스택은 항상 16바이트로 정렬된 상태로 유지되어야 합니다. AArch64에는 SP가 16바이트로 정렬되지 않고 SP 기준 로드나 저장이 완료할 때마다 스택 맞춤 오류를 생성하는 하드웨어 기능이 포함되어 있습니다. Windows는 이 기능을 항상 사용하도록 설정하여 실행합니다.

스택에서 4k 이상에 해당하는 함수는 최종 페이지 이전의 각 페이지에 순서대로 연결해야 합니다. 이 작업은 Windows에서 스택을 확장하기 위해 사용하는 가드 페이지를 코드가 “건너뛰지 않도록” 보장합니다. 일반적으로 접촉은 x15에서 총 스택 할당을 16으로 나눈 값을 전달하는 사용자 지정 호출 규칙을 가진 __chkstk 도우미에 의해 수행됩니다.

위험 영역

현재 스택 포인터 바로 아래의 16바이트 영역은 분석 및 동적 패치 시나리오에 사용하기 위해 예약됩니다. 이 영역에 신중하게 생성된 코드를 다시 삽입할 수 있으며 [sp, #-16]에서 2개 레지스터를 저장하고 임의의 용도로 해당 레지스터를 일시적으로 사용할 수 있습니다. Windows 커널은 사용자 모드 및 커널 모드 둘 다에서 예외나 인터럽트가 생긴 경우 16바이트를 덮어쓰지 않음을 보장합니다.

커널 스택

Windows의 기본 커널 모드 스택은 6개 페이지(24k)입니다. 커널 모드에서 대형 스택 버퍼를 사용하는 함수에 특히 주의를 기울여야 합니다. 시간이 잘못 지정된 인터럽트에 스택 위쪽 공간이 거의 없어서 스택에서 비상 버그 검사가 생성될 수 있습니다.

스택 워크

빠른 스택 워크를 사용할 수 있도록 Windows 내의 코드는 프레임 포인터가 사용하도록 설정(/Oy-)된 상태로 컴파일됩니다. 일반적으로 x29(fp)는 체인의 다음 링크를 가리킵니다. 이 링크는 포인터를 스택의 이전 프레임 및 반환 주소를 표시하는 {fp, lr} 쌍입니다. 타사 코드는 향상된 프로파일링 및 추적을 허용하기 위해 프레임 포인터도 사용하도록 설정하는 것이 좋습니다.

예외 해제

해제 코드를 사용하여 예외 처리 중의 스택 해제를 지원합니다. 해제 코드는 실행 파일의 .xdata 섹션에 저장되는 바이트 시퀀스입니다. 프롤로그 및 에필로그의 작업을 요약하여 설명하므로 호출자 스택 프레임 백업을 준비하기 위해 적용된 함수 프롤로그의 효과를 취소할 수 있습니다. 해제 코드에 대한 자세한 내용은 ARM64 예외 처리를 참조하세요.

ARM EABI도 해제 코드를 사용하는 예외 해제 모델을 지정합니다. 그러나 프로세서가 함수 프롤로그 또는 에필로그 중간에 PC가 있는 사례를 처리해야 하는 Windows의 해제에는 표시된 사양만으로는 부족합니다.

RtlAddFunctionTable 및 관련 함수를 통해 동적 함수 테이블을 사용하여 동적으로 생성된 코드를 설명하는 것이 좋습니다. 그러면 생성된 코드가 예외 처리에 참여할 수 있습니다.

사이클 카운터

모든 ARMv8 CPU는 Windows에서 사용자 모드를 비롯한 모든 예외 수준에서 읽을 수 있도록 구성하는 64비트 레지스터인 사이클 카운터를 지원하는 데 필요합니다. 이는 특수 PMCCNTR_EL0 레지스터를 통해 어셈블리 코드의 MSR opcode 또는 C/C++ 코드의 _ReadStatusReg 내장 함수를 사용하여 액세스할 수 있습니다.

여기서 사이클 카운터는 벽시계가 아닌 진짜 사이클 카운터입니다. 빈도 계산은 프로세서 빈도에 따라 달라집니다. 사이클 카운터의 빈도를 알고 있어야 할 것 같다면 사이클 카운터를 사용하지 않아야 합니다. 대신 QueryPerformanceCounter를 사용해야 하는 벽시계 시간을 측정하는 것이 좋습니다.

참고 항목

일반적인 Visual C++ ARM 마이그레이션 문제
ARM64 예외 처리