Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
Arm64EC ("Kompatibel Emulasi") adalah antarmuka biner aplikasi baru (ABI) untuk membangun aplikasi untuk Windows 11 di Arm. Untuk gambaran umum Arm64EC dan cara mulai membangun aplikasi Win32 sebagai Arm64EC, lihat Menggunakan Arm64EC untuk membangun aplikasi untuk Windows 11 di perangkat Arm.
Artikel ini memberikan tampilan terperinci tentang Arm64EC ABI dengan informasi yang cukup bagi pengembang aplikasi untuk menulis dan men-debug kode yang dikompilasi untuk Arm64EC, termasuk debugging tingkat rendah/perakitan dan menulis kode perakitan yang menargetkan ABI Arm64EC.
Desain Arm64EC
Arm64EC memberikan fungsionalitas dan performa tingkat asli, sekaligus memberikan interoperabilitas transparan dan langsung dengan kode x64 yang berjalan di bawah emulasi.
Arm64EC sebagian besar merupakan aditif dari ABI Arm64 Klasik. ABI Klasik berubah sangat sedikit, tetapi Arm64EC ABI menambahkan bagian untuk mengaktifkan interoperabilitas x64.
Dalam dokumen ini, ABI Arm64 standar asli disebut sebagai "ABI Klasik". Istilah ini menghindari ambiguitas yang melekat pada istilah yang kelebihan beban seperti "Native". Arm64EC setiap bit sebanding dengan ABI asli.
Arm64EC vs. Arm64 Classic ABI
Daftar berikut menunjukkan di mana Arm64EC berbeda dari Arm64 Classic ABI.
- Mendaftarkan pemetaan dan register yang diblokir
- Pemeriksa panggilan
- Pemeriksa tumpukan
- Konvensi panggilan variadik
Perbedaan ini adalah perubahan kecil jika dilihat dalam konteks apa yang didefinisikan oleh keseluruhan ABI.
Mendaftarkan pemetaan dan register yang diblokir
Untuk mengaktifkan interoperabilitas tingkat jenis dengan kode x64, kode Arm64EC dikompilasi dengan definisi arsitektur praprosesor yang sama dengan kode x64.
Dengan kata lain, _M_AMD64 dan _AMD64_ didefinisikan. Salah satu jenis yang terpengaruh oleh aturan ini adalah CONTEXT struktur. Struktur CONTEXT mendefinisikan status CPU pada titik tertentu. Ini digunakan untuk hal-hal seperti Exception Handling dan GetThreadContext API. Kode x64 yang ada mengharapkan konteks CPU diwakili sebagai struktur x64 CONTEXT atau, dengan kata lain, CONTEXT struktur seperti yang didefinisikan selama kompilasi x64.
Anda harus menggunakan struktur ini untuk mewakili konteks CPU saat menjalankan kode x64 dan kode Arm64EC. Kode yang ada tidak memahami konsep baru, seperti set register CPU yang berubah dari fungsi ke fungsi. Jika Anda menggunakan struktur x64 CONTEXT untuk mewakili status eksekusi Arm64, Anda secara efektif memetakan register Arm64 ke dalam register x64.
Pemetaan ini juga berarti Anda tidak dapat menggunakan register Arm64 apa pun yang tidak sesuai dengan x64 CONTEXT. Nilainya dapat hilang kapan saja operasi menggunakan CONTEXT (dan beberapa operasi dapat menjadi asinkron dan tidak terduga, seperti operasi Pengumpulan Sampah dari Runtime Bahasa Terkelola, atau APC).
Header Windows dalam SDK mewakili aturan pemetaan antara register Arm64EC dan x64 dengan struktur ARM64EC_NT_CONTEXT. Struktur ini pada dasarnya adalah suatu union dari struktur CONTEXT, persis seperti yang didefinisikan untuk x64, tetapi dengan overlay register Arm64 tambahan.
Misalnya, RCX memetakan ke X0, RDX ke X1, RSP ke SP, RIP ke PC, dan sebagainya. Register x13, x14, x23, x24, x28, dan v16 hingga v31 tidak memiliki representasi dan, dengan demikian, tidak dapat digunakan di Arm64EC.
Pembatasan penggunaan register ini adalah perbedaan pertama antara ARM64 Classic dan EC ABIs.
Pemeriksa panggilan
Pemeriksa panggilan telah menjadi bagian dari Windows sejak Control Flow Guard (CFG) diperkenalkan di Windows 8.1. Pemeriksa panggilan adalah pembersih alamat untuk penunjuk fungsi (sebelum hal-hal ini disebut pembersih alamat). Setiap kali Anda mengkompilasi kode dengan opsi /guard:cf, pengkompilasi menghasilkan panggilan tambahan ke fungsi pemeriksa tepat sebelum setiap panggilan tidak langsung atau melompat. Windows menyediakan fungsi pemeriksa itu sendiri. Untuk CFG, CFG melakukan pemeriksaan validitas terhadap target panggilan yang diketahui baik. Biner yang dikompilasi dengan /guard:cf juga menyertakan informasi ini.
Contoh ini menunjukkan penggunaan pemeriksa panggilan di Classic Arm64:
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
Dalam kasus CFG, pemeriksa panggilan hanya mengembalikan hasil jika target valid, atau menghentikan proses secara cepat jika tidak. Pemeriksa panggilan memiliki konvensi panggilan kustom. Mereka mengambil penunjuk fungsi dalam register yang tidak digunakan oleh konvensi panggilan normal dan mempertahankan semua register konvensi panggilan normal. Dengan cara ini, mereka tidak memperkenalkan tumpahan register di sekitar mereka.
Pemeriksa panggilan bersifat opsional pada semua ABI Windows lainnya, tetapi wajib di Arm64EC. Di Arm64EC, pemeriksa panggilan mengakumulasi tugas memverifikasi arsitektur fungsi yang dipanggil. Mereka memverifikasi apakah panggilan adalah fungsi EC lain ("Kompatibel Emulasi") atau fungsi x64 yang harus dijalankan di bawah emulasi. Dalam banyak kasus, ini hanya dapat diverifikasi saat runtime.
Pemeriksa panggilan Arm64EC dibangun di atas pemeriksa Arm64 yang ada, tetapi mereka memiliki konvensi panggilan kustom yang sedikit berbeda. Mereka mengambil parameter tambahan dan mereka dapat memodifikasi register yang berisi alamat target. Misalnya, jika target adalah kode x64, kontrol harus ditransfer ke logika perancah emulasi terlebih dahulu.
Di Arm64EC, penggunaan pemeriksa panggilan yang sama akan menjadi:
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
Sedikit perbedaan dari Classic Arm64 meliputi:
- Nama simbol untuk pemeriksa panggilan berbeda.
- Alamat target disediakan di
x11alih-alihx15. - Alamat target (
x11) bukan[in, out][in]. - Ada parameter tambahan, disediakan melalui
x10, yang disebut "Exit Thunk".
Exit Thunk adalah funclet yang mengubah parameter fungsi dari konvensi panggilan Arm64EC ke konvensi panggilan x64.
Pemeriksa panggilan Arm64EC terletak melalui simbol yang berbeda dari yang digunakan untuk ABI lain di Windows. Pada ABI Arm64 Klasik, simbol untuk pemeriksa panggilan adalah __guard_check_icall_fptr. Simbol ini akan ada di Arm64EC, tetapi ada untuk kode x64 yang terkait secara statis untuk digunakan, bukan kode Arm64EC itu sendiri. Kode Arm64EC akan menggunakan atau __os_arm64x_check_icall__os_arm64x_check_icall_cfg.
Di Arm64EC, pemeriksa panggilan tidak opsional. Namun, CFG masih opsional, seperti halnya untuk ARI lainnya. CFG dapat dinonaktifkan pada waktu kompilasi, atau mungkin ada alasan yang sah untuk tidak melakukan pemeriksaan CFG bahkan ketika CFG diaktifkan (misalnya penunjuk fungsi tidak pernah berada di memori RW). Untuk panggilan tidak langsung dengan pemeriksaan CFG, pemeriksa __os_arm64x_check_icall_cfg harus digunakan. Jika CFG dinonaktifkan atau tidak perlu, __os_arm64x_check_icall harus digunakan sebagai gantinya.
Di bawah ini adalah tabel ringkasan penggunaan pemeriksa panggilan pada Classic Arm64, x64 dan Arm64EC yang mencatat fakta bahwa biner Arm64EC dapat memiliki dua opsi tergantung pada arsitektur kode.
| Biner | Kode | Panggilan tidak langsung yang tidak terlindungi | Panggilan tidak langsung yang dilindungi CFG |
|---|---|---|---|
| x64 | x64 | tidak ada pemeriksa panggilan |
__guard_check_icall_fptr atau __guard_dispatch_icall_fptr |
| Arm64 Klasik | Arm64 | tidak ada pemeriksa panggilan | __guard_check_icall_fptr |
| Arm64EC | x64 | tidak ada pemeriksa panggilan |
__guard_check_icall_fptr atau __guard_dispatch_icall_fptr |
| Arm64EC | __os_arm64x_check_icall |
__os_arm64x_check_icall_cfg |
Secara independen dari ABI, memiliki kode yang diaktifkan CFG (kode dengan referensi ke pemeriksa panggilan CFG), tidak menyiratkan perlindungan CFG pada runtime. Biner yang dilindungi CFG dapat berjalan di tingkat bawah, pada sistem yang tidak mendukung CFG: pemeriksa panggilan diinisialisasi dengan pembantu tanpa operasi pada waktu kompilasi. Proses mungkin juga menonaktifkan CFG berdasarkan konfigurasi. Ketika CFG dinonaktifkan (atau dukungan OS tidak ada) pada ARI sebelumnya, OS tidak akan memperbarui pemeriksa panggilan saat biner dimuat. Pada Arm64EC, jika perlindungan CFG dinonaktifkan, OS akan diatur __os_arm64x_check_icall_cfg sama dengan __os_arm64x_check_icall, yang masih akan memberikan pemeriksaan arsitektur target yang diperlukan dalam semua kasus, tetapi bukan perlindungan CFG.
Seperti halnya CFG di Classic Arm64, panggilan ke fungsi target (x11) harus segera mengikuti panggilan ke Pemeriksa Panggilan. Alamat Pemeriksa Panggilan harus ditempatkan dalam register volatil dan tidak, atau alamat fungsi target, harus pernah disalin ke register lain atau ditumpahkan ke memori.
Pemeriksa Tumpukan
__chkstk digunakan secara otomatis oleh pengkompilasi setiap kali fungsi mengalokasikan area pada tumpukan yang lebih besar dari halaman. Untuk menghindari melompati halaman stack guard yang melindungi akhir tumpukan, __chkstk dipanggil untuk memastikan semua halaman di area yang dialokasikan diselimuti.
__chkstk biasanya dipanggil dari prolog fungsi. Untuk alasan itu, dan untuk pembuatan kode yang optimal, ia menggunakan konvensi panggilan kustom.
Ini menyiratkan bahwa kode x64 dan kode Arm64EC membutuhkan sendiri, berbeda, __chkstk fungsi, karena thunk Entri dan Keluar mengasumsikan konvensi panggilan standar.
x64 dan Arm64EC berbagi namespace simbol yang sama sehingga tidak boleh ada dua fungsi bernama __chkstk. Untuk mengakomodasi kompatibilitas dengan kode x64 yang sudah ada sebelumnya, __chkstk nama akan dikaitkan dengan pemeriksa tumpukan x64. Kode Arm64EC akan digunakan __chkstk_arm64ec sebagai gantinya.
Konvensi panggilan kustom untuk __chkstk_arm64ec sama dengan untuk Classic Arm64 __chkstk: x15 menyediakan ukuran alokasi dalam byte, dibagi dengan 16. Semua register non-volatil, serta semua register volatil yang terlibat dalam konvensi panggilan standar dipertahankan.
Semua yang dikatakan di atas tentang __chkstk berlaku sama untuk __security_check_cookie dan mitra Arm64EC-nya: __security_check_cookie_arm64ec.
Konvensi panggilan variadik
Arm64EC mengikuti konvensi panggilan ABI Arm64 Klasik, kecuali untuk fungsi variadik (juga dikenal sebagai vararg atau fungsi dengan kata kunci parameter elipsis (. . .).
Untuk kasus spesifik variadik, Arm64EC mengikuti konvensi panggilan yang sangat mirip dengan variadik x64, dengan hanya beberapa perbedaan. Daftar berikut menunjukkan aturan utama untuk variadik Arm64EC:
- Hanya empat register pertama yang digunakan untuk pengiriman parameter:
x0,x1,x2,x3. Parameter yang tersisa meluap ke tumpukan. Aturan ini mengikuti konvensi panggilan variadik x64 persis dan berbeda dari Arm64 Classic, di mana daftarx0melaluix7digunakan. - Parameter titik mengambang dan SIMD yang diteruskan melalui register menggunakan register tujuan umum, bukan SIMD. Aturan ini mirip dengan Arm64 Classic dan berbeda dari x64, di mana parameter FP/SIMD diteruskan melalui register tujuan umum dan register SIMD. Misalnya, untuk fungsi
f1(int, …)yang disebut sebagaif1(int, double), pada x64, parameter kedua ditugaskan keRDXdanXMM1. Pada Arm64EC, parameter kedua ditetapkan hanya kex1. - Saat meneruskan struktur menurut nilai melalui register, aturan ukuran x64 berlaku: Struktur dengan ukuran tepat 1, 2, 4, dan 8 byte dimuat langsung ke dalam register tujuan umum. Struktur dengan ukuran lain meluap ke tumpukan, dan penunjuk ke lokasi tumpahan ditetapkan ke register. Aturan ini pada dasarnya menurunkan nilai demi nilai menjadi referensi berdasarkan pada tingkat rendah. Pada ABI Arm64 Klasik, struktur dengan ukuran apa pun hingga 16 byte ditetapkan langsung ke register tujuan umum.
- Register
x4memuat pointer ke parameter pertama yang diteruskan melalui tumpukan, yaitu parameter kelima. Aturan ini tidak termasuk struktur yang ditumpahkan karena pembatasan ukuran yang diuraikan sebelumnya. - Register
x5memuat ukuran, dalam byte, dari semua parameter yang diteruskan melalui tumpukan (ukuran semua parameter, berawal dari yang kelima). Aturan ini tidak termasuk struktur yang diteruskan oleh nilai yang ditumpahkan karena pembatasan ukuran yang diuraikan sebelumnya.
Dalam contoh berikut, pt_nova_function mengambil parameter dalam bentuk non-variadik, sehingga mengikuti konvensi panggilan Classic Arm64. Kemudian memanggil pt_va_function dengan parameter yang sama persis tetapi dalam panggilan variadik sebagai gantinya.
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 mengambil lima parameter, yang ditetapkannya mengikuti aturan konvensi panggilan Classic Arm64:
- 'f' adalah ganda. Ini menetapkan ke
d0. - 'tc' adalah struct dengan ukuran 3 byte. Ini menetapkan ke
x0. -
ull1adalah bilangan bulat 8-byte. Ini menetapkan kex1. -
ull2adalah bilangan bulat 8-byte. Ini memberikan tugas kepadax2. -
ull3adalah bilangan bulat 8-byte. Ini mengarahkan kex3.
pt_va_function adalah fungsi variadik, sehingga mengikuti aturan variadik Arm64EC yang diuraikan sebelumnya:
- 'f' adalah ganda. Ini mengalokasikan ke
x0. - 'tc' adalah struct dengan ukuran 3 byte. Ini meluap ke tumpukan dan lokasinya dimuat ke dalam
x1. -
ull1adalah bilangan bulat 8-byte. Ini menetapkan kex2. -
ull2adalah bilangan bulat 8-byte. Ini menetapkan kex3. -
ull3adalah bilangan bulat 8-byte. Ini menetapkan langsung ke tumpukan. -
x4memuat lokasiull3dalam tumpukan. -
x5memuat ukuranull3.
Contoh berikut menunjukkan kemungkinan output kompilasi untuk pt_nova_function, yang mengilustrasikan perbedaan penetapan parameter yang diuraikan sebelumnya.
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
Penambahan ABI
Untuk mencapai interoperabilitas transparan dengan kode x64, buat banyak tambahan ke ABI Arm64 klasik. Penambahan ini menangani perbedaan konvensi panggilan antara Arm64EC dan x64.
Daftar berikut ini mencakup penambahan ini:
Thunk masuk dan keluar
Prosedur thunk masuk dan keluar menguraikan konvensi pemanggilan Arm64EC (yang sebagian besar sama dengan Arm64 klasik) menjadi konvensi pemanggilan x64, dan sebaliknya.
Kesalahpahaman umum adalah Anda dapat mengonversi konvensi panggilan dengan mengikuti satu aturan yang diterapkan ke semua tanda tangan fungsi. Kenyataannya adalah bahwa konvensi panggilan memiliki aturan penetapan parameter. Aturan ini bergantung pada jenis parameter dan berbeda dari ABI dengan ABI. Akibatnya adalah bahwa terjemahan antara ABI adalah khusus untuk setiap signature fungsi, bervariasi dengan jenis setiap parameter.
Pertimbangkan fungsi berikut:
int fJ(int a, int b, int c, int d);
Penetapan parameter terjadi sebagai berikut:
- Arm64: a -> x0, b -> x1, c -> x2, d -> x3
- x64: a -> RCX, b -> RDX, c -> R8, d -> r9
- Arm64 -> terjemahan x64: x0 -> RCX, x1 -> RDX, x2 -> R8, x3 -> R9
Sekarang pertimbangkan fungsi yang berbeda:
int fK(int a, double b, int c, double d);
Penetapan parameter terjadi sebagai berikut:
- Arm64: a -> x0, b -> d0, c -> x1, d -> d1
- x64: a -> RCX, b -> XMM1, c -> R8, d -> XMM3
- Arm64 -> terjemahan x64: x0 -> RCX, d0 -> XMM1, x1 -> R8, d1 -> XMM3
Contoh-contoh ini menunjukkan bahwa penetapan parameter dan terjemahan bervariasi menurut jenis, tetapi juga bergantung pada jenis parameter sebelumnya dalam daftar. Detail ini diilustrasikan oleh parameter ketiga. Dalam kedua fungsi, jenis parameter adalah int, tetapi terjemahan yang dihasilkan berbeda.
Thunk masuk dan keluar ada untuk alasan ini dan khusus disesuaikan untuk signature fungsi masing-masing.
Kedua jenis thunks adalah fungsi. Emulator secara otomatis memanggil entry thunk ketika fungsi x64 memanggil fungsi Arm64EC (eksekusi Memasuki Arm64EC). Pemeriksa panggilan secara otomatis menjalankan thunk keluar ketika fungsi Arm64EC melakukan panggilan ke fungsi x64 (menjalankan Keluar Arm64EC).
Saat mengompilasi kode Arm64EC, pengompilasi menghasilkan entry thunk untuk setiap fungsi Arm64EC yang sesuai dengan tanda tangannya. Pengkompilasi juga menghasilkan thunk keluar untuk setiap fungsi yang dipanggil oleh fungsi Arm64EC.
Pertimbangkan contoh berikut:
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);
}
Saat mengkompilasi kode sebelumnya yang menargetkan Arm64EC, kompiler menghasilkan:
- Kode untuk
fA. - Thunk entri untuk
fA - Keluar dari thunk untuk
fB - Keluar dari thunk untuk
fC
Pengkompilasi menghasilkan entri thunk fA jika fA dipanggil dari kode x64. Pengkompilasi menghasilkan thunk keluar untuk fB dan fC jika fB dan fC adalah kode x64.
Pengkompilasi mungkin menghasilkan "exit thunk" yang sama beberapa kali karena menghasilkannya di lokasi panggilan daripada fungsi itu sendiri. Duplikasi ini dapat mengakibatkan sejumlah besar thunk redundan. Untuk menghindari duplikasi ini, kompilator menerapkan aturan pengoptimalan sepele untuk memastikan hanya thunk yang diperlukan yang berhasil masuk ke biner akhir.
Misalnya, dalam biner di mana fungsi A Arm64EC memanggil fungsi BArm64EC , B tidak diekspor dan alamatnya tidak pernah diketahui di luar A. Aman untuk menghapus thunk keluar dari A ke B, serta thunk masuk untuk B. Ini juga aman untuk menggabungkan alias dari semua thunk keluar dan masuk yang berisi kode yang sama, bahkan jika dihasilkan pada fungsi yang berbeda.
Keluar dari thunks
Menggunakan fungsi contoh fA, fB, dan fC di bagian sebelumnya, pengkompilasi menghasilkan keluar dari thunks fB dan fC sebagai berikut:
Keluar dari thunk ke 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
Keluar dari thunk ke 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
Dalam kasus fB ini, keberadaan parameter double menyebabkan penugasan register GP yang tersisa diacak ulang, sebagai hasil dari perbedaan aturan penugasan antara Arm64 dan x64. Anda juga dapat melihat bahwa x64 hanya menetapkan empat parameter ke register, sehingga parameter kelima harus dituangkan ke stack.
Dalam kasus ini fC , parameter kedua adalah struktur dengan panjang 3 byte. Arm64 memungkinkan struktur ukuran apa pun untuk ditetapkan langsung ke register. x64 hanya memungkinkan ukuran 1, 2, 4, dan 8. Exit Thunk ini harus mentransfer struct dari register ke tumpukan dan menetapkan register sebagai pointer gantinya. Pendekatan ini masih menggunakan satu register (untuk membawa pointer), sehingga tidak mengubah tugas untuk register yang tersisa: tidak ada perombakan register yang terjadi untuk parameter ketiga dan keempat. Sama seperti untuk kasus fB, parameter kelima harus disimpan ke tumpukan.
Pertimbangan tambahan untuk Exit Thunks:
- Kompiler menamainya bukan berdasarkan nama fungsi yang mereka terjemahkan dari dan ke, melainkan berdasarkan tanda tangan fungsi yang mereka alamatkan. Konvensi penamaan ini memudahkan untuk menemukan redundansi.
- Pemeriksa panggilan mengatur register
x9untuk memuat alamat dari fungsi target (x64). Exit Thunk memanggil emulator, melewatix9tanpa perubahan apa pun.
Setelah mengatur ulang parameter, Exit Thunk memanggil ke emulator melalui __os_arm64x_dispatch_call_no_redirect.
Pada titik ini, ada baiknya meninjau fungsi pemeriksa panggilan dan ABI kustomnya. Berikut adalah tampilan dari panggilan fB tidak langsung:
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
Saat memanggil pemeriksa panggilan:
-
x11memasok alamat fungsi target untuk dipanggil (fBdalam hal ini). Pada titik ini, pemeriksa panggilan mungkin tidak tahu apakah fungsi target adalah Arm64EC atau x64. -
x10memasok Exit Thunk yang cocok dengan tanda tangan fungsi yang dipanggil (fBdalam hal ini).
Data yang dikembalikan pemeriksa panggilan tergantung pada apakah fungsi target adalah Arm64EC atau x64.
Jika targetnya adalah Arm64EC:
-
x11mengembalikan alamat kode Arm64EC untuk dipanggil. Nilai ini mungkin sama dengan nilai yang disediakan.
Jika targetnya adalah kode x64:
-
x11mengembalikan alamat Exit Thunk. Alamat ini disalin dari input yang disediakan dalamx10. -
x10mengembalikan alamat Exit Thunk tanpa terganggu dari input. -
x9mengembalikan fungsi x64 yang ditargetkan. Nilai ini mungkin sama dengan yang disediakan melaluix11.
Pemeriksa panggilan selalu membiarkan register parameter konvensi panggilan tetap tidak terganggu. Kode panggilan harus segera mengikuti panggilan ke pemeriksa panggilan dengan blr x11 (atau br x11 jika terjadi panggilan ekor). Pemeriksa panggilan selalu mempertahankan register ini di atas dan di luar register non-volatil standar: x0-x8, (x15chkstk), dan .q0-q7
Thunks Entri
Entry Thunks mengurus transformasi yang diperlukan dari konvensi panggilan x64 ke Arm64. Transformasi ini pada dasarnya adalah kebalikan dari Exit Thunks tetapi melibatkan beberapa aspek lagi untuk dipertimbangkan.
Pertimbangkan contoh sebelumnya untuk mengkompilasi fA. Entry Thunk dihasilkan agar kode x64 dapat memanggil fA.
Entry Thunk untuk int fA(int a, double b, struct SC c, int i1, int i2, int i3)
$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
Emulator menyediakan alamat fungsi target di x9.
Sebelum memanggil Entry Thunk, emulator x64 memunculkan alamat pemulangan dari tumpukan ke dalam register LR.
LR kemudian diharapkan untuk menunjuk pada kode x64 ketika kontrol mentransfer ke Entry Thunk.
Emulator mungkin juga melakukan penyesuaian lain pada tumpukan, tergantung pada hal berikut: ABIs Arm64 dan x64 menentukan persyaratan perataan tumpukan di mana tumpukan harus diselaraskan ke 16 byte pada titik fungsi dipanggil. Saat menjalankan kode Arm64, perangkat keras memberlakukan aturan ini, tetapi tidak ada penegakan perangkat keras untuk x64. Saat menjalankan kode x64, secara keliru memanggil fungsi dengan tumpukan yang tidak selaras mungkin tidak terlihat untuk waktu yang tidak terbatas, sampai beberapa instruksi perataan 16 byte digunakan, yang dilakukan oleh beberapa instruksi SSE, atau saat kode Arm64EC dipanggil.
Untuk mengatasi potensi masalah kompatibilitas ini, sebelum memanggil Entry Thunk, emulator selalu menyelaraskan Stack Pointer ke 16 byte dan menyimpan nilai aslinya di x4 register. Dengan cara ini, Entry Thunks selalu mulai mengeksekusi dengan tumpukan yang selaras tetapi masih dapat mereferensikan parameter yang diteruskan pada tumpukan dengan benar, melalui x4.
Dalam hal register SIMD non-volatil, ada perbedaan signifikan antara konvensi panggilan Arm64 dan x64. Pada Arm64, 8 byte rendah (64 bit) dari register dianggap tidak volatil. Dengan kata lain, hanya Dn bagian dari Qn register yang tidak volatil. Pada x64, seluruh 16 byte register XMMn dianggap tidak volatil. Selain itu, pada x64, XMM6 dan XMM7 merupakan register non-volatil sedangkan D6 dan D7 (register Arm64 yang sesuai) volatil.
Untuk mengatasi asimetri manipulasi register SIMD ini, Entry Thunks harus secara eksplisit menyimpan semua register SIMD yang dianggap non-volatil di x64. Penghematan ini hanya diperlukan pada Entry Thunks (bukan Exit Thunks) karena x64 lebih ketat dari Arm64. Dengan kata lain, aturan penyimpanan dan pelestarian register di x64 melebihi semua persyaratan yang diminta oleh Arm64 dalam semua kasus.
Untuk mengatasi pemulihan yang benar dari nilai-nilai register ini saat membongkar tumpukan (misalnya, setjmp + longjmp, atau throw + catch), sebuah opcode unwind baru diperkenalkan: save_any_reg (0xE7). Opcode unwind 3 byte baru ini memungkinkan penyimpanan register Tujuan Umum atau SIMD (termasuk yang dianggap volatil) dan termasuk register berukuran Qn penuh. Opcode baru ini digunakan untuk operasi pengisian dan tumpahan register.
save_any_reg kompatibel dengan save_next_pair (0xE6).
Sebagai referensi, informasi unwind berikut milik Entry Thunk yang disajikan sebelumnya:
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)
Setelah fungsi Arm64EC selesai, __os_arm64x_dispatch_ret rutinitas masuk kembali ke emulator, beralih ke kode x64 (ditunjukkan oleh LR).
Fungsi Arm64EC mencadangkan empat byte sebelum instruksi pertama dalam fungsi untuk menyimpan informasi yang akan digunakan saat runtime. Dalam keempat byte ini, alamat relatif dari Entry Thunk untuk fungsi dapat ditemukan. Saat melakukan panggilan dari fungsi x64 ke fungsi Arm64EC, emulator membaca empat byte sebelum memulai fungsi, menutupi dua bit yang lebih rendah, dan menambahkan jumlah tersebut ke alamat fungsi. Proses ini menghasilkan alamat Entry Thunk untuk dipanggil.
Thunks Penyesuaian
Adjustor Thunks adalah fungsi tanpa tanda tangan yang mentransfer kontrol ke (panggilan ekor) fungsi lain. Sebelum mentransfer kontrol, mereka mengubah salah satu parameter. Jenis parameter yang diubah diketahui, tetapi semua parameter yang tersisa dapat berupa apa pun dan dapat berada dalam angka apa pun. Adjustor Thunks tidak menyentuh register apa pun yang berpotensi memegang parameter, dan tidak menyentuh tumpukan. Karakteristik ini menjadikan fungsi Adjustor Thunks tanpa tanda tangan.
Pengkompilasi dapat secara otomatis menghasilkan Adjustor Thunks. Generasi ini umum, misalnya, dengan warisan ganda C++, di mana metode virtual apa pun dapat mendelegasikan ke kelas induk tanpa modifikasi, kecuali untuk penyesuaian penunjuk this.
Contoh berikut menunjukkan skenario dunia nyata:
[thunk]:CObjectContext::Release`adjustor{8}':
sub x0,x0,#8
b CObjectContext::Release
Thunk mengurangi 8 byte ke this pointer dan meneruskan panggilan ke kelas induk.
Singkatnya, fungsi Arm64EC yang dapat dipanggil dari fungsi x64 harus memiliki Entry Thunk terkait. Entry Thunk khusus tanda tangan. Fungsi tanpa tanda tangan Arm64, seperti Adjustor Thunks membutuhkan mekanisme berbeda yang dapat menangani fungsi tanpa tanda tangan.
Entry Thunk dari Adjustor Thunk menggunakan __os_arm64x_x64_jump pembantu untuk menunda eksekusi pekerjaan Entry Thunk yang sebenarnya (sesuaikan parameter dari satu konvensi ke konvensi lainnya) ke panggilan berikutnya. Pada saat inilah tanda tangan menjadi jelas. Ini termasuk opsi untuk tidak melakukan penyesuaian konvensi panggilan sama sekali, jika target Adjustor Thunk ternyata merupakan fungsi x64. Ingatlah bahwa pada saat Entry Thunk mulai berjalan, parameter berada dalam bentuk x64 mereka.
Dalam contoh di atas, pertimbangkan bagaimana kode terlihat di Arm64EC.
Adjustor Thunk di Arm64EC
[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
Trunk Entri Adjustor Thunk
[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
Mempercepat urutan
Beberapa aplikasi membuat modifikasi saat berjalan pada fungsi yang berada di biner yang tidak mereka miliki tetapi bergantung pada — umumnya biner sistem operasi — untuk tujuan mengalihkan jalannya eksekusi ketika fungsi dipanggil. Proses ini juga dikenal sebagai hooking.
Secara garis besar, proses pengaitannya sederhana. Namun, secara rinci, pengait adalah arsitektur khusus dan cukup kompleks mengingat variasi potensial yang harus ditangani logika pengait.
Secara umum, prosesnya melibatkan langkah-langkah berikut:
- Tentukan alamat fungsi yang akan dikaitkan.
- Ganti instruksi pertama fungsi dengan lompat ke rutinitas kait.
- Ketika kait selesai, kembali ke logika asli, yang mencakup menjalankan instruksi asli yang terlantar.
Variasi muncul dari hal-hal seperti:
- Ukuran instruksi pertama: Ada baiknya menggantinya dengan JMP dengan ukuran yang sama atau lebih kecil, untuk menghindari mengganti bagian atas fungsi sementara utas lain mungkin menjalankannya dalam penerbangan.
- Jenis instruksi pertama: Jika instruksi pertama memiliki beberapa sifat PC relatif terhadapnya, merelokasinya mungkin memerlukan perubahan hal-hal seperti bidang perpindahan. Karena instruksi berpotensi meluap ketika dipindahkan ke tempat yang jauh, perubahan ini mungkin memerlukan penyediaan logika yang setara dengan instruksi yang benar-benar berbeda.
Karena semua kompleksitas ini, logika pengait yang kuat dan generik jarang ditemukan. Sering kali, logika yang ada dalam aplikasi hanya dapat mengatasi serangkaian kasus terbatas yang diharapkan aplikasi untuk ditemui dalam API tertentu yang diminatinya. Tidak sulit untuk membayangkan berapa banyak masalah kompatibilitas aplikasi ini. Bahkan perubahan sederhana dalam pengoptimalan kode atau pengkompilasi mungkin membuat aplikasi tidak dapat digunakan jika kode tidak lagi terlihat persis seperti yang diharapkan.
Apa yang akan terjadi pada aplikasi ini jika mereka menemukan kode Arm64 saat menyiapkan hook? Mereka pasti akan gagal.
Fungsi urutan maju cepat (FFS) mengatasi persyaratan kompatibilitas ini di Arm64EC.
FFS adalah fungsi x64 yang sangat kecil yang tidak memiliki logika yang nyata dan berfungsi sebagai pemanggilan akhir ke fungsi Arm64EC sesungguhnya. Fitur ini bersifat opsional tetapi diaktifkan secara default untuk semua ekspor DLL dan untuk fungsi apa pun yang didekorasi dengan __declspec(hybrid_patchable).
Saat kode memperoleh pointer ke fungsi tertentu, baik dengan GetProcAddress dalam kasus ekspor, atau dengan &function dalam kasus __declspec(hybrid_patchable), alamat yang dihasilkan berisi kode x64. Kode x64 tersebut dianggap sebagai fungsi x64 yang sah, memenuhi sebagian besar logika hooking yang saat ini tersedia.
Pertimbangkan contoh berikut (penanganan kesalahan yang dihilangkan untuk brevity):
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);
Nilai penunjuk fungsi dalam pgma variabel berisi alamat GetMachineTypeAttributesFFS.
Contoh ini menunjukkan Urutan Cepat-Maju:
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
Fungsi FFS x64 memiliki prolog dan epilog kanonis, diakhir dengan panggilan ekor (lompat) ke fungsi nyata GetMachineTypeAttributes dalam kode Arm64EC:
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
[...]
Akan sangat tidak efisien jika diperlukan untuk menjalankan lima instruksi x64 yang ditimulasi antara dua fungsi Arm64EC. Fungsi FFS bersifat khusus. Fungsi FFS tidak benar-benar berjalan jika tetap tidak diubah. Pembantu pengecek panggilan secara efisien memastikan bahwa FFS belum diubah. Jika demikian, panggilan akan ditransfer langsung ke tujuan nyata. Jika FFS diubah dengan cara apa pun, maka itu bukan lagi FFS. Transfer eksekusi ke FFS yang diubah dan menjalankan kode mana pun yang mungkin ada, meniru jalur menyimpang dan logika pengait apa pun.
Ketika kait mentransfer eksekusi kembali ke akhir FFS, akhirnya mencapai panggilan-ekor ke kode Arm64EC, yang kemudian dieksekusi setelah kait, persis seperti yang diharapkan oleh aplikasi.
Penulisan Arm64EC dalam bahasa assembly
Header Windows SDK dan kompilator C menyederhanakan pekerjaan penulisan kode assembly Arm64EC. Misalnya, Anda dapat menggunakan compiler C untuk menghasilkan entry dan exit thunks untuk fungsi yang tidak berasal dari kode C.
Pertimbangkan contoh yang setara dengan fungsi fD berikut yang harus Anda tulis dalam assembly (ASM). Kode Arm64EC dan x64 dapat memanggil fungsi ini, dan pfE penunjuk fungsi dapat menunjuk ke kode Arm64EC atau x64.
typedef int (PF_E)(int, double);
extern PF_E * pfE;
int fD(int i, double d) {
return (*pfE)(i, d);
}
Menulis fD di ASM mungkin terlihat seperti kode berikut:
#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
Dalam contoh sebelumnya:
- Arm64EC menggunakan deklarasi prosedur dan makro prolog/epilog yang sama dengan Arm64.
- Membungkus nama fungsi dengan
A64NAMEmakro. Saat Anda mengkompilasi kode C atau C++ sebagai Arm64EC, pengkompilasi menandaiOBJsebagaiARM64ECberisi kode Arm64EC. Penandaan ini tidak terjadi denganARMASM. Ketika Anda mengkompilasi kode ASM, Anda dapat menginformasikan linker bahwa kode yang dihasilkan adalah Arm64EC dengan mengawali nama fungsi dengan#.A64NAMEMakro melakukan operasi ini ketika_ARM64EC_ditentukan dan membiarkan nama tidak berubah ketika_ARM64EC_tidak ditentukan. Pendekatan ini memungkinkan untuk berbagi kode sumber antara Arm64 dan Arm64EC. - Anda harus terlebih dahulu menjalankan pointer fungsi
pfEmelalui pemeriksa panggilan EC, bersama dengan fungsi penundaan keluar yang sesuai, jika fungsi target adalah x64.
Membuat thunks masuk dan keluar
Langkah selanjutnya adalah menghasilkan thunk entri untuk fD dan thunk keluar untuk pfE. Pengkompilasi C dapat melakukan tugas ini dengan upaya minimal dengan menggunakan _Arm64XGenerateThunk kata kunci pengkompilasi.
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;
}
Kata kunci _Arm64XGenerateThunk memberi tahu pengkompilasi C untuk menggunakan deklarasi fungsi, mengabaikan tubuh fungsi, dan menghasilkan thunk keluar (ketika parameter adalah 1) atau thunk entri (ketika parameter adalah 2).
Tempatkan proses pembuatan thunk ke dalam file C terpisah. Berada dalam file terisolasi memudahkan untuk mengonfirmasi nama simbol dengan mencadangkan simbol yang sesuai OBJ atau bahkan pembongkaran.
Thunk entri khusus
SDK menyertakan makro yang membantu Anda membuat "entry thunks" kustom yang ditulis secara manual. Anda dapat menggunakan makro ini saat membuat penyesuaian thunk kustom.
Sebagian besar thunk penyesuaian dihasilkan oleh pengkompilasi C++, tetapi Anda juga dapat membuatnya secara manual. Anda mungkin secara manual menghasilkan penyesuaian thunk ketika panggilan balik generik mentransfer kontrol ke panggilan balik riil, dan salah satu parameter mengidentifikasi panggilan balik riil tersebut.
Contoh berikut menunjukkan thunk penyesuaian dalam kode Arm64 Classic:
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
Dalam contoh ini, parameter pertama memberikan referensi ke struktur. Kode mengambil alamat fungsi target dari elemen struktur ini. Karena struktur dapat ditulis, Control Flow Guard (CFG) harus memvalidasi alamat target.
Contoh berikut menunjukkan cara memindahkan penyesuaian thunk yang setara ke Arm64EC:
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
Kode sebelumnya tidak menyediakan exit thunk (dalam register x10). Pendekatan ini tidak dimungkinkan karena kode dapat dijalankan untuk banyak tanda tangan yang berbeda. Kode ini memanfaatkan pengaturan pemanggil x10 ke thunk keluar. Pemanggil melakukan panggilan dengan menargetkan signature spesifik.
Kode sebelumnya memerlukan thunk entri untuk mengatasi kasus ketika pemanggil adalah kode x64. Contoh berikut menunjukkan cara menulis thunk entri yang sesuai dengan menggunakan makro untuk thunk entri kustom:
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
Tidak seperti fungsi lain, thunk entri ini tidak pernah mentransfer kontrol ke fungsi terkait (thunk penyesuai). Dalam hal ini, entry thunk menyematkan fungsionalitas itu sendiri (melakukan penyesuaian parameter) dan secara langsung mengalihkan kontrol ke target akhir melalui __os_arm64x_x64_jump helper.
Membuat kode Arm64EC secara dinamis (kompilasi JIT)
Dalam proses Arm64EC, ada dua jenis memori yang dapat dieksekusi: kode Arm64EC dan kode x64.
Sistem operasi mengekstrak informasi ini dari biner yang dimuat. Biner x64 adalah semua biner x64, dan Arm64EC berisi tabel rentang untuk halaman kode Arm64EC versus x64.
Bagaimana dengan kode yang dihasilkan secara dinamis? Kompiler just-in-time (JIT) menghasilkan kode pada runtime yang tidak didukung oleh file biner apa pun.
Biasanya, proses ini melibatkan langkah-langkah berikut:
- Mengalokasikan memori bisa-tulis (
VirtualAlloc). - Menghasilkan kode dalam memori yang dialokasikan.
- Melindungi kembali memori dari baca-tulis ke baca-jalankan (
VirtualProtect). - Menambahkan entri fungsi unwind untuk semua fungsi yang dihasilkan non-sepele (non-daun) (
RtlAddFunctionTableatauRtlAddGrowableFunctionTable).
Untuk alasan kompatibilitas sepele, jika aplikasi melakukan langkah-langkah ini dalam proses Arm64EC, sistem operasi mempertimbangkan kode sebagai kode x64. Perilaku ini terjadi untuk proses apa pun yang menggunakan Java Runtime x64 yang tidak dimodifikasi, runtime .NET, mesin JavaScript, dan sebagainya.
Untuk menghasilkan kode dinamis Arm64EC, ikuti proses yang sama dengan dua perbedaan:
- Saat mengalokasikan memori, gunakan yang lebih baru
VirtualAlloc2(bukanVirtualAllocatauVirtualAllocEx) dan berikan atributMEM_EXTENDED_PARAMETER_EC_CODE. - Saat menambahkan entri fungsi:
- Mereka harus dalam format Arm64. Saat mengkompilasi kode Arm64EC, jenisnya
RUNTIME_FUNCTIONcocok dengan format x64. Untuk format Arm64 saat mengkompilasi Arm64EC, gunakan jenis sebagai gantinyaARM64_RUNTIME_FUNCTION. - Jangan gunakan
RtlAddFunctionTableAPI yang lebih lama. Selalu gunakan API yang lebihRtlAddGrowableFunctionTablebaru.
- Mereka harus dalam format Arm64. Saat mengkompilasi kode Arm64EC, jenisnya
Contoh berikut menunjukkan alokasi memori:
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);
Dan contoh berikut menunjukkan cara menambahkan satu entri fungsi unwind:
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