Хабрахабр

Как перестать писать прошивки для микроконтроллеров и начать жить. Часть I

Как это это случилось и что с этим делать, давайте разберемся. Здравствуйте, меня зовут Евгений, и мне надоело писать прошивки для микроконтроллеров.

Д. После того как поработаешь в большом программировании С++, Java, Python, и т. К их скудным инструментам и библиотекам. Возвращаться к маленьким и пузатым микроконтроллерам совсем не хочется. Но есть некоторые типы задач, которые просто выбешивает в этой области решать. Но делать иногда нечего, задачи real-time и автономности, не оставляют выбора.

Вообщем как и удобных инструментов для этого. К примеру тестирование оборудования, ничего более скучного и занудного занятия в embedded программировании, вряд ли можно придумать. Все ручками, без специализированных инструментов для тестирования. Пишешь… Прошиваешь… моргаешь… светодиодиком (иногда логи по UART).

Все только через прошивочку и через дебагер тестировать. Еще удручает что нет инструментальных тестов, для наших маленьких микроконтроллеров.

Одна ошибка и программу надо каждый раз перекомпилировать и заново запускать. Да и изучение работы с новыми устройствами и периферией требует много сил и времени.

Для таких экспериментов больше подходит что-то типа REPL, дабы можно было просто и безболезненно делать вот такие, хотя бы банальные, вещи:

\

Как к этому прийти, посвящен этот цикл статей.

Особое веселье было, то что к плате у меня не было исходных кодов прошивки, так что все тесты пришлось бы писать с нуля, без использования наработок из исходного кода. И в этот раз мне попался проект где потребовалось тестировать довольно сложное устройство, с уймой всяких датчиков и прочих, неизвестных мне ранее, микросхем, которые использовали множество периферии МК и кучу разных интерфейсов.

Проект обещал хорошего тамаду и конкурсы интересные на месяца два так ( а скорее всего и больше).

Надо либо снова окунуться в дебри С и бесконечных прошивок, либо отказаться либо что-то придумать, что бы облегчить себе это занятие. Ладно, мы же тут не плакаться собрались. В конце концов лень да любопытство — двигатель прогресса.

В прошлый раз, когда разбирался с OpenOCD, наткнулся на такой интересный пункт в документации как

4 Memory access commands
mdw, mdh, mdb — позволяют считывать значению по физическому адресу на микроконтроллере
mww, mwh, mwb — позволяют записывать по физическому адресу на микроконтроллере http://openocd.org/doc/html/General-Commands.html
15.

А регистры периферии читать и писать с их помощью можно?.. Интересно…. оказывается можно, да к тому же эти команды можно выполнять удаленно через 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)
} 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:

Давайте разберемся по порядку что же здесь происходит:

  1. запускается PinTool клиент, делает инициализацию наших перехватчиков, запускает программу
  2. Программа запускается, ей нужно отремапить адреса регистров на какой-нить массив, вызывается ф-ция getMemoryMap, которую перехватывает наш PinTool. Для примера один из регистров отрепамился на адрес 0х123, его будем отслеживать
  3. PinTool клиент сохраняет значения отремапленных адресов
  4. Передает управление обратно нашей программе
  5. Дальше где-то происходит запись по нашему отслеживаемому адресу 0x123. Ф-ция storeReg2Addr отслеживает это
  6. И передает запрос на запись в OpenOCD клиент
  7. Client возвращает ответ, тот парсится. Если все нормально, то возращается управление программе
  8. Дальше где-то в программе происходит чтение по отслеживаемому адресу 0x123.
  9. loadAddr2Reg отслеживает это и посылает запрос OpenOCD клиенту.
  10. OpenOCD клиент обрабатывает его и возвращает ответ
  11. Если все нормально, но в программу возвращается значение из регистра МК
  12. Программа продолжается.

На этом пока все, полные исходники и примеры будут в следующих частях.

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»