Dalam
artikel sebelumnya, saya berjanji untuk mengungkapkan secara lebih rinci beberapa detail yang saya abaikan selama investigasi [Gmail hang di Chrome di Windows - kira-kira. Per.], Termasuk tabel halaman, kunci, WMI, dan kesalahan vmmap. Sekarang saya mengisi celah ini bersama dengan contoh kode yang diperbarui. Tapi pertama-tama, jelaskan esensi secara singkat.
Intinya adalah bahwa proses yang mendukung
Control Flow Guard (CFG) mengalokasikan memori yang dapat dieksekusi, sementara juga mengalokasikan memori CFG yang tidak pernah dibebaskan oleh Windows. Oleh karena itu, jika Anda terus mengalokasikan dan membebaskan memori yang dapat dieksekusi
di alamat yang berbeda , proses mengakumulasikan jumlah memori CFG yang sewenang-wenang. Browser Chrome melakukan ini, yang mengarah pada kebocoran memori yang hampir tidak terbatas dan membeku di beberapa mesin.
Perlu dicatat bahwa pembekuan sulit untuk dihindari jika VirtualAlloc mulai berjalan lebih dari satu juta kali lebih lambat dari biasanya.
Selain CFG, ada memori lain yang terbuang, meskipun tidak sebanyak klaim vmmap.
CFG dan halaman
Memori program dan memori CFG pada akhirnya dialokasikan dengan halaman 4 kilobyte (lebih lanjut tentang ini nanti). Karena 4 KB memori CFG dapat menggambarkan 256 KB memori program (lebih lanjut tentang itu nanti), ini berarti bahwa jika Anda memilih blok memori 256 KB selaras dengan 256 KB, Anda akan mendapatkan satu halaman CFG 4 KB. Dan jika Anda mengalokasikan blok 4 KB yang dapat dieksekusi, Anda masih akan mendapatkan halaman 4 KB CFG, tetapi sebagian besar tidak akan digunakan.

Semuanya lebih rumit jika memori yang dapat dieksekusi dibebaskan. Jika Anda menggunakan fungsi VirtualFree pada blok memori yang dapat dieksekusi yang bukan kelipatan 256 KB atau tidak sejajar pada 256 KB, maka OS harus melakukan beberapa analisis dan memverifikasi bahwa beberapa memori yang dapat dieksekusi lainnya tidak menggunakan halaman CFG. Para penulis CFG memutuskan untuk tidak repot - dan selamanya meninggalkan memori CFG yang dialokasikan. Sangat disayangkan. Ini berarti bahwa ketika program pengujian saya mengalokasikan dan kemudian membebaskan 1 gigabyte memori yang dapat dieksekusi selaras, ia menyisakan 16 MB memori CFG.
Dalam praktiknya, ternyata ketika mesin JavaScript Chrome mengalokasikan dan kemudian melepaskan 128 MB memori yang dapat dieksekusi yang selaras (tidak semua itu digunakan, tetapi seluruh rentang dialokasikan dan segera dibebaskan), maka memori CFG hingga 2 MB akan tetap dialokasikan, meskipun sepele untuk membebaskannya seluruhnya . Karena Chrome berulang kali mengalokasikan dan membebaskan memori pada alamat acak, ini mengarah pada masalah yang dijelaskan di atas.
Memori hilang tambahan
Dalam OS modern apa pun, setiap proses mendapatkan ruang alamat memori virtualnya sendiri, sehingga OS mengisolasi proses dan melindungi memori. Ini dilakukan dengan menggunakan
unit manajemen memori (MMU) dan
tabel halaman . Memori dibagi menjadi 4 halaman KB. Ini adalah jumlah memori minimum yang diberikan OS kepada Anda. Setiap halaman ditandai dengan catatan delapan byte dalam tabel halaman, dan catatan itu sendiri disimpan dalam halaman 4 KB. Masing-masing dari mereka menunjuk maksimum 512 halaman memori yang berbeda, jadi kita membutuhkan hierarki tabel halaman. Untuk ruang alamat 48-bit dalam sistem operasi 64-bit, sistemnya adalah sebagai berikut:
- Tabel Level 1 mencakup 256 TB (48 bit), menunjuk ke 512 tabel level 2 halaman berbeda
- Setiap tabel level 2 mencakup 512 GB, menunjuk ke tabel level 512
- Setiap tabel level 3 mencakup 1 GB, menunjuk ke tabel level 512 4
- Setiap tabel level 4 mencakup 2 MB, menunjuk ke 512 halaman fisik
MMU mengindeks tabel level 1 di 9 (dari 48) bit alamat pertama, tabel level 2 di 9 bit berikutnya, dan level yang tersisa diberikan 9 bit, yaitu, hanya 36 bit. 12 bit yang tersisa digunakan untuk mengindeks halaman 4 kilobyte dari tabel tingkat 4. Baik, baik.
Jika Anda segera mengisi semua level tabel, maka Anda membutuhkan lebih dari 512 GB RAM, sehingga mereka diisi seperlunya. Ini berarti bahwa ketika mengalokasikan halaman memori, OS memilih beberapa tabel halaman - dari nol hingga tiga, tergantung pada apakah alamat yang dialokasikan berada di area 2 MB yang sebelumnya tidak digunakan, area yang sebelumnya tidak digunakan sebesar 1 GB atau area yang sebelumnya tidak digunakan sebesar 512 GB (tabel level 1 halaman) selalu menonjol).
Singkatnya, pengalokasian ke alamat acak jauh lebih mahal daripada pengalokasian ke alamat terdekat, karena dalam kasus halaman pertama tabel tidak dapat dibagikan. Kebocoran CFG jarang terjadi, jadi ketika
vmmap menunjukkan
412.480 KB tabel halaman yang digunakan di Chrome, saya berasumsi angkanya benar. Berikut ini adalah screenshot vmmap dengan tata letak memori chrome.exe dari artikel sebelumnya, tetapi dengan baris Tabel Tabel:

Tapi sepertinya ada yang salah. Saya memutuskan untuk menambahkan simulator tabel halaman ke alat
VirtualScan saya. Ini menghitung berapa banyak halaman dari tabel halaman yang dibutuhkan untuk semua memori yang dialokasikan selama proses pemindaian. Anda hanya perlu memindai memori yang dialokasikan, menambahkan ke counter satu setiap kelipatan dari 2 MB, 1 GB atau 512 GB.
Dengan cepat ditemukan bahwa hasil simulator sesuai dengan vmmap pada proses normal, tetapi tidak pada proses dengan sejumlah besar memori CFG. Perbedaannya kira-kira sesuai dengan memori CFG yang dialokasikan. Untuk proses di atas, di mana vmmap berbicara tentang 402.8 MB (412.480 KB) dari tabel halaman, alat saya menunjukkan 67,7 MB.
Memindai waktu, Berkomitmen, tabel halaman, blok berkomitmen
Total: 41,763s, 1457,7 MiB, 67,7 MiB, 32112, 98 blok kode
CFG: 41,759s, 353,3 MiB, 59,2 MiB, 24866
Saya memverifikasi kesalahan vmmap dengan menjalankan
VAllocStress , yang dalam pengaturan default menyebabkan Windows mengalokasikan 2 gigabytes memori CFG. vmmap mengklaim telah mengalokasikan 2 gigabita tabel halaman:

Dan ketika saya menyelesaikan proses melalui Task Manager, vmmap menunjukkan bahwa jumlah memori yang dialokasikan berkurang hanya 2 gigabytes. Jadi, vmmap salah, perhitungan saya dengan tabel halaman sudah benar, dan setelah
diskusi bermanfaat
di Twitter, saya mengirim laporan tentang kesalahan vmmap, yang harus diperbaiki. Memori CFG masih mengkonsumsi banyak entri tabel halaman (59,2 MB dalam contoh di atas), tetapi tidak sebanyak yang dikatakan vmmap, dan setelah memperbaikinya praktis tidak akan menghabiskan apa pun.
Apa itu CFG dan CFG?
Saya ingin mundur sedikit dan memberi tahu lebih detail apa itu CFG.
CFG adalah singkatan dari Control Flow Guard. Ini adalah metode perlindungan terhadap eksploitasi dengan menulis ulang pointer fungsi. Dengan CFG diaktifkan, kompiler dan OS bersama-sama memeriksa validitas target cabang. Pertama, byte kontrol CFG yang sesuai dimuat dari area CFG 2 TB yang dipesan. Proses 64-bit di Windows mengelola ruang alamat 128 TB, jadi membagi alamat dengan 64 memungkinkan Anda menemukan byte CFG yang sesuai untuk objek ini.
uint8_t cfg_byte = cfg_base[size_t(target_addr) / 64];
Kami sekarang memiliki satu byte yang harus menjelaskan alamat mana dalam rentang 64-byte yang merupakan target cabang yang valid. Untuk melakukan ini, CFG memperlakukan byte sebagai empat nilai dua bit, yang masing-masing sesuai dengan rentang 16 byte. Angka dua bit ini (yang nilainya dari nol hingga tiga)
ditafsirkan sebagai berikut :
- 0 - semua target dalam blok 16-byte ini adalah target tidak sah dari cabang tidak langsung
- 1 - alamat awal dalam blok 16-byte ini adalah target yang valid dari cabang tidak langsung
- 2 - terkait dengan panggilan CFG "ditekan" ; alamat berpotensi tidak valid
- 3 - alamat yang tidak selaras dalam blok 16-byte ini adalah target yang valid dari cabang tidak langsung, namun alamat rata 16-byte berpotensi tidak valid
Jika target cabang tidak langsung tidak valid, proses berakhir dan eksploitasi dicegah. Hore!

Dari sini kita dapat menyimpulkan bahwa untuk keamanan maksimum, tujuan tidak langsung dari cabang harus disejajarkan dengan 16 byte, dan kita dapat memahami mengapa memori CFG untuk proses tersebut adalah sekitar 1/64 dari memori program.
Sebenarnya CFG memuat 32 bit pada satu waktu, tetapi ini adalah detail implementasi. Banyak sumber menggambarkan memori CFG sebagai 8-bit single-bit daripada 16-bit double-bit. Penjelasan saya lebih baik.
Itu sebabnya semuanya buruk
Gmail hang karena dua alasan. Pertama, pemindaian memori CFG pada Windows 10 16299 atau sebelumnya
sangat lambat. Saya melihat bagaimana pemindaian ruang alamat dari suatu proses membutuhkan waktu 40 detik atau lebih, dan secara harfiah 99,99% dari waktu ini memori CFG yang dipindai dipindai, meskipun hanya sekitar 75% dari blok memori tetap. Saya tidak tahu mengapa pemindaian sangat lambat, tetapi mereka memperbaikinya di Windows 10 17134, jadi tidak masuk akal untuk mempelajari masalah lebih detail.
Pemindaian lambat menyebabkan pelambatan karena Gmail menginginkan redundansi CFG, dan WMI memegang kunci selama pemindaian. Tetapi kunci reservasi memori tidak ditahan selama pemindaian. Dalam contoh saya, ada sekitar 49.000 blok di area CFG, dan fungsi
NtQueryVirtualMemory , yang menerima dan melepaskan kunci, dipanggil sekali untuk masing-masing. Oleh karena itu, kunci diperoleh dan dilepaskan ~ 49.000 kali dan setiap kali ditahan kurang dari 1 milidetik.
Tetapi meskipun kunci dirilis 49.000 kali, proses Chrome untuk beberapa alasan tidak bisa mendapatkannya. Ini tidak adil!
Itulah esensi masalahnya. Seperti yang saya tulis terakhir kali:
Ini karena kunci Windows secara inheren tidak adil - dan jika utas melepaskan kunci dan kemudian segera memintanya lagi, kunci itu bisa mendapatkannya selamanya.
Penguncian yang adil berarti bahwa dua utas yang bersaing akan menerimanya secara bergantian. Tetapi ini berarti banyak saklar konteks yang mahal, jadi untuk waktu yang lama kunci tidak akan digunakan.

Kunci tidak adil lebih murah, dan itu tidak membuat utas menunggu dalam antrean. Mereka hanya menangkap kunci, seperti yang disebutkan dalam
artikel Joe Duffy . Ia juga menulis:
Pengenalan kunci yang tidak adil tidak diragukan lagi dapat menyebabkan kelaparan. Namun secara statistik, waktu dalam sistem paralel cenderung sangat bervariasi sehingga setiap utas pada akhirnya akan menerima giliran untuk dieksekusi, dari sudut pandang probabilistik.
Bagaimana cara menghubungkan pernyataan Joe dari 2006 tentang kelangkaan kelaparan dengan pengalaman saya tentang masalah yang berulang dan 100% tahan lama? Saya pikir alasan utama adalah apa yang terjadi pada tahun 2006. Intel
merilis Core Duo , dan komputer multi-core ada di mana-mana.
Bagaimanapun, ternyata masalah kelaparan ini hanya terjadi pada sistem multi-inti! Dalam sistem seperti itu, utas WMI akan melepaskan kunci, memberi tanda utas Chrome untuk bangun, dan melanjutkan. Karena streaming WMI sudah berjalan, ia memiliki "cacat" di depan aliran Chrome, sehingga dapat dengan mudah memanggil
NtQueryVemualMemory lagi dan mendapatkan kunci lagi sebelum Chrome memiliki kesempatan untuk melakukan ini.
Jelas, dalam sistem inti tunggal, hanya satu utas yang dapat bekerja pada satu waktu. Sebagai aturan, Windows meningkatkan prioritas utas baru, dan meningkatkan prioritas berarti bahwa ketika kunci dilepaskan, utas Chrome baru akan siap dan segera
unggul dari utas WMI. Ini memberi thread Chrome banyak waktu untuk bangun dan mendapatkan kunci, dan rasa lapar tidak pernah datang.
Apakah kamu mengerti Dalam sistem multi-core, peningkatan prioritas dalam banyak kasus tidak mempengaruhi aliran WMI, karena akan berjalan pada kernel yang berbeda!
Ini berarti bahwa sistem dengan core tambahan dapat
merespons lebih lambat daripada sistem dengan beban kerja yang sama dan lebih sedikit core. Kesimpulan lainnya adalah rasa ingin tahu: jika komputer saya memiliki beban yang berat - utas dari prioritas yang sesuai, bekerja pada semua core prosesor - maka hang dapat dihindari (jangan coba ulangi ini di rumah).
Dengan demikian,
kunci yang tidak adil meningkatkan produktivitas, tetapi dapat menyebabkan kelaparan. Saya menduga bahwa solusinya mungkin apa yang saya sebut kunci "terkadang adil". Katakanlah, 99% dari waktu mereka akan tidak adil, tetapi dalam 1% memberikan kunci untuk proses lain. Ini akan menjaga manfaat produktivitas dengan lebih banyak, menghindari masalah kelaparan. Sebelumnya, kunci di Windows didistribusikan secara adil dan Anda mungkin dapat kembali ke sini sebagian, menemukan keseimbangan yang sempurna. Penafian: Saya bukan ahli kunci atau insinyur OS, tapi saya tertarik mendengar pemikiran tentang hal itu, dan setidaknya saya
bukan yang pertama menawarkan sesuatu seperti itu .
Linus Torvalds baru-baru ini menghargai pentingnya kunci yang adil: di
sini dan di
sini . Mungkin sudah waktunya untuk perubahan pada Windows juga.
Untuk meringkas : Mengunci selama beberapa detik tidak baik, itu membatasi konkurensi. Tetapi pada sistem multi-core dengan kunci tidak adil, melepas dan kemudian segera menerima kunci lagi berperilaku
persis seperti itu - utas lainnya tidak memiliki cara untuk bekerja.
Hampir gagal dengan ETW

Untuk semua penelitian ini, saya mengandalkan penelusuran ETW, jadi saya sedikit takut ketika ternyata pada awal penyelidikan bahwa Windows Performance Analyzer (WPA) tidak dapat memuat karakter Chrome. Saya yakin bahwa minggu lalu semuanya berjalan dengan baik. Apa yang terjadi ...
Kebetulan Chrome M68 keluar, dan itu ditautkan menggunakan lld-link bukan VC ++ linker. Jika Anda menjalankan
dumpbin dan melihat informasi debug, Anda akan melihat:
C:\b\c\b\win64_clang\src\out\Release_x64\./initialexe/chrome.exe.pdb
Yah, mungkin WPA tidak suka garis miring ini. Tapi itu masih tidak masuk akal, karena saya mengubah linker ke lld-link, dan saya ingat bahwa saya menguji WPA sebelumnya, jadi apa yang terjadi ...
Ternyata alasannya ada pada versi WPA 17134 yang baru. Saya menguji tata letak lld-Link - dan itu bekerja dengan baik di WPA 16299. Sungguh kebetulan! Linker baru dan WPA baru tidak kompatibel.
Saya menginstal versi lama WPA untuk melanjutkan penyelidikan (xcopy dari mesin dengan versi lama) dan melaporkan
bug lld-link , yang dengan cepat diperbaiki oleh pengembang. Sekarang Anda dapat kembali ke WPA 17134 ketika M69 dirakit dengan tautan tetap.
Wmi
Pemicu pembekuan WMI adalah
snap-in Instrumentasi Manajemen Windows , dan saya tidak pandai. Saya menemukan bahwa pada tahun
2014 atau sebelumnya, seseorang mengalami masalah dalam penggunaan CPU secara signifikan di
WmiPrvSE.exe di dalam
perfproc! GetProcessVaData , tetapi mereka tidak memberikan informasi yang cukup untuk memahami penyebab bug. Pada titik tertentu, saya membuat kesalahan dan mencoba mencari tahu apa permintaan WMI gila yang mungkin menggantung Gmail selama beberapa detik. Saya menghubungkan
beberapa pakar ke investigasi dan menghabiskan banyak waktu untuk mencari pertanyaan ajaib ini. Saya mencatat aktivitas
Microsoft-Windows-WMI-Activity dalam jejak ETW, bereksperimen dengan PowerShell untuk menemukan semua pertanyaan Win32_Perf, dan tersesat dalam beberapa cara bundaran yang terlalu membosankan untuk didiskusikan. Pada akhirnya, saya menemukan bahwa hang Gmail menyebabkan penghitung ini,
Win32_PerfRawData_PerfProc_ProcessAddressSpace_Costly , dipicu oleh PowerShell baris tunggal:
measure-command {Get-WmiObject -Query βSELECT * FROM Win32_PerfFormattedData_PerfProc_ProcessAddressSpace_Costlyβ}
Saya kemudian menjadi
semakin bingung karena nama penghitung ("sayang"? Benarkah?) Dan karena penghitung ini muncul dan menghilang berdasarkan faktor-faktor yang tidak saya mengerti.
Namun detail WMI tidak masalah. WMI tidak melakukan kesalahan - tidak benar-benar - hanya memindai memori. Menulis kode pindai Anda sendiri ternyata jauh lebih berguna dalam menyelidiki masalah.
Kerumitan untuk Microsoft
Chrome telah merilis tambalan, sisanya untuk Microsoft.
Mempercepat Pemindaian Wilayah CFG - OK, Selesai- Kosongkan memori CFG saat memori yang dapat dieksekusi dibebaskan - setidaknya dalam kasus penyelarasan 256 ribu, mudah
- Pertimbangkan bendera yang memungkinkan alokasi memori yang dapat dieksekusi tanpa memori CFG, atau gunakan PAGE_TARGETS_INVALID untuk tujuan ini. Perhatikan bahwa manual Windows Internal Bagian 1 Edisi 7 mengatakan bahwa "Anda harus memilih halaman [CFG] dengan setidaknya satu bit yang diset {1, X}" - jika Windows 10 mengimplementasikan ini, maka bendera PAGE_TARGETS_INVALID (yang saat ini digunakan oleh mesin) v8 ) akan menghindari alokasi memori
- Perbaiki perhitungan tabel halaman dalam vmmap untuk proses dengan sejumlah besar alokasi CFG
Pembaruan Kode
Saya memperbarui
contoh kode , terutama VAllocStress. Ada 20 baris yang disertakan untuk menunjukkan cara menemukan reservasi CFG untuk suatu proses. Saya juga menambahkan kode uji yang menggunakan
SetProcessValidCallTarget untuk memeriksa nilai bit CFG dan menunjukkan trik yang diperlukan untuk memanggil mereka dengan sukses (petunjuk: menelepon melalui GetProcAddress kemungkinan akan melanggar CFG!)