Для языка C++, кстати, C#-регулярка поиска начала функции (без ref-квалификаторов) выглядит так:
@"((?<!\b(?:switch|for|while|if|catch)\s*)\((?:[^'""()]|'(?:[^']|\\.)*'|""(?:[^""]|\\.)*""|(?'open'\()|(?'-open'\)))*(?(open)(?!))\)\s*(?:const\s*)?(?:override\s*)?\{)"
А кто мешает просто использовать регулярные выражения на этапе компиляции?
Делается таким образом:
1) Написать класс, скажем FunctionEntry, который в конструкторе через Reflection сохраняет все параметры функции, в которой он используется, а также ссылку на внешний класс FunctionEntry (из внешней вызывающей функции);
2) Список классов FunctionEntry сохранять в локальной памяти потока (для того, чтобы можно было узнать внешний класс FunctionEntry без передачи его в качестве параметра функции);
3) Написать утилиту времени компиляции (вызов которой будет происходить каждый раз при сборке проекта), которая с использованием регулярных выражений
a) определяет начало каждой используемой функции и вставляет в нее создание класса FunctionEntry; а также начало блока try;
b) определяет конец каждой используемой функции и вставляет в нее блок catch, который ловит исключение и при помощи списка классов FunctionEntry из локальной памяти потока находит значения всех параметров
4) После завершения компиляции обратная утилита убирает все вставленные блоки.
Используется при защите соединений с базами данных (сфера информационных технологий). В двух словах: при перехвате функции создания соединения с MSSQL-сервером выполняется функциональность дополнительной двухфакторной аутентификации для последующего зашифрования/расшифрования передаваемых данных «на-лету».
Замечу, правда, что приведенный по ссылке метод не является переносимым (т.е. может работать не всегда). И вот почему:
1) Иногда поток управления проходит через переходник, минуя адрес в таблице слотов;
2) Способ определения адреса слота в таблице MethodTable может изменяться с каждой версией CLR;
3) Вызов
в командах jmp NativeCode (см. описание FixupPrecode после компиляции) находят смещение сгенерированного кода относительно окончания самих команд (которые занимают 5 байт);
5) Следующая строка вычисляет относительное смещение сгенерированного внедряемого кода относительно окончания команды jmp NativeCode для перехватываемого кода(!!!) и записывает его в команду jmp NativeCode для перехватываемого кода
Команда mov eax, imm для x86 содержит 4-байтовый операнд imm по смещению 1 от начала команды (которая занимает 5 байт).
Команда mov rax, imm для x64 содержит 8-байтовый операнд imm по смещению 2 от начала команды (которая занимает 10 байт).
Способ вызова через переходники не меняется с самого начала CLR (указанный способ можно мониторить в исходниках CLR на github).
Единственное, что приходится учитывать — не изменилась ли реализация ThePreStub, поскольку способ поиска PrestubWorker основывается на том, что ThePreStub не вызывает других функций, кроме PrestubWorker. Функции ThePreStub нет в исходниках (поскольку она реализована на ассемблере), приходится проверять на практике.
Да, для простоты в примере приведен класс для одного метода.
Но в этом же классе можно перехватить сколько угодно методов для произвольных классов.
Для этого в функции GetTypes нужно указать все требуемые классы, а в функции OnLoad
(в зависимости от принятого класса) перехватить сколько угодно его функций.
Пока, особо не афишируясь, используется в промышленных разработках, где необходимо перехватывать функции .NET.
Например, при перехвате обращений к базе данных SqlServer для обеспечения безопасного соединения.
@"((?<!\b(?:switch|for|while|if|catch)\s*)\((?:[^'""()]|'(?:[^']|\\.)*'|""(?:[^""]|\\.)*""|(?'open'\()|(?'-open'\)))*(?(open)(?!))\)\s*(?:const\s*)?(?:override\s*)?\{)"
Делается таким образом:
1) Написать класс, скажем FunctionEntry, который в конструкторе через Reflection сохраняет все параметры функции, в которой он используется, а также ссылку на внешний класс FunctionEntry (из внешней вызывающей функции);
2) Список классов FunctionEntry сохранять в локальной памяти потока (для того, чтобы можно было узнать внешний класс FunctionEntry без передачи его в качестве параметра функции);
3) Написать утилиту времени компиляции (вызов которой будет происходить каждый раз при сборке проекта), которая с использованием регулярных выражений
a) определяет начало каждой используемой функции и вставляет в нее создание класса FunctionEntry; а также начало блока try;
b) определяет конец каждой используемой функции и вставляет в нее блок catch, который ловит исключение и при помощи списка классов FunctionEntry из локальной памяти потока находит значения всех параметров
4) После завершения компиляции обратная утилита убирает все вставленные блоки.
https://github.com/dotnet/coreclr/blob/775003a4c72f0acc37eab84628fcef541533ba4e/Documentation/botr/method-descriptor.md
1) Иногда поток управления проходит через переходник, минуя адрес в таблице слотов;
2) Способ определения адреса слота в таблице MethodTable может изменяться с каждой версией CLR;
3) Вызов
RuntimeHelpers.PrepareMethod(methodToReplace.MethodHandle);
не всегда гарантирует выполнение JIT-компиляции;
4) Переходники могут не быть FixupPrecode (особенно для NGen-модулей).
int* inj = (int*)methodToInject.MethodHandle.Value.ToPointer() + 2;
long* inj = (long*)methodToInject.MethodHandle.Value.ToPointer()+1;
только в размерности адреса.
2) Следующие строки (скорее всего) получают адреса слотов в таблице MethodTable (см. приведенную картинку)
int* inj = (int*)methodToInject.MethodHandle.Value.ToPointer() + 2;
int* tar = (int*)methodToReplace.MethodHandle.Value.ToPointer() + 2;
3) Следующие строки определяют адрес переходника FixupPrecode после компиляции для двух функций
byte* injInst = (byte*)*inj;
byte* tarInst = (byte*)*tar;
4) Следующие строки
int* injSrc = (int*)(injInst + 1);
int* tarSrc = (int*)(tarInst + 1);
в командах jmp NativeCode (см. описание FixupPrecode после компиляции) находят смещение сгенерированного кода относительно окончания самих команд (которые занимают 5 байт);
5) Следующая строка вычисляет относительное смещение сгенерированного внедряемого кода относительно окончания команды jmp NativeCode для перехватываемого кода(!!!) и записывает его в команду jmp NativeCode для перехватываемого кода
*tarSrc = (((int)injInst + 5) + *injSrc) — ((int)tarInst + 5);
Таким образом, в переходнике команда jmp NativeCode(Source) заменяется на jmp NativeCode(Inject)
6) В Release-версии адрес напрямую заменяется в слотах таблицы MethodTable;
Команда mov rax, imm для x64 содержит 8-байтовый операнд imm по смещению 2 от начала команды (которая занимает 10 байт).
Единственное, что приходится учитывать — не изменилась ли реализация ThePreStub, поскольку способ поиска PrestubWorker основывается на том, что ThePreStub не вызывает других функций, кроме PrestubWorker. Функции ThePreStub нет в исходниках (поскольку она реализована на ассемблере), приходится проверять на практике.
Команда смысла не несет, а используется в качестве идентифицирующего признака переходника.
Но в этом же классе можно перехватить сколько угодно методов для произвольных классов.
Для этого в функции GetTypes нужно указать все требуемые классы, а в функции OnLoad
(в зависимости от принятого класса) перехватить сколько угодно его функций.
Например, при перехвате обращений к базе данных SqlServer для обеспечения безопасного соединения.