ARM32 ABI 규칙 개요

Windows on ARM 프로세서용으로 컴파일된 코드의 ABI(애플리케이션 이진 인터페이스)는 표준 ARM EABI를 기반으로 합니다. 이 문서에서는 Windows on ARM과 표준 간의 주요 차이점에 대해 주로 설명합니다. 이 문서에서는 ARM32 ABI에 관해 설명합니다. ARM64 ABI에 대한 자세한 내용은 ARM64 ABI 규칙 개요를 참조하세요. 표준 ARM EABI에 대한 자세한 내용은 ARM 아키텍처용 애플리케이션 이진 인터페이스(ABI)(외부 링크)를 참조하세요.

기본 요구 사항

Windows on ARM은 항상 ARMv7 아키텍처에서 실행된다고 가정합니다. 하드웨어에서 VFPv3-D32 이상 형식의 부동 소수점 지원 기능이 제공되어야 합니다. VFP는 하드웨어에서 단정밀도 및 배정밀도 부동 소수점을 모두 지원해야 합니다. Windows 런타임은 VFP가 아닌 하드웨어에서 실행할 수 있도록 부동 소수점 에뮬레이션을 지원하지 않습니다.

고급 SIMD 확장명(NEON)도 하드웨어에서 지원되어야 합니다. 여기에는 정수 및 부동 소수점 작업이 모두 포함됩니다. 에뮬레이션에 대한 런타임 지원은 제공되지 않습니다.

정수 나누기(UDIV/SDIV) 지원은 권장 사항이지 필수 사항이 아닙니다. 정수 나누기가 지원되지 않는 플랫폼에서는 이러한 작업을 트래핑해야 하며 패치해야 할 수 있으므로 성능이 저하될 수 있습니다.

endian

Windows on ARM은 little endian 모드에서 실행됩니다. MSVC 컴파일러와 Windows 런타임에서는 항상 little endian 데이터를 사용해야 합니다. ARM ISA(명령 집합 아키텍처)의 SETEND 명령은 사용자 모드 코드도 현재 endian을 변경하도록 허용합니다. 그러나 이 작업은 애플리케이션에 위험하므로 수행하지 않는 것이 좋습니다. Big-endian 모드에서 예외가 생성되면 동작을 예측할 수 없습니다. 사용자 모드에서 애플리케이션 오류가 발생하거나 커널 모드에서 버그 검사가 발생할 수 있습니다.

맞춤

Windows에서는 ARM 하드웨어가 불일치하는 정수 액세스를 투명하게 처리할 수 있지만 일부 상황에서는 정렬 오류가 계속 발생할 수 있습니다. 정렬에 대해서는 다음 규칙을 따르세요.

  • 절반 단어 크기(16비트) 및 단어 크기(32비트) 정수의 로드와 저장은 정렬할 필요가 없습니다. 하드웨어에서 이러한 로드와 저장을 효율적으로 투명하게 처리합니다.

  • 부동 소수점 로드 및 저장은 정렬해야 합니다. 커널은 정렬되지 않은 로드 및 저장을 투명하게 처리하지만 이 경우 큰 오버헤드가 발생합니다.

  • 로드 또는 저장 double(LDRD/STRD) 및 multiple(LDM/STM) 연산은 정렬해야 합니다. 커널은 이러한 대부분의 연산을 투명하게 처리하지만 역시 큰 오버헤드가 발생합니다.

  • 캐시되지 않은 모든 메모리 액세스는 정수 액세스의 경우에도 정렬해야 합니다. 액세스를 정렬하지 않으면 정렬 오류가 발생할 수 있습니다.

명령 집합

Windows on ARM의 명령 집합은 Thumb-2로 엄격하게 제한됩니다. 이 플랫폼에서 실행되는 모든 코드는 시작된 후 항상 Thumb 모드에서 유지될 것으로 예상됩니다. 레거시 ARM 명령 집합으로 전환하려는 시도가 성공할 수 있습니다. 그러나 그럴 경우 예외나 인터럽트로 인해 사용자 모드에서 애플리케이션 오류가 발생하거나 커널 모드에서 버그 검사가 발생할 수 있습니다.

이 요구 사항을 적용하는 경우 모든 코드 포인터에 낮은 비트를 설정해야 한다는 단점이 있습니다. 그런 다음, BLX 또는 BX를 통해 로드 및 분기될 때 프로세서는 Thumb 모드로 유지됩니다. 대상 코드를 32비트 ARM 명령으로 실행하려고 하지 않습니다.

SDIV/UDIV 명령

정수 나누기 명령 SDIV 및 UDIV 사용은 이러한 명령을 처리하는 네이티브 하드웨어가 없는 플랫폼에서도 완전하게 지원됩니다. Cortex-A9 프로세서에서 SDIV 또는 UDIV 나누기당 추가 오버헤드는 약 80개 사이클입니다. 이는 입력에 따라 20~250개 사이클의 전체 나누기 시간에 추가됩니다.

정수 레지스터

ARM 프로세서는 16개 정수 레지스터를 지원합니다.

등록 휘발성 여부 역할
r0 휘발성 매개 변수, 결과, 스크래치 레지스터 1
r1 휘발성 매개 변수, 결과, 스크래치 레지스터 2
r2 휘발성 매개 변수, 스크래치 레지스터 3
r3 휘발성 매개 변수, 스크래치 레지스터 4
r4 비휘발성
r5 비휘발성
r6 비휘발성
r7 비휘발성
r8 비휘발성
r9 비휘발성
r10 비휘발성
r11 비휘발성 프레임 포인터
r12 휘발성 프로시저 호출 내 스크래치 레지스터
r13(SP) 비휘발성 스택 포인터
r14(LR) 비휘발성 링크 레지스터
r15(PC) 비휘발성 프로그램 카운터

매개 변수 및 반환 값 레지스터를 사용하는 방법에 대한 자세한 내용은 이 문서의 매개 변수 전달 섹션을 참조하세요.

Windows에서는 빠른 스택 프레임 워크를 위해 r11을 사용합니다. 자세한 내용은 스택 워크 섹션을 참조하세요. 이 요구 사항으로 인해 r11은 항상 체인의 최상위 링크를 가리켜야 합니다. 일반적인 용도에는 r11을 사용하지 마세요. 코드가 분석 중에 올바른 스택 워크를 생성하지 않기 때문입니다.

VFP 레지스터

Windows에서는 VFPv3-D32 보조 프로세서가 지원되는 ARM 변형만을 지원합니다. 즉, 부동 소수점 레지스터는 항상 존재하며 매개 변수 전달 시 이에 의존할 수 있습니다. 또한 32개 레지스터의 전체 집합을 사용할 수 있습니다. 아래 테이블에는 VFP 레지스터와 해당 사용법이 요약되어 있습니다.

single double quad 휘발성 여부 역할
s0-s3 d0-d1 q0 휘발성 매개 변수, 결과, 스크래치 레지스터
s4-s7 d2-d3 q1 휘발성 매개 변수, 스크래치 레지스터
s8-s11 d4-d5 q2 휘발성 매개 변수, 스크래치 레지스터
s12-s15 d6-d7 q3 휘발성 매개 변수, 스크래치 레지스터
s16-s19 d8-d9 q4 비휘발성
s20-s23 d10-d11 q5 비휘발성
s24-s27 d12-d13 q6 비휘발성
s28-s31 d14-d15 q7 비휘발성
d16-d31 q8-q15 휘발성

다음 테이블에는 부동 소수점 상태 및 제어 레지스터(FPSCR) 비트 필드가 나와 있습니다.

비트 의미 휘발성 여부 역할
31-28 NZCV 휘발성 상태 플래그
27 QC 휘발성 누적 포화도
26 AHP 비휘발성 대체 반정밀도 컨트롤
25 DN 비휘발성 기본 NaN 모드 컨트롤
24 FZ 비휘발성 0으로 플러시 모드 컨트롤
23-22 RMode 비휘발성 반올림 모드 컨트롤
21-20 Stride 비휘발성 벡터 진행 속도, 항상 0이어야 함
18-16 Len 비휘발성 벡터 길이, 항상 0이어야 함
15, 12-8 IDE, IXE 등 비휘발성 예외 트랩 사용 비트, 항상 0이어야 함
7, 4-0 IDC, IXC 등 휘발성 누적 예외 플래그

부동 소수점 예외

대부분의 ARM 하드웨어는 IEEE 부동 소수점 예외를 지원하지 않습니다. 하드웨어 부동 소수점 예외가 발생하지 않는 프로세서 변형에서는 Windows 커널이 예외를 자동으로 catch하고 FPSCR 레지스터에서 암시적으로 사용하지 않도록 설정합니다. 따라서 이 작업은 프로세서 변형 전체에서 정규화된 동작을 보장합니다. 반면 예외가 지원되지 않는 플랫폼에서 개발된 코드는 예외가 지원되는 플랫폼에서 실행 중일 때 예기치 않은 예외를 받을 수 있습니다.

매개 변수 전달

Windows on ARM ABI는 비 variadic 함수에 전달할 매개 변수에 대해 ARM 규칙을 따릅니다. ABI 규칙은 VFP 및 고급 SIMD 확장을 포함합니다. 이러한 규칙은 VFP 확장과 통합된 ARM 아키텍처에 대한 프로시저 호출 표준을 따릅니다. 기본적으로 처음 4개 정수와 최대 8개의 부동 소수점 또는 벡터 인수가 레지스터로 전달됩니다. 추가 인수는 스택에 전달됩니다. 인수는 다음 절차를 통해 레지스터나 스택에 할당됩니다.

A단계: 초기화

초기화는 인수 전달이 시작되기 전에 정확히 한 번 수행됩니다.

  1. NCRN(다음 코어 레지스터 번호)이 r0으로 설정됩니다.

  2. VFP 레지스터가 할당되지 않은 것으로 표시됩니다.

  3. NSAA(다음 누적 인수 주소)가 현재 SP로 설정됩니다.

  4. 메모리에서 결과를 반환하는 함수를 호출하면 결과의 주소가 r0에 배치되고 NCRN은 r1로 설정됩니다.

B단계: 인수의 사전 패딩 및 확장

목록의 각 인수에 대해 다음 목록에서 일치하는 첫 번째 규칙이 적용됩니다.

  1. 복합 형식(호출자와 수신자가 모두 해당 크기를 정적으로 확인할 수 없음) 인수는 메모리에 복사되며 복사본의 포인터로 대체됩니다.

  2. 바이트 또는 16비트 절반 단어 인수는 0 또는 부호를 사용하여 32비트 전체 단어로 확장되며 4바이트 인수로 처리됩니다.

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

C단계: 레지스터 및 스택에 인수 할당

목록의 각 인수에 대해 인수가 할당될 때까지 다음 규칙이 차례로 적용됩니다.

  1. 인수가 VFP 형식이고 해당하는 형식의 할당되지 않은 연속 VFP 레지스터가 충분히 많으면 인수가 해당 레지스터의 최소 번호 시퀀스에 할당됩니다.

  2. 인수가 VFP 형식이면 할당되지 않은 모든 나머지 레지스터가 사용할 수 없는 것으로 표시됩니다. NSAA는 인수 형식에 대해 올바르게 정렬될 때까지 상향 조정되며 인수는 조정된 NSAA에서 스택에 복사됩니다. 그러고 나면 NSAA가 인수 크기만큼 증분됩니다.

  3. 인수에 8바이트 정렬을 적용해야 하는 경우 NCRN은 다음 짝수 레지스터 번호로 반올림됩니다.

  4. 32비트 단어의 인수 크기가 r4에서 NCRN을 뺀 값보다 작으면 인수가 NCRN부터 시작하여 코어 레지스터로 복사됩니다. 이때 가장 중요하지 않은 비트가 최소 번호 레지스터에 채워집니다. NCRN은 사용된 레지스터 수만큼 증분됩니다.

  5. NCRN이 r4보다 작고 NSAA가 SP와 같으면 인수는 코어 레지스터와 스택 간에 분할됩니다. 인수의 첫 부분은 NCRN부터 시작하여 r3까지(r3 포함) 코어 레지스터로 복사됩니다. 인수의 나머지 부분은 NSAA부터 시작하여 스택에 복사됩니다. NCRN은 r4로 설정되며 NSAA는 인수 크기에서 레지스터에 전달된 부분을 뺀 크기만큼 증분됩니다.

  6. 인수에 8바이트 정렬을 적용해야 하는 경우 NSAA는 다음 8바이트 정렬 주소로 반올림됩니다.

  7. 인수가 NSAA에서 메모리에 복사됩니다. NSAA는 인수 크기만큼 증분됩니다.

Variadic 함수에는 VFP 레지스터가 사용되지 않으며 C단계 규칙 1과 2는 무시됩니다. 즉, 호출자가 전달한 추가 인수 앞에 레지스터 인수를 추가한 다음, 스택에서 전체 인수 목록에 직접 액세스할 수 있도록 variadic 함수는 선택적 푸시 {r0-r3}으로 시작될 수 있습니다.

정수 형식 값은 r0에서 반환되어 필요에 따라 64비트 반환 값에 대해 r1로 확장됩니다. VFP/NEON 부동 소수점 또는 SIMB 형식 값은 s0, d0 또는 q0에서 적절하게 반환됩니다.

Stack

스택은 항상 4바이트 정렬 상태로 유지되어야 하며 모든 함수 경계에서는 8바이트로 정렬되어야 합니다. 이는 64비트 스택 변수에 대해 빈번한 연관 작업 사용을 지원하기 위해 필요합니다. ARM EABI는 모든 공용 인터페이스에서 스택이 8바이트로 정렬됨을 설명합니다. 일관성을 위해 Windows on ARM ABI는 모든 함수 경계를 공용 인터페이스로 간주합니다.

alloca를 호출하는 함수나 스택 포인터를 동적으로 변경하는 함수 등 프레임 포인터를 사용해야 하는 함수는 함수 프롤로그에서 r11에 프레임 포인터를 설정해야 하며 에필로그까지 해당 설정 상태를 그대로 유지해야 합니다. 프레임 포인터가 필요하지 않은 함수는 모든 스택 업데이트를 프롤로그에서 수행해야 하며 에필로그까지 스택 포인터를 변경하지 않고 유지해야 합니다.

스택에서 4KB 이상을 할당하는 함수는 최종 페이지 이전의 각 페이지에 순서대로 연결해야 합니다. 그러면 Windows에서 스택을 확장하기 위해 사용하는 가드 페이지를 코드가 “건너뛰지” 않습니다. 일반적으로는 __chkstk 도우미를 사용하여 이 확장을 수행합니다. 이 도우미에는 r4에서 총 스택 할당(바이트)을 4로 나눈 값이 전달되며, 그 결과로 최종 스택 할당량(바이트)이 r4에 다시 전달됩니다.

위험 영역

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

커널 스택

Windows의 기본 커널 모드 스택은 3개 페이지(12KB)입니다. 커널 모드에서 큰 스택 버퍼가 포함된 함수를 만들지 않도록 주의해야 합니다. 인터럽트에 스택 위쪽 공간이 거의 없어서 스택에서 비상 버그 검사가 수행될 수 있습니다.

C/C++ 관련 사항

열거형은 포함된 값 하나 이상에 64비트 2배 워드 스토리지가 필요하지 않으면 32비트 정수 형식입니다. 이러한 저장소가 필요한 경우 열거형 수준이 64비트 정수 형식으로 올라갑니다.

다른 플랫폼과의 호환성을 유지하기 위해 wchar_tunsigned short와 동일하게 정의됩니다.

스택 워크

빠른 스택 워크를 사용할 수 있도록 Windows 코드는 프레임 포인터를 사용하도록 설정된 상태(/Oy(프레임 포인터 생략))로 컴파일됩니다. 일반적으로 r11 레지스터는 체인의 다음 링크를 가리킵니다. 이 링크는 포인터를 스택의 이전 프레임 및 반환 주소로 지정하는 {r11, lr} 쌍입니다. 프로파일링 및 추적 개선을 위해 코드에서 프레임 포인터도 사용하도록 설정하는 것이 좋습니다.

예외 해제

해제 코드를 사용하여 예외 처리 중의 스택 해제를 사용하도록 설정합니다. 해제 코드는 실행 가능 이미지의 .xdata 섹션에 저장되는 바이트 시퀀스로, 함수 프롤로그 및 에필로그 코드의 작동을 요약하여 설명하므로 호출자의 스택 프레임 해제를 준비하기 위해 적용된 함수의 프롤로그를 실행 취소할 수 있습니다.

ARM EABI는 해제 코드를 사용하는 예외 해제 모델을 지정합니다. 그러나 프로세서가 함수의 프롤로그 또는 에필로그 중간에 포함된 사례를 처리해야 하는 Windows의 해제에는 이 사양만으로는 부족합니다. Windows on ARM 예외 데이터 및 해제에 대한 자세한 내용은 ARM 예외 처리를 참조하세요.

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

사이클 카운터

Windows를 실행하는 ARM 프로세서는 사이클 카운터를 지원해야 합니다. 그러나 카운터를 직접 사용하면 문제가 발생할 수 있습니다. 이러한 문제를 방지하기 위해 Windows on ARM은 정의되지 않은 opcode를 사용하여 정규화된 64비트 사이클 카운터 값을 요청합니다. C 또는 C++에서는 __rdpmccntr64 내장 함수를 사용하여 해당 opcode를 내보내고 어셈블리에서는 __rdpmccntr64 명령을 사용합니다. Cortex-A9에서는 사이클 카운터를 읽는 데 약 60개 사이클이 소요됩니다.

카운터는 클록이 아닌 실제 사이클 카운터이므로 계산 빈도는 프로세서 빈도에 따라 달라집니다. 경과된 클록 시간을 측정하려면 QueryPerformanceCounter를 사용합니다.

참고 항목

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