👨💻 Два в одном: валидация и вызов в Control Flow Guard (CFG) Рэймонд Чен (Raymond Chen) в своем блоге The Old New Thin
Рэймонд Чен (Raymond Chen) в своем блоге The Old New Thing поделился интересными деталями реализации механизма Control Flow Guard в Windows.
Обычно CFG работает в два этапа: сначала мы вызываем функцию вроде LdrpValidateUserCallTarget для проверки указателя, и если всё в порядке, возвращаемся и делаем вызов. Но существует и комбинированная версия, которая не просто проверяет указатель, но и сразу же делает по нему переход (jump).
🤔 Зачем это нужно и в чем подвох?
Раз уж после валидации почти всегда следует вызов, объединение операций кажется логичным шагом для оптимизации. Но здесь кроется проблема: для такого трюка приходится менять соглашение о вызовах (calling convention). Регистры, которые использует функция валидации, ни в коем случае не должны пересекаться с регистрами, в которых лежат аргументы для итоговой функции.
🔧 Как это реализовано на уровне ассемблера:
🔹 x86-64: Обычная функция-валидатор принимает адрес в rcx и использует rax и rdx как временные регистры. Комбинированная версия принимает адрес в rax, использует r10 и r11 для своих расчетов, а в конце просто делает jmp rax. Все исходные параметры целевой функции остаются нетронутыми!
🔹 AArch64: Логика абсолютно та же. Обычная версия принимает адрес в x15, а комбинированная - в x9, завершаясь инструкцией br x9 вместо классического ret.
Кстати, есть и приятный микро-бонус при обработке ошибок: если указатель оказывается «битым», комбинированной функции не нужно тратить инструкции на перекладывание адреса в нужный регистр перед прыжком в блок ошибки - плохой адрес уже находится там, где нужно (rax или x9).
🔗 Оригинальная статья и листинги дизассемблера:
https://devblogs.microsoft.com/oldnewthing/20260708-00/?p=112510
#cpp #programming
👉 @cpp_lib