В прошлый раз мы посмотрели на способ полной замены класса. Как мы говорили, у этой техники есть несколько проблем. Но есть много способов снять шкуру с кошки (извините, любители кошек!), и в нашем случае (исправление мерцания
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.
Правда, при создании профиля они просят вводить телефон и адрес, но эти поля - для тех, кто регистрирует на этот аккаунт лицензии. А по-простому туда можно и нули забить.