Arm64EC ABI ve derleme kodunu anlama

Arm64EC ("Öykünme Uyumlu"), Arm üzerinde Windows 11 için uygulama oluşturmaya yönelik yeni bir uygulama ikili arabirimidir (ABI). Arm64EC'e genel bakış ve Win32 uygulamalarını Arm64EC uyumlu olarak oluşturmaya başlama konusunda bilgi almak için Arm cihazlarda Windows 11 için uygulama derlemek üzere Arm64EC kullanmasayfasına bakın.

Bu makale, bir uygulama geliştiricisinin Arm64EC için derlenmiş kod yazması ve bu kodun hatalarını ayıklaması için yeterli bilgiyi sağlayarak Arm64EC ABI'nin ayrıntılı bir görünümünü sunar. Bu bilgi, Arm64EC ABI'yi hedefleyen alt düzey/assembler hata ayıklama ve assembly kodu yazmayı da kapsar.

Arm64EC Tasarımı

Arm64EC, öykünme altında çalışan x64 koduyla saydam ve doğrudan birlikte çalışabilirlik sağlarken yerel düzeyde işlevsellik ve performans sunar.

Arm64EC çoğunlukla Klasik Arm64 ABI'ye eklenir. Klasik ABI çok az değişti, ancak Arm64EC ABI x64 birlikte çalışabilirliği sağlamak için bölümler ekledi.

Bu belgede, özgün, standart Arm64 ABI "Klasik ABI" olarak adlandırılır. Bu terim, "Yerel" gibi aşırı yüklenmiş terimlerin belirsizliğini önler. Arm64EC, her biti özgün ABI kadar yereldir.

Arm64EC ve Arm64 Klasik ABI karşılaştırması

Aşağıdaki liste Arm64EC'in Arm64 Klasik ABI'den nerelerden farklı olduğunu gösterir.

Bu farklılıklar, ABI'nin tamamının ne kadar tanımladığı açısından bakıldığında küçük değişikliklerdir.

Yazmaç eşleme ve kısıtlanmış yazmaçlar

x64 koduyla tür düzeyinde birlikte çalışabilirliği etkinleştirmek için Arm64EC kodu, x64 koduyla aynı ön işlemci mimarisi tanımlarıyla derler.

Başka bir deyişle _M_AMD64 ve _AMD64_ tanımlanır. Bu kuraldan etkilenen türlerden biri yapısıdır CONTEXT . Yapısı, CONTEXT belirli bir noktada CPU'nun durumunu tanımlar. Exception Handling ve GetThreadContext API'leri gibi şeyler için kullanılır. Mevcut x64 kodu, CPU bağlamının, CONTEXT yapısı veya başka bir deyişle x64 derlemesi sırasında tanımlandığı haliyle bir x64 CONTEXT yapısı olarak temsil edilmesini bekler.

x64 kodu ve Arm64EC kodunu yürütürken CPU bağlamını temsil etmek için bu yapıyı kullanmanız gerekir. Mevcut kod, CPU yazmaç kümesinin işlevden işleve değişmesi gibi yeni bir kavramı anlamaz. Arm64 CONTEXT yürütme durumlarını temsil etmek için x64 yapısını kullanırsanız Arm64 yazmaçlarını x64 yazmaçlarına etkili bir şekilde eşlersiniz.

Bu eşleme, x64'e sığmayan Arm64 CONTEXTyazmaçlarını kullanamamanızı da sağlar. Bir operasyon CONTEXT kullandığında değerler herhangi bir zamanda kaybolabilir (ve Yönetilen Dil Çalışma Zamanı'nın Garbage Collection operasyonu veya APC gibi bazı operasyonlar zaman uyumsuz ve beklenmedik olabilir).

SDK'daki Windows üst bilgileri, Arm64EC ile x64 registerleri arasındaki eşleme kurallarını ARM64EC_NT_CONTEXT yapısıyla temsil eder. Bu yapı, x64 için tanımlandığı şekliyle CONTEXT yapısının esasen bir birleşimidir ve fazladan bir Arm64 yazmaç katmanı içerir.

Örneğin, RCXX0 ile, RDXX1 ile, RSPSP ile, RIPPC ile eşleşir ve benzeri. Kayıtçılar x13, x14, x23, x24, x28 ve v16 ile v31'nın gösterimi yoktur ve bu nedenle Arm64EC'de kullanılamazlar.

Bu kayıt kullanım kısıtlaması, Arm64 Klasik ile EC ABI'leri arasındaki ilk farktır.

Arama denetleyicileri

Control Flow Guard (CFG) Windows 8.1'de kullanıma sunulduğundan beri arama denetleyicisi Windows'un bir parçası olmuştur. Çağrı denetleyicileri, işlev işaretçileri için adres dezenfektanlarıdır; bu adı almadan önce de böyle adlandırılırlardı. seçeneğiyle /guard:cfkod derlediğinizde, derleyici her dolaylı çağrı veya atlamadan hemen önce denetleyicisi işlevine ek bir çağrı oluşturur. Windows, denetleyici işlevinin kendisini sağlar. CFG, bilinen güvenilir çağrı hedeflerine karşı bir geçerlilik denetimi gerçekleştirir. ile /guard:cf derlenen ikili dosyalar da bu bilgileri içerir.

Bu örnekte Klasik Arm64'te bir arama denetleyicisi kullanımı gösterilmektedir:

mov     x15, <target>
adrp    x16, __guard_check_icall_fptr
ldr     x16, [x16, __guard_check_icall_fptr]
blr     x16                                     ; check target function
blr     x15                                     ; call function

CFG örneğinde, çağrı denetleyicisi yalnızca hedef geçerliyse döndürür veya geçerli değilse işlemde hızlı bir şekilde başarısız olur. Arama denetleyicileri özel çağrı kurallarına sahiptir. İşlev işaretçisini normal çağırma kuralı tarafından kullanılmayan bir yazmaçta alır ve tüm normal çağrı kuralı yazmaçlarını korurlar. Bu şekilde, çevrelerinde register taşması olmaz.

Arama denetleyicisi diğer tüm Windows ABI'lerinde isteğe bağlıdır, ancak Arm64EC'de zorunludur. Arm64EC'de, çağrı denetleyicisi çağrılan işlevin mimarisini doğrulama görevini biriktirir. Çağrının başka bir EC ("Öykünme Uyumlu") işlevi mi yoksa öykünme altında yürütülmesi gereken bir x64 işlevi mi olduğunu doğrular. Çoğu durumda, bu yalnızca çalışma zamanında doğrulanabilir.

Arm64EC çağrı denetleyicileri, mevcut Arm64 denetleyicileri üzerine inşa edilir, ancak biraz farklı bir özel çağrı kuralı kullanır. Ekstra bir parametre alırlar ve hedef adresi içeren yazmaçta değişiklik yapabilirler. Örneğin, hedef x64 koduysa, denetim önce öykünme yapı sistemine aktarılmalıdır.

Arm64EC'de aynı çağrı denetleyicisi kullanımı şu hale gelir:

mov     x11, <target>
adrp    x9, __os_arm64x_check_icall_cfg
ldr     x9, [x9, __os_arm64x_check_icall_cfg] 
adrp    x10, <name of the exit thunk>
add     x10, x10, <name of the exit thunk>
blr     x9                                      ; check target function
blr     x11                                     ; call function

Klasik Arm64'ten küçük farklar şunlardır:

  • Arama denetleyicisinin sembol adı farklıdır.
  • Hedef adres x11 yerine x15 içinde sağlanır.
  • Hedef adres (x11) yerine [in, out]olur[in].
  • "Exit Thunk" adlı ek bir parametre, x10 aracılığıyla sağlanmaktadır.

Exit Thunk, işlev parametrelerini Arm64EC çağırma kuralından x64 çağırma kuralına dönüştüren bir funclet'tir.

Arm64EC çağrı denetleyicisi, Windows'taki diğer ABI'ler için kullanılandan farklı bir simge aracılığıyla bulunur. Klasik Arm64 ABI'de, çağrı denetleyicisinin simgesi şeklindedir __guard_check_icall_fptr. Bu simge Arm64EC'de bulunur, ancak Arm64EC kodunun kendisini değil, x64 statik olarak bağlı kodun kullanımına yöneliktir. Arm64EC kodu ya __os_arm64x_check_icall ya da __os_arm64x_check_icall_cfg kullanır.

Arm64EC'de, çağrı denetleyicileri isteğe bağlı değildir. Ancak CFG, diğer ABI'lerde olduğu gibi isteğe bağlıdır. CFG derleme zamanında devre dışı bırakılabilir veya CFG etkinleştirildiğinde bile CFG denetimi yapılmaması için geçerli bir neden olabilir (örneğin işlev işaretçisi rw bellekte hiçbir zaman bulunmayabilir). CFG denetimine sahip dolaylı bir çağrı için __os_arm64x_check_icall_cfg denetleyici kullanılmalıdır. CFG devre dışıysa veya gereksizse, __os_arm64x_check_icall bunun yerine kullanılmalıdır.

Aşağıda, bir Arm64EC ikili dosyasının kodun mimarisine bağlı olarak iki seçenek olabileceğine dikkat çekerek Klasik Arm64, x64 ve Arm64EC'de çağrı denetleyicisi kullanımının özet tablosu yer almaktadır.

İkilik Kod Korumasız dolaylı çağrı CFG korumalı dolaylı çağrı
x64 x64 arama denetleyicisi yok __guard_check_icall_fptr veya __guard_dispatch_icall_fptr
Arm64 Klasik Arm64 arama denetleyicisi yok __guard_check_icall_fptr
Arm64EC x64 arama denetleyicisi yok __guard_check_icall_fptr veya __guard_dispatch_icall_fptr
Arm64EC __os_arm64x_check_icall __os_arm64x_check_icall_cfg

ABI'den bağımsız olarak, CFG özellikli koda (CFG çağrı denetleyicisi başvurusu olan kod) sahip olmak, çalışma zamanında CFG koruması anlamına gelmez. CFG korumalı ikili dosyalar, CFG'yi desteklemeyen sistemlerde düşük düzeyde çalışabilir: çağrı kontrolcüsü derleme esnasında no-op yardımcı fonksiyonu ile başlatılır. Bir işlemde cfg yapılandırma tarafından devre dışı bırakılmış da olabilir. CFG önceki ABI'lerde devre dışı bırakıldığında (veya işletim sistemi desteği mevcut olmadığında), ikili dosya yüklendiğinde işletim sistemi çağrı denetleyicisini güncelleştirmez. Arm64EC'de CFG koruması devre dışı bırakılırsa, işletim sistemi __os_arm64x_check_icall_cfg__os_arm64x_check_icallolarak ayarlar; bu da her durumda gerekli hedef mimari kontrolünü sağlar, ancak CFG korumasını sağlamaz.

Klasik Arm64'teki CFG'de olduğu gibi, hedef işleve (x11) yapılan çağrı hemen Çağrı Denetleyicisi çağrısını izlemelidir. Arama Denetleyicisi'nin adresi, uçucu bir yazmaç içinde yerleştirilmelidir ve ne bu adres ne de hedef işlevin adresi başka bir yazmaçta kopyalanmalı veya belleğe dökülmemelidir.

Yığın Denetleyicisi

__chkstk , bir işlev yığında bir sayfadan daha büyük bir alan ayırdığı her seferde derleyici tarafından otomatik olarak kullanılır. Yığının sonunu koruyan yığın koruma sayfasının atlanmasını önlemek için, ayrılan alandaki tüm sayfaların yoklandığından emin olmak amacıyla __chkstk çağrılır.

__chkstk genellikle işlevin giriş bölümünden çağrılır. Bu nedenle ve en iyi kod oluşturma için özel bir çağırma kuralı kullanır.

Bu, x64 kodu ve Arm64EC kodunun, Giriş ve Çıkış işlevleri standart çağrı kurallarını kabul ettiği için kendi ayrı __chkstk işlevlerine ihtiyaç duyduğu anlamına gelir.

x64 ve Arm64EC aynı sembol ad alanını paylaşır, bu nedenle adlı __chkstkiki işlev olamaz. Önceden var olan x64 koduyla uyumluluğu sağlamak için ad, __chkstk x64 yığın denetleyicisiyle ilişkilendirilecektir. Bunun yerine Arm64EC kodu kullanılır __chkstk_arm64ec .

"__chkstk_arm64ec için özel çağırma protokolü, Klasik Arm64 __chkstk ile aynıdır: x15, ayırmanın boyutunu bayt cinsinden belirler ve bu boyutu 16'ya böler." Tüm geçici olmayan yazmaçların yanı sıra standart çağrı kuralına dahil olan tüm geçici yazmaçlar korunur.

Yukarıda söylenen her şey, __chkstk ve Arm64EC karşılığı olan __security_check_cookie için __security_check_cookie_arm64ec ile eşit şekilde geçerlidir.

Variadic çağırma kuralı

Arm64EC, variadic işlevler (üç nokta (. . .) parametre anahtar sözcüğü ile varargs veya işlevler olarak da bilinir) dışında Klasik Arm64 ABI çağırma kuralını izler.

Arm64EC, variadic özel durumu için x64 variadic'e çok benzeyen bir çağrı kuralı izler ve yalnızca birkaç fark vardır. Aşağıdaki listede Arm64EC variadic için ana kurallar gösterilmektedir:

  • Parametre geçirme için yalnızca ilk dört kayıt kullanılır: x0, x1, x2, x3. Kalan parametreler yığına aktarılır. Bu kural, x64 variadic çağırma kuralını tam olarak izler ve yazmaçların x0'dan x7'e kadar kullanıldığı Arm64 Klasik'ten farklıdır.
  • Yazmaç üzerinden iletilen kayan nokta ve SIMD parametreleri, SIMD yazmaçları yerine genel amaçlı bir yazmaç kullanır. Bu kural Arm64 Classic'e benzer ve FP/SIMD parametrelerinin hem genel amaçlı hem de SIMD yazmaçta geçirildiği x64'ten farklıdır. Örneğin, x64 üzerinde f1(int, …) olarak adlandırılan bir işlev f1(int, double) için, ikinci parametre hem RDX hem de XMM1 öğelerine atanır. Arm64EC'de, ikinci parametre yalnızca x1öğesine atanır.
  • Yapıları bir yazmaç aracılığıyla değere göre geçirirken, x64 boyut kuralları uygulanır: Tam olarak 1, 2, 4 ve 8 bayt boyutlarına sahip yapılar doğrudan genel amaçlı yazmaca yüklenir. Diğer boyutlara sahip yapılar yığına dökülür ve dökülen konuma bir işaretçi yazmaçta atanır. Bu kural temelde değere göre alt düzeyde başvuruya indirger. Klasik Arm64 ABI'de, 16 bayta kadar herhangi bir boyuttaki yapılar doğrudan genel amaçlı yazmaçlara atanır.
  • Yazmaç, x4 yığın (beşinci parametre) aracılığıyla geçirilen ilk parametreye bir işaretçi yükler. Bu kural, daha önce açıklanan boyut kısıtlamaları nedeniyle dökülen yapıları içermez.
  • Yazmaç x5 , yığın tarafından geçirilen tüm parametrelerin boyutunu bayt cinsinden yükler (beşinciden başlayarak tüm parametrelerin boyutu). Bu kural, daha önce açıklanan boyut kısıtlamaları nedeniyle taşan değer tarafından geçirilen yapıları içermez.

Aşağıdaki örnekte, pt_nova_function parametreleri variadic olmayan bir biçimde alır, bu nedenle Klasik Arm64 çağırma kuralını izler. Ardından pt_va_function, tam olarak aynı parametrelerle, bu kez bir variadic çağrıyla çağırılır.

struct three_char {
    char a;
    char b;
    char c;
};

void
pt_va_function (
    double f,
    ...
);

void
pt_nova_function (
    double f,
    struct three_char tc,
    __int64 ull1,
    __int64 ull2,
    __int64 ull3
)
{
    pt_va_function(f, tc, ull1, ull2, ull3);
}

pt_nova_function , Klasik Arm64 çağırma kuralı kurallarına göre atadığı beş parametreyi alır:

  • 'f' bir çifttir. O, d0 öğesine atar.
  • 'tc', boyutu 3 bayt olan bir yapıdır. O, x0 öğesine atar.
  • ull1 8 baytlık bir tamsayıdır. O, x1 öğesine atar.
  • ull2 8 baytlık bir tamsayıdır. Bu öğeye atar x2.
  • ull3 8 baytlık bir tamsayıdır. Bunu x3 öğesine atar.

pt_va_function variadic işlevi olduğundan, daha önce açıklanan Arm64EC variadic kurallarını izler:

  • 'f' bir çifttir. x0 öğesine atar.
  • 'tc', boyutu 3 bayt olan bir yapıdır. Yığının üzerine dökülür ve konumu x1 öğesine yüklenir.
  • ull1 8 baytlık bir tamsayıdır. Bu, x2 ögesine atar.
  • ull2 8 baytlık bir tamsayıdır. O, x3 öğesine atar.
  • ull3 8 baytlık bir tamsayıdır. Doğrudan yığına atanır.
  • x4 öğesinin konumunu ull3 yığında yükler.
  • x5 boyut bilgisini ull3 yükler.

Aşağıdaki örnek, daha önce özetlenen parametre ataması farklarını göstermek için pt_nova_function ile ilgili olası derleme çıktısını gösterir.

stp         fp,lr,[sp,#-0x30]!
mov         fp,sp
sub         sp,sp,#0x10

str         x3,[sp]          ; Spill 5th parameter
mov         x3,x2            ; 4th parameter to x3 (from x2)
mov         x2,x1            ; 3rd parameter to x2 (from x1)
str         w0,[sp,#0x20]    ; Spill 2nd parameter
add         x1,sp,#0x20      ; Address of 2nd parameter to x1
fmov        x0,d0            ; 1st parameter to x0 (from d0)
mov         x4,sp            ; Address of the 1st in-stack parameter to x4
mov         x5,#8            ; Size of the in-stack parameter area

bl          pt_va_function

add         sp,sp,#0x10
ldp         fp,lr,[sp],#0x30
ret

ABI eklemeleri

x64 koduyla saydam birlikte çalışabilirlik elde etmek için klasik Arm64 ABI'ye birçok ekleme yapın. Bu eklemeler Arm64EC ile x64 arasındaki çağrı kuralı farklarını işler.

Aşağıdaki liste şu eklemeleri içerir:

Giriş ve çıkış thunks

Giriş ve çıkış thunks Arm64EC çağrı kuralını (çoğunlukla klasik Arm64 ile aynıdır) x64 çağrı kuralına çevirir ve tam tersi de geçerlidir.

Yaygın bir yanlış anlama, tüm işlev imzalarına uygulanan tek bir kuralı izleyerek çağırma kurallarını dönüştürebilmenizdir. Gerçek şu ki, çağırma kurallarının parametre atama kuralları vardır. Bu kurallar parametre türüne bağlıdır ve ABI'den ABI'ye farklıdır. Bunun bir sonucu, ABI'ler arasındaki çevirinin her parametrenin türüne göre değişiklik gösteren her işlev imzasına özel olmasıdır.

Aşağıdaki işlevi göz önünde bulundurun:

int fJ(int a, int b, int c, int d);

Parametre ataması aşağıdaki gibi gerçekleşir:

  • Arm64: a -> x0, b -> x1, c -> x2, d -> x3
  • x64: a -> RCX, b -> RDX, c -> R8, d -> r9
  • Arm64 -> x64 çevirisi: x0 -> RCX, x1 -> RDX, x2 -> R8, x3 -> R9

Şimdi farklı bir işlevi göz önünde bulundurun:

int fK(int a, double b, int c, double d);

Parametre ataması aşağıdaki gibi gerçekleşir:

  • Arm64: a -> x0, b -> d0, c -> x1, d -> d1
  • x64: a -> RCX, b -> XMM1, c -> R8, d -> XMM3
  • Arm64 -> x64 çevirisi: x0 -> RCX, d0 -> XMM1, x1 -> R8, d1 -> XMM3

Bu örnekler, parametre ataması ve çevirisinin türe göre değişiklik gösterdiğini, ancak listedeki önceki parametrelerin türlerine de bağlı olduğunu gösterir. Bu ayrıntı üçüncü parametre tarafından gösterilmiştir. Her iki işlevde de parametresinin türü olur int, ancak sonuçta elde edilen çeviri farklıdır.

Bu nedenle giriş ve çıkış thunk'ları vardır ve her işlev imzası için özel olarak uyarlanmıştır.

Her iki thunk türü de işlevdir. Öykünücü, x64 işlevleri Arm64EC işlevlerine çağrıldığında giriş thunk'larını otomatik olarak çağırır (yürütme Arm64EC'ye girer ). Arama denetleyicileri, Arm64EC işlevleri x64 işlevlerini (yürütme sırasında Arm64EC'den çıkılır) çağırdığında, çıkış thunks'larını otomatik olarak çağırır.

Arm64EC kodunu derlerken, derleyici her Arm64EC işlevinin imzasına uygun bir giriş thunk'u oluşturur. Derleyici, Arm64EC işlevinin çağırdığı her işlev için bir exit thunk da oluşturur.

Aşağıdaki örneği göz önünde bulundurun:

struct SC {
    char a;
    char b;
    char c;
};

int fB(int a, double b, int i1, int i2, int i3);

int fC(int a, struct SC c, int i1, int i2, int i3);

int fA(int a, double b, struct SC c, int i1, int i2, int i3) {
    return fB(a, b, i1, i2, i3) + fC(a, c, i1, i2, i3);
}

Arm64EC'i hedefleyen yukarıdaki kodu derlerken derleyici şunları oluşturur:

  • için fAkod.
  • Giriş thunk için fA
  • fB için çıkış thunk
  • fC için çıkış thunk

Derleyici, fA giriş thunk'ını oluşturur, x64 kodundan fA çağrılması durumunda. Derleyici, fB ve fC x64 kodu olduğunda, fB ve fC için çıkış thunk'ları oluşturur.

Derleyici aynı çıkış pernk'ini birden çok kez oluşturabilir çünkü bunları işlevin kendisi yerine çağrı sitesinde oluşturur. Bu yineleme, önemli miktarda yedekli thunk ile sonuçlanabilir. Bu yinelemeyi önlemek için derleyici, yalnızca gerekli thunk'ların son ikili dosyaya dahil ettiğinden emin olmak için basit optimizasyon kuralları uygular.

Örneğin, Arm64EC işlevi A, Arm64EC işlevi B'i çağırdığında, B dışarıya aktarılmaz ve onun adresi A dışında hiçbir zaman bilinmez. Çıkış thunk'unu A ile B arasında ve B için giriş thunk'unu ortadan kaldırmak güvenlidir. Ayrıca, ayrı işlevler için oluşturulmuş olsalar bile aynı kodu içeren tüm çıkış ve giriş thunk'larını bir araya getirmek de güvenlidir.

Çıkış thunk'ları

Yukarıdaki bölümdeki fA, fB, ve fC örnek işlevlerini kullanarak derleyici, hem fB hem de fC çıkış thunks'larını aşağıdaki gibi oluşturur:

Çıkış thunk için int fB(int a, double b, int i1, int i2, int i3);

$iexit_thunk$cdecl$i8$i8di8i8i8:
    stp         fp,lr,[sp,#-0x10]!
    mov         fp,sp
    sub         sp,sp,#0x30
    adrp        x8,__os_arm64x_dispatch_call_no_redirect
    ldr         xip0,[x8]
    str         x3,[sp,#0x20]  ; Spill 5th param (i3) into the stack
    fmov        d1,d0          ; Move 2nd param (b) from d0 to XMM1 (x1)
    mov         x3,x2          ; Move 4th param (i2) from x2 to R9 (x3)
    mov         x2,x1          ; Move 3rd param (i1) from x1 to R8 (x2)
    blr         xip0           ; Call the emulator
    mov         x0,x8          ; Move return from RAX (x8) to x0
    add         sp,sp,#0x30
    ldp         fp,lr,[sp],#0x10
    ret

Çıkış aşaması int fC(int a, struct SC c, int i1, int i2, int i3);

$iexit_thunk$cdecl$i8$i8m3i8i8i8:
    stp         fp,lr,[sp,#-0x20]!
    mov         fp,sp
    sub         sp,sp,#0x30
    adrp        x8,__os_arm64x_dispatch_call_no_redirect
    ldr         xip0,[x8]
    str         w1,[sp,#0x40]       ; Spill 2nd param (c) onto the stack
    add         x1,sp,#0x40         ; Make RDX (x1) point to the spilled 2nd param
    str         x4,[sp,#0x20]       ; Spill 5th param (i3) into the stack
    blr         xip0                ; Call the emulator
    mov         x0,x8               ; Move return from RAX (x8) to x0
    add         sp,sp,#0x30
    ldp         fp,lr,[sp],#0x20
    ret

fB Bu durumda, bir double parametrenin varlığı kalan GP yazmaç atamasının yeniden dağıtılarak Arm64 ve x64'ün farklı atama kurallarının bir sonucu olmasına neden olur. Ayrıca x64'un kayıtlara yalnızca dört parametre atadığını, bu nedenle beşinci parametrenin yığına taşması gerektiğini de görebilirsiniz.

fC Bu durumda, ikinci parametre 3 bayt uzunluğunda bir yapıdır. Arm64, herhangi bir boyut yapısının doğrudan bir kayıt defterine atanmasını sağlar. x64 yalnızca 1, 2, 4 ve 8 boyutlarına izin verir. Bu çıkış işlemi, struct bileşenini yazmaçtan yığına aktarmalı ve bunun yerine yazmacın içine bir işaretçi atamalıdır. Bu yaklaşım yine de bir yazmaç (işaretçiyi taşımak için) tüketir, bu nedenle kalan yazmaçların atamalarını değiştirmez: üçüncü ve dördüncü parametreler için kayıt yeniden eşlemesi gerçekleşmez. Tıpkı fB durumunda olduğu gibi, beşinci parametre de yığına aktarılmalıdır.

Çıkış Thunks için ek dikkat edilmesi gerekenler:

  • Derleyici, bunlara çeviri yaptıkları işlevin adıyla değil, adresledikleri imza ile isim verir. Bu adlandırma kuralı, yedekliliklerin bulunmasını kolaylaştırır.
  • Çağrı denetleyicisi, kayıt defterini x9 hedef (x64) işlevinin adresini taşıyacak şekilde ayarlar. Exit Thunk, öykünücüyü herhangi bir değişiklik yapmadan x9 geçirerek çağırır.

Parametreleri yeniden düzenledikten sonra Exit Thunk aracılığıyla öykünücüye __os_arm64x_dispatch_call_no_redirect çağırır.

Bu noktada, çağrı denetleyicisinin ve özel ABI'sinin işlevini gözden geçirmeye değer. dolaylı çağrı fB şöyle görünür:

mov     x11, <target>
adrp    x9, __os_arm64x_check_icall_cfg
ldr     x9, [x9, __os_arm64x_check_icall_cfg] 
adrp    x10, $iexit_thunk$cdecl$i8$i8di8i8i8    ; fB function's exit thunk
add     x10, x10, $iexit_thunk$cdecl$i8$i8di8i8i8
blr     x9                                      ; check target function
blr     x11                                     ; call function

Arama denetleyicisini çağırırken:

  • x11 çağrılacak hedef işlevin adresini sağlar (fB bu durumda). Bu noktada, çağrı denetleyicisi hedef işlevin Arm64EC mi yoksa x64 mi olduğunu bilmiyor olabilir.
  • x10 çağrılan işlevin imzası ile eşleşen bir Exit Thunk sağlar (fB bu durumda).

Çağrı denetleyicisinin döndürdüğü veriler, hedef işlevin Arm64EC mi yoksa x64 mi olduğuna bağlıdır.

Hedef Arm64EC ise:

  • x11 çağrılacak Arm64EC kodunun adresini döndürür. Bu değer, içinde sağlanan değerle aynı olabilir.

Hedef x64 koduysa:

  • x11 Çıkış Thunk'un adresini döndürür. Bu adres, x10 içinde sağlanan girişten kopyalandı.
  • x10 Çıkış Thunk'un adresini, girdi değişmeden olarak döndürür.
  • x9 hedef x64 işlevini döndürür. Bu değer, x11 aracılığıyla sağlanan değerle aynı olabilir.

Çağrı denetleyicileri çağrı kuralları parametre yazmaçlarını daima rahatsız edilmeden bırakır. Arama kodu, çağrı denetleyicisinden hemen sonra blr x11 (veya kuyruk çağrısı durumunda br x11) ile izlenmelidir. Çağrı denetleyicisi, bu yazmaçları her zaman standart geçici olmayan yazmaçların üzerinde ve ötesinde korur: x0-x8, x15(chkstk) ve q0-q7.

Giriş Thunks

Giriş Thunk'ları, x64'ten Arm64 çağrı kurallarına gerekli dönüştürmeleri halleder. Bu dönüşüm temelde Exit Thunks'un tersidir, ancak dikkate alınması gereken birkaç yönü daha içerir.

derlemenin önceki örneğini fAgöz önünde bulundurun. fA'i x64 kodunun çağırabilmesi için bir Giriş Thunk oluşturulur.

int fA(int a, double b, struct SC c, int i1, int i2, int i3) için Giriş Thunk

$ientry_thunk$cdecl$i8$i8dm3i8i8i8:
    stp         q6,q7,[sp,#-0xA0]!  ; Spill full non-volatile XMM registers
    stp         q8,q9,[sp,#0x20]
    stp         q10,q11,[sp,#0x40]
    stp         q12,q13,[sp,#0x60]
    stp         q14,q15,[sp,#0x80]
    stp         fp,lr,[sp,#-0x10]!
    mov         fp,sp
    ldrh        w1,[x2]             ; Load 3rd param (c) bits [15..0] directly into x1
    ldrb        w8,[x2,#2]          ; Load 3rd param (c) bits [16..23] into temp w8
    bfi         w1,w8,#0x10,#8      ; Merge 3rd param (c) bits [16..23] into x1
    mov         x2,x3               ; Move the 4th param (i1) from R9 (x3) to x2
    fmov        d0,d1               ; Move the 2nd param (b) from XMM1 (d1) to d0
    ldp         x3,x4,[x4,#0x20]    ; Load the 5th (i2) and 6th (i3) params
                                    ; from the stack into x3 and x4 (using x4)
    blr         x9                  ; Call the function (fA)
    mov         x8,x0               ; Move the return from x0 to x8 (RAX)
    ldp         fp,lr,[sp],#0x10
    ldp         q14,q15,[sp,#0x80]  ; Restore full non-volatile XMM registers
    ldp         q12,q13,[sp,#0x60]
    ldp         q10,q11,[sp,#0x40]
    ldp         q8,q9,[sp,#0x20]
    ldp         q6,q7,[sp],#0xA0
    adrp        xip0,__os_arm64x_dispatch_ret
    ldr         xip0,[xip0,__os_arm64x_dispatch_ret]
    br          xip0

Emülatör, hedef işlevin adresini x9 içinde sağlar.

Entry Thunk çağrılmadan önce, x64 öykünücüsü dönüş adresini yığından yazmaca LR açar. LR daha sonra denetim Giriş Thunk'a aktarılırken x64 koduna işaret etmek beklenir.

Öykünücü, aşağıdakilere bağlı olarak yığında başka bir ayarlama da gerçekleştirebilir: Hem Arm64 hem de x64 ABI'leri, bir işlevin çağrıldığı noktada yığının 16 bayta hizalanması gereken bir yığın hizalama gereksinimi tanımlar. Arm64 kodunu çalıştırırken donanım bu kuralı uygular, ancak x64 için donanım zorlaması yoktur. x64 kodu çalıştırılırken, bazı 16 baytlık hizalama yönergesi (bazı SSE yönergeleri kullanılır) veya Arm64EC kodu çağrılana kadar, hizalanmamış bir yığına sahip işlevlerin yanlışlıkla çağrılması süresiz olarak fark edilmeyebilir.

Bu olası uyumluluk sorununu gidermek için, Öykünücü, Entry Thunk'u çağırmadan önce yığın işaretçisini her zaman 16 bayta hizalar ve özgün değerini c0 kayıt’ına depolar. Bu şekilde, Giriş Thunk'ları her zaman hizalı bir yığınla çalışmaya başlar, ancak yine de yığına geçirilen parametrelere x4 aracılığıyla doğru şekilde başvurabilir.

Geçici olmayan SIMD yazmaçları söz konusu olduğunda Arm64 ve x64 çağrı kuralları arasında önemli bir fark vardır. Arm64 mimarisinde, yazmacın alt 8 baytı (64 bit) değişken olmayan olarak kabul edilir. Başka bir deyişle, Dn yazmaçlarının yalnızca Qn kısmı kalıcıdır. x64'te, XMMn yazmacının 16 baytının tamamı kalıcı olarak kabul edilir. Ayrıca x64'te XMM6 ve XMM7 geçici olmayan yazmaçlarken D6 ve D7 (karşılık gelen Arm64 yazmaçları) geçicidir.

Bu SIMD yazmaç işleme asimetrilerini ele almak için, Entry Thunks'un x64'te geçici olmayan olarak kabul edilen tüm SIMD yazmaçlarını açıkça kaydetmesi gerekir. X64 Arm64'ten daha katı olduğundan, bu tasarruf yalnızca Giriş Thunk'larında (Çıkış Thunk'larında değil) gereklidir. Başka bir deyişle, x64'te kaydetme ve koruma kuralları, her durumda Arm64 gereksinimlerini aşıyor.

Bu yazmaç değerlerinin yığının çözülmesi sırasında doğru bir şekilde kurtarılmasını sağlamak için (örneğin, setjmp + longjmp veya throw + catch), yeni bir çözülme opcode'u tanıtılmıştır: save_any_reg (0xE7). Bu yeni 3 baytlık geri sarma opcode'u, tüm Genel Amaçlı veya SIMD yazmaçlarının (geçici olarak kabul edilenler dahil) ve tam boyutlu Qn yazmaçlarının kaydedilmesini mümkün kılar. Bu yeni opcode, Qn register taşma ve doldurma işlemleri için kullanılır. save_any_reg ile save_next_pair (0xE6)uyumludur.

Referans için, aşağıdaki çözülme bilgileri daha önce sunulan Entry Thunk'a aittir.

   Prolog unwind:
      06: E76689.. +0004 stp   q6,q7,[sp,#-0xA0]! ; Actual=stp   q6,q7,[sp,#-0xA0]!
      05: E6...... +0008 stp   q8,q9,[sp,#0x20]   ; Actual=stp   q8,q9,[sp,#0x20]
      04: E6...... +000C stp   q10,q11,[sp,#0x40] ; Actual=stp   q10,q11,[sp,#0x40]
      03: E6...... +0010 stp   q12,q13,[sp,#0x60] ; Actual=stp   q12,q13,[sp,#0x60]
      02: E6...... +0014 stp   q14,q15,[sp,#0x80] ; Actual=stp   q14,q15,[sp,#0x80]
      01: 81...... +0018 stp   fp,lr,[sp,#-0x10]! ; Actual=stp   fp,lr,[sp,#-0x10]!
      00: E1...... +001C mov   fp,sp              ; Actual=mov   fp,sp
                   +0020 (end sequence)
   Epilog #1 unwind:
      0B: 81...... +0044 ldp   fp,lr,[sp],#0x10   ; Actual=ldp   fp,lr,[sp],#0x10
      0C: E74E88.. +0048 ldp   q14,q15,[sp,#0x80] ; Actual=ldp   q14,q15,[sp,#0x80]
      0F: E74C86.. +004C ldp   q12,q13,[sp,#0x60] ; Actual=ldp   q12,q13,[sp,#0x60]
      12: E74A84.. +0050 ldp   q10,q11,[sp,#0x40] ; Actual=ldp   q10,q11,[sp,#0x40]
      15: E74882.. +0054 ldp   q8,q9,[sp,#0x20]   ; Actual=ldp   q8,q9,[sp,#0x20]
      18: E76689.. +0058 ldp   q6,q7,[sp],#0xA0   ; Actual=ldp   q6,q7,[sp],#0xA0
      1C: E3...... +0060 nop                      ; Actual=90000030
      1D: E3...... +0064 nop                      ; Actual=ldr   xip0,[xip0,#8]
      1E: E4...... +0068 end                      ; Actual=br    xip0
                   +0070 (end sequence)

Arm64EC işlevi geri döndükten sonra, rutin, __os_arm64x_dispatch_ret öykünücüye yeniden girer ve LR tarafından işaret edilen x64 koduna döner.

Arm64EC işlevleri, çalışma zamanında kullanılacak bilgileri depolama işlevindeki ilk yönergeden önce dört bayt ayırır. Bu dört baytta, işlevin Entry Thunk göreli adresi bulunabilir. Bir x64 işlevinden Arm64EC işlevine çağrı yaparken öykünücü işlevin başlangıcından önceki dört baytı okur, alt iki biti maskeler ve bu miktarı işlevin adresine ekler. Bu işlem, çağrılması gereken Entry Thunk adresini üretir.

Ayarlayıcı Thunks

Ayarlayıcı Thunks, denetimi başka bir işleve (tail-call) aktaran imzasız işlevlerdir. Denetimi aktarmadan önce parametrelerden birini dönüştürürler. Dönüştürülen parametrelerin türü bilinir, ancak kalan tüm parametreler herhangi bir şey olabilir ve herhangi bir sayıda olabilir. Adjustor Thunk'lar, parametre bulundurabilecek herhangi bir kayda veya yığına dokunmaz. Adjustor Thunks'ı imzasız fonksiyonlar yapan bu özelliktir.

Derleyici otomatik olarak Adjustor Thunks oluşturabilir. Bu yapı, örneğin C++'daki çoklu kalıtımda yaygındır; burada herhangi bir sanal yöntem, this göstericide yapılan bir ayarlamanın dışında, üst sınıfa değişiklik yapmadan devredebilir.

Aşağıdaki örnekte gerçek dünya senaryosu gösterilmektedir:

[thunk]:CObjectContext::Release`adjustor{8}':
    sub         x0,x0,#8
    b           CObjectContext::Release

thunk, this işaretçisine 8 bayt çıkarır ve çağrıyı üst sınıfa iletir.

Özetle, x64 işlevlerinden çağrılabilen Arm64EC işlevlerinin bir Giriş Thunk'u (giriş noktası) ile ilişkili olması gerekir. Giriş Thunk belirli bir imzaya özgüdür. Ayarlayıcı Thunk'lar gibi Arm64 imzasız işlevler, imzasız işlevleri işleyebilen farklı bir mekanizmaya ihtiyaç duyar.

Bir Adjustor Thunk'un Entry Thunk'ı, gerçek giriş işlevi görevlerinin yürütülmesini (parametreleri bir kuraldan diğerine ayarlamak) sonraki çağrıya ertelemek için __os_arm64x_x64_jump yardımcısını kullanır. şu anda imza görünür hale gelir. Bu, adjustor thunk'un hedefi bir x64 fonksiyonu olduğunda, hiçbir şekilde çağrı düzenlemeleri yapmama seçeneğini içerir. Giriş Thunk çalışmaya başladığında parametrelerin x64 biçiminde olduğunu unutmayın.

Yukarıdaki örnekte kodun Arm64EC'de nasıl göründüğünü göz önünde bulundurun.

Arm64EC'de Adjustor Thunk

[thunk]:CObjectContext::Release`adjustor{8}':
    sub         x0,x0,#8
    adrp        x9,CObjectContext::Release
    add         x11,x9,CObjectContext::Release
    stp         fp,lr,[sp,#-0x10]!
    mov         fp,sp
    adrp        xip0, __os_arm64x_check_icall
    ldr         xip0,[xip0, __os_arm64x_check_icall]
    blr         xip0
    ldp         fp,lr,[sp],#0x10
    br          x11

Ayarlayıcı Thunk'un Giriş Gövdesi

[thunk]:CObjectContext::Release$entry_thunk`adjustor{8}':
    sub         x0,x0,#8
    adrp        x9,CObjectContext::Release
    add         x9,x9,CObjectContext::Release
    adrp        xip0,__os_arm64x_x64_jump
    ldr         xip0,[xip0,__os_arm64x_x64_jump]
    br          xip0

Hızlı iletme dizileri

Bazı uygulamalar, işlev çağrıldığında yürütmeyi saptırmak amacıyla sahip olmadığı ancak bağımlı oldukları ikili dosyalarda bulunan işlevlerde (genellikle işletim sistemi ikili dosyaları) çalışma zamanı değişiklikleri yapar. Bu işlem, kancalama olarak da bilinir.

Yüksek düzeyde, hook işlemi basittir. Ayrıntılı olarak, kancalamanın mimariye özgü ve oldukça karmaşık olduğu, kancalama mantığının ele alması gereken olası varyasyonlar göz önünde bulundurulduğunda anlaşılır.

Genel olarak, süreç aşağıdaki adımları içerir:

  • Bağlanacak işlevin adresini belirleyin.
  • İşlevin ilk yönergesini kanca rutinine bir atlama ile değiştirin.
  • Kanca tamamlandığında, orijinal yerinden çıkarılmış olan yönergeyi çalıştırmayı içeren özgün mantığa geri dönün.

Çeşitlemeler aşağıdakilerden kaynak alır:

  • İlk komutun boyutu: Diğer bir iş parçacığı bu sırada çalışıyor olabilirken işlevin üst kısmının değiştirilmesini önlemek için bunu aynı boyutta veya daha küçük bir JMP ile değiştirmek iyi bir fikirdir.
  • İlk yönergenin türü: İlk yönergenin buna göre bir bilgisayar yapısı varsa, yeniden konumlandırmak yer değiştirme alanları gibi şeylerin değiştirilmesini gerektirebilir. Bir yönerge uzak bir yere taşındığında taşabileceğinden, bu değişiklik tamamen farklı yönergelerle eşdeğer mantık sağlamayı gerektirebilir.

Tüm bu karmaşıklık nedeniyle, sağlam ve genel yakalama mantığını bulmak nadirdir. Genellikle, uygulamalarda mevcut olan mantık yalnızca uygulamanın ilgilendiği belirli API'lerde karşılaşmayı beklediği sınırlı bir dizi durumla başa çıkabilir. Bunun bir uygulama uyumluluk sorununun ne kadar olduğunu hayal etmek zor değildir. Kod veya derleyici iyileştirmelerindeki basit bir değişiklik bile, kod artık tam olarak beklendiği gibi görünmüyorsa uygulamaları kullanılamaz hale getirebilir.

Kancayı ayarlarken Arm64 koduyla karşılaşırlarsa bu uygulamalara ne olur? Kesinlikle başarısız olurlar.

Hızlı ileriye doğru sıra (FFS) işlevleri Arm64EC'de bu uyumluluk gereksinimini karşılar.

FFS, gerçek mantık içermeyen ve gerçek Arm64EC işlevine kuyruk çağrısı yapmayan çok küçük x64 işlevleridir. İsteğe bağlıdır, ancak tüm DLL ihracatları ve __declspec(hybrid_patchable) ile dekore edilmiş herhangi bir işlev için varsayılan olarak etkindir.

Bu gibi durumlarda, kod ihracat durumu GetProcAddress veya &function__declspec(hybrid_patchable) durumunda belirli bir işlevin işaretçisini elde ettiğinde, sonuçta elde edilen adres x64 kodu içerir. Bu x64 kodu, geçerli bir x64 işlevi olarak kabul edilir ve şu anda kullanılabilir olan yakalama mantığının çoğunu karşılar.

Aşağıdaki örneği göz önünde bulundurun (kısa süre için hata işleme atlanır):

auto module_handle = 
    GetModuleHandleW(L"api-ms-win-core-processthreads-l1-1-7.dll");

auto pgma = 
    (decltype(&GetMachineTypeAttributes))
        GetProcAddress(module_handle, "GetMachineTypeAttributes");

hr = (*pgma)(IMAGE_FILE_MACHINE_Arm64, &MachineAttributes);

pgma değişkenindeki işlev işaretçisi değeri, GetMachineTypeAttributes'in FFS adresini içerir.

Bu örnekte bir Fast-Forward Dizisi gösterilmektedir:

kernelbase!EXP+#GetMachineTypeAttributes:
00000001`800034e0 488bc4          mov     rax,rsp
00000001`800034e3 48895820        mov     qword ptr [rax+20h],rbx
00000001`800034e7 55              push    rbp
00000001`800034e8 5d              pop     rbp
00000001`800034e9 e922032400      jmp     00000001`80243810

FFS x64 işlevi, Arm64EC kodundaki gerçek GetMachineTypeAttributes işlevine bir kuyruk çağrısı (atlama) ile biten kurallı bir giriş ve kapsam içeriyor:

kernelbase!GetMachineTypeAttributes:
00000001`80243810 d503237f pacibsp
00000001`80243814 a9bc7bfd stp         fp,lr,[sp,#-0x40]!
00000001`80243818 a90153f3 stp         x19,x20,[sp,#0x10]
00000001`8024381c a9025bf5 stp         x21,x22,[sp,#0x20]
00000001`80243820 f9001bf9 str         x25,[sp,#0x30]
00000001`80243824 910003fd mov         fp,sp
00000001`80243828 97fbe65e bl          kernelbase!#__security_push_cookie
00000001`8024382c d10083ff sub         sp,sp,#0x20
                           [...]

İki Arm64EC işlevi arasında beş öykünmüş x64 yönergesi çalıştırmak gerekseydi oldukça verimsiz olurdu. FFS işlevleri özeldir. FFS işlevleri, değiştirilmeden kalırsa aslında çalışmaz. Arama denetleyici yardımcısı, FFS'nin değiştirilip değiştirilmediğini verimli bir şekilde kontrol eder. Böyle bir durumda çağrı doğrudan gerçek hedefe aktarılır. FFS mümkün olan herhangi bir şekilde değiştirilirse, artık bir FFS değildir. Yürütme, değiştirilmiş FFS'ye aktarılır ve orada bulunan herhangi bir kodu çalıştırarak sapma ve kancalama mantığını öykünür.

Kanca yürütmeyi FFS'nin sonuna geri aktarır ve sonunda Arm64EC koduna tail-call'a erişir; ardından, uygulamanın beklediği gibi, kancadan sonra yürütülür.

Assembly'de Arm64EC oluşturma

Windows SDK üst bilgileri ve C derleyicisi Arm64EC derlemesi yazma işini basitleştirir. Örneğin, C kodundan derlenmemiş işlevler için giriş ve çıkış thunks oluşturmak için C derleyicisini kullanabilirsiniz.

Derlemede (ASM) yazmanız gereken aşağıdaki işleve fD eşdeğer bir örnek düşünün. Hem Arm64EC hem de x64 kodu bu işlevi çağırabilir ve pfE işlev işaretçisi Arm64EC veya x64 kodunu işaret edebilir.

typedef int (PF_E)(int, double);

extern PF_E * pfE;

int fD(int i, double d) {
    return (*pfE)(i, d);
}

ASM'de yazma fD aşağıdaki kod gibi görünebilir:

#include "ksarm64.h"

        IMPORT  __os_arm64x_check_icall_cfg
        IMPORT |$iexit_thunk$cdecl$i8$i8d|
        IMPORT pfE

        NESTED_ENTRY_COMDAT A64NAME(fD)
        PROLOG_SAVE_REG_PAIR fp, lr, #-16!

        adrp    x11, pfE                                  ; Get the global function
        ldr     x11, [x11, pfE]                           ; pointer pfE

        adrp    x9, __os_arm64x_check_icall_cfg           ; Get the EC call checker
        ldr     x9, [x9, __os_arm64x_check_icall_cfg]     ; with CFG
        adrp    x10, |$iexit_thunk$cdecl$i8$i8d|          ; Get the Exit Thunk for
        add     x10, x10, |$iexit_thunk$cdecl$i8$i8d|     ; int f(int, double);
        blr     x9                                        ; Invoke the call checker

        blr     x11                                       ; Invoke the function

        EPILOG_RESTORE_REG_PAIR fp, lr, #16!
        EPILOG_RETURN

        NESTED_END

        end

Yukarıdaki örnekte:

  • Arm64EC, Arm64 ile aynı yordam bildirimini ve prolog/epilog makrolarını kullanır.
  • İşlev adlarını makroyla kaydırma A64NAME . C veya C++ kodunu Arm64EC olarak derlediğinizde, derleyici arm64EC kodunu içeren olarak OBJ işaretlerARM64EC. Bu işaretleme ARMASM ile gerçekleşmez. ASM kodunu derlerken, işlev adının #önüne ile ön ekini ekleyerek bağlayıcıya üretilen kodun Arm64EC olduğunu bildirebilirsiniz. Makro A64NAME tanımlandığında _ARM64EC_ bu işlemi gerçekleştirir ve tanımlanmadığında _ARM64EC_ adı değişmeden bırakır. Bu yaklaşım Arm64 ile Arm64EC arasında kaynak kodu paylaşmayı mümkün kılar.
  • Hedef işlevin pfE x64 olması durumunda önce işlev işaretçisini uygun çıkış thunk ile birlikte EC çağrı denetleyicisi aracılığıyla çalıştırmanız gerekir.

Giriş ve çıkış thunk'ları oluşturma

Sonraki adım, fD için giriş thunk'ını ve pfE için çıkış thunk'ını oluşturmaktır. C derleyicisi, derleyici anahtar sözcüğünü kullanarak _Arm64XGenerateThunk bu görevi en az çabayla gerçekleştirebilir.

void _Arm64XGenerateThunk(int);

int fD2(int i, double d) {
    UNREFERENCED_PARAMETER(i);
    UNREFERENCED_PARAMETER(d);
    _Arm64XGenerateThunk(2);
    return 0;
}

int fE(int i, double d) {
    UNREFERENCED_PARAMETER(i);
    UNREFERENCED_PARAMETER(d);
    _Arm64XGenerateThunk(1);
    return 0;
}

_Arm64XGenerateThunk anahtar sözcüğü C derleyicisine işlev imzasını kullanmasını, gövdeyi yoksayıp çıkış thunk (parametre 1 olduğunda) veya bir giriş thunk (parametre 2 olduğunda) oluşturmasını söyler.

Thunk neslini kendi C dosyasına yerleştirin. Ayrı ayrı dosyalarda olmak, ilgili OBJ simgelerin dökümünü çıkararak ve hatta parçalara ayırma işlemiyle sembol adlarını doğrulamayı kolaylaştırır.

Özel giriş 'thunk'lar

SDK, özel, el ile kodlanmış giriş thunk'ları oluşturmanıza yardımcı olan makrolar içerir. Özel ayarlayıcı thunk'ları oluştururken bu makroları kullanabilirsiniz.

Çoğu ayarlayıcı thunk C++ derleyicisi tarafından oluşturulur, ancak bunları el ile de oluşturabilirsiniz. Genel bir geri çağırma kontrolü gerçek geri çağırmaya devrettiğinde ve parametrelerden biri gerçek geri çağırmayı tanımladığında, el ile bir ayarlayıcı thunk (adjustor thunk) oluşturabilirsiniz.

Aşağıdaki örnekte Arm64 Klasik kodunda bir ayarlayıcı thunk gösterilmektedir:

    NESTED_ENTRY MyAdjustorThunk
    PROLOG_SAVE_REG_PAIR    fp, lr, #-16!
    ldr     x15, [x0, 0x18]
    adrp    x16, __guard_check_icall_fptr
    ldr     x16, [x16, __guard_check_icall_fptr]
    blr     xip0
    EPILOG_RESTORE_REG_PAIR fp, lr, #16
    EPILOG_END              br  x15
    NESTED_END

Bu örnekte, ilk parametre bir yapıya başvuru sağlar. Kod, bu yapının bir öğesinden hedef işlev adresini alır. Yapı yazılabilir olduğundan, Denetim Akış Koruyucusu (CFG) hedef adresi doğrulamalıdır.

Aşağıdaki örnekte eşdeğer ayarlayıcı thunk'un Arm64EC'ye nasıl taşınabilir olduğu gösterilmektedir:

    NESTED_ENTRY_COMDAT A64NAME(MyAdjustorThunk)
    PROLOG_SAVE_REG_PAIR    fp, lr, #-16!
    ldr     x11, [x0, 0x18]
    adrp    xip0, __os_arm64x_check_icall_cfg
    ldr     xip0, [xip0, __os_arm64x_check_icall_cfg]
    blr     xip0
    EPILOG_RESTORE_REG_PAIR fp, lr, #16
    EPILOG_END              br  x11
    NESTED_END

Önceki kod, bir çıkış thunk sağlamaz (x10 kayıtında). Bu yaklaşım mümkün değildir çünkü kod birçok farklı imza için yürütülebilir. Bu kod, çağırıcı ayarının x10'u çıkış thunk olarak ayarlamasından yararlanır. Çağıran, açık bir imzayı hedefleyen bir çağrı yapar.

Çağıran x64 kodu olduğunda, önceki kod satırındaki durumu çözmek için bir giriş thunk'u gerekir. Aşağıdaki örnekte, özel giriş thunk'ları için makro kullanılarak ilgili giriş thunk'unun nasıl yazdığı gösterilmektedir:

    ARM64EC_CUSTOM_ENTRY_THUNK A64NAME(MyAdjustorThunk)
    ldr     x9, [x0, 0x18]
    adrp    xip0, __os_arm64x_x64_jump
    ldr     xip0, [xip0, __os_arm64x_x64_jump]
    br      xip0
    LEAF_END

Diğer işlevlerin aksine, bu giriş thunk sonunda denetimi ilişkili işleve (ayarlayıcı thunk) aktarmaz. Bu durumda, giriş thunk, fonksiyonelliği kendisi içinde barındırır (parametre ayarlamasını gerçekleştirir) ve denetimi, __os_arm64x_x64_jump yardımcı işlev aracılığıyla doğrudan uç hedefe aktarır.

Anında Derleme (JIT) ile Dinamik Biçimde Arm64EC Kodu Oluşturma

Arm64EC işlemlerinde iki tür yürütülebilir bellek vardır: Arm64EC kodu ve x64 kodu.

İşletim sistemi bu bilgileri yüklenen ikili dosyalardan ayıklar. x64 ikili dosyalarının tümü x64'dür ve Arm64EC ikilileri Arm64EC ile x64 kod sayfaları için bir aralık tablosu içerir.

Dinamik olarak oluşturulan kod ne olacak? Tam zamanında (JIT) derleyiciler çalışma zamanında herhangi bir ikili dosya tarafından desteklenmeyen kod oluşturur.

Bu işlem genellikle aşağıdaki adımları içerir:

  • Yazılabilir bellek ayırma (VirtualAlloc).
  • Ayrılan bellekte kod üretilmesi.
  • Belleği okuma-yazmadan okuma-yürütmeye olacak şekilde yeniden yapılandırma (VirtualProtect).
  • Önemsiz olmayan (yapraksız) tüm işlevler (RtlAddFunctionTable veya RtlAddGrowableFunctionTable) için geri sarma işlevi girdileri ekleme.

Önemsiz uyumluluk nedenleriyle, bir uygulama arm64EC işleminde bu adımları gerçekleştirirse, işletim sistemi kodu x64 kodu olarak değerlendirir. Bu davranış, değiştirilmemiş x64 Java Runtime, .NET çalışma zamanı, JavaScript altyapısı vb. kullanan tüm işlemler için oluşur.

Arm64EC dinamik kodu oluşturmak için iki farkla aynı işlemi izleyin:

  • Belleği ayırırken daha yenisini VirtualAlloc2 (veya VirtualAllocyerineVirtualAllocEx) kullanın ve özniteliğini MEM_EXTENDED_PARAMETER_EC_CODE sağlayın.
  • İşlev girdileri eklenirken:
    • Arm64 biçiminde olmaları gerekir. Arm64EC kodu derlenirken, RUNTIME_FUNCTION tür x64 biçimiyle eşleşir. Arm64EC derlerken Arm64 biçimi için ARM64_RUNTIME_FUNCTION türünü kullanın.
    • Eski RtlAddFunctionTable API'yi kullanmayın. Her zaman daha yeni RtlAddGrowableFunctionTable API'yi kullanın.

Aşağıdaki örnekte bellek ayırma gösterilmektedir:

    MEM_EXTENDED_PARAMETER Parameter = { 0 };
    Parameter.Type = MemExtendedParameterAttributeFlags;
    Parameter.ULong64 = MEM_EXTENDED_PARAMETER_EC_CODE;

    HANDLE process = GetCurrentProcess();
    ULONG allocationType = MEM_RESERVE;
    DWORD protection = PAGE_EXECUTE_READ | PAGE_TARGETS_INVALID;

    address = VirtualAlloc2 (
        process,
        NULL,
        numBytesToAllocate,
        allocationType,
        protection,
        &Parameter,
        1);

Aşağıdaki örnekte bir unwind işlevi girişinin nasıl ekleneceği gösterilmektedir:

ARM64_RUNTIME_FUNCTION FunctionTable[1];

FunctionTable[0].BeginAddress = 0;
FunctionTable[0].Flags = PdataPackedUnwindFunction;
FunctionTable[0].FunctionLength = nSize / 4;
FunctionTable[0].RegF = 0;                   // no D regs saved
FunctionTable[0].RegI = 0;                   // no X regs saved beyond fp,lr
FunctionTable[0].H = 0;                      // no home for x0-x7
FunctionTable[0].CR = PdataCrChained;        // stp fp,lr,[sp,#-0x10]!
                                             // mov fp,sp
FunctionTable[0].FrameSize = 1;              // 16 / 16 = 1

this->DynamicTable = NULL;
Result == RtlAddGrowableFunctionTable(
    &this->DynamicTable,
    reinterpret_cast<PRUNTIME_FUNCTION>(FunctionTable),
    1,
    1,
    reinterpret_cast<ULONG_PTR>(pBegin),
    reinterpret_cast<ULONG_PTR>(reinterpret_cast<PBYTE>(pBegin) + nSize)
);