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.

Desain 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.

Arm64EC vs Arm64 Classic ABI

Daftar berikut menunjukkan di mana Arm64EC telah berbeda dari Arm64 Classic ABI.

Ini adalah perubahan kecil jika dilihat dalam perspektif berapa banyak yang didefinisikan seluruh ABI.

Mendaftarkan pemetaan dan register yang diblokir

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, x23x24, ,-x28v16v31 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 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-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 (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, , x1x2, x3. Parameter yang tersisa ditumpahkan ke tumpukan. Ini mengikuti konvensi panggilan variadik x64 dengan tepat, dan berbeda dari Arm64 Classic, di mana register x0->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 sebagai f1(int, double), pada x64, parameter kedua akan ditetapkan ke dan RDXXMM1. Pada Arm64EC, parameter kedua akan ditetapkan ke hanya x1.
  • 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

Penambahan ABI

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:

Thunks Masuk dan Keluar

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 BArm64EC , 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.

Keluar dari Thunks

Menggunakan fungsi contoh fA, fB dan fC di atasnya, ini adalah bagaimana pengkompilasi akan menghasilkan dan fBfC 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 dalam x10.
  • 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 melalui x11.

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: x0x8-, (x15chkstk) dan .q0-q7

Thunks Entri

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 fAsebelumnya , 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.

Thunks Penyesuaian

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

Urutan Maju Cepat

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 GetMachineTypeAttributesFFS.

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.

Penulisan Arm64EC di Assembly

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 menandai OBJ sebagai ARM64EC berisi kode Arm64EC. Ini tidak terjadi dengan ARMASM. 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.

Menghasilkan Entry dan Exit Thunks

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.

Thunk Entri Kustom

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 .

Membuat Kode Dynamically Generating (JIT Compiling) Arm64EC

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 atau RtlAddGrowableFunctionTable).

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 (bukan VirtualAlloc atau VirtualAllocEx) dan berikan MEM_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 gantinya ARM64_RUNTIME_FUNCTION .
    • Jangan gunakan API yang lebih RtlAddFunctionTable lama. Selalu gunakan API yang lebih RtlAddGrowableFunctionTable baru sebagai gantinya.

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