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.

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.

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 x11 alih-alih x15.
  • 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 daftar x0 melalui x7 digunakan.
  • 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 sebagai f1(int, double), pada x64, parameter kedua ditugaskan ke RDX dan XMM1. Pada Arm64EC, parameter kedua ditetapkan hanya ke x1.
  • 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 x4 memuat 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 x5 memuat 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.
  • ull1 adalah bilangan bulat 8-byte. Ini menetapkan ke x1.
  • ull2 adalah bilangan bulat 8-byte. Ini memberikan tugas kepada x2.
  • ull3 adalah bilangan bulat 8-byte. Ini mengarahkan ke x3.

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.
  • ull1 adalah bilangan bulat 8-byte. Ini menetapkan ke x2.
  • ull2 adalah bilangan bulat 8-byte. Ini menetapkan ke x3.
  • ull3 adalah bilangan bulat 8-byte. Ini menetapkan langsung ke tumpukan.
  • x4 memuat lokasi ull3 dalam tumpukan.
  • x5 memuat ukuran ull3.

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 x9 untuk memuat alamat dari fungsi target (x64). Exit Thunk memanggil emulator, melewati x9 tanpa 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:

  • x11 memasok alamat fungsi target untuk dipanggil (fB dalam hal ini). Pada titik ini, pemeriksa panggilan mungkin tidak tahu apakah 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 pemeriksa panggilan tergantung pada apakah fungsi target adalah Arm64EC atau x64.

Jika targetnya adalah Arm64EC:

  • x11 mengembalikan alamat kode Arm64EC untuk dipanggil. Nilai ini mungkin sama dengan nilai yang disediakan.

Jika targetnya adalah kode x64:

  • x11 mengembalikan alamat Exit Thunk. Alamat ini disalin dari input yang disediakan dalam x10.
  • x10 mengembalikan alamat Exit Thunk tanpa terganggu dari input.
  • x9 mengembalikan fungsi x64 yang ditargetkan. Nilai ini mungkin sama dengan yang disediakan melalui x11.

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 A64NAME makro. Saat Anda mengkompilasi kode C atau C++ sebagai Arm64EC, pengkompilasi menandai OBJ sebagai ARM64EC berisi kode Arm64EC. Penandaan ini tidak terjadi dengan ARMASM. Ketika Anda mengkompilasi kode ASM, Anda dapat menginformasikan linker bahwa kode yang dihasilkan adalah Arm64EC dengan mengawali nama fungsi dengan #. A64NAME Makro 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 pfE melalui 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) (RtlAddFunctionTable atau RtlAddGrowableFunctionTable).

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 (bukan VirtualAlloc atau VirtualAllocEx) dan berikan atribut MEM_EXTENDED_PARAMETER_EC_CODE.
  • Saat menambahkan entri fungsi:
    • Mereka harus dalam format Arm64. Saat mengkompilasi kode Arm64EC, jenisnya RUNTIME_FUNCTION cocok dengan format x64. Untuk format Arm64 saat mengkompilasi Arm64EC, gunakan jenis sebagai gantinya ARM64_RUNTIME_FUNCTION .
    • Jangan gunakan RtlAddFunctionTable API yang lebih lama. Selalu gunakan API yang lebih RtlAddGrowableFunctionTable baru.

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)
);