다음을 통해 공유


ARM64 예외 처리

Windows on ARM64는 하드웨어에서 생성하는 비동기식 예외 및 소프트웨어에서 생성하는 동기식 예외에 대해 동일한 구조적 예외 처리 메커니즘을 사용합니다. 언어별 예외 처리기가 언어 도우미 함수를 사용하여 Windows의 구조적 예외 처리를 기반으로 작성됩니다. 이 문서에서는 ARM64의 Windows에서 예외 처리를 설명합니다. Microsoft ARM 어셈블러 및 MSVC 컴파일러에서 생성되는 코드에서 사용하는 언어 도우미를 보여 줍니다.

목표 및 동기

예외 해제 데이터 규칙 및 이 설명의 목적은 다음과 같습니다.

  • 모든 경우에 코드를 검색하지 않고 해제할 수 있는 충분한 설명을 제공합니다.

    • 코드를 분석하려면 코드를 호출해야 합니다. 유용한 경우(추적, 샘플링, 디버깅) 해제를 방지합니다.

    • 코드를 분석하는 것은 복잡합니다. 따라서 컴파일러는 해제기에서 디코딩할 수 있는 명령만 생성하도록 주의해야 합니다.

    • 해제 코드를 사용하여 해제를 완전히 설명할 수 없는 경우 경우에 따라 명령 디코딩으로 대체해야 합니다. 명령 디코딩은 전반적인 복잡성을 증가시키고 이상적으로는 피해야 합니다.

  • 중간 프롤로그 및 중간 에필로그에서 해제를 지원합니다.

    • 해제는 Windows에서 예외 처리보다 더 많이 사용됩니다. 프롤로그 또는 에필로그 코드 시퀀스의 중간에 있는 경우에도 코드가 정확하게 해제될 수 있습니다.
  • 최소한의 공간을 차지합니다.

    • 해제 코드는 이진 크기를 크게 늘리도록 집계되어서는 안 됩니다.

    • 해제 코드는 메모리에서 잠길 수 있으므로 작은 메모리 공간은 로드된 각 이진에 대해 최소한의 오버헤드를 보장합니다.

가정

예외 처리 설명에 다음의 가정이 적용됩니다.

  • 프롤로그와 에필로그는 서로 미러링되는 경향이 있습니다. 일반적인 특성을 활용하여 해제를 설명하는 데 필요한 메타 데이터의 크기를 크게 줄일 수 있습니다. 함수 본문 내에서는 프롤로그 작업을 실행 취소했는지 아니면 에필로그 작업을 정방향으로 수행했는지에 중요하지 않습니다. 두 작업에서 모두 같은 결과가 생성됩니다.

  • 함수는 전체적으로 비교적 크기가 작은 경우가 많습니다. 공간에 대한 몇 가지 최적화는 이 팩트에 의존하여 가장 효율적인 데이터 압축을 구현합니다.

  • 에필로그에는 조건부 코드가 없습니다.

  • 전용 프레임 포인터 레지스터: 프롤로그의 sp 다른 레지스터(x29)에 저장된 경우 해당 레지스터는 함수 전체에서 그대로 다시 기본. 즉, 원본 sp 은 언제든지 복구될 수 있습니다.

  • 다른 레지스터에 sp 저장되지 않는 한 스택 포인터의 모든 조작은 프롤로그 및 에필로그 내에서 엄격하게 발생합니다.

  • 스택 프레임 레이아웃은 다음 섹션에 설명된 대로 구성됩니다.

ARM64 스택 프레임 레이아웃

Diagram that shows the stack frame layout for functions.

프레임 연결 함수의 fp 경우 최적화 고려 사항에 따라 지역 변수 영역의 모든 위치에 쌍을 lr 저장할 수 있습니다. 목표는 프레임 포인터() 또는 스택 포인터sp(x29)를 기반으로 단일 명령으로 도달할 수 있는 로컬 수를 최대화하는 것입니다. 그러나 함수의 경우 alloca 연결해야 하며 x29 스택의 아래쪽을 가리킵니다. 레지스터 쌍 주소 지정 모드를 향상하기 위해 비휘발성 레지스터 저장 영역은 로컬 영역 스택의 맨 위에 배치됩니다. 가장 효율적인 여러 프롤로그 시퀀스를 보여 주는 예제는 다음과 같습니다. 명확성 및 보다 나은 캐시 위치를 위해 모든 정식 프롤로그에서 호출 수신자 저장 레지스터의 순서는 “증가”순입니다. #framesz 아래는 전체 스택의 크기(영역 제외 alloca )를 나타냅니다. #localsz#outsz는 각각 로컬 영역 크기(<x29, lr> 쌍의 저장 영역 포함)와 나가는 매개 변수 크기를 나타냅니다.

  1. 연결됨, #localsz <= 512

        stp    x19,x20,[sp,#-96]!        // pre-indexed, save in 1st FP/INT pair
        stp    d8,d9,[sp,#16]            // save in FP regs (optional)
        stp    x0,x1,[sp,#32]            // home params (optional)
        stp    x2,x3,[sp,#48]
        stp    x4,x5,[sp,#64]
        stp    x6,x7,[sp,#82]
        stp    x29,lr,[sp,#-localsz]!   // save <x29,lr> at bottom of local area
        mov    x29,sp                   // x29 points to bottom of local
        sub    sp,sp,#outsz             // (optional for #outsz != 0)
    
  2. 체인, #localsz > 512

        stp    x19,x20,[sp,#-96]!        // pre-indexed, save in 1st FP/INT pair
        stp    d8,d9,[sp,#16]            // save in FP regs (optional)
        stp    x0,x1,[sp,#32]            // home params (optional)
        stp    x2,x3,[sp,#48]
        stp    x4,x5,[sp,#64]
        stp    x6,x7,[sp,#82]
        sub    sp,sp,#(localsz+outsz)   // allocate remaining frame
        stp    x29,lr,[sp,#outsz]       // save <x29,lr> at bottom of local area
        add    x29,sp,#outsz            // setup x29 points to bottom of local area
    
  3. Unchained, leaf 함수(lr 저장되지 않음)

        stp    x19,x20,[sp,#-80]!       // pre-indexed, save in 1st FP/INT reg-pair
        stp    x21,x22,[sp,#16]
        str    x23,[sp,#32]
        stp    d8,d9,[sp,#40]           // save FP regs (optional)
        stp    d10,d11,[sp,#56]
        sub    sp,sp,#(framesz-80)      // allocate the remaining local area
    

    모든 로컬은 에 sp따라 액세스됩니다. <x29,lr>는 이전 프레임을 가리킵니다. 프레임 크기 <= 512의 sub sp, ... 경우 regs 저장 영역을 스택의 맨 아래로 이동하면 최적화할 수 있습니다. 단점은 위의 다른 레이아웃과 일치하지 않는다는 것입니다. 또한 저장된 regs는 pair-regs 및 사전 및 사후 인덱싱된 오프셋 주소 지정 모드에 대한 범위의 일부를 차지합니다.

  4. Unchained, non-leaf 함수(Int 저장된 영역에 저장 lr )

        stp    x19,x20,[sp,#-80]!       // pre-indexed, save in 1st FP/INT reg-pair
        stp    x21,x22,[sp,#16]         // ...
        stp    x23,lr,[sp,#32]          // save last Int reg and lr
        stp    d8,d9,[sp,#48]           // save FP reg-pair (optional)
        stp    d10,d11,[sp,#64]         // ...
        sub    sp,sp,#(framesz-80)      // allocate the remaining local area
    

    또는 짝수 저장 Int 레지스터를 사용하여

        stp    x19,x20,[sp,#-80]!       // pre-indexed, save in 1st FP/INT reg-pair
        stp    x21,x22,[sp,#16]         // ...
        str    lr,[sp,#32]              // save lr
        stp    d8,d9,[sp,#40]           // save FP reg-pair (optional)
        stp    d10,d11,[sp,#56]         // ...
        sub    sp,sp,#(framesz-80)      // allocate the remaining local area
    

    저장만 x19 :

        sub    sp,sp,#16                // reg save area allocation*
        stp    x19,lr,[sp]              // save x19, lr
        sub    sp,sp,#(framesz-16)      // allocate the remaining local area
    

    * 미리 인덱싱된 reg-lr stp 을 해제 코드로 stp 나타낼 수 없으므로 reg 저장 영역 할당이 접되지 않습니다.

    모든 로컬은 에 sp따라 액세스됩니다. <x29>는 이전 프레임을 가리킵니다.

  5. 연결됨, #framesz <= 512, #outsz = 0

        stp    x29,lr,[sp,#-framesz]!       // pre-indexed, save <x29,lr>
        mov    x29,sp                       // x29 points to bottom of stack
        stp    x19,x20,[sp,#(framesz-32)]   // save INT pair
        stp    d8,d9,[sp,#(framesz-16)]     // save FP pair
    

    위의 첫 번째 프롤로그 예제와 비교할 때 이 예제에는 장점이 있습니다. 모든 레지스터 저장 지침은 스택 할당 명령이 하나만 지나면 실행할 수 있습니다. 즉, 명령 수준 병렬 처리를 방지하는 반 의존성이 없음 sp 을 의미합니다.

  6. 연결된 프레임 크기 > 512(함수가 없는 alloca경우 선택 사항)

        stp    x29,lr,[sp,#-80]!            // pre-indexed, save <x29,lr>
        stp    x19,x20,[sp,#16]             // save in INT regs
        stp    x21,x22,[sp,#32]             // ...
        stp    d8,d9,[sp,#48]               // save in FP regs
        stp    d10,d11,[sp,#64]
        mov    x29,sp                       // x29 points to top of local area
        sub    sp,sp,#(framesz-80)          // allocate the remaining local area
    

    최적화를 위해 x29 "reg-pair" 및 사전/사후 인덱싱된 오프셋 주소 지정 모드에 대한 더 나은 범위를 제공하기 위해 로컬 영역의 모든 위치에 배치할 수 있습니다. 프레임 포인터 아래의 로컬은 에 sp따라 액세스할 수 있습니다.

  7. 연결된 프레임 크기 > 4K(alloca(),

        stp    x29,lr,[sp,#-80]!            // pre-indexed, save <x29,lr>
        stp    x19,x20,[sp,#16]             // save in INT regs
        stp    x21,x22,[sp,#32]             // ...
        stp    d8,d9,[sp,#48]               // save in FP regs
        stp    d10,d11,[sp,#64]
        mov    x29,sp                       // x29 points to top of local area
        mov    x15,#(framesz/16)
        bl     __chkstk
        sub    sp,sp,x15,lsl#4              // allocate remaining frame
                                            // end of prolog
        ...
        sub    sp,sp,#alloca                // more alloca() in body
        ...
                                            // beginning of epilog
        mov    sp,x29                       // sp points to top of local area
        ldp    d10,d11,[sp,#64]
        ...
        ldp    x29,lr,[sp],#80              // post-indexed, reload <x29,lr>
    

ARM64 예외 처리 정보

.pdata 레코드

레코드는 PE 이 .pdata 진 파일의 모든 스택 조작 함수를 설명하는 고정 길이 항목의 정렬된 배열입니다. "스택 조작"이라는 구는 중요합니다. 로컬 스토리지가 필요하지 않고 비휘발성 레지스터를 저장/복원할 필요가 없는 리프 함수는 레코드가 .pdata 필요하지 않습니다. 레코드는 공간을 절약하기 위해 명시적으로 생략해야 합니다. 이러한 함수 중 하나에서 해제하면 반환 주소를 직접 lr 가져와 호출자 위로 이동할 수 있습니다.

ARM64의 각 .pdata 레코드 길이는 8바이트입니다. 각 레코드의 일반 형식은 함수의 32비트 RVA를 첫 번째 단어에서 시작한 다음, 가변 길이 .xdata 블록에 대한 포인터 또는 정식 함수 해제 시퀀스를 설명하는 압축된 단어가 포함된 두 번째 단어를 배치합니다.

.pdata record layout.

필드는 다음과 같습니다.

  • 함수 시작 RVA는 함수 시작 부분의 32비트 RVA입니다.

  • 플래그는 두 번째 .pdata 단어의 다시 기본 30비트를 해석하는 방법을 나타내는 2비트 필드입니다. 플래그가 0이면 나머지 비트는 예외 정보 RVA가 됩니다(최하위 2비트는 암시적으로 0이 됨). 플래그가 0이 아니면 나머지 비트는 압축된 해제 데이터 구조체가 됩니다.

  • ‘예외 정보 RVA’는 .xdata 섹션에 저장되는 가변 길이 예외 정보 구조체의 주소입니다. 이 데이터는 4비트 단위로 정렬되어야 합니다.

  • 압축된 해제 데이터는 함수에서 해제하는 데 필요한 작업의 압축된 설명이며 정규형을 취합니다. 이 경우에는 .xdata 레코드가 필요하지 않습니다.

.xdata 레코드

압축된 해제 형식만으로는 함수 해제를 설명하는 데 부족한 경우에는 가변 길이 .xdata 레코드를 만들어야 합니다. 이 레코드의 주소는 .pdata 레코드의 두 번째 단어에 저장됩니다. 형식 .xdata 은 압축된 가변 길이 단어 집합입니다.

.xdata record layout.

이 데이터는 네 개의 섹션으로 구분됩니다.

  1. 구조체의 전체 크기를 설명하고 키 함수 데이터를 제공하는 1 단어 또는 2 단어 머리글입니다. 두 번째 단어는 에필로그 개수코드 단어 필드가 모두 0으로 설정된 경우에만 존재합니다. 헤더에는 다음과 같은 비트 필드가 있습니다.

    a. 함수 길이는 18비트 필드입니다. 함수의 전체 길이(바이트 단위)를 4로 나눈 결과를 나타냅니다. 함수가 1M보다 큰 경우 함수를 설명하는 데 여러 .pdata 레코드와 .xdata 레코드를 사용해야 합니다. 자세한 내용은 큰 함수 섹션을 참조하세요.

    b. Vers는 2비트 필드입니다. 다시 기본 버전을 설명합니다.xdata. 현재 버전 0만 정의되었으므로 1-3은 허용되지 않습니다.

    c. X는 1비트 필드입니다. 예외 데이터가 있는지(1) 없는지(0)를 나타냅니다.

    d. E는 1비트 필드입니다. 단일 에필로그를 설명하는 정보가 나중에 더 많은 범위 단어를 요구하지 않고 헤더(1)에 압축됨을 나타냅니다(0).

    e. 에필로그 개수E 비트의 상태에 따라 두 가지 의미가 될 수 있는 5비트 필드입니다.

    1. E가 0이면 섹션 2에 설명된 총 에필로그 범위의 수를 지정합니다. 함수에 31개가 넘는 범위가 있는 경우 코드 단어 필드를 0으로 설정하여 확장 단어가 필요함을 나타내야 합니다.

    2. E가 1이면 이 필드는 유일한 에필로그를 설명하는 첫 번째 해제 코드의 인덱스를 지정합니다.

    f. 코드 단어는 섹션 3의 모든 해제 코드를 포함하는 데 필요한 32비트 단어의 수를 지정하는 5비트 필드입니다. 31개 이상의 단어(즉, 해제 코드 124개)가 필요한 경우 확장 단어가 필요함을 나타내려면 이 필드는 0이어야 합니다.

    g. 확장된 에필로그 개수확장 코드 단어는 각각 16비트 및 8비트 필드입니다. 매우 많은 수의 에필로그 또는 매우 많은 수의 해제 코드 단어를 인코딩하기 위한 추가 공간을 제공합니다. 관련 필드를 포함하는 확장 단어는 에필로그 개수코드 단어 필드가 모두 0인 경우에만 존재합니다.

  2. 에필로그 수가 0이 아니면 단어에 하나씩 압축된 에필로그 범위에 대한 정보 목록이 헤더와 선택적 확장 헤더 뒤를 옵니다. 이는 시작 오프셋이 늘어나는 순서로 저장됩니다. 각 범위에는 다음 비트가 포함됩니다.

    a. 에필로그 시작 오프셋은 함수 시작 위치를 기준으로 하는 에필로그 오프셋(바이트 단위)을 4로 나눈 결과를 가지는 18비트 필드입니다.

    b. Res는 이후 확장을 위해 예약되는 4비트 필드입니다. 해당 값은 0이어야 합니다.

    c. 에필로그 시작 인덱스는 10비트 필드입니다(확장된 코드 단어보다 2비트 많음). 에필로그를 설명하는 첫 번째 해제 코드의 바이트 인덱스를 나타냅니다.

  3. 에필로그 범위 목록 다음에는 해제 코드를 포함하는 바이트 배열이 오며, 이는 이후 섹션에서 자세히 설명합니다. 이 배열은 가장 가까운 전체 단어 경계 끝에 채워집니다. 해제 코드는 이 배열에 기록됩니다. 함수 본문에 가장 가까운 것에서 시작하여 함수의 가장자리로 이동합니다. 각 해제 코드의 바이트는 big-endian 순서로 저장되므로 가장 중요한 바이트가 먼저 페치되어 나머지 코드의 작업과 길이를 식별합니다.

  4. 마지막으로 해제 코드 바이트 이후에 헤더의 X 비트가 1로 설정된 경우는 예외 처리기 정보가 나옵니다. 예외 처리기 자체의 주소를 제공하는 단일 예외 처리기 RVA로 구성됩니다. 예외 처리기에서 요구하는 가변 길이 데이터 바로 뒤에 옵니다.

레코드는 .xdata 처음 8바이트를 가져와서 레코드의 전체 크기를 계산하는 데 사용할 수 있도록 설계되었으며, 그 다음에 나오는 변수 크기 예외 데이터의 길이를 뺀 값입니다. 다음 코드 조각은 레코드 크기를 계산합니다.

ULONG ComputeXdataSize(PULONG Xdata)
{
    ULONG Size;
    ULONG EpilogScopes;
    ULONG UnwindWords;

    if ((Xdata[0] >> 22) != 0) {
        Size = 4;
        EpilogScopes = (Xdata[0] >> 22) & 0x1f;
        UnwindWords = (Xdata[0] >> 27) & 0x1f;
    } else {
        Size = 8;
        EpilogScopes = Xdata[1] & 0xffff;
        UnwindWords = (Xdata[1] >> 16) & 0xff;
    }

    if (!(Xdata[0] & (1 << 21))) {
        Size += 4 * EpilogScopes;
    }

    Size += 4 * UnwindWords;

    if (Xdata[0] & (1 << 20)) {
        Size += 4;  // Exception handler RVA
    }

    return Size;
}

프롤로그와 각 에필로그에 해제 코드로의 자체 인덱스가 있지만 테이블은 프롤로그와 에필로그에서 공유됩니다. 모두 동일한 코드를 공유하는 것은 완전히 가능하며 전혀 이상하지 않습니다. (예제는 다음의 예제 2를 참조하세요. 예제 섹션. ) 컴파일러 작성기는 특히 이 경우에 최적화해야 합니다. 지정 가능한 가장 큰 인덱스는 255이므로 특정 함수에 대한 해제 코드의 총 수를 제한합니다.

해제 코드

해제 코드의 배열은 프롤로그의 효과를 실행 취소하는 방법을 정확하게 설명하는 시퀀스 풀입니다. 작업을 실행 취소해야 하는 순서와 동일한 순서로 저장됩니다. 해제 코드는 바이트 문자열로 인코딩된 작은 명령 집합으로 생각할 수 있습니다. 실행이 완료되면 호출 함수에 대한 반환 주소가 레지스터에 있습니다 lr . 그리고 모든 비휘발성 레지스터는 함수가 호출될 때 해당 값으로 복원됩니다.

예외가 함수 본문 내에서만 발생하며 프롤로그 또는 에필로그 내에서는 발생하지 않음이 보장된다면 단일한 시퀀스 하나만 있으면 됩니다. 그러나 Windows 해제 모델에서는 코드가 부분적으로 실행된 프롤로그 또는 에필로그 내에서 해제할 수 있어야 합니다. 이 요구 사항을 충족하기 위해 해제 코드가 정교하게 설계하여 프롤로그와 에필로그의 각 관련 opcode로 명확하게 1:1 매핑되도록 했습니다. 이 설계에는 다음과 같은 여러 가지 의미가 있습니다.

  • 해제 코드 수를 계산하여 프롤로그 및 에필로그의 길이를 컴퓨팅할 수 있습니다.

  • 에필로그 범위가 시작된 후의 명령 수를 계산하면 해당하는 해제 코드 수를 건너뛸 수 있습니다. 시퀀스의 나머지 부분을 실행하여 에필로그에서 수행한 부분적으로 실행된 해제를 완료할 수 있습니다.

  • 프롤로그의 끝 이전에 명령 수를 계산하면 해당하는 해제 코드 수를 건너뛸 수 있습니다. 시퀀스의 나머지 부분을 실행하여 실행을 완료한 프롤로그 부분만 실행 취소할 수 있습니다.

해제 코드는 아래 표에 따라 인코딩됩니다. 모든 해제 코드는 거대한 스택(alloc_l)을 할당하는 코드를 제외하고 단일/더블 바이트입니다. 총 22개의 해제 코드가 있습니다. 각 해제 코드는 프롤로그/에필로그에서 정확히 하나의 명령만 매핑하여 부분적으로 실행된 프롤로그 및 에필로그를 해제할 수 있습니다.

해제 코드 비트 및 해석
alloc_s 000xxxxx: < 512(2^5 * 16) 크기의 작은 스택 할당
save_r19r20_x 001zzzzz: 쌍 저장 <x19,x20>[sp-#Z*8]!, 미리 인덱싱된 오프셋 >= -248
save_fplr 01zzzzzz: [sp+#Z*8]<x29,lr> 쌍 저장, 오프셋 <= 504.
save_fplr_x 10zzzzzz: 쌍 저장 <x29,lr>[sp-(#Z+1)*8]!, 미리 인덱싱된 오프셋 >= -512
alloc_m 11000xxx'xxxxxxxx: 크기 < 가 32K인 큰 스택을 할당합니다(2^11 * 16).
save_regp 110010xx'xxzzzzzz: save x(19+#X) pair at [sp+#Z*8], offset <= 504
save_regp_x 110011xx'xxzzzzzz: 쌍 저장 x(19+#X)[sp-(#Z+1)*8]!, 미리 인덱싱된 오프 >셋 = -512
save_reg 110100xx'xxzzzzzz: save reg x(19+#X) at [sp+#Z*8], offset <= 504
save_reg_x 1101010x'xxxzzzzz: save reg x(19+#X) at [sp-(#Z+1)*8]!, pre-indexed offset >= -256
save_lrpair 1101011x'xxzzzzzz: [sp+#Z*8]에 쌍 <x(19+2*#X),lr> 저장, 오프셋 <= 504
save_fregp 1101100x'xxzzzzzz: save pair d(8+#X) at [sp+#Z*8], offset <= 504
save_fregp_x 1101101x'xxzzzzzz: 쌍 d(8+#X) 저장 시 [sp-(#Z+1)*8]!, 미리 인덱싱된 오프 >셋 = -512
save_freg 1101110x'xxzzzzzz: save reg d(8+#X) at [sp+#Z*8], offset <= 504
save_freg_x 11011110'xxxzzzzz: save reg d(8+#X) at [sp-(#Z+1)*8]!, pre-indexed offset >= -256
alloc_l 11100000'xxxxxxxx'xxxxxxxx'xxxxxxxx: 크기 < 가 256M인 큰 스택 할당(2^24 * 16)
set_fp 11100001: 다음으로 설정 x29mov x29,sp
add_fp 11100010'xxxxxxxx: 설정 x29add x29,sp,#x*8
nop 11100011: 해제 작업 필요하지 않음
end 11100100: 해제 코드의 끝 에필로그를 의미 ret 합니다.
end_c 11100101: 현재 연결된 범위에서 해제 코드의 끝
save_next 11100110: 다음 비휘발성 Int 또는 FP 레지스터 쌍 저장
11100111: 예약됨
11101xxx: asm 루틴에 대해서만 생성된 아래 사용자 지정 사례에 대해 예약됨
11101000: 사용자 지정 스택 MSFT_OP_TRAP_FRAME
11101001: 사용자 지정 스택 MSFT_OP_MACHINE_FRAME
11101010: 사용자 지정 스택 MSFT_OP_CONTEXT
11101011: 사용자 지정 스택 MSFT_OP_EC_CONTEXT
11101100: 사용자 지정 스택 MSFT_OP_CLEAR_UNWOUND_TO_CALL
11101101: 예약됨
11101110: 예약됨
11101111: 예약됨
11110xxx: 예약됨
11111000'yyyyyyy: reserved
11111001'yyy'yyyyyyy: reserved
11111010'yyy'yyy'yyyyyyy : reserved
11111011'yy'yyyy'yy'yyyyy : reserved
pac_sign_lr 11111100: 다음을 사용하여 반환 주소에 로그인합니다.lrpacibsp
11111101: 예약됨
11111110: 예약됨
11111111: 예약됨

여러 바이트를 포함하는 큰 값의 명령에서는 가장 중요한 비트가 먼저 저장됩니다. 이 디자인을 사용하면 코드의 첫 번째 바이트만 조회하여 해제 코드의 총 크기(바이트)를 찾을 수 있습니다. 각 해제 코드는 프롤로그 또는 에필로그의 명령에 정확히 매핑되므로 프롤로그 또는 에필로그의 크기를 계산할 수 있습니다. 시퀀스에서 끝까지 이동하고 조회 테이블 또는 유사한 디바이스를 사용하여 해당 opcode의 길이를 확인합니다.

사후 인덱싱된 오프셋 주소 지정은 프롤로그에서 허용되지 않습니다. 모든 오프셋 범위(#Z)는 모든 저장 영역에 대해 248이 충분한 주소 save_r19r20_x지정의strstp/인코딩과 일치합니다(Int 레지스터 10개 + FP 레지스터 8개 + 입력 레지스터 8개).

save_next는 Int 또는 FP 휘발성 레지스터 쌍에 대한 저장, save_regp, save_regp_x, save_fregp, save_fregp_x, save_r19r20_x 또는 다른 save_next 다음에 나와야 합니다. 다음 레지스터 쌍을 “증가” 순서대로 다음 16바이트 슬롯에 저장합니다. save_next는 마지막 Int 레지스터 쌍을 나타내는 save-next 뒤에 오는 첫 번째 FP 레지스터 쌍을 참조합니다.

일반 반환 및 점프 명령의 크기는 동일하므로 비상 호출 시나리오에서는 분리된 end 해제 코드가 필요하지 않습니다.

end_c는 최적화를 위해 인접하지 않은 함수 조각을 처리하도록 설계되었습니다. end_c 현재 범위에서 해제 코드의 끝을 나타내는 다음, 실제 end코드로 끝나는 다른 일련의 해제 코드가 와야 합니다. 부모 영역의 프롤로그 작업("팬텀" 프롤로그) 사이의 end_c 해제 코드입니다 end . 자세한 내용과 예제는 아래 섹션에 설명되어 있습니다.

압축된 해제 데이터

프롤로그 및 에필로그가 아래에서 설명하는 정규형을 따르는 함수의 경우에는 압축된 해제 데이터를 사용할 수 있습니다. 레코드의 필요성을 .xdata 완전히 없애고 해제 데이터 제공 비용을 크게 줄입니다. 정식 프롤로그 및 에필로그는 간단한 함수의 일반적인 요구 사항을 충족하도록 설계되었습니다. 예외 처리기가 필요하지 않고 표준 순서로 설정 및 해체 작업을 수행하는 함수입니다.

압축 해제 데이터가 포함된 레코드의 .pdata 형식은 다음과 같습니다.

.pdata record with packed unwind data.

필드는 다음과 같습니다.

  • 함수 시작 RVA는 함수 시작 부분의 32비트 RVA입니다.
  • 플래그 는 위에서 설명한 대로 2비트 필드이며 다음과 같은 의미가 있습니다.
    • 00 = 압축 해제 데이터가 사용되지 않음; re기본 비트는 레코드를 .xdata 가리킵니다.
    • 01 = 범위 시작과 끝에서 단일 프롤로그 및 에필로그에 사용되는 압축 해제 데이터
    • 10 = 프롤로그 및 에필로그 없이 코드에 사용되는 압축된 해제 데이터이며, 구분된 함수 세그먼트를 설명하는 데 유용함
    • 11 = 예약됨
  • 함수 길이는 전체 함수의 길이(바이트 단위)를 4로 나눈 결과를 제공하는 11비트 필드입니다. 함수가 8k보다 큰 경우 전체 .xdata 레코드를 대신 사용해야 합니다.
  • 프레임 크기는 이 함수에 대해 할당된 스택 바이트 수를 16으로 나눈 값을 나타내는 9비트 필드입니다. (8k-16) 바이트보다 큰 스택을 할당하는 함수는 전체 .xdata 레코드를 사용해야 합니다. 여기에는 지역 변수 영역, 나가는 매개 변수 영역, 호출 수신자가 저장한 Int 및 FP 영역 및 홈 매개 변수 영역이 포함됩니다. 동적 할당 영역을 제외합니다.
  • CR 은 함수에 프레임 체인을 설정하고 링크를 반환하는 추가 지침이 포함되어 있는지 여부를 나타내는 2비트 플래그입니다.
    • 00 = 연결되지 않은 함수, <x29,lr> 쌍이 스택에 저장되지 않음
    • 01 = 연결되지 않은 함수, <lr>은 스택에 저장됩니다.
    • 10 = 서명된 반환 주소가 있는 pacibsp 연결된 함수
    • 11 = 연결된 함수, 저장/로드 쌍 명령은 프롤로그/에필로그 <x29,lr>에 사용됩니다.
  • H는 함수가 정수 매개 변수 레지스터(x0-x7)를 함수 시작 부분에 저장하여 호밍할지를 나타내는 1비트 플래그입니다. (0 = 홈 레지스터를 하지 않습니다., 1 = 주택 등록).
  • RegI은 정식 스택 위치에 저장된 비휘발성 INT 레지스터(x19-x28)의 수를 나타내는 4비트 필드입니다.
  • RegF는 정식 스택 위치에 저장된 비휘발성 FP 레지스터(d8-d15)의 수를 나타내는 3비트 필드입니다. (RegF=0: FP 레지스터가 저장되지 않음; RegF>0: RegF+1 FP 레지스터가 저장됨). 하나의 FP 레지스터만 저장하는 함수에는 압축 해제 데이터를 사용할 수 없습니다.

위의 섹션에서 범주 1, 2(나가는 매개 변수 영역 없음), 3 및 4에 속하는 정식 프롤로그는 압축 해제 형식으로 나타낼 수 있습니다. 정식 함수에 대한 에필로그는 H가 효과가 없고, set_fp 명령이 생략되며, 단계의 순서와 각 단계의 명령이 에필로그에서 반전되는 것을 제외하면 비슷한 형태를 따릅니다. 압축 알고리즘 .xdata 은 다음 표에 자세히 설명된 다음 단계를 따릅니다.

0단계: 각 영역의 크기를 미리 계산합니다.

1단계: 반환 주소에 서명합니다.

2단계: Int 호출 수신자 저장 레지스터를 저장합니다.

3단계: 이 단계는 초기 섹션의 형식 4에만 해당됩니다. lr 는 Int 영역의 끝에 저장됩니다.

4단계: FP 호출 수신자 저장 레지스터를 저장합니다.

5단계: 홈 매개 변수 영역에 입력 인수를 저장합니다.

6단계: 로컬 영역, <x29,lr> 쌍 및 나가는 매개 변수 영역을 포함하여 다시 기본 스택을 할당합니다. 6a는 정식 형식 1에 해당합니다. 6b 및 6c는 정식 유형 2용입니다. 6d와 6e는 형식 3과 형식 4 모두에 대한 것입니다.

단계 # 플래그 값 명령 # Opcode 해제 코드
0 #intsz = RegI * 8;
if (CR==01) #intsz += 8; // lr
#fpsz = RegF * 8;
if(RegF) #fpsz += 8;
#savsz=((#intsz+#fpsz+8*8*H)+0xf)&~0xf)
#locsz = #famsz - #savsz
1 CR == 10 1 pacibsp pac_sign_lr
2 0 <RegI<= 10 RegI / 2 +
RegI % 2
stp x19,x20,[sp,#savsz]!
stp x21,x22,[sp,#16]
...
save_regp_x
save_regp
...
3 CR == 01* 1 str lr,[sp,#(intsz-8)]* save_reg
4 0 <RegF<= 7 (RegF + 1) / 2 +
(RegF + 1) % 2)
stp d8,d9,[sp,#intsz]**
stp d10,d11,[sp,#(intsz+16)]
...
str d(8+RegF),[sp,#(intsz+fpsz-8)]
save_fregp
...
save_freg
5 H == 1 4 stp x0,x1,[sp,#(intsz+fpsz)]
stp x2,x3,[sp,#(intsz+fpsz+16)]
stp x4,x5,[sp,#(intsz+fpsz+32)]
stp x6,x7,[sp,#(intsz+fpsz+48)]
nop
nop
nop
nop
6a (CR == 10 || CR == 11) &&
#locsz<= 512
2 stp x29,lr,[sp,#-locsz]!
mov x29,sp***
save_fplr_x
set_fp
6b (CR == 10 || CR == 11) &&
512 <#locsz<= 4080
3 sub sp,sp,#locsz
stp x29,lr,[sp,0]
add x29,sp,0
alloc_m
save_fplr
set_fp
6c (CR == 10 || CR == 11) &&
#locsz> 4080
4 sub sp,sp,4080
sub sp,sp,#(locsz-4080)
stp x29,lr,[sp,0]
add x29,sp,0
alloc_m
alloc_s/alloc_m
save_fplr
set_fp
6d (CR == 00 || CR == 01) &&
#locsz<= 4080
1 sub sp,sp,#locsz alloc_s/alloc_m
6e (CR == 00 || CR == 01) &&
#locsz> 4080
2 sub sp,sp,4080
sub sp,sp,#(locsz-4080)
alloc_m
alloc_s/alloc_m

* CR == 01 및 RegI가 홀수이면 2단계와 1단계의 마지막 save_rep 항목이 하나로 save_regp병합됩니다.

** RegI CR == 0 및 RegF != 0이면 부동 소수점의 첫 번째 stp 값이 미리 증가합니다. ==

에필로그에 mov x29,sp 해당하는 명령이 없습니다. 함수에서 복원 spx29해야 하는 경우 압축 해제 데이터를 사용할 수 없습니다.

부분 프롤로그 및 에필로그 해제

가장 일반적인 해제 상황에서 예외 또는 호출은 프롤로그 및 모든 에필로그에서 멀리 떨어진 함수 본문에서 발생합니다. 이러한 상황에서 해제는 간단합니다. 해제는 해제 배열의 코드를 실행하기만 하면 됩니다. 인덱스 0에서 시작하여 opcode가 end 검색될 때까지 계속됩니다.

프롤로그 또는 에필로그를 실행하는 동안 예외 또는 인터럽트가 발생하는 경우 올바르게 해제하기가 더 어렵습니다. 관련 상황에서는 스택 프레임이 일부만 생성됩니다. 문제는 올바르게 실행을 취소하기 위해 정확히 어떤 작업이 수행되었는지 판단하는 것입니다.

예를 들어 다음 프롤로그 및 에필로그 시퀀스를 사용합니다.

0000:    stp    x29,lr,[sp,#-256]!          // save_fplr_x  256 (pre-indexed store)
0004:    stp    d8,d9,[sp,#224]             // save_fregp 0, 224
0008:    stp    x19,x20,[sp,#240]           // save_regp 0, 240
000c:    mov    x29,sp                      // set_fp
         ...
0100:    mov    sp,x29                      // set_fp
0104:    ldp    x19,x20,[sp,#240]           // save_regp 0, 240
0108:    ldp    d8,d9,[sp,224]              // save_fregp 0, 224
010c:    ldp    x29,lr,[sp],#256            // save_fplr_x  256 (post-indexed load)
0110:    ret    lr                          // end

각 opcode 옆에는 이 작업을 설명하는 적절한 해제 코드가 있습니다. 프롤로그에 대한 일련의 해제 코드가 에필로그에 대한 해제 코드의 정확한 미러 이미지임을 알 수 있습니다(에필로그의 최종 명령은 계산에 포함되지 않음). 일반적인 상황입니다. 프롤로그의 해제 코드가 프롤로그의 실행 순서에서 역순으로 저장된다고 항상 가정하는 이유입니다.

따라서 프롤로그 및 에필로그 모두에 대해 공통의 해제 코드 집합이 남게 됩니다.

set_fp, save_regp 0,240, save_fregp,0,224, save_fplr_x_256end

에필로그의 경우는 일반적인 순서로 되어 있기 때문에 간단합니다. 에필로그 내의 오프셋 0부터(함수의 오프셋 0x100 시작) 아직 클린 작업이 수행되지 않았기 때문에 전체 해제 시퀀스가 실행될 것으로 예상됩니다. 하나의 명령이 있는 경우(에필로그의 오프셋 2에서) 첫 번째 해제 코드를 건너뛰고 성공적으로 해제할 수 있습니다. 관련 상황을 일반화하고 opcode와 해제 코드 간의 1:1 매핑을 가정할 수 있습니다. 그런 다음 에필로그에서 명령 n의 해제를 시작하려면 첫 번째 n 해제 코드를 건너뛰고 거기서부터 실행을 시작해야 합니다.

역방향인 것을 제외하면 프롤로그에 대해서도 유사한 논리가 작동합니다. 프롤로그에서 오프셋 0부터 해제를 시작하는 경우 아무것도 실행하지 않으려고 합니다. 하나의 명령인 오프셋 2부터 해제하는 경우 끝부터 해제 시퀀스 하나의 해제 코드를 실행하려고 합니다. (코드는 역순으로 저장됩니다.) 여기서도 일반화할 수 있습니다. 프롤로그의 명령 n에서 해제를 시작하면 코드 목록의 끝에서 n개의 해제 코드를 실행해야 합니다.

프롤로그 및 에필로그 코드가 항상 정확히 일치하지는 않으므로 해제 배열에 여러 코드 시퀀스가 포함되어야 할 수 있습니다. 코드 처리 시작 위치의 오프셋을 결정하려면 다음 논리를 사용합니다.

  1. 함수 본문 내에서 해제하는 경우 인덱스 0에서 해제 코드를 실행하기 시작하고 opcode에 end 도달할 때까지 계속합니다.

  2. 에필로그 내에서 해제하는 경우 에필로그 범위에서 제공되는 에필로그별 시작 인덱스를 시작점으로 사용합니다. 에필로그 시작 위치에서 대상 PC까지의 거리(바이트 수)를 계산합니다. 그런 다음 해제 코드에서 정방향으로 진행하고 이미 실행된 모든 명령의 수만큼 해제 코드를 건너뜁니다. 그런 다음 해당 지점에서 시작을 실행합니다.

  3. 프롤로그 내에서 해제하는 경우 인덱스 0을 시작점으로 사용합니다. 시퀀스에서 프롤로그 코드의 길이를 계산하고, 프롤로그 종료 위치에서 대상 PC까지의 거리(바이트 수)를 계산합니다. 그런 다음 해제 코드에서 정방향으로 진행하고, 아직 실행되지 않은 명령의 수만큼 해제 코드를 건너뜁니다. 그런 다음 해당 지점에서 시작을 실행합니다.

관련 규칙은 프롤로그의 해제 코드는 항상 배열의 첫 번째 코드여야 한다는 의미입니다. 그리고 본문 내에서 일반적인 해제 사례를 해제하는 데 사용되는 코드이기도 합니다. 에필로그별 코드 시퀀스는 바로 뒤에 와야 합니다.

함수 조각

코드 최적화 및 기타 이유로 함수를 분리된 조각(지역이라고도 함)으로 분할하는 것이 좋습니다. 분할할 때 각 결과 함수 조각에는 고유한 별도의 .pdata (그리고 가능한) 레코드가 .xdata필요합니다.

자체 프롤로그를 포함하는 분리된 각 보조 조각에 대해 해당 프롤로그에서 스택 조정이 수행되지 않아야 합니다. 보조 지역에 필요한 모든 스택 공간은 부모 지역(또는 호스트 영역)에 의해 미리 할당되어야 합니다. 이 사전 할당은 함수의 원래 프롤로그에서 스택 포인터 조작을 엄격하게 유지합니다.

함수 조각의 일반적인 경우는 컴파일러가 호스트 함수에서 코드 영역을 이동할 수 있는 "코드 분리"입니다. 코드 분리로 인해 발생할 수 있는 세 가지 특이한 경우가 있습니다.

예시

  • (지역 1: 시작)

        stp     x29,lr,[sp,#-256]!      // save_fplr_x  256 (pre-indexed store)
        stp     x19,x20,[sp,#240]       // save_regp 0, 240
        mov     x29,sp                  // set_fp
        ...
    
  • (지역 1: 끝)

  • (지역 3: 시작)

        ...
    
  • (지역 3: 끝)

  • (지역 2: 시작)

        ...
        mov     sp,x29                  // set_fp
        ldp     x19,x20,[sp,#240]       // save_regp 0, 240
        ldp     x29,lr,[sp],#256        // save_fplr_x  256 (post-indexed load)
        ret     lr                      // end
    
  • (지역 2: 끝)

  1. 프롤로그만(지역 1: 모든 에필로그는 분리된 지역에 있음):

    프롤로그만 설명해야 합니다. 이 프롤로그는 압축 .pdata 형식으로 나타낼 수 없습니다. 전체 .xdata 경우 에필로그 개수 = 0을 설정하여 나타낼 수 있습니다. 위의 예제에서 지역 1을 참조하세요.

    해제 코드: set_fp, save_regp 0,240, save_fplr_x_256, end.

  2. 에필로그만(지역 2: 프롤로그는 호스트 영역에 있음.)

    시간 컨트롤이 이 지역으로 이동하면 모든 프롤로그 코드가 실행된 것으로 가정합니다. 부분 해제는 일반 함수와 동일한 방식으로 에필로그에서 발생할 수 있습니다. 이 유형의 지역은 압축 .pdata으로 나타낼 수 없습니다. 전체 .xdata 레코드에서는 "팬텀" 프롤로그로 인코딩할 수 있으며 코드 쌍 및 end 해제 코드 쌍으로 대괄호로 end_c 묶을 수 있습니다. 선행 end_c는 프롤로그의 크기가 0임을 나타냅니다. 단일 에필로그 지점의 에필로그 시작 인덱스는 set_fp를 가리킵니다.

    영역 2에 대한 해제 코드: end_c, set_fp, save_regp 0,240, save_fplr_x_256, end.

  3. 프롤로그 또는 에필로그 없음(지역 3: 프롤로그 및 모든 에필로그는 다른 조각에 있음.):

    플래그 = 10 설정을 통해 컴팩트 .pdata 형식을 적용할 수 있습니다. 전체 .xdata 레코드를 사용하면 에필로그 개수 = 1입니다. 해제 코드는 위의 지역 2에 대한 코드와 동일하지만 에필로그 시작 인덱스는 end_c도 가리킵니다. 부분 해제는 이 코드 영역에서 발생하지 않습니다.

함수 조각의 또 다른 복잡한 사례는 "축소 래핑"입니다. 컴파일러는 함수 항목 프롤로그 외부에 있을 때까지 일부 호출 수신자 저장 레지스터 저장을 지연하도록 선택할 수 있습니다.

  • (지역 1: 시작)

        stp     x29,lr,[sp,#-256]!      // save_fplr_x  256 (pre-indexed store)
        stp     x19,x20,[sp,#240]       // save_regp 0, 240
        mov     x29,sp                  // set_fp
        ...
    
  • (지역 2: 시작)

        stp     x21,x22,[sp,#224]       // save_regp 2, 224
        ...
        ldp     x21,x22,[sp,#224]       // save_regp 2, 224
    
  • (지역 2: 끝)

        ...
        mov     sp,x29                  // set_fp
        ldp     x19,x20,[sp,#240]       // save_regp 0, 240
        ldp     x29,lr,[sp],#256        // save_fplr_x  256 (post-indexed load)
        ret     lr                      // end
    
  • (지역 1: 끝)

지역 1의 프롤로그에서 스택 공간은 미리 할당됩니다. 해당 호스트 함수 밖으로 이동하더라도 지역 2는 동일한 해제 코드를 사용할 수 있습니다.

지역 1: set_fp, save_regp 0,240, save_fplr_x_256end. 에필로그 시작 인덱스는 평소와 같이 가리킵니 set_fp 다.

지역 2: save_regp 2, 224, end_c, set_fp, save_regp 0,240, save_fplr_x_256, end. 에필로그 시작 인덱스는 첫 번째 해제 코드 save_regp 2, 224를 가리킵니다.

큰 함수

조각은 헤더의 비트 필드에 .xdata 의해 적용되는 1M 제한보다 큰 함수를 설명하는 데 사용할 수 있습니다. 이와 같이 비정상적으로 큰 함수를 설명하려면 1M보다 작은 조각으로 나뉘어야 합니다. 각 조각은 에필로그를 여러 부분으로 분할하지 않도록 조정해야 합니다.

함수의 첫 조각에만 프롤로그가 포함되며 나머지 모든 조각은 프롤로그가 없는 것으로 표시됩니다. 나타난 에필로그 수에 따라 각 조각은 에필로그를 포함하지 않을 수도 있고 포함할 수도 있습니다. 조각의 각 에필로그 범위는 함수 시작 위치가 아닌 조각 시작 위치를 기준으로 시작 오프셋을 지정합니다.

프롤로그가 없고 에필로그가 없는 조각에는 함수 본문 내에서 해제하는 방법을 설명하기 위해 자체 .pdata (및 가능한 .xdata) 레코드가 필요합니다.

예제

예제 1: 프레임 체인, 컴팩트 폼

|Foo|     PROC
|$LN19|
    str     x19,[sp,#-0x10]!        // save_reg_x
    sub     sp,sp,#0x810            // alloc_m
    stp     fp,lr,[sp]              // save_fplr
    mov     fp,sp                   // set_fp
                                    // end of prolog
    ...

|$pdata$Foo|
    DCD     imagerel     |$LN19|
    DCD     0x416101ed
    ;Flags[SingleProEpi] functionLength[492] RegF[0] RegI[1] H[0] frameChainReturn[Chained] frameSize[2080]

예제 2: 프레임 연결, 미러 프롤로그 및 에필로그가 있는 전체 형식

|Bar|     PROC
|$LN19|
    stp     x19,x20,[sp,#-0x10]!    // save_regp_x
    stp     fp,lr,[sp,#-0x90]!      // save_fplr_x
    mov     fp,sp                   // set_fp
                                    // end of prolog
    ...
                                    // begin of epilog, a mirror sequence of Prolog
    mov     sp,fp
    ldp     fp,lr,[sp],#0x90
    ldp     x19,x20,[sp],#0x10
    ret     lr

|$pdata$Bar|
    DCD     imagerel     |$LN19|
    DCD     imagerel     |$unwind$cse2|
|$unwind$Bar|
    DCD     0x1040003d
    DCD     0x1000038
    DCD     0xe42291e1
    DCD     0xe42291e1
    ;Code Words[2], Epilog Count[1], E[0], X[0], Function Length[6660]
    ;Epilog Start Index[0], Epilog Start Offset[56]
    ;set_fp
    ;save_fplr_x
    ;save_r19r20_x
    ;end

에필로그 시작 인덱스 [0]은 프롤로그 해제 코드의 동일한 시퀀스를 가리킵니다.

예제 3: Variadic unchained 함수

|Delegate| PROC
|$LN4|
    sub     sp,sp,#0x50
    stp     x19,lr,[sp]
    stp     x0,x1,[sp,#0x10]        // save incoming register to home area
    stp     x2,x3,[sp,#0x20]        // ...
    stp     x4,x5,[sp,#0x30]
    stp     x6,x7,[sp,#0x40]        // end of prolog
    ...
    ldp     x19,lr,[sp]             // beginning of epilog
    add     sp,sp,#0x50
    ret     lr

    AREA    |.pdata|, PDATA
|$pdata$Delegate|
    DCD     imagerel |$LN4|
    DCD     imagerel |$unwind$Delegate|

    AREA    |.xdata|, DATA
|$unwind$Delegate|
    DCD     0x18400012
    DCD     0x200000f
    DCD     0xe3e3e3e3
    DCD     0xe40500d6
    DCD     0xe40500d6
    ;Code Words[3], Epilog Count[1], E[0], X[0], Function Length[18]
    ;Epilog Start Index[4], Epilog Start Offset[15]
    ;nop        // nop for saving in home area
    ;nop        // ditto
    ;nop        // ditto
    ;nop        // ditto
    ;save_lrpair
    ;alloc_s
    ;end

에필로그 시작 인덱스 [4]는 프롤로그 해제 코드의 중간을 가리킵니다(해제 배열을 부분적으로 재사용).

참고 항목

ARM64 ABI 규칙 개요
ARM 예외 처리