Not
Bu sayfaya erişim yetkilendirme gerektiriyor. Oturum açmayı veya dizinleri değiştirmeyi deneyebilirsiniz.
Bu sayfaya erişim yetkilendirme gerektiriyor. Dizinleri değiştirmeyi deneyebilirsiniz.
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.
- Yazmaç eşleme ve engellenmiş yazmaçlar
- Arama denetleyicileri
- Stack denetleyicileri
- Variadic çağırma kuralı
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
x11yerinex15içinde sağlanır. - Hedef adres (
x11) yerine[in, out]olur[in]. - "Exit Thunk" adlı ek bir parametre,
x10aracı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ınx0'danx7'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şlevf1(int, double)için, ikinci parametre hemRDXhem deXMM1öğelerine atanır. Arm64EC'de, ikinci parametre yalnızcax1öğ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ç,
x4yığı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. -
ull18 baytlık bir tamsayıdır. O,x1öğesine atar. -
ull28 baytlık bir tamsayıdır. Bu öğeye atarx2. -
ull38 baytlık bir tamsayıdır. Bunux3öğ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. -
ull18 baytlık bir tamsayıdır. Bu,x2ögesine atar. -
ull28 baytlık bir tamsayıdır. O,x3öğesine atar. -
ull38 baytlık bir tamsayıdır. Doğrudan yığına atanır. -
x4öğesinin konumunuull3yığında yükler. -
x5boyut bilgisiniull3yü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
- Çıkış İşlev Çağrıları (Thunks)
- Giriş Thunk'ları
- Ayarlayıcı Thunks
- Fast-Forward Dizileri
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 -
fBiçin çıkış thunk -
fCiç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
x9hedef (x64) işlevinin adresini taşıyacak şekilde ayarlar. Exit Thunk, öykünücüyü herhangi bir değişiklik yapmadanx9geç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 (fBbu 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 (fBbu 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,x10içinde sağlanan girişten kopyalandı. -
x10Çıkış Thunk'un adresini, girdi değişmeden olarak döndürür. -
x9hedef x64 işlevini döndürür. Bu değer,x11aracı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
[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 olarakOBJişaretlerARM64EC. Bu işaretlemeARMASMile 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. MakroA64NAMEtanı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
pfEx64 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 (
RtlAddFunctionTableveyaRtlAddGrowableFunctionTable) 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(veyaVirtualAllocyerineVirtualAllocEx) kullanın ve özniteliğiniMEM_EXTENDED_PARAMETER_EC_CODEsağlayın. - İşlev girdileri eklenirken:
- Arm64 biçiminde olmaları gerekir. Arm64EC kodu derlenirken,
RUNTIME_FUNCTIONtür x64 biçimiyle eşleşir. Arm64EC derlerken Arm64 biçimi içinARM64_RUNTIME_FUNCTIONtürünü kullanın. - Eski
RtlAddFunctionTableAPI'yi kullanmayın. Her zaman daha yeniRtlAddGrowableFunctionTableAPI'yi kullanın.
- Arm64 biçiminde olmaları gerekir. Arm64EC kodu derlenirken,
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)
);
Windows on Arm