Memahami Arm64EC ABI dan kode rakitan
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.
Tujuan dari dokumen ini adalah untuk 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 rakitan yang menargetkan ABI Arm64EC.
Arm64EC dirancang untuk 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. Sangat sedikit dari ABI Klasik diubah, tetapi bagian ditambahkan untuk mengaktifkan interoperabilitas x64.
Dalam dokumen ini, ABI Arm64 standar asli akan disebut sebagai "ABI Klasik". Ini menghindari ambiguitas yang melekat pada istilah yang kelebihan beban seperti "Asli". Arm64EC, untuk menjadi jelas, adalah setiap bit sebagai asli sebagai ABI asli.
Daftar berikut menunjukkan di mana Arm64EC telah berbeda dari Arm64 Classic ABI.
- Mendaftarkan pemetaan dan register yang diblokir
- Pemeriksa panggilan
- Pemeriksa tumpukan
- Konvensi panggilan variadik
Ini adalah perubahan kecil jika dilihat dalam perspektif berapa banyak yang didefinisikan seluruh ABI.
Agar ada interoperabilitas tingkat jenis dengan kode x64, kode Arm64EC dikompilasi dengan definisi arsitektur pra-prosesor 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.
Struktur ini harus digunakan untuk mewakili konteks CPU saat menjalankan kode x64, serta kode Arm64EC. Kode yang ada tidak akan memahami konsep baru, seperti set register CPU yang berubah dari fungsi ke fungsi. Jika struktur x64 CONTEXT
digunakan untuk mewakili status eksekusi Arm64, ini menyiratkan register Arm64 secara efektif dipetakan ke dalam register x64.
Ini juga menyiratkan bahwa setiap register Arm64 yang tidak dapat dimasukkan ke dalam x64 CONTEXT
tidak boleh digunakan, karena nilainya dapat hilang kapan saja operasi menggunakan CONTEXT
terjadi (dan beberapa dapat asinkron dan tidak terduga, seperti operasi Pengumpulan Sampah dari Runtime Bahasa Terkelola, atau APC).
Aturan pemetaan antara register Arm64EC dan x64 diwakili oleh ARM64EC_NT_CONTEXT
struktur di header Windows, yang ada di SDK. Struktur ini pada dasarnya adalah persatuan CONTEXT
struktur, persis seperti yang didefinisikan untuk x64, tetapi dengan overlay register Arm64 tambahan.
Misalnya, RCX
peta ke X0
, RDX
ke X1
, RSP
ke SP
, RIP
ke PC
, dll. Kita juga dapat melihat bagaimana register x13
, , x14
, x23
x24
, ,-x28
v16
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 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 kode dikompilasi dengan opsi /guard:cf
pengkompilasi akan menghasilkan panggilan tambahan ke fungsi pemeriksa tepat sebelum setiap panggilan/lompat tidak langsung. Fungsi pemeriksa itu sendiri disediakan oleh Windows dan, untuk CFG, ia melakukan pemeriksaan validitas terhadap target panggilan yang dikenal baik. Informasi ini juga termasuk dalam biner yang dikompilasi dengan /guard:cf
.
Ini adalah contoh 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 akan kembali jika target valid, atau proses yang gagal 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
x11
alih-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.
__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
.
Arm64EC mengikuti konvensi panggilan ABI Arm64 Klasik, kecuali untuk fungsi Variadik (alias vararg, alias fungsi dengan kata kunci parameter elipsis (. . .).
Untuk kasus spesifik variadik, Arm64EC mengikuti konvensi panggilan yang sangat mirip dengan variadik x64, dengan hanya beberapa perbedaan. Di bawah ini adalah aturan utama untuk variadik Arm64EC:
- Hanya 4 register pertama yang digunakan untuk parameter yang melewati:
x0
, ,x1
x2
,x3
. Parameter yang tersisa ditumpahkan ke tumpukan. Ini mengikuti konvensi panggilan variadik x64 dengan tepat, dan berbeda dari Arm64 Classic, di mana registerx0
->x7
digunakan. - Parameter Floating Point / SIMD yang diteruskan oleh register akan menggunakan register General-Purpose, bukan SIMD. Ini mirip dengan Arm64 Classic, dan berbeda dari x64, di mana parameter FP/SIMD diteruskan dalam register Tujuan Umum dan SIMD. Misalnya, untuk fungsi
f1(int, …)
yang disebut sebagaif1(int, double)
, pada x64, parameter kedua akan ditetapkan ke danRDX
XMM1
. Pada Arm64EC, parameter kedua akan ditetapkan ke hanyax1
. - Saat meneruskan struktur berdasarkan nilai melalui register, aturan ukuran x64 berlaku: Struktur dengan ukuran tepat 1, 2, 4 dan 8 akan dimuat langsung ke dalam register Tujuan Umum. Struktur dengan ukuran lain ditumpahkan ke tumpukan, dan penunjuk ke lokasi tumpahan ditetapkan ke register. Ini pada dasarnya menurunkan nilai menurut referensi, pada tingkat rendah. Pada ABI Arm64 Klasik, struktur dengan ukuran apa pun hingga 16 byte ditetapkan langsung ke register Tujuan Umum.
- Register X4 dimuat dengan pointer ke parameter pertama yang diteruskan melalui tumpukan (parameter ke-5). Ini tidak termasuk struktur yang tumpah karena pembatasan ukuran yang diuraikan di atas.
- Register X5 dimuat dengan ukuran, dalam byte, dari semua parameter yang diteruskan oleh tumpukan (ukuran semua parameter, dimulai dengan yang ke-5). Ini tidak termasuk struktur yang diteruskan oleh nilai yang ditumpahkan karena pembatasan ukuran yang diuraikan di atas.
Dalam contoh berikut: pt_nova_function
di bawah ini mengambil parameter dalam bentuk non-variadik, oleh karena itu 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 5 parameter yang akan ditetapkan mengikuti aturan konvensi panggilan Arm64 Klasik:
- 'f' adalah ganda. Ini akan ditetapkan ke d0.
- 'tc' adalah struct, dengan ukuran 3 byte. Ini akan ditetapkan ke x0.
- ull1 adalah bilangan bulat 8-byte. Ini akan ditetapkan ke x1.
- ull2 adalah bilangan bulat 8-byte. Ini akan ditetapkan ke x2.
- ull3 adalah bilangan bulat 8-byte. Ini akan ditetapkan ke x3.
pt_va_function
adalah fungsi variadik, sehingga akan mengikuti aturan variadik Arm64EC yang diuraikan di atas:
- 'f' adalah ganda. Ini akan ditetapkan ke x0.
- 'tc' adalah struct, dengan ukuran 3 byte. Ini akan ditumpahkan ke tumpukan dan lokasinya dimuat ke x1.
- ull1 adalah bilangan bulat 8-byte. Ini akan ditetapkan ke x2.
- ull2 adalah bilangan bulat 8-byte. Ini akan ditetapkan ke x3.
- ull3 adalah bilangan bulat 8-byte. Ini akan ditetapkan langsung ke tumpukan.
- x4 dimuat dengan lokasi ull3 di tumpukan.
- x5 dimuat dengan ukuran ull3.
Berikut ini menunjukkan kemungkinan output kompilasi untuk pt_nova_function
, yang mengilustrasikan perbedaan penetapan parameter yang diuraikan di atas.
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
Untuk mencapai interoperabilitas transparan dengan kode x64, banyak penambahan telah dilakukan pada ABI Arm64 Klasik. Mereka menangani perbedaan konvensi panggilan antara Arm64EC dan x64.
Daftar berikut ini mencakup penambahan ini:
Entry dan Exit Thunks mengurus penerjemahan konvensi panggilan Arm64EC (sebagian besar sama dengan Classic Arm64) ke dalam konvensi panggilan x64, dan sebaliknya.
Kesalahpahaman umum adalah bahwa konvensi panggilan dapat dikonversi 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. Konsekuensinya adalah bahwa terjemahan antara ARI akan khusus untuk setiap tanda tangan fungsi, bervariasi dengan jenis setiap parameter.
Pertimbangkan fungsi berikut:
int fJ(int a, int b, int c, int d);
Penetapan parameter akan 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 akan 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 jenis parameter sebelumnya dalam daftar tergantung pada. Detail ini diilustrasikan oleh parameter ke-3. Dalam kedua fungsi, jenis parameter adalah "int", tetapi terjemahan yang dihasilkan berbeda.
Thunk Masuk dan Keluar ada karena alasan ini dan khusus disesuaikan untuk setiap tanda tangan fungsi individu.
Kedua jenis thunks adalah, sendiri, fungsi. Entry Thunks secara otomatis dipanggil oleh emulator ketika fungsi x64 memanggil ke fungsi Arm64EC (eksekusi Memasuki Arm64EC). Exit Thunks secara otomatis dipanggil oleh pemeriksa panggilan ketika fungsi Arm64EC memanggil ke fungsi x64 (eksekusi Keluar arm64EC).
Saat mengkompilasi kode Arm64EC, pengkompilasi akan menghasilkan Entry Thunk untuk setiap fungsi Arm64EC, yang cocok dengan tanda tangannya. Pengkompilasi juga akan menghasilkan Exit Thunk untuk setiap fungsi panggilan 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 di atas yang menargetkan Arm64EC, pengkompilasi akan menghasilkan:
- Kode untuk 'fA'.
- Entry Thunk untuk 'fA'
- Keluar dari Thunk untuk 'fB'
- Keluar dari Thunk untuk 'fC'
fA
Entry Thunk dihasilkan dalam kasus fA
dan dipanggil dari kode x64. Exit Thunks untuk fB
dan fC
dihasilkan dalam kasus fB
dan/atau fC
dan ternyata kode x64.
Exit Thunk yang sama dapat dihasilkan beberapa kali, mengingat pengkompilasi akan menghasilkannya di situs panggilan daripada fungsi itu sendiri. Ini dapat mengakibatkan sejumlah besar thunk redundan sehingga, pada kenyataannya, kompilator akan menerapkan aturan pengoptimalan sepele untuk memastikan hanya thunk yang diperlukan yang masuk ke biner akhir.
Misalnya, dalam biner di mana fungsi A
Arm64EC memanggil fungsi B
Arm64EC , B
tidak diekspor dan alamatnya tidak pernah diketahui di luar A
. Aman untuk menghilangkan Exit Thunk dari A
ke B
, bersama dengan Entry Thunk untuk B
. Ini juga aman untuk alias bersama-sama semua thunk Exit dan Entry yang berisi kode yang sama, bahkan jika mereka dihasilkan untuk fungsi yang berbeda.
Menggunakan fungsi contoh fA
, fB
dan fC
di atasnya, ini adalah bagaimana pengkompilasi akan menghasilkan dan fB
fC
Exit Thunks:
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 ini fB
, kita dapat melihat bagaimana kehadiran parameter 'ganda' akan menyebabkan penetapan pendaftaran GP yang tersisa untuk di-reshuffle, hasil dari aturan penugasan arm64 dan x64 yang berbeda. Kita juga dapat melihat x64 hanya menetapkan 4 parameter untuk mendaftar, sehingga parameter ke-5 harus ditumpahkan ke tumpukan.
Dalam kasus ini fC
, parameter kedua adalah struktur dengan panjang 3 byte. Arm64 akan memungkinkan struktur ukuran apa pun untuk ditetapkan ke register secara langsung. x64 hanya memungkinkan ukuran 1, 2, 4 dan 8. Exit Thunk ini kemudian harus mentransfer ini struct
dari register ke tumpukan dan menetapkan pointer ke register sebagai gantinya. Ini masih menggunakan satu register (untuk membawa pointer) sehingga tidak mengubah tugas untuk register yang tersisa: tidak ada perombakan register yang terjadi untuk parameter ke-3 dan ke-4. Sama seperti untuk kasus ini fB
, parameter ke-5 harus ditumpahkan ke tumpukan.
Pertimbangan tambahan untuk Exit Thunks:
- Pengkompilasi akan menamainya bukan dengan nama fungsi yang mereka terjemahkan dari-ke>, melainkan tanda tangan yang mereka alamat. Ini membuatnya lebih mudah untuk menemukan redundansi.
- Exit Thunk dipanggil dengan register
x9
yang membawa alamat fungsi target (x64). Ini diatur oleh pemeriksa panggilan dan melewati Exit Thunk, tidak terganggu, ke emulator.
Setelah mengatur ulang parameter, Exit Thunk kemudian memanggil ke emulator melalui __os_arm64x_dispatch_call_no_redirect
.
Layak, pada titik ini, meninjau fungsi pemeriksa panggilan, dan detail tentang ABI kustomnya sendiri. Inilah yang akan terlihat seperti 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:
x11
memasok alamat fungsi target untuk dipanggil (fB
dalam hal ini). Mungkin tidak diketahui, pada titik ini, jika fungsi target adalah Arm64EC atau x64.x10
memasok Exit Thunk yang cocok dengan tanda tangan fungsi yang dipanggil (fB
dalam hal ini).
Data yang dikembalikan oleh pemeriksa panggilan akan bergantung pada fungsi target adalah Arm64EC atau x64.
Jika targetnya adalah Arm64EC:
x11
akan mengembalikan alamat kode Arm64EC untuk dipanggil. Ini mungkin atau mungkin bukan nilai yang sama dengan yang disediakan.
Jika targetnya adalah kode x64:
x11
akan mengembalikan alamat Exit Thunk. Ini disalin dari input yang disediakan dalamx10
.x10
akan mengembalikan alamat Exit Thunk, yang tidak terganggu dari input.x9
akan mengembalikan fungsi target x64. Ini mungkin atau mungkin bukan nilai yang sama dengan yang disediakan melaluix11
.
Pemeriksa panggilan akan selalu membiarkan parameter konvensi panggilan mendaftar tidak terganggu, sehingga kode panggilan harus mengikuti panggilan ke pemeriksa panggilan segera dengan blr x11
(atau br x11
dalam kasus panggilan ekor). Ini adalah daftar pemeriksa panggilan. Mereka akan selalu mempertahankan register non-volatil standar di atas dan di luar standar: x0
x8
-, (x15
chkstk
) dan .q0
-q7
Entry Thunks mengurus transformasi yang diperlukan dari konvensi panggilan x64 ke Arm64. Ini, pada dasarnya, kebalikan Exit Thunks tetapi ada beberapa aspek lagi yang perlu dipertimbangkan.
Pertimbangkan contoh kompilasi fA
sebelumnya , Entry Thunk dihasilkan sehingga fA
dapat dipanggil oleh kode x64.
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
Alamat fungsi target disediakan oleh emulator di x9
.
Sebelum memanggil Entry Thunk, emulator x64 akan memunculkan alamat pengembalian dari tumpukan ke LR
dalam register. Kemudian diharapkan bahwa LR
akan menunjuk pada kode x64 ketika kontrol ditransfer ke Entry Thunk.
Emulator juga dapat 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 ditandatangani dapat tanpa disadari tanpa batas waktu, sampai beberapa instruksi penyelarasan 16 byte digunakan (beberapa instruksi SSE lakukan) atau kode Arm64EC dipanggil.
Untuk mengatasi potensi masalah kompatibilitas ini, sebelum memanggil Entry Thunk, emulator akan 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 pendaftaran 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 pendaftaran SIMD ini, Entry Thunks harus secara eksplisit menyimpan semua register SIMD yang dianggap non-volatil di x64. Ini hanya diperlukan pada Entry Thunks (bukan Exit Thunks) karena x64 lebih ketat daripada Arm64. Dengan kata lain, daftarkan aturan penyimpanan/pelestarian di x64 melebihi persyaratan Arm64 dalam semua kasus.
Untuk mengatasi pemulihan yang benar dari nilai register ini saat melepas stack (misalnya setjmp + longjmp, atau throw + catch), 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 Qn
operasi register spills/fill di atas. save_any_reg
kompatibel dengan save_next_pair (0xE6)
.
Sebagai referensi, di bawah ini adalah informasi unwind yang sesuai milik Entry Thunk yang disajikan di atas:
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 kembali, __os_arm64x_dispatch_ret
rutinitas digunakan untuk memasukkan kembali emulator, kembali ke kode x64 (ditujukkan oleh LR
).
Fungsi Arm64EC memiliki 4 byte sebelum instruksi pertama dalam fungsi yang disediakan untuk menyimpan informasi yang akan digunakan pada runtime. Dalam 4 byte inilah alamat relatif Entry Thunk untuk fungsi dapat ditemukan. Saat melakukan panggilan dari fungsi x64 ke fungsi Arm64EC, emulator akan membaca 4 byte sebelum memulai fungsi, menutupi dua bit yang lebih rendah dan menambahkan jumlah tersebut ke alamat fungsi. Ini akan menghasilkan alamat Entry Thunk untuk dipanggil.
Adjustor Thunks adalah fungsi tanpa tanda tangan yang hanya mentransfer kontrol ke (tail-call) fungsi lain, setelah melakukan beberapa transformasi ke salah satu parameter. Jenis parameter yang diubah diketahui, tetapi semua parameter yang tersisa dapat berupa apa pun dan, dalam angka apa pun - Adjustor Thunks tidak akan menyentuh register apa pun yang berpotensi memegang parameter dan tidak akan menyentuh tumpukan. Inilah yang membuat Fungsi tanpa tanda tangan Adjustor Thunks.
Adjustor Thunks dapat dihasilkan secara otomatis oleh pengkompilasi. Ini umum, misalnya, dengan C++ multi-warisan, di mana metode virtual apa pun dapat didelegasikan ke kelas induk, tidak dimodifikasi, selain dari penyesuaian ke this
penunjuk.
Di bawah ini adalah contoh 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
Beberapa aplikasi membuat modifikasi run-time pada fungsi yang berada di biner yang tidak mereka miliki tetapi bergantung pada - biasanya mengoperasikan biner sistem - untuk tujuan memutar eksekusi ketika fungsi dipanggil. Ini juga dikenal sebagai kait.
Pada tingkat tinggi, proses kaitnya sederhana. Namun, secara rinci, pengait adalah arsitektur khusus dan cukup kompleks mengingat variasi potensial yang harus ditangani logika pengait.
Secara umum, prosesnya melibatkan hal-hal 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 ke-1: Adalah ide yang baik untuk menggantinya dengan JMP yang ukurannya 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 kemungkinan akan meluap ketika instruksi dipindahkan ke tempat yang jauh, ini mungkin memerlukan penyediaan logika yang setara dengan instruksi yang berbeda sama sekali.
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 dapat 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 kait? Mereka pasti akan gagal.
Fungsi Fast-Forward Sequence (FFS) mengatasi persyaratan kompatibilitas ini di Arm64EC.
FFS adalah fungsi x64 yang sangat kecil yang tidak berisi logika nyata dan panggilan ekor ke fungsi Arm64EC nyata. Mereka opsional tetapi diaktifkan secara default untuk semua ekspor DLL dan untuk fungsi apa pun yang dihiasi dengan __declspec(hybrid_patchable)
.
Untuk kasus ini, ketika kode mendapatkan penunjuk ke fungsi tertentu, baik dengan GetProcAddress
dalam kasus ekspor, atau dengan &function
dalam __declspec(hybrid_patchable)
kasus ini, alamat yang dihasilkan akan berisi kode x64. Kode x64 itu akan diteruskan untuk fungsi x64 yang sah, memuaskan sebagian besar logika kait 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 akan berisi alamat GetMachineTypeAttributes
FFS.
Ini adalah contoh Urutan Maju Cepat:
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 5 instruksi x64 yang ditimulasi antara dua fungsi Arm64EC. Fungsi FFS bersifat khusus. Fungsi FFS tidak benar-benar berjalan jika tetap tidak diubah. Pembantu pemeriksa panggilan akan memeriksa secara efisien apakah FFS belum diubah. Jika demikian, panggilan akan ditransfer langsung ke tujuan nyata. Jika FFS telah diubah dengan cara apa pun, maka itu tidak akan lagi menjadi FFS. Eksekusi akan ditransfer ke FFS yang diubah dan menjalankan kode mana pun yang mungkin ada di sana, meniru memutar dan logika pengait apa pun.
Ketika kait mentransfer eksekusi kembali ke akhir FFS, itu akhirnya akan mencapai panggilan ekor ke kode Arm64EC, yang kemudian akan dijalankan setelah kait, seperti yang diharapkan aplikasi.
Header Windows SDK dan pengkompilasi C dapat menyederhanakan pekerjaan penulisan rakitan Arm64EC. Misalnya, pengkompilasi C dapat digunakan untuk menghasilkan Entry dan Exit Thunks untuk fungsi yang tidak dikompilasi dari kode C.
Pertimbangkan contoh yang setara dengan fungsi fD
berikut yang harus ditulis di Assembly (ASM). Fungsi ini dapat dipanggil oleh kode Arm64EC dan x64 dan pfE
penunjuk fungsi juga dapat mengarah 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 akan terlihat seperti:
#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 di atas:
- Arm64EC menggunakan deklarasi prosedur dan makro prolog/epilog yang sama dengan Arm64.
- Nama fungsi harus dibungkus oleh
A64NAME
makro. Saat mengkompilasi kode C/C++ sebagai Arm64EC, pengkompilasi menandaiOBJ
sebagaiARM64EC
berisi kode Arm64EC. Ini tidak terjadi denganARMASM
. Saat mengkompilasi kode ASM ada cara alternatif untuk menginformasikan linker bahwa kode yang dihasilkan adalah Arm64EC. Ini dengan awalan nama fungsi dengan#
.A64NAME
Makro melakukan operasi ini ketika_ARM64EC_
didefinisikan dan membiarkan nama tidak berubah ketika_ARM64EC_
tidak ditentukan. Hal ini memungkinkan untuk berbagi kode sumber antara Arm64 dan Arm64EC. - Penunjuk
pfE
fungsi harus terlebih dahulu dijalankan melalui pemeriksa panggilan EC, bersama dengan Exit Thunk yang sesuai, jika fungsi target adalah x64.
Langkah selanjutnya adalah menghasilkan Entry Thunk untuk fD
dan Exit Thunk untuk pfE
. Pengkompilasi C dapat melakukan tugas ini dengan upaya minimal, 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 _Arm64XGenerateThunk
kunci memberi tahu pengkompilasi C untuk menggunakan tanda tangan fungsi, mengabaikan isi, dan menghasilkan Exit Thunk (ketika parameter adalah 1) atau Entry Thunk (ketika parameter adalah 2).
Disarankan untuk menempatkan pembuatan thunk dalam file C-nya sendiri. Berada dalam file terisolasi memudahkan untuk mengonfirmasi nama simbol dengan mencadangkan simbol yang sesuai OBJ
atau bahkan pembongkaran.
Makro telah ditambahkan ke SDK untuk membantu penulisan kustom, kode tangan, Entry Thunks. Satu kasus di mana ini dapat digunakan adalah saat menulis Adjustor Thunks kustom.
Sebagian besar Adjustor Thunks dihasilkan oleh pengkompilasi C++, tetapi juga dapat dihasilkan secara manual. Ini dapat ditemukan dalam kasus di mana panggilan balik generik mentransfer kontrol ke panggilan balik nyata, yang diidentifikasi oleh salah satu parameter.
Di bawah ini adalah contoh 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, alamat fungsi target diambil dari elemen struktur, disediakan oleh referensi, melalui parameter ke-1. Karena struktur dapat ditulis, alamat target harus divalidasi melalui Control Flow Guard (CFG).
Contoh di bawah ini menunjukkan bagaimana tampilan Adjustor Thunk yang setara saat di-port 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 di atas tidak menyediakan Exit Thunk (dalam register x10). Ini tidak dimungkinkan karena kode dapat dijalankan untuk banyak tanda tangan yang berbeda. Kode ini memanfaatkan pemanggil yang telah mengatur x10 ke Exit Thunk. Pemanggil akan melakukan panggilan yang menargetkan tanda tangan eksplisit.
Kode di atas memang memerlukan Entry Thunk untuk mengatasi kasus ketika pemanggil adalah kode x64. Ini adalah cara menulis Entry Thunk yang sesuai, menggunakan makro untuk Entry Thunks 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, Entry Thunk ini akhirnya tidak mentransfer kontrol ke fungsi terkait (Adjustor Thunk). Dalam hal ini, fungsionalitas itu sendiri (melakukan penyesuaian parameter) disematkan ke dalam Entry Thunk dan kontrol ditransfer langsung ke target akhir, melalui pembantu __os_arm64x_x64_jump
.
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 semuanya x64 dan Arm64EC berisi tabel rentang untuk halaman kode Arm64EC vs 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 ini menyiratkan:
- Mengalokasikan memori bisa-tulis (
VirtualAlloc
). - Menghasilkan kode ke dalam memori yang dialokasikan.
- Melindungi kembali memori dari Baca-Tulis ke Baca-Jalankan (
VirtualProtect
). - Tambahkan entri fungsi unwind untuk semua fungsi yang dihasilkan non-sepele (non-daun) (
RtlAddFunctionTable
atauRtlAddGrowableFunctionTable
).
Untuk alasan kompatibilitas sepele, aplikasi apa pun yang melakukan langkah-langkah ini dalam proses Arm64EC akan mengakibatkan kode dianggap kode x64. Ini akan terjadi untuk proses apa pun menggunakan Java Runtime x64 yang tidak dimodifikasi, runtime .NET, mesin JavaScript, dll.
Untuk menghasilkan kode dinamis Arm64EC, prosesnya sebagian besar sama hanya dengan dua perbedaan:
- Saat mengalokasikan memori, gunakan yang lebih
VirtualAlloc2
baru (bukanVirtualAlloc
atauVirtualAllocEx
) dan berikanMEM_EXTENDED_PARAMETER_EC_CODE
atribut . - Saat menambahkan entri fungsi:
- Mereka harus dalam format Arm64. Saat mengkompilasi kode Arm64EC, jenisnya
RUNTIME_FUNCTION
akan cocok dengan format x64. Untuk format Arm64 saat mengkompilasi Arm64EC, gunakan jenis sebagai gantinyaARM64_RUNTIME_FUNCTION
. - Jangan gunakan API yang lebih
RtlAddFunctionTable
lama. Selalu gunakan API yang lebihRtlAddGrowableFunctionTable
baru sebagai gantinya.
- Mereka harus dalam format Arm64. Saat mengkompilasi kode Arm64EC, jenisnya
Di bawah ini adalah contoh 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 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)
);
Umpan balik Windows on Arm
Windows on Arm adalah proyek sumber terbuka. Pilih tautan untuk memberikan umpan balik: