"Что б они ни делали -- не идут дела. Видимо в прошивке багов дофига". Как я напомнил в прошлой статье (где я подготовил утилиты для перепрошивки сенсоров) -- я рассказываю про платформу для 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_BREAK 0x306
./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].