«Что б они ни делали — не идут дела. Видимо в прошивке багов дофига». Как я напомнил в прошлой статье (где я подготовил утилиты для перепрошивки сенсоров) — я рассказываю про платформу для VR игр, как с ней интегрироваться и как добраться до ее сенсоров напрямую.
Её исходный ресивер обновляет сенсоры с частотой в 86Гц, тогда как технически возможно разогнать до 133 Гц, получив ощутимо ниже задержки, но связь была нестабильной.
Давайте начнём погружение в сенсоры — посмотрим, что за игра ghidra_11.0_PUBLIC установлена у меня в C:\Games, заглянем одним глазком в саму прошивку и поковыряемся там грязными патчиками, да исправим race condition плюс выкинем немного отладочных глюков. В общем, готовимся к погружению. В этот раз — всё серьёзно.
Гидра: как накормить дракона бинарником под ARM
Так как развлечения не позволяют покупать полноценную IDA с поддержкой ARM, а IDA Freeware только работает с x86, придётся пользоваться чем-нибудь другим. Этим чем-нибудь, разумеется, оказалась Ghidra — нашумевший несколько лет назад выложенный в опенсорс мощный дизассемблер, с системой скриптования, поддержкой множества архитектур и так далее.
Всё никак было не до него, а тут прекрасный повод подвернулся.
Скачиваем гидру куда-нибудь (C:\Games), распаковываем и запускаем через C:\Games\ghidra_11.0_PUBLIC\ghidraRun.bat. Создаём проект, через «File=>Import File» импортируем наш Bin файл. Гидра умеет разные форматы, но вот HEX не умеет, хорошо, что мы уже превратили в bin. Еще не определяет сама содержимое, хорошо, что мы уже знаем что это ARM, Cortex M3, little endian:
Гидра справшивает, не проанализировать ли файл, соглашаемся, и получаем ничего практически интересного, практически, чистый лист!
Регионы памяти
Не будем опускать руки, для начала, заполним карту памяти, которую я нашел где-то в документации у TI:
Идём в «Windows => Memory Map» и правим. Уже загруженный регион переименовываем во «flash» и убираем галочку «W» (это не записываемый регион). Через зеленый плюс в наборе инструментов в правом верхнем углу добавляем «rom» с адресом начинающимся с 10000000, длинною в 0x20000 (точнее в 0x1CC00 но это не важно), галочки ставим в R и X (убираем W). Еще добавляем регион «ram» с адреса 20000000, размером в 0x5000, R/W но не X.
С новой информацией мы понимаем, что в самом начале у нас адрес на стек, в RAM, 0x20004000, как и положено согласно карты памяти. Затем есть вектор, с которого начнётся исполнение, затем обработчики прерываний — все указывающие внутрь ROM, которого у нас нет, и в конце два каких-то кривых указателя. Не похожи на правду.
Но с картой памяти мы еще не закончили, еще есть «Peripherals» регион, через чтение-запись адресов в котором идёт доступ и настройка железа. Самое удобное, скачать CMSIS-SVD пакет, в котором собраны в машиночитаемом виде описание железа для большинства ARM чипов и модулей. А чтобы воспользоваться им, скачиваем SVD-Loader-Ghidra, после чего идём в «Window=>Script Manager», жмём на третью справа иконку (три таких полосочки, левее крестика и красного плюсика, называется «Manage Script Directories»), где через зелёный плюсик добавляем путь до скачанного плагина ($USER_HOME/Documents/GitHub/SVD-Loader-Ghidra в моём случае). Закрываем менеджер директорий, и в фильтр в менеджере скриптов вводим SVD, чтоб быстро найти его:
Двойным кликом запускаем его, он попросит выбрать требуемый SVD файл. Выбираем ...GitHub\cmsis-svd-data\data\TexasInstruments\CC26x0.svd.
Скрипт отработает и создаст требуемые регионы памяти в 0x40000000.
Разметка недоступного ROM
Когда код представляет собой сырую портянку, каждый бит полезной информации на вес золота. А потому, скачиваем SDK, распаковываем, и начинаем копаться. Мы знаем, что отладка возможна. То есть какие-то отладочные символы должны лежать. Осталось их найти 🙂
Проще всего начать с адреса прерываний: 1001c901, на который указывают все вектора. Возвращаемся в линукс, и:
$ cd /mnt/c/ti/simplelink_cc2640r2_sdk_5_30_00_03 $ find . -name '*map' | xargs grep -i '1001c90' ./kernel/tirtos/packages/ti/sysbios/rom/cortexm/cc26xx/r2/golden/CC26xx/rtos_rom.map: 1001c900 00000020 arm_m3_Hwi_asm_rom.obj (.text:ti_sysbios_family_arm_m3_Hwi_excHandlerAsm__I) ./kernel/tirtos/packages/ti/sysbios/rom/cortexm/cc26xx/r2/golden/CC26xx/rtos_rom.map:1001c901 ti_sysbios_family_arm_m3_Hwi_excHandlerAsm__I ./kernel/tirtos/packages/ti/sysbios/rom/cortexm/cc26xx/r2/golden/CC26xx/rtos_rom.map:1001c901 ti_sysbios_family_arm_m3_Hwi_excHandlerAsm__I
Отлично, выглядит интересно! Надо только прогрузить. В плагинах с гидрой уже есть ImportSymbolsScript.py, но игры с ним оказались неудобными. Под свой случай я подправил его до ArmImportSymbolsScript.py. Основная модификация: если символ это ссылка на функцию, то адрес округляется до четного и на этом месте создаётся dword, чтобы адрес+1 указывал прямо на этот символ. Это позволило импортировать символы более удобно для автоанализа и чтения когда потом.
Впрочем, нам всё равно надо символы превратить в подходящий формат: <имя> <адрес> <тип f или нет>.
А еще, карта хоть и совпадает для символов, то, что ниже 1xxxxxxx — не всё правда. Например, по адресу 00001a25 main — совсем не main.
Самое простое — не разбираться с этой картой, а разобрать напрямую приложенный объектник:
$ cd /mnt/c/ti/simplelink_cc2640r2_sdk_5_30_00_03 $ mkdir symbols $ objdump -t ./kernel/tirtos/packages/ti/sysbios/rom/cortexm/cc26xx/r2/golden/CC26xx/CC2640R2F_rtos_rom_syms.out | perl -ne '@a=split;print "$a[-1] $a[0] $a[2]\n" if $a[1] eq "g"' > symbols/rom.txt
И теперь мы можем его подгрузить двойным кликом по ArmImportSymbolsScript.py в Scripts Manager, выберем файл из C:\ti\simplelink...\scripts\rom.txt:
$ for k in source/ti/ble5stack/rom/ble_rom_releases/cc26xx_r2/Final_Release/*symbols; do objdump -t $k | perl -ne '@a=split;print "$a[-1] $a[0] $a[2]\n" if $a[1] eq "g"' > symbols/${k##*/}.txt done
Импортируем ble_r2.symbols.txt и common_r2.symbols.txt.
Разметка API SDK
Как мы видели в карте памяти, с адреса 0x1000 начинается «TI RTOS ROM Jump Table». Если сходим туда, увидим кучу указателей. Но не джампов… Впрочем, в TI системе есть еще одна огромная табличка переходов — «ROM_Flash_JT» — она расположена совсем не по 0x1000, но найти её легко: достаточно взять функцию из тех, что мы уже нашли в символах — я использую HCI_bm_alloc — и найти ссылку на неё:
Это и есть она. Переименовываем в «ROM_Flash_JT». Теперь мы можем сравнить табличку с содержимым в rom_init.c (полный путьC:\ti\simplelink_cc2640r2_sdk_5_30_00_03\source\ti\blestack\rom\r2\rom_init.c), чтобы расставить еще кучу имён. Чтобы не делать это вручную, Я накидал скриптик ROM_Flash_JT — запускаем, выбираем rom_init.c, наслаждаемся:
Распаковка ROM2RAM
Еще один важный момент: при работе часто используется статическая инициализация разных структур в оперативной памяти. То есть когда в коде написано что-то типа
const char * str = "MyString"; char * str2 = "OtherString";
то str можно ставить как указатель на строку в ROM, то str2 должна быть скопирована в RAM, так как мы можем её менять. То же самое с константами в статических структурах и так далее. Чтобы это работало, компилятор перед запуском пользовательского кода производит распаковку констант, сохранённых в ROM. Насколько понимаю, разные компиляторы делают это по разному, но в случае с GCC использованным в TI SDK (как часть XDC Tools), таблица перекидывания прописана в самом начале кода. Скачиваем скрипт arm-romtotram, затем идём по адресу указанному в Reset:
Из неё нам требуется проставить три метки:
-
«ROMtoRAMtable» — начальное значение цикла,
-
«ROMtoRAMtableEnd» — финальное значение цикла (гидра может не позволить сразу поставить там метку, придётся дважды кликнуть на неё, создать там указатель кнопкой «p» и потом уже через «L» поставить метку «ROMtoRAMtableEnd»),
-
Указатель на массив обработчиков использованный внутри цикла — «ROMtoRAM_Processors».
Функция принимает вид:
void unpackRomToRam(void) { byte **ppbVar1; for (ppbVar1 = (byte **)&ROMtoRAMtable; ppbVar1 < &ROMtoRAMtableEnd; ppbVar1 = ppbVar1 + 2) { (*(code *)(&ROMtoRAM_Processors)[**ppbVar1])(*ppbVar1 + 1,ppbVar1[1]); } xdc_runtime_Startup_exec__E(); return; }
Таблица процессоров содержит три функции: функция распаковки чего-то напоминающего LZ (переносим байт как есть, или копируем N байт начиная с M байт в прошлое), memcpy и обнуления региона. Сама таблица rom2ram содержит просто пары адресов — адрес в ROM аргумент для распаковщика и адрес в RAM для получателя.
Теперь, когда мы подписали все три метки, можно запустить «arm-romtoram» скрипт, и бульк — всё готово.
Анализ кода
Когда мы собрали всё, что может нам понять код, можем приступать. Для начала поправим непонятные ссылки, которые висят в SysTick и IRQ. Это не указатели, а уже начальный код прошивки, поэтому сбросим из через «c», переименуем SysTick в «Begin» и сделаем его кодом через F12, затем функцией через «f».
Код выглядит как инициализация чего-то:
FUN_0000c9d0(&DAT_20001130,&DAT_20001150); ti_sysbios_knl_Queue_construct(&DAT_2000118c,0); DAT_20001154 = &DAT_2000118c; FUN_0000cdfc(&DAT_2000120c,&LAB_000100e2+1,0,8); FUN_0000cdfc(&DAT_20001230,&LAB_000100e2+1,3,4); FUN_0000cdfc(&DAT_20001254,&LAB_000100e2+1,0,0xb); FUN_0000cdfc(&DAT_200011e8,&LAB_000100e2+1,0,3); DAT_20002038 = FUN_00009730(&DAT_20002040,&DAT_20002064); if (DAT_20002038 == 0) { do { /* WARNING: Do nothing block with infinite loop */ } while( true ); } FUN_00005bf4(0x10,0xe165,0x1f,6); uVar20 = 0; local_5c = 9; local_5e = 8; local_53 = 0; local_56 = 0; local_5a = 100; local_58 = 1000; FUN_0000363c(0x306,2,&local_56); FUN_0000363c(0x308,0x10,&DAT_200011a4); FUN_0000363c(0x307,7,&DAT_20001174); FUN_0000363c(0x310,1,&local_53); FUN_0000363c(0x311,2,&local_5e); FUN_0000363c(0x312,2,&local_5c); FUN_0000363c(0x313,2,&local_5a); FUN_0000363c(0x314,2,&local_58); FUN_00005bf4(0x10,0xe165,6,0xa0); FUN_00005bf4(0x10,0xe165,7,0xa0); FUN_00005bf4(0x10,0xe165,8,0xa0); FUN_00005bf4(0x10,0xe165,9,0xa0); local_64 = 0; local_50 = 0; local_52 = 1; local_51 = 1; local_4f = 1; FUN_00005bf4(0x10,&DAT_00003fb5,0x408,4,&local_64); FUN_00005bf4(0x10,&DAT_00003fb5,0x400,1,&local_52); FUN_00005bf4(0x10,&DAT_00003fb5,0x402,1,&local_51); FUN_00005bf4(0x10,&DAT_00003fb5,0x403,1,&local_50); FUN_00005bf4(0x10,&DAT_00003fb5,0x406,1,&local_4f); FUN_00005bf4(0x10,&LAB_0000f3f8+1,&DAT_2000117c);
Осталось понять чего. Для начала, сделаем поиск по константам (через Notepad++ или grep’ом):
datacompboy@NUUBOX:/mnt/c/ti/simplelink_cc2640r2_sdk_5_30_00_03$ find . -name '*.h' | xargs grep 0x306 ./kernel/tirtos/packages/gnu/targets/arm/libs/install-native/arm-none-eabi/include/elf.h:#define NT_S390_LAST_BREAK0x306 ./source/ti/blestack/profiles/roles/cc26xx/broadcaster.h:#define GAPROLE_ADV_EVENT_TYPE 0x306 //!< Advertisement Type. Read/Write. Size is uint8_t. Default is GAP_ADTYPE_ADV_IND (defined in GAP.h). ./source/ti/blestack/profiles/roles/cc26xx/multi.h:#define GAPROLE_ADVERT_OFF_TIME 0x306 ./source/ti/blestack/profiles/roles/cc26xx/peripheral.h:#define GAPROLE_ADVERT_OFF_TIME 0x306 ./source/ti/blestack/profiles/roles/peripheral_broadcaster.h:#define GAPROLE_ADVERT_OFF_TIME 0x306 //!< Advertising Off Time for Limited advertisements (in milliseconds). Read/Write. Size is uint16. Default is 30 seconds.
О, прекрасно, у нас же сенсор, значит из peripheral. Давайте подгрузим константы в гидру:
File=>Parse C source=>зеленый плюс=>»c:\ti\simplelink_cc2640r2_sdk_5_30_00_03\source\ti\blestack\profiles\roles\cc26xx\peripheral.h»=>Parse to program.
Ругнётся на что-то, но константы добавится. Тыкаем в «0x306», жмём «E» видим GAPROLE_ADVERT_OFF_TIME — двойным кликом применяем. Ниже видим 408/400/402… Можем повторить с ним, но лучше понять что за функции, и почему они не подписаны.
Поищем на тему примеров с GAPROLE_SCAN_RSP_DATA:
$ find . -name '*.c' | xargs grep GAPROLE_SCAN_RSP_DATA ./examples/rtos/CC2640R2_LAUNCHXL/blestack/multi_role/src/app/multi_role.c: GAPRole_SetParameter(GAPROLE_SCAN_RSP_DATA, sizeof(scanRspData), ./examples/rtos/CC2640R2_LAUNCHXL/blestack/project_zero/src/app/project_zero.c: GAPRole_SetParameter(GAPROLE_SCAN_RSP_DATA, sizeof(scanRspData), scanRspData); ./examples/rtos/CC2640R2_LAUNCHXL/blestack/simple_broadcaster/src/app/simple_broadcaster.c: GAPRole_SetParameter(GAPROLE_SCAN_RSP_DATA, sizeof (scanRspData), ./examples/rtos/CC2640R2_LAUNCHXL/blestack/simple_np/src/app/simple_np_gap.c: GAPRole_SetParameter(GAPROLE_SCAN_RSP_DATA, sizeof(scanRspData), ./examples/rtos/CC2640R2_LAUNCHXL/blestack/simple_np/src/app/simple_np_gap.c: status = GAPRole_SetParameter(GAPROLE_SCAN_RSP_DATA, len, pDataPtr); ./examples/rtos/CC2640R2_LAUNCHXL/blestack/simple_peripheral/src/app/simple_peripheral.c: GAPRole_SetParameter(GAPROLE_SCAN_RSP_DATA, sizeof(scanRspData), ./examples/rtos/CC2640R2_LAUNCHXL/blestack/simple_peripheral/src/app/simple_peripheral_dbg.c: GAPRole_SetParameter(GAPROLE_SCAN_RSP_DATA, sizeof(scanRspData), ./examples/rtos/CC2640R2_LAUNCHXL/blestack/simple_peripheral_oad_offchip/src/app/simple_peripheral_oad_offchip.c: GAPRole_SetParameter(GAPROLE_SCAN_RSP_DATA, sizeof(scanRspData), ./examples/rtos/CC2640R2_LAUNCHXL/blestack/simple_peripheral_oad_onchip/src/app/simple_peripheral_oad_onchip.c: GAPRole_SetParameter(GAPROLE_SCAN_RSP_DATA, sizeof(scanRspData), ./examples/rtos/CC2640R2_LAUNCHXL/blestack/simple_peripheral_oad_onchip/src/persistent_app/oad_persistent_app.c: GAPRole_SetParameter(GAPROLE_SCAN_RSP_DATA, sizeof(scanRspData), ./examples/rtos/CC2640R2_LAUNCHXL/blestack/simple_peripheral_secure_fw/src/app/simple_peripheral_dbg.c: GAPRole_SetParameter(GAPROLE_SCAN_RSP_DATA, sizeof(scanRspData),
Что у нас там в simple_peripheral.c:
// Setup the Peripheral GAPRole Profile. For more information see the User's // Guide: // http://software-dl.ti.com/lprf/sdg-latest/html/ { // By setting this to zero, the device will go into the waiting state after // being discoverable for 30.72 second, and will not being advertising again // until re-enabled by the application uint16_t advertOffTime = 0; uint8_t enableUpdateRequest = DEFAULT_ENABLE_UPDATE_REQUEST; uint16_t desiredMinInterval = DEFAULT_DESIRED_MIN_CONN_INTERVAL; uint16_t desiredMaxInterval = DEFAULT_DESIRED_MAX_CONN_INTERVAL; uint16_t desiredSlaveLatency = DEFAULT_DESIRED_SLAVE_LATENCY; uint16_t desiredConnTimeout = DEFAULT_DESIRED_CONN_TIMEOUT; GAPRole_SetParameter(GAPROLE_ADVERT_OFF_TIME, sizeof(uint16_t), &advertOffTime); GAPRole_SetParameter(GAPROLE_SCAN_RSP_DATA, sizeof(scanRspData), scanRspData); GAPRole_SetParameter(GAPROLE_ADVERT_DATA, sizeof(advertData), advertData); GAPRole_SetParameter(GAPROLE_PARAM_UPDATE_ENABLE, sizeof(uint8_t), &enableUpdateRequest); GAPRole_SetParameter(GAPROLE_MIN_CONN_INTERVAL, sizeof(uint16_t), &desiredMinInterval); GAPRole_SetParameter(GAPROLE_MAX_CONN_INTERVAL, sizeof(uint16_t), &desiredMaxInterval); GAPRole_SetParameter(GAPROLE_SLAVE_LATENCY, sizeof(uint16_t), &desiredSlaveLatency); GAPRole_SetParameter(GAPROLE_TIMEOUT_MULTIPLIER, sizeof(uint16_t), &desiredConnTimeout); }
Ха! 1-в-1: GAPROLE_ADVERT_OFF_TIME, GAPROLE_SCAN_RSP_DATA, GAPROLE_ADVERT_DATA, GAPROLE_PARAM_UPDATE_ENABLE…
Переименовываем FUN_0000363c => GAPRole_SetParameter, смотрим ниже:
// Set the Device Name characteristic in the GAP GATT Service // For more information, see the section in the User's Guide: // http://software-dl.ti.com/lprf/sdg-latest/html GGS_SetParameter(GGS_DEVICE_NAME_ATT, GAP_DEVICE_NAME_LEN, attDeviceName); // Set GAP Parameters to set the advertising interval // For more information, see the GAP section of the User's Guide: // http://software-dl.ti.com/lprf/sdg-latest/html { // Use the same interval for general and limited advertising. // Note that only general advertising will occur based on the above configuration uint16_t advInt = DEFAULT_ADVERTISING_INTERVAL; GAP_SetParamValue(TGAP_LIM_DISC_ADV_INT_MIN, advInt); GAP_SetParamValue(TGAP_LIM_DISC_ADV_INT_MAX, advInt); GAP_SetParamValue(TGAP_GEN_DISC_ADV_INT_MIN, advInt); GAP_SetParamValue(TGAP_GEN_DISC_ADV_INT_MAX, advInt); }
Хмм… У нас следом за пачкой GAPRole_SetParameter идет просто четыре вызова, ничего напоминающего вызов GGS_SetParameter нет. Кажется, начинаю понимать, почему нет таблицы параметров — её вырезали из примера. Но еще один непонятный момент — GAP_SetParamValue должен иметь два аргумента, а у нас 4:
FUN_00005bf4(0x10,0xe165,6,0xa0); FUN_00005bf4(0x10,0xe165,7,0xa0); FUN_00005bf4(0x10,0xe165,8,0xa0); FUN_00005bf4(0x10,0xe165,9,0xa0);
Давайте убедимся, что это они:
$ find . -name '*.h' | xargs grep TGAP_LIM_DISC_ADV_INT_MAX ./source/ti/blestack/inc/gap.h:#define TGAP_LIM_DISC_ADV_INT_MAX 7
Да, они…. А что такое GAP_SetParamValue, может, это макрос?
$ find . -name '*.h' | xargs grep GAP_SetParamValue ./source/ti/ble5stack/icall/inc/ble_dispatch_lite_idx.h:#define IDX_GAP_SetParamValue JT_INDEX(152) ./source/ti/ble5stack/icall/inc/icall_api_idx.h:#define IDX_GAP_SetParamValue GAP_SetParamValue ./source/ti/ble5stack/icall/inc/icall_ble_api.h:#define GAP_SetParamValue(...) (icall_directAPI(ICALL_SERVICE_CLASS_BLE, (uint32_t) IDX_GAP_SetParamValue , ##__VA_ARGS__)) ./source/ti/ble5stack/icall/inc/icall_ble_apimsg.h: * @see GAP_SetParamValue() ./source/ti/ble5stack/inc/gap.h: * Parameters set via @ref GAP_SetParamValue ./source/ti/ble5stack/inc/gap.h:extern bStatus_t GAP_SetParamValue(uint16_t paramID, uint16_t paramValue); ./source/ti/ble5stack/rom/map_direct.h:#define MAP_GAP_SetParamValue GAP_SetParamValue
Вот оно что! В зависимости от флагов компиляции, вызов либо прямой, либо косвенный через таблицу джампов (с индексом 152), либо косвенный по прямому адресу. Убедимся, что ICALL_SERVICE_CLASS_BLE == 0x10:
$ find . -name '*.h' | xargs grep ICALL_SERVICE_CLASS_BLE ... ./source/ti/blestack/icall/src/inc/icall.h:#define ICALL_SERVICE_CLASS_BLE 0x0010 ./source/ti/blestack/icall/src/inc/icall.h:#define ICALL_SERVICE_CLASS_BLE_MSG 0x0050 ./source/ti/blestack/icall/src/inc/icall.h:#define ICALL_SERVICE_CLASS_BLE_BOARD 0x0088 ...
переименуем FUN_00005bf4 в icall_directAPI, адрес 0xe165 в GAP_SetParamValue. Смотрим дальше пример:
GAPBondMgr_SetParameter(GAPBOND_PAIRING_MODE, sizeof(uint8_t), &pairMode); GAPBondMgr_SetParameter(GAPBOND_MITM_PROTECTION, sizeof(uint8_t), &mitm); GAPBondMgr_SetParameter(GAPBOND_IO_CAPABILITIES, sizeof(uint8_t), &ioCap); GAPBondMgr_SetParameter(GAPBOND_BONDING_ENABLED, sizeof(uint8_t), &bonding); GAPBondMgr_SetParameter(GAPBOND_LRU_BOND_REPLACEMENT, sizeof(uint8_t), &replaceBonds);
как раз 408, 400, … переименовываем 0x3fb5 => GAPBondMgr_SetParameter.
Процесс дальше, впрочем не похож на инициализацию таблицы. Таки либо пример неверный, либо таки отдельные сервисы типа GSS вырезаны.
Заметки на полях: поиск полезных данных никогда не бывает лишним. Выше я показывал вывод strings, в котором были интересные вещи — «inputGyroRv» и «inputNormal». Поиск по ним на github дал сходу интересную вещь, что позволило еще разметить часть функций и структур, которыми пользуется сенсор направления. Для ног однако подобной фкусности не обнаружилось.
Тут можно бесконечно пытаться понять дальше, однако ж, давайте научимся менять код прошивки. Иначе зачем нам это всё?
Играемся с прошивкой
Для эксперимента, мы уже пропатчили прошивку, заменив «KATVR», которая используется для анонса данных, на «KAT-F» (типа «Ноги»). Но это не интересно. Каждый сенсор прошивается его типом — левый или правый — хорошо бы, чтобы он анонсировал себя «KAT-R» или «KAT-L»! Для этого надо найти где же хранится его тип (левый-правый).
Мы знаем, что спаривание висит на USB, и пакеты 0x55/0xAA. Простым пролистыванием вниз натыкаемся на кусок:
case 0xc: cVar6 = *(char *)(puVar23 + 1); pcVar26 = *(char **)(puVar23 + 2); FUN_0000af16(&DAT_200011c8,0,0x1f); if ((*pcVar26 == 'U') && (pcVar26[1] == -0x56)) { DAT_200011cb = 0; DAT_200011c8 = 0x55; DAT_200011c9 = 0xaa; cVar4 = (char)local_48; cVar7 = DAT_200011c0; if (cVar6 == '\x01') { ....
который явственно обрабатывает событие «пакет по USB» и реагирует на него. WriteDeviceId это команда 0x04:
if (cVar6 == '\x03') { DAT_200011cc = '\x03'; cVar4 = DAT_200011cc; cVar7 = DAT_200011c1; goto LAB_000009b8; } if (cVar6 == '\x04') { DAT_200011c1 = pcVar26[5]; PostMsg(7,0,0); DAT_200011cd = 0; DAT_200011cc = '\x04'; FUN_0000fa20(); FUN_0000f8ec(); break; }
Вывод — DAT_200011c1 это искомый ID сенсора, он же использован как аргумент в ReadDeviceId (команда 0x03). Кросс-реферес по ссылкам на него находит интересную простую функцию:
void FUN_0000f8ec(void) { if (ParamDeviceId == '\x03') { DAT_20001132 = 5; } else { DAT_20001132 = 4; } return; }
Которая вызывается дважды из Begin() — при инициализации (очевидно, после подгрузки параметров) и после WriteDeviceId. Идеальная точка врезки!
Изучим ассемблер:
************************************************************* * FUNCTION ************************************************************* undefined FUN_0000f8ec () undefined r0:1 <RETURN> FUN_0000f8ec XREF[2]: 0000038c (c) , 000009a4 (c) 0000f8ec 04 49 ldr r1,[DAT_0000f900 ] = 20001130h 0000f8ee 91 f8 91 00 ldrb.w r0,[r1,#0x91 ]=>ParamDeviceId 0000f8f2 03 28 cmp r0,#0x3 0000f8f4 14 bf ite ne 0000f8f6 04 20 mov.ne r0,#0x4 0000f8f8 05 20 mov.eq r0,#0x5 0000f8fa 88 70 strb r0,[r1,#0x2 ]=>DAT_20001132 0000f8fc 70 47 bx lr 0000f8fe c0 ?? C0h 0000f8ff 46 ?? 46h F DAT_0000f900 XREF[1]: FUN_0000f8ec:0000f8ec (R) 0000f900 30 11 00 20 undefine 20001130h ? -> 20001130
Итак, функция грузит в r1 адрес объекта из неподалёку лежащей константы; затем грузит в r0 байт из объекта+смещение. Сравнивает этот байт с 0x3 (левая нога), и затем использует инструкцию «ite ne».
Очень красивая система условного выполнения без переходов. В полноценном ARM режиме у каждой команды просто приписано выполняется ли она при флагах, в thumb режиме всё задаётся командой — (I)f,(T)hen,(E)lse, которая может быть просто «IT» (if-then, выполнить инструкцию если совпадает условие), ITT (if-then-then, выполнить две). Управляется от 1 до 4 инструкций, и первая следующая всегда then.
Затем два mov в R0, первая выполнится если R0 != 3, вторая если R0 == 3.
Затем новое значение сохраняется в другой байт в структуре и идёт переход на адрес lr. Так как функция трогает только регистры r0 и r1 (они же — параметры функций), их сохранять похоже не обязательно.
Так как после возврата у нас есть еще два байта (выравнивание), мы можем заменить (bx lr + nop) на один длинный jmp куда-нибудь.
Сразу по окончании ROMtoRAM таблицы у нас как раз пустое место, всё нули. Для удобства отмотаем до круглого числа (0x12e10) и придумаем что мы хотим вписать. Превратим «KATVR» в «KAT-L» или «KAT-R» в зависимости от настройки. Соответственно, надо просто превратить R0 в L или R, и записать куда-надо. А куда? Строка «KATVR» нас дважды: один раз в ПЗУ части, один раз в ОЗУ в пакете для анонса:
DAT_200011a4 XREF[1]: 000000ee (*) 200011a4 06 ?? 06h 200011a5 09 ?? 09h 200011a6 4b ?? 4Bh K 200011a7 41 ?? 41h A 200011a8 54 ?? 54h T 200011a9 56 ?? 56h V 200011aa 52 ?? 52h R 200011ab 05 ?? 05h
Итого, нам надо вписать эквивалент:
if(left) { scanRsp[6] = 'L'; } else { scanRsp[6] = 'R'; } scanRsp[5] = '-'; // можно сделать патч в ROM, можно вписать тут
К моменту выхода из функции в R0 у нас 4 или 5 в зависимости от ноги, а в R1 у нас указатель на 20001130. Расстояние от 20001130 до 200011A9 — 0x79. В принципе, у нас еще и флаги уже стоят с прошлого сравнения. То есть можно сделать нечто вроде:
ite ne mov.ne r0,#'L' mov.eq r0,#'R' strb r0,[R1,#0x7A] mov r0,#'-' strb r0,[R1,#0x79] bx lr
Чтобы править код в гидре нужно очистить область от режима (если там уже есть инструкции), затем через ctrl+shift+g включить ассемблер для текущей строки, она ругнётся что процессор не тестирован. Вписываем инструкцию и аргументы, воюем с ней так как не всегда понимает, что мы хотим, но вроде получается. Правда на «mov.ne r0,#’L’» она откаывается реагировать! Когда такое начиналось, я пользовался online assembler где вводил инстуркции и потом переносил байтики. «mov r0, #’L’» => «4f f0 4c 00″… Не не, надо 2хбайтную, «movs r0, #’L’» => «4c 20» — идеально. «movs r0, #’R’» => «52 20». И так далее…
Эм…. Что-то гидре поплохело:
MoveFeetNumNew 00012e10 14 bf ite ne 00012e12 4c 20 mov.ne r0,#0x4c 00012e14 52 20 mov.eq r0,#0x52 00012e16 81 f8 7a 00 strb.eq.w r0,[r1,#0x7a ] 00012e1a 81 f8 79 00 strb.eq.w r0,[r1,#0x79 ] 00012e1e 70 47 bx.eq lr
Непонятно почему, но в дизассемблере залип отслеживатель флагов. Жаль, но, проигнорируем. Зальем патч… Вот только не сработало. 🙁
Надо не только сменить структуру, но еще и вызвать GAPRole_SetParameter(GAPROLE_SCAN_RSP_DATA, 0x10, &scanRsp).
Хорошо, тогда нам нужен указатель на scanRsp в R2. Поправим код:
ite ne mov.ne r0,#'L' mov.eq r0,#'R' adds.w r2,r1,#0x74 strb r0,[R2,#6] movw r0,#0x308 movs r1,#0x10 b.w GAPRole_SetParameter
Всё хорошо, но тут гидре сорвало крышу, декомпиляция совсем сошла с ума. Ткнём в «b.w» инструкцию правой кнопкой мыши и через «modify instruction flow» сменим её на «CALL_RETURN». Уже лучше. Чтобы вылечить залипшие «.eq», можно ткнуть на первую кривую инструкцию (adds.eq.w) правой, и выбрать «Clear Flow and Repair», после чего опять F12 — код исправится (более-менее):
MoveFeetNumNew XREF[1]: MoveFeetNumSmt:0000f8fc (j) 00012e10 14 bf ite ne 00012e12 4c 20 mov.ne r0,#0x4c 00012e14 52 20 mov.eq r0,#0x52 00012e16 11 f1 74 02 adds.w r2,r1,#0x74 00012e1a 90 71 strb r0,[r2,#0x6 ] 00012e1c 40 f2 08 30 movw r0,#0x308 00012e20 10 21 movs r1,#0x10 00012e22 f0 f7 0b bc b.w GAPRole_SetParameter undefined GAPRole_SetParameter() -- Flow Override: CALL_RETURN (CALL_TERMINATOR)
Декомпиляция тоже станет лучше:
void MoveFeetNumSmt(void) { if (DeviceId == '\x03') { DAT_20001132 = 5; UNK_200011aa = 0x52; } else { DAT_20001132 = 4; UNK_200011aa = 0x4c; } GAPRole_SetParameter(0x308,0x10); return; }
Единственное, она потеряла аргумент к функции. Если нажать на ней правой кнопкой и сделать Edit Function, добавить три аргумента и выставить им простые типы:
Код окончательно примет нормальный вид:
void MoveFeetNumSmt(void) { if (DeviceId == '\x03') { DAT_20001132 = 5; UNK_200011aa = 0x52; } else { DAT_20001132 = 4; UNK_200011aa = 0x4c; } GAPRole_SetParameter(0x308,0x10,&scanRspData); return; }
Прекрасно, опять экспортируем патч (File=>Export Program), «fc.exe /b .\katvr_foot_orig.bin .\katvr_foot.bin» и так далее. Загружаем в сенсор. Урра, работает! 🙂 Видим «KAT-R» и «KAT-L» устройства плавающие вокруг. Красота.
Поиграли — пора и делом заняться.
Разгоняем ноги на KatWalk C2 до 133 Гц
Чтобы понять в чем проблема с сенсорами, надо думать как сенсоры. Итак, мы знаем, что сенсоры отдают данные через Notification. Пойдём искать, где же они используются. Слева в «Symbol tree» в поле поиска вводим Notifi и легко переходим на GATT_Noficiation. Ссылок не видно. Значит, используется косвенный вызов, делаем поиск по 0x10010045 (адрес+1, ибо код в Thumb режиме) и находим одну единственную функцию, где идёт подготовка, потом вызов:
_DAT_20001186 = 0x14; _DAT_20001188 = (undefined *)thunk_EXT_FUN_10018404(DAT_20001142,0x1b,0x14,0,in_r3); cVar1 = DAT_20000521; if (_DAT_20001188 != (undefined *)0x0) { _DAT_20001184 = 0x2e; if (_DAT_20001146 == 0) { ... cVar1 = icall_directAPI(0x10,(int)&GATT_Notification + 1,DAT_20001142,&DAT_20001184,0);
0x2E — это же наш handle, так что да, мы нашли то самое место.
Если упростить, код получается такой:
void KatSendNotification() { out = malloc(...); if (out) { if (packetNo == 0) { out->_type = 0; // status packet fill_charge_levels(out); } else { out->_type = 1; // data packet if (!DATA_READY || !DATA_OK) { out->_x = 0; out->_y = 0; } else { DATA_READY = false; out->_x = DATA_X; out->_y = DATA_Y; } out->status = STATUS; } if (something) { out[0] = 0; out[1] = 1; out[2] = 0; out[3] = 1; out[4] = 0; out[5] = 1; } attHandleValueNoti_t notification = { 0x2e, 0x14, out }; if (!GATT_Notification(0x2E, ¬ification, 0)) { free(out); } if (++packetNo == 500) { packetNo = 0; } }
То есть, каждые 500 пакетов отправляется уровень заряда (плюс версия прошивки и ID сенсора), все остальные пакеты содержат данные. Причем если данные не готовы (или была ошибка связи с сенсором), то отсылаются нули. Еще, если стоит какой-то флаг, то начало пакета перетирается последовательностью 0-1-0-1-0-1.
Теперь нужно понять, как часто этот GATT_Nofication вызывается. Ссылок на «KatSendNotification» (как я назвал эту функцию) две, и обе из основного потока, обе внутри цикла обработки событий:
while(!QueueEmpty(...)) { Entry* event = QueueGet(...); switch (event->id) { ... case 4: KatSendNotification(); ClockStop(...); break; case 6: if (Flag1 == 1 || Flag2 == 1) { KatSendNotification(); } break ... } }
Событие №4 должно бы посылаться таймером, но таймер не запущен — и даже если будет запущен, сразу будет остановлен. Никаких других ссылок на этот таймер я не нашел.
Событие №6 генерируется по коллбэку на обработку события BLE, так что похоже на то, что события посылаются после того как два флага взведены (один из них — соединение установлено, второй не совсем понял сходу), причем следующее событие формируется после отправки предыдущего. Окей, это совпадает с наблюдением, что поток начинается после изменения параметра соединения.
Пока непонятно, но, вроде, сколько раз спросили — столько раз мы должны ответить. Что ж, посмотрим, на когда данные появляются? Кросс-референс на DATA_OK приводит нас к функции:
void ReadSensorData(...) { do { Semaphore_Pend(SensorSemaphore, -1); Task_sleep(700); GPIO_SET(..., 0); SPI_Send(0x50); Task_sleep(10); char* out = &SensorData; for (int i = 0x0C; i; --i) { *(out++) = SPI_Recv(); } GPIO_SET(..., 1); Task_sleep(0.1); DATA_OK = 0; if ((SensorData[0] & 0x80 != 0) && (SensorData[0] & 0x20 != 0)) { DATA_OK = 1; } DATA_READY = 1; SensorReads++; If (SensorReads > 99) { // refresh something SensorReads = 0; } if (SomeFlag == 0) { Semaphore_Post(SensorSemaphore); } } while(true); }
Функция занимается обновлением данных с сенсора до тех пор, пока SomeFlag не будет взведён. Как показало дальнейшее расследование — этот флаг означает переход в режим сна. Семафор инициализируется на старте и сразу взводится, то есть датчик читается непрерывно пока мы не спим, с перерывами в 700+10+13байтSPI + обновление еще чего-то раз в 100 чтений. SPI настроен на 4 мегабод. Единицы сна в десятках микросекунд. То есть сенсор обновляется раз в ~7.11 миллисекунды, или около 140-141 Гц. Выглядит так, что не должно бы быть проблем с обновлением сенсора на 133Гц. Однако же они есть.
Наблюдаемое поведение — иногда мы видим нулевые пакеты (ведешь себе ровненько сенсор, а точка в гейтвее внезапно срывается в центр).
Как ни странно, это поведение вполне объяснимо: при обновлении данных каждые ~140Гц и чтении 133Гц, мы двигаемся рядом, и вероятность того, что пакет был обновлён вот-только-что, а мы обновляем данные — крайне высока. Мы наблюдаем состояние гонки, так как никакой синхронизации между отправкой, формированием и обновлением данных нет.
Решение, как ни странно, очевидно. Нам нужны новые данные для отправки, поэтому читать сенсор нужно только когда есть связь. Пакеты запрашиваются регулярно. А что если… Перенести вызов Semaphore_Post из ReadSensorData в KatSendNotification? Тогда получится идеальная связка: ReadSensorData() подготовит данные, KatSendNotification их заберёт — и запросит опять. Идеально. Единственное, что KatSendNotification вызывается после отправки прошлого пакета, то есть задержку изнутри ReadSensorData убирать нельзя. Я бы, пожалуй, чуток ее даже понизил, чтобы пакет точно был готов. Так как мы обновляем пакеты каждые 86..133Гц, момент замера скорости не так критичен, до тех пор, пока задержка одна и та же — 5 миллисекунд погоды не сделают.
Достанем блокнотик и спланируем наш патч. Во-1х снизим задержку:
000079d0 41 f6 58 31 movw r1,#7000 # The delay => 000079d0 41 f2 88 31 movw r1,#5000
Во-2х, замкнём цикл до вызова SensorSemaphore. Способов замкнуть цикл множество: можно заменить в IF условный jmp (bne) на безусловный, можно просто заNOPитьвызов функции, а можно тупо вписать переход вместо всего IFа:
LAB_00007ab0 XREF[1]: 00007a54 (j) 00007ab0 28 78 ldrb r0,[r5,#0x0 ]=>SleepState = 52h 00007ab2 00 28 cmp r0,#0x0 00007ab4 98 d1 bne LAB_000079e8 00007ab6 68 68 ldr r0,[r5,#0x4 ]=>SensorReadSem 00007ab8 f9 f7 7e fa bl Semaphore_post undefined Semaphore_post() 00007abc 94 e7 b LAB_000079e8 => LAB_00007ab0 XREF[1]: 00007a54 (j) 00007ab0 9a e7 b LAB_000079e8
И в-3их, поправить KatSendNotification. Действуем аналогично прошлому подходу с реакцией на лампочки — уходим на свободный кусочек в конце (после прошлого патча, я взял 00012e40). Соответственно, в конце функции меняем return на jmp:
LAB_00006b14 XREF[1]: 00006a08 (j) 00006b14 f8 bd pop {r3,r4,r5,r6,r7,pc} 00006b16 c0 ?? C0h 00006b17 46 ?? 46h F => LAB_00006b14 XREF[1]: 00006a08 (j) 00006b14 0c f0 94 b9 b.w KatSendNotificationTail
И на новом месте формируем кусочек подобный вырезанному из ReadSensorData. Главная сложность, по сравнению с ReadSensorData, это спланировать сколько надо места перед хранением константы для загрузки указателя на семафор. Так же может понадобиться поправить переход, сбросить/вызвать Repair Flow после правок — но после заполнения всего кода и сброса, всё получается:
=> KatSendNotificationTail XREF[1]: KatSendNotification:00006b14 (j) 00012e40 03 4d ldr r5,[->SleepState ] = 20001584 00012e42 28 78 ldrb r0,[r5,#0x0 ] 00012e44 00 28 cmp r0,#0x0 00012e46 02 d1 bne LAB_00012e4e 00012e48 68 68 ldr r0,[r5,#0x4 ] 00012e4a ee f7 b5 f8 bl Semaphore_post undefined Semaphore_post() LAB_00012e4e XREF[1]: 00012e46 (j) 00012e4e f8 bd pop {r3,r4,r5,r6,r7,pc} PTR_SleepState_00012e50 XREF[1]: 00012e40 (R) 00012e50 84 15 00 20 addr SleepState = 52h
Заливка патча, на удивление, сработала с первого раза, ресивер на 133Гц получает пакеты стабильно. Если медленно вести, в гейтвее нет никаких проблем и не сбрасывается на ноль. Ура!
Исправляем баги (выкидываем забытый скальпель)
Я уже начал радоваться, но вот Utopia Machina (да-да, еще раз спасибо за кучу тестирования) жаловался на то, что сенсор направления периодически залипает и требует перезагрузки. «Залипает» это когда направление меняется только на пару градусов, еще и перестаёт показывать уровень заряда. Еще жаловался на то, что иногда пропадают данные с одной из ног. При этом на оригинальном ресивере практически не воспроизводится.
У меня не воспроизводилось… Но как-то выяснилось, что у меня не воспроизводится когда висит на зарядке, а у него без зарядки. Выкрутил сенсор, оставил на пару часов периодически пошевеливая чтоб убедиться что работает. И… Да! Воспроизвел!
Когда произошло, я через Wireshark посмотрел на пакеты, и выяснилось, что в пакетах вместо направления и номера сенсора — идёт «0 1 0 1 0 1». Хвост данных был как всегда, таким образом кусоче квартерниона направления таки менялся, потому и получалось что-то видеть.
Это сработала вот эта мина отложенного действия, которая есть и в прошивке сенсоров ног и в прошивке направления:
if (something) { out[0] = 0; out[1] = 1; out[2] = 0; out[3] = 1; out[4] = 0; out[5] = 1; }
Это же объясняет, почему пропадала одна из ног — когда этот код срабатывал, координаты-то не затирались (в ногах они в самом конце пакета), а вот данные о батарее и номер сенсора — затирались.
Прекрасно, мы знаем, что случилось — но почему? К счастью, флаг «somthing» трогался только один раз, внутри обработчика таймера тикающего раз в секунду; и только если некий счетчик доходил до 1800, и не сбрасывался по дороге. Сбрасывался он при определённых условиях связанными с разрядом батареи. Проще говоря, это оказался отладочный код для настройки скорости разряда батареи и тюнинга перехода в режим сна. Я нашел в обоих сенсорах хвосты для настройки этого параметра по USB.
В любом случае, это отладочный кусок который в релизной прошивке не нужен — а значит, просто можно безопасно вырезать.
Вырезать опять же можно несколькими споосбами — через замыкание if’а, через затирание nop’ами… Я просто заNOPил строку, где ставилось «something = 1».
А что не так
Примечательно, что исправленные сенсоры остаются совместимы с оригинальными ресивером… Почти. Главная разница — значения, которые читаются сенсорами, теперь читаются с частотой обновления (86Гц..133Гц) а не с фиксированными ~140Гц.
Оптический сенсор на каждом чтении возвращает некое расстояние (в попугаях), которые он насчитал с прошлого чтения.
Оригинальный сенсор читает последнее доступное значение 86 раз в секунду, получается, использует данные о расстоянии как скорость — часть пройденного расстояния при этом теряется, но, с некоторой точностью, восстанавливается перемножая на время.
Патченный сенсор начинает читать данные со скоростью опроса — то есть данные о пройденном расстоянии почти не теряются (теряется каждый 500й пакет + часть пакетов просто теряется по радио), но при этом каждое значение получается больше, чем в оригинале: при обновлении 86 раз в секунду значения будут амплитудой до 163% по отношению к исходным, а на 133Гц всего 105%.
Представляет ли это проблему? Зависит от того, как этими данными пользоваться. Если использовать данные для вычисления скорости напрямую (как, к сожалению, делает гейтвей) — то и да и нет. Нет — так как при использовании 133Гц ресивера и исправленные сенсоры практически не чувствуется разницы, зато задержки ощутимо ниже (реально ощущается, особенно при игре на 120fps). Да — так как при использовании исправленного сенсора и исходного ресивера все скорости сильно выше и нужно исправлять настройки для каждой игры — понижать скорость разбега.
Можно ли это тоже исправить? Да, есть несколько способов исправления: патч гейтвея, патча исходного ресивера, более сложный патч сенсоров… Есть где разгуляться. Но это тема отдельного разговора.
Что дальше
А дальше на самом деле — избавление от гейтвея, как минимум для нативных игр — сейчас, используя KAT SDK невозможно сделать Standalone игры, так как SDK жестко прибит гвоздями к винде и ресиверу подключенному к нему. А я раньше уже показал как связаться с ресивером напрямую, сделал маленький ресивер который прикидывается оригинальным… То есть есть всё, что нужно, для создания действительно standalone игры — идеально вписывается в концепцию игры, которую разрабатывает Utopia Machina. Так что в следующей серии я покажу итог совместной наработки — UE SDK с прямым доступом до платформы хоть под виндой хоть нативно на Quest 2/3 🙂 Не переключайтесь!
Ссылки
-
Часть 1: «Играем с платформой» на [Habr], [Medium] и [LinkedIn].
-
Часть 2: «Начинаем погружение» на [Habr], [Medium] и [LinkedIn].
-
Часть 3: «Отрезаем провод» на [Habr], [Medium] и [LinkedIn].
-
Часть 4: «Играемся с прошивкой» на [Habr], [Medium] и [LinkedIn].
-
Часть 5: «Оверклокинг и багфиксинг» на [Habr], [Medium] и [LinkedIn].
ссылка на оригинал статьи https://habr.com/ru/articles/805747/
Добавить комментарий