В прошлый раз мы посмотрели на способ полной замены класса. Как мы говорили, у этой техники есть несколько проблем. Но есть много способов снять шкуру с кошки (извините, любители кошек!), и в нашем случае (исправление мерцания
TProgressBar
без изменения интерфейса этого класса) есть как минимум три других решения.Одно из решений заключается в исправлении VMT
TProgressBar
в run-time. Мы знаем из предыдущих статей, что VMT содержит много информации о классе - включая массив указателей на реализацию виртуальных методов (это и есть собственно VMT - таблица виртуальных методов), а также разрежённый массив замещённых и новых динамических и message-методов (известный как DMT или таблица динамических методов). В этой статье мы посмотрим на то, как мы можем исправить VMT существующего класса и подменить записи в таблицах методов на те, которые нам нужны.Заметьте, что компилятор хранит таблицы VMT классов в защищённых от записи страницах памяти (даже хотя нельзя строго сказать, что они содержат исполняемый код). Это означает, что если мы наивно попытаемся записать что-то в область VMT, то немедленно схлопочем Access Violation, сгенерированный аппаратной защитой страниц. Правильный метод написания само-модифицирующегося кода заключается в использовании функции
VirtualProtect
для изменения атрибутов страницы памяти перед выполнением записи и последующего восстановления атрибутов после записи. Вот функция, которая умеет изменять один DWORD в сегменте кода:
procedure PatchCodeDWORD(Code: PDWORD; Value: DWORD); var RestoreProtection, Ignore: DWORD; begin if VirtualProtect(Code, SizeOf(Code^), PAGE_EXECUTE_READWRITE, RestoreProtection) then try Code^ := Value; finally VirtualProtect(Code, SizeOf(Code^), RestoreProtection, Ignore); FlushInstructionCache(GetCurrentProcess, Code, SizeOf(Code^)); end else RaiseLastOSError; end;Чтобы перестраховаться - мы следуем рекомендациям Microsoft и сбрасываем кэш инструкций процессора после изменения страниц кода - вызовом
FlushInstructionCache
. Это необходимо для избежания мерзких проблем, когда только что пропатченный код игнорируется процессором, потому что его старый вариант уже загружен в его кэш.Теперь нам надо определить, что же нам патчить и на что патчить. Мы уже в деталях разобрали работу динамических и виртуальных методов в предыдущих постах. Изучая исходный код
TProgressBar
и подтверждая это в отладчике, мы видим, что у него нет замещения динамических и message-методов. Хорошие новости - указатель DynamicTable
в VMT будет равен nil
, что делает наш патч проще. Так что мы знаем, где надо патчить - будем патчить слот DMT в VMT класса TProgressBar
. Чтобы было проще, мы используем константу vmtDynamicTable
из модуля System
:
{ Virtual method table entries } const // ... vmtDynamicTable = -48;Мы можем вычислить адрес DMT слота в VMT по данной ссылке на класс
TProgressBar
, используя этот код:
var OriginalDmt: PDWORD; begin OriginalDmt := PDWORD(TProgressBar); Inc(OriginalDmt, vmtDynamicTable div SizeOf(DWORD)); if OriginalDmt^ = 0 then // Проверка на отсутствие DmtТеперь надо определиться с тем, на что заменять
nil
DMT. Мы можем попытаться построить DMT таблицу сами, вручную - используя информацию по реверсированию таблицы, но это будет пустой тратой сил. Компилятор справится с задачей генерации DMT значительно лучше. Мы можем просто использовать класс TProgressBarVistaFix
из нашего предыдущего поста:
type TProgressBarVistaFix = class(TProgressBar) private procedure WMEraseBkgnd(var Message: TWmEraseBkgnd); message WM_ERASEBKGND; end; procedure TProgressBarVistaFix.WMEraseBkgnd(var Message: TWmEraseBkgnd); begin DefaultHandler(Message); end;Мы можем использовать готовый DMT от этого класса для внедрения его в DMT слот класса
TProgressBar
. Вот первый вариант моего патча:
procedure PatchTProgressBarDMT; var OriginalDmt, NewDmt: PDWORD; begin OriginalDmt := PDWORD(TProgressBar); Inc(OriginalDmt, vmtDynamicTable div SizeOf(DWORD)); if OriginalDmt^ = 0 then begin NewDmt := PDWORD(TProgressBarVistaFix); Inc(NewDmt, vmtDynamicTable div SizeOf(DWORD)); PatchCodeDWORD(OriginalDmt, NewDmt^); end; end;Тестирование этого кода показывает, что он работает, вызывается обработчик
WM_ERASEBKFND
и мерцание в Vista пропадает. Но я не удовлетворён этим - так что я переписал его в отдельный модуль. На этот раз - используя модуль HVVMT
и указатель PVmt
:
unit HVPatching; interface uses Windows; procedure PatchCodeDWORD(Code: PDWORD; Value: DWORD); procedure ReplaceClassDmt(TargetClass, SourceClass: TClass); implementation uses HVVMT; procedure PatchCodeDWORD(Code: PDWORD; Value: DWORD); var RestoreProtection, Ignore: DWORD; begin if VirtualProtect(Code, SizeOf(Code^), PAGE_EXECUTE_READWRITE, RestoreProtection) then begin Code^ := Value; VirtualProtect(Code, SizeOf(Code^), RestoreProtection, Ignore); FlushInstructionCache(GetCurrentProcess, Code, SizeOf(Code^)); end; end; procedure ReplaceClassDmt(TargetClass, SourceClass: TClass); begin Assert(Assigned(TargetClass) and Assigned(SourceClass)); PatchCodeDWORD(@GetVmt(TargetClass).DynamicTable, DWORD(GetVmt(SourceClass).DynamicTable)); end; end.Теперь мы можем использовать это так:
initialization ReplaceClassDmt(TProgressBar, TProgressBarVistaFix); end.В этом случае нам повезло. В других случаях класс для патча уже может иметь один или несколько динамических или message-методов - так что класс будет иметь не пустой DMT слот в VMT. В этом случае мы должны сыграть в компилятор и создать DMT с дополнительной записью, скопировать туда исходную DMT (вместе с индексами и указателями методов) и, наконец, вписать новую DMT в класс. Это существенно сложнее описанного здесь алгоритма, но если будет интерес в дальнейшей раскопке этого хака, мы можем вернуться к нему в будущих постах.
Не могли бы вы поделиться модулем HVRTTIUtils а то доступа к сайту производителя нету :(
ОтветитьУдалитьMegaVoltik@gmail.com
Заранее благодарен :)
Да вроде работает ссылка.
ОтветитьУдалитьНет EDN учётки? Завести можно здесь: https://members.embarcadero.com/newuser.aspx
Бесплатно. Наличие Embarcadero-ских лицензий не требуется. Это обычная учётка на форуме. Можно входить по OpenID.
Правда, при создании профиля они просят вводить телефон и адрес, но эти поля - для тех, кто регистрирует на этот аккаунт лицензии. А по-простому туда можно и нули забить.