Здравствуйте, меня зовут Евгений, и мне надоело писать прошивки для микроконтроллеров. Как это это случилось и что с этим делать, давайте разберемся.
После того как поработаешь в большом программировании С++, Java, Python, и т. Д. Возвращаться к маленьким и пузатым микроконтроллерам совсем не хочется. К их скудным инструментам и библиотекам. Но делать иногда нечего, задачи real-time и автономности, не оставляют выбора. Но есть некоторые типы задач, которые просто выбешивает в этой области решать.
К примеру тестирование оборудования, ничего более скучного и занудного занятия в embedded программировании, вряд ли можно придумать. Вообщем как и удобных инструментов для этого. Пишешь… Прошиваешь… моргаешь… светодиодиком (иногда логи по UART). Все ручками, без специализированных инструментов для тестирования.
Еще удручает что нет инструментальных тестов, для наших маленьких микроконтроллеров. Все только через прошивочку и через дебагер тестировать.
Да и изучение работы с новыми устройствами и периферией требует много сил и времени. Одна ошибка и программу надо каждый раз перекомпилировать и заново запускать.
Для таких экспериментов больше подходит что-то типа REPL, дабы можно было просто и безболезненно делать вот такие, хотя бы банальные, вещи:
Как к этому прийти, посвящен этот цикл статей.
И в этот раз мне попался проект где потребовалось тестировать довольно сложное устройство, с уймой всяких датчиков и прочих, неизвестных мне ранее, микросхем, которые использовали множество периферии МК и кучу разных интерфейсов. Особое веселье было, то что к плате у меня не было исходных кодов прошивки, так что все тесты пришлось бы писать с нуля, без использования наработок из исходного кода.
Проект обещал хорошего тамаду и конкурсы интересные на месяца два так ( а скорее всего и больше).
Ладно, мы же тут не плакаться собрались. Надо либо снова окунуться в дебри С и бесконечных прошивок, либо отказаться либо что-то придумать, что бы облегчить себе это занятие. В конце концов лень да любопытство — двигатель прогресса.
В прошлый раз, когда разбирался с OpenOCD, наткнулся на такой интересный пункт в документации как
http://openocd.org/doc/html/General-Commands.html
15.4 Memory access commands
mdw, mdh, mdb — позволяют считывать значению по физическому адресу на микроконтроллере
mww, mwh, mwb — позволяют записывать по физическому адресу на микроконтроллере
Интересно…. А регистры периферии читать и писать с их помощью можно?.. оказывается можно, да к тому же эти команды можно выполнять удаленно через TCL сервер, который запускается при старте openOCD.
Вот пример моргания светодиодиком для stm32f103C8T6
// Step 1: Enable the clock to PORT B
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
// Step 2: Change PB0's mode to 0x3 (output) and cfg to 0x0 (push-pull)
GPIOC->CRH = GPIO_CRH_MODE13_0 | GPIO_CRH_MODE13_1;
// Step 3: Set PB0 high
GPIOC->BSRR = GPIO_BSRR_BS13;
// Step 4: Reset PB0 low
GPIOC->BSRR = GPIO_BSRR_BR13;
и аналогичный ему последовательность команд openOCD
mww 0x40021018 0x10
mww 0x40011004 0x300000
mww 0x40011010 0x2000
mww 0x40011010 0x20000000
А теперь, если задуматься о вечном и рассмотреть прошивки для МК… то основное предназначение этих программ это запись в регистры чипа; прошивка, которая будет просто что-то делать и работать только с процессорным ядром, не имеет никакого практического применения!
Хотя конечно можно и крипту считать(=
Многие вспомнят, еще про работу с прерываниями. Но они не всегда требуются, и в моем случае можно обойтись и без них.
И так, жизнь налаживается. В исходниках openOCD можно даже найти, интересный пример использования данного интерфейса.
Очень хорошая заготовочка на питоне.
Вполне можно конвертировать адреса регистров из заголовочных файлов, и начать писать на кошерном скриптовом языке. Уже можно готовить шампанское, но мне показалось этого мало, ведь хочется вместо возни с регистрами использовать Standard Peripherals Library или новый HAL для работы с периферией.
Портировать библиотеки на питон … в каком-нибудь страшном сне этим займемся. Значит надо как использовать эти библиотеки в С или … С++. А в плюсах же можно переопределить почти все операторы … для своих классов.
А базовые адреса в заголовочных файлах, подменить на объекты своих классов.
К примеру в файле stm32f10x.h
#define PERIPH_BB_BASE ((uint32_t)0x42000000) /*!< Peripheral base address in the bit-band region */
Заменить на
class InterceptAddr;
InterceptAddr addr;
#define PERIPH_BB_BASE (addr) /*!< Peripheral base address in the bit-band region */
Но игры с указателями в библиотеке, рубят на корню эту идею...
Вот к примеру файл stm32f10x_i2c.c :
FlagStatus I2C_GetFlagStatus(I2C_TypeDef* I2Cx, uint32_t I2C_FLAG)
{
__IO uint32_t i2creg = 0, i2cxbase = 0;
….
/* Get the I2Cx peripheral base address */
i2cxbase = (uint32_t)I2Cx;
….
Значит надо как-то по другому перехватывать обращения к адресам. Как это делать наверно стоит посмотреть у Valgrind, не зря у него есть memchecker. Уж он то точно должен знать как перехватывать обращения по адресам.
Забегая вперед скажу, что лучше туда не заглядывать… мне почти удалось сделать перехват обращений по адресам. Почти для всех случаев, кроме такого
Int * p = ...
*p = 0x123;
Перехватить адрес есть возможность, а вот перехватить записываемые данные уже не получалось. Только название внутреннего регистра в котором это значение лежит, но до которого не добраться из memcheck.
На самом деле Valgrind удивил меня, внутри используется древний монстр libVEX, о котором я вообще не нашел никакой информации в интернете. Хорошо что немного документации удалось найти в заголовочных файлах.
Потом были другие инструменты DBI.
Frida, Dynamic RIO, еще какой-то, и наконец попался Pintool.
У PinTool оказалась неплохая документация и примеры. Хотя мне их все равно не хватило, и с некоторыми вещами пришлось делать эксперименты. Инструмент оказался очень мощный, единственно огорчает закрытый код и ограничение только платформой intel (хотя в дальнейшем это можно будет обойти)
Итак, нам нужно перехватывать запись и чтение по определенным адресам. Посмотрим какие инструкции отвечают за это https://godbolt.org/z/nJS9ci.
Для х64 это будет MOV для обоих операций.
А для х86 это будет MOV для записи и MOVZ для чтения.
Примечание: лучше всего не включать оптимизацию, иначе могут повылазить другие инструкции.
INS_AddInstrumentFunction(EmulateLoad, 0);
INS_AddInstrumentFunction(EmulateStore, 0);
.....
static VOID EmulateLoad(INS ins, VOID *v) {
// Find the instructions that move a value from memory to a register
if ((INS_Opcode(ins) == XED_ICLASS_MOV ||
INS_Opcode(ins) == XED_ICLASS_MOVZX) &&
INS_IsMemoryRead(ins) && INS_OperandIsReg(ins, 0) &&
INS_OperandIsMemory(ins, 1)) {
INS_InsertCall(ins, IPOINT_BEFORE, AFUNPTR(loadAddr2Reg),
IARG_MEMORYREAD_EA, IARG_MEMORYREAD_SIZE, IARG_RETURN_REGS,
INS_OperandReg(ins, 0), IARG_END);
// Delete the instruction
INS_Delete(ins);
}
}
static VOID EmulateStore(INS ins, VOID *v) {
if (INS_Opcode(ins) == XED_ICLASS_MOV && INS_IsMemoryWrite(ins) &&
INS_OperandIsMemory(ins, 0)) {
if (INS_hasKnownMemorySize(ins)) {
if (INS_OperandIsReg(ins, 1)) {
INS_InsertCall(ins, IPOINT_BEFORE, AFUNPTR(multiMemAccessStore),
IARG_MULTI_MEMORYACCESS_EA, IARG_REG_VALUE,
INS_OperandReg(ins, 1), IARG_END);
} else if (INS_OperandIsImmediate(ins, 1)) {
INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)multiMemAccessStore,
IARG_MULTI_MEMORYACCESS_EA, IARG_UINT64,
INS_OperandImmediate(ins, 1), IARG_END);
}
} else {
if (INS_OperandIsReg(ins, 1)) {
INS_InsertCall(ins, IPOINT_BEFORE, AFUNPTR(storeReg2Addr),
IARG_MEMORYWRITE_EA, IARG_REG_VALUE,
INS_OperandReg(ins, 1), IARG_MEMORYWRITE_SIZE, IARG_END);
} else if (INS_OperandIsImmediate(ins, 1)) {
INS_InsertCall(ins, IPOINT_BEFORE, AFUNPTR(storeReg2Addr),
IARG_MEMORYWRITE_EA, IARG_UINT64,
INS_OperandImmediate(ins, 1), IARG_UINT32,
IARG_MEMORYWRITE_SIZE, IARG_END);
}
}
}
}
В случае чтения из адреса мы вызываем ф-цию loadAddr2Reg и удаляем оригинальную инструкцию. Исходя из этого loadAddr2Reg нам должна возвращать необходимое значение.
С записью все сложнее… аргументы могут быть разных типов и к тому же передаваться по разному, поэтому приходится перед командой вызывать разные ф-ции. На 32-битной платформе multiMemAccessStore, а на 64 будет вызываться storeReg2Addr. Причем здесь инструкцию из конвеера не удаляем. Удалить проблем её нет, но вот сымитировать её действие в некоторых случаях не получается. Программа почему-то иногда валится в sigfault. Для нас это не критично, пусть себе пишет, главное что есть возможность перехвата аргументов.
Дальше надо посмотреть, а какие адреса нам надо перехватывать, посмотрим на Memory Map для нашего чипа stm32f103C8T6:
Нас интересуют адреса с SRAM и PERIPH_BASE, т.е с 0x20000000 по 0x20000000 + 128*1024 и с 0x40000000 по 0x40030000. Отлично, вернее не совсем, как помним инструкцию записи мы удалить не смогли. Поэтому запись по этим адресам будет вываливаться в sigfault. К тому же есть неиллюзорная вероятность того что на эти адреса будет приходится данные нашей программы, не у этого чипа так у другого. Поэтому однозначно надо их куда-то отремапить. Допустим на какой нибудь массив.
Создаем массивы нужного размера, и дальше их указатели подставляем в дефайны базовых адресов.
В нашей программе, в заголовчниках вместо
#define SRAM_BASE ((uint32_t)0x20000000) /*!< SRAM base address in the alias region */
#define PERIPH_BASE ((uint32_t)0x40000000) /*!< Peripheral base address in the alias region */
Делаем
#define SRAM_BASE ((AddrType)pAddrSRAM)
#define PERIPH_BASE ((AddrType)pAddrPERIPH)
и где pAddrSRAM и pAddrPERIPH указатели на заранее выделенные массивы.
Теперь нашему PinTool клиенту надо как-то передать как мы отремапили необходимые адреса.
Самое простое что мне показалось, как сделать это перехват ф-ции, которая возвращает структуру массив с такого формата:
typedef struct
{
addr_t start_addr; //адрес массива куда ремапятся нужные адреса
addr_t end_addr; //размер этого массива
addr_t reference_addr; // отремапленные адрес
} memoryTranslate;
К примеру для нашего чипа это будет так заполняться
map->start_addr = (addr_t)pAddrSRAM;
map->end_addr = 96*1024;
map->reference_addr = (addr_t)0x20000000U;
Перехватить ф-цию и взять из нее требуемые значения не составляет большого труда:
IMG_AddInstrumentFunction(ImageReplace, 0);
....
static memoryTranslate *replaceMemoryMapFun(CONTEXT *context,
AFUNPTR orgFuncptr,
sizeMemoryTranslate_t *size) {
PIN_CallApplicationFunction(context, PIN_ThreadId(), CALLINGSTD_DEFAULT,
orgFuncptr, NULL, PIN_PARG(memoryTranslate *),
&addrMap, PIN_PARG(sizeMemoryTranslate_t *), size,
PIN_PARG_END());
sizeMap = *size;
return addrMap;
}
static VOID ImageReplace(IMG img, VOID *v) {
RTN freeRtn = RTN_FindByName(img, NAME_MEMORY_MAP_FUNCTION);
if (RTN_Valid(freeRtn)) {
PROTO proto_free =
PROTO_Allocate(PIN_PARG(memoryTranslate *), CALLINGSTD_DEFAULT,
NAME_MEMORY_MAP_FUNCTION,
PIN_PARG(sizeMemoryTranslate_t *), PIN_PARG_END());
RTN_ReplaceSignature(freeRtn, AFUNPTR(replaceMemoryMapFun), IARG_PROTOTYPE,
proto_free, IARG_CONTEXT, IARG_ORIG_FUNCPTR,
IARG_FUNCARG_ENTRYPOINT_VALUE, 0, IARG_END);
}
}
memoryTranslate * getMemoryMap(sizeMemoryTranslate_t * size){
...
return memoryMap;
}
Что же самая нетривиальная работа сделана, осталось сделать клиента к OpenOCD, в PinTool клиенте мне не хотелось его реализовать, поэтому я делал отдельным приложением, с которым наш PinTool клиент общается через named fifo.
Таким образом схема интерфейсов и коммуникаций получается такая:
А упрощенный workflow работы на примере перехвата адреса 0х123:
Давайте разберемся по порядку что же здесь происходит:
- запускается PinTool клиент, делает инициализацию наших перехватчиков, запускает программу
- Программа запускается, ей нужно отремапить адреса регистров на какой-нить массив, вызывается ф-ция getMemoryMap, которую перехватывает наш PinTool. Для примера один из регистров отрепамился на адрес 0х123, его будем отслеживать
- PinTool клиент сохраняет значения отремапленных адресов
- Передает управление обратно нашей программе
- Дальше где-то происходит запись по нашему отслеживаемому адресу 0x123. Ф-ция storeReg2Addr отслеживает это
- И передает запрос на запись в OpenOCD клиент
- Client возвращает ответ, тот парсится. Если все нормально, то возращается управление программе
- Дальше где-то в программе происходит чтение по отслеживаемому адресу 0x123.
- loadAddr2Reg отслеживает это и посылает запрос OpenOCD клиенту.
- OpenOCD клиент обрабатывает его и возвращает ответ
- Если все нормально, но в программу возвращается значение из регистра МК
- Программа продолжается.
На этом пока все, полные исходники и примеры будут в следующих частях.
Автор: Евгений