Доступ к памяти ловушек внутри стандартного исполняемого файла, созданного с помощью MinGW
Так что моя проблема звучит так.
У меня есть некоторый зависимый от платформы код (встроенная система), который записывает в некоторые местоположения MMIO, которые жестко закодированы по определенным адресам.
Я скомпилировал этот код с некоторым кодом управления внутри стандартного исполняемого файла (в основном для тестирования), но также и для моделирования (поскольку нахождение основных ошибок на реальной платформе HW занимает больше времени).
Чтобы облегчить жестко закодированные указатели, я просто переопределяю их для некоторых переменных в пуле памяти. И это работает очень хорошо.
Проблема заключается в том, что в некоторых местах MMIO (например, w1c) существует определенное аппаратное поведение, что делает "правильное" тестирование трудным или невозможным.
Вот решения, о которых я подумал:
1 - Каким-то образом переопределить доступ к этим регистрам и попытаться вставить некоторую непосредственную функцию для имитации динамического поведения. Это не очень удобно, поскольку существуют различные способы записи в местоположения MMIO (указатели и прочее).
2 - Каким-то образом оставить адреса жестко закодированными и перехватить нелегальный доступ через ошибку сегмента, найти местоположение, которое сработало, извлечь точно, где был сделан доступ, обработать и вернуть. Я не совсем уверен, как это будет работать (и даже если это возможно).
3 - Используйте какую-то эмуляцию. Это, безусловно, сработает, но лишит цели быстрой и стандартной работы на стандартном компьютере.
4 - Виртуализация Вероятно, потребуется много времени для реализации. Не совсем уверен, оправдан ли выигрыш.
Кто-нибудь имеет какие-либо идеи, если это можно сделать, не вдаваясь слишком глубоко? Может быть, есть способ манипулировать компилятором каким-либо образом, чтобы определить область памяти, для которой каждый доступ будет генерировать обратный вызов. Не совсем эксперт по x86/gcc.
Изменить: Кажется, что на самом деле это не возможно сделать независимым от платформы способом, и так как это будут только окна, я буду использовать доступный API (который, кажется, работает как ожидалось). Нашел этот Q здесь:
Доступна ли одношаговая ловушка на win 7?
Я помещу весь "смоделированный" регистровый файл на несколько страниц, буду их защищать и вызову обратный вызов, из которого я извлеку всю необходимую информацию, сделаю свои вещи и продолжу выполнение.
Спасибо всем за отклик.
3 ответа
Итак, решение (фрагмент кода) выглядит следующим образом:
Прежде всего, у меня есть переменная:
__attribute__ ((aligned (4096))) int g_test;
Во-вторых, внутри моей основной функции я делаю следующее:
AddVectoredExceptionHandler(1, VectoredHandler);
DWORD old;
VirtualProtect(&g_test, 4096, PAGE_READWRITE | PAGE_GUARD, &old);
Обработчик выглядит так:
LONG WINAPI VectoredHandler(struct _EXCEPTION_POINTERS *ExceptionInfo)
{
static DWORD last_addr;
if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_GUARD_PAGE_VIOLATION) {
last_addr = ExceptionInfo->ExceptionRecord->ExceptionInformation[1];
ExceptionInfo->ContextRecord->EFlags |= 0x100; /* Single step to trigger the next one */
return EXCEPTION_CONTINUE_EXECUTION;
}
if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP) {
DWORD old;
VirtualProtect((PVOID)(last_addr & ~PAGE_MASK), 4096, PAGE_READWRITE | PAGE_GUARD, &old);
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}
Это только базовый каркас для функциональности. В основном я защищаю страницу, на которой находится переменная, у меня есть несколько связанных списков, в которых я держу указатели на функцию и значения для рассматриваемого адреса. Я проверяю, что адрес, генерирующий ошибку, находится внутри моего списка, затем я запускаю обратный вызов.
При первом попадании в систему защита страницы будет отключена системой, но я могу вызвать мой обратный вызов PRE_WRITE, где я могу сохранить состояние переменной. Поскольку через EFlags выдается один шаг, за ним сразу же следует исключение из одного шага (что означает, что переменная была записана), и я могу вызвать обратный вызов WRITE. Все данные, необходимые для операции, содержатся в массиве ExceptionInformation.
Когда кто-то пытается записать в эту переменную:
*(int *)&g_test = 1;
Будет активирован PRE_WRITE, за которым следует WRITE,
Когда я делаю:
int x = *(int *)&g_test;
ЧИТАТЬ будет выпущен.
Таким образом, я могу манипулировать потоком данных таким образом, чтобы не требовалось модифицировать исходный исходный код. Примечание: это предназначено для использования в качестве части тестовой структуры, и любой штрафной удар считается приемлемым.
Например, операция W1C (запись 1 для очистки) может быть выполнена:
void MYREG_hook(reg_cbk_t type)
{
/** We need to save the pre-write state
* This is safe since we are assured to be called with
* both PRE_WRITE and WRITE in the correct order
*/
static int pre;
switch (type) {
case REG_READ: /* Called pre-read */
break;
case REG_PRE_WRITE: /* Called pre-write */
pre = g_test;
break;
case REG_WRITE: /* Called after write */
g_test = pre & ~g_test; /* W1C */
break;
default:
break;
}
}
Это было возможно и с ошибками сегмента на недопустимых адресах, но мне приходилось выдавать по одному для каждого R/W и отслеживать "виртуальный регистровый файл", чтобы получить больший штрафной удар. Таким образом, я могу охранять только определенные области памяти или нет, в зависимости от зарегистрированных мониторов.
Я думаю, что №2 - лучший подход. Я обычно использую подход № 4, но использую его для тестирования кода, работающего в ядре, поэтому мне нужен слой ниже ядра, чтобы перехватывать и эмулировать доступы. Поскольку вы уже поместили свой код в приложение пользовательского режима, #2 должен быть проще.
Ответы на этот вопрос могут помочь в реализации #2. Как написать обработчик сигнала для отлова SIGSEGV?
Однако вы действительно хотите эмулировать доступ к памяти, а затем обработчик segv возвращается к инструкции после доступа. Этот пример кода работает в Linux. Однако я не уверен, что поведение, которым он пользуется, не определено.
#include <stdint.h>
#include <stdio.h>
#include <signal.h>
#define REG_ADDR ((volatile uint32_t *)0x12340000f000ULL)
static uint32_t read_reg(volatile uint32_t *reg_addr)
{
uint32_t r;
asm("mov (%1), %0" : "=a"(r) : "r"(reg_addr));
return r;
}
static void segv_handler(int, siginfo_t *, void *);
int main()
{
struct sigaction action = { 0, };
action.sa_sigaction = segv_handler;
action.sa_flags = SA_SIGINFO;
sigaction(SIGSEGV, &action, NULL);
// force sigsegv
uint32_t a = read_reg(REG_ADDR);
printf("after segv, a = %d\n", a);
return 0;
}
static void segv_handler(int, siginfo_t *info, void *ucontext_arg)
{
ucontext_t *ucontext = static_cast<ucontext_t *>(ucontext_arg);
ucontext->uc_mcontext.gregs[REG_RAX] = 1234;
ucontext->uc_mcontext.gregs[REG_RIP] += 2;
}
Код для чтения регистра написан на ассемблере, чтобы гарантировать, что регистр назначения и длина инструкции известны.
Вот как может выглядеть версия prl для Windows:
#include <stdint.h>
#include <stdio.h>
#include <windows.h>
#define REG_ADDR ((volatile uint32_t *)0x12340000f000ULL)
static uint32_t read_reg(volatile uint32_t *reg_addr)
{
uint32_t r;
asm("mov (%1), %0" : "=a"(r) : "r"(reg_addr));
return r;
}
static LONG WINAPI segv_handler(EXCEPTION_POINTERS *);
int main()
{
SetUnhandledExceptionFilter(segv_handler);
// force sigsegv
uint32_t a = read_reg(REG_ADDR);
printf("after segv, a = %d\n", a);
return 0;
}
static LONG WINAPI segv_handler(EXCEPTION_POINTERS *ep)
{
// only handle read access violation of REG_ADDR
if (ep->ExceptionRecord->ExceptionCode != EXCEPTION_ACCESS_VIOLATION ||
ep->ExceptionRecord->ExceptionInformation[0] != 0 ||
ep->ExceptionRecord->ExceptionInformation[1] != (ULONG_PTR)REG_ADDR)
return EXCEPTION_CONTINUE_SEARCH;
ep->ContextRecord->Rax = 1234;
ep->ContextRecord->Rip += 2;
return EXCEPTION_CONTINUE_EXECUTION;
}