Что нам стоит пофиксить баг, которого «нет»

1213da6c2a2b6b43368452ae15a23df3.jpg Итак, у нас есть задача: пофиксить баг, производитель от которого открещивается, клиенты не замечают, а жить хочется. Есть камера, поток от неё на UDP просто адово ломается, поток на TCP работает, но постоянно рвутся коннекты (и при каждом обрыве пропадает 3–5 сек видео). Виновны в проблеме все (и камера и софт), но обе стороны утверждают что у них всё зашибись, то есть ситуация обычная: ты баг видишь? нет. А он есть.Так как софт обновляется гораздо чаще, чем камера, имеет смысл править то место, которое потом не придётся трогать. Значит, будем фиксить со стороны камеры.Исследование плацдарма Перво-наперво берём свежайшую прошивку (в моём случае — firmware_TS38ABFG031-ONVIF-P2P-V2.5.0.6_20140126120110.bin), и выясняем что же она такое: $ file firmware_TS38ABFG006-ONVIF-P2P-V2.5.0.6_20140126120110.bin firmware_TS38ABFG006-ONVIF-P2P-V2.5.0.6_20140126120110.bin: data

$ du -b firmware_TS38ABFG006-ONVIF-P2P-V2.5.0.6_20140126120110.bin 15222724 firmware_TS38ABFG006-ONVIF-P2P-V2.5.0.6_20140126120110.bin

$ xxd firmware_TS38ABFG006-ONVIF-P2P-V2.5.0.6_20140126120110.bin | head 0000000: 4649 524d 5741 5245 6481 db15 c447 e800 FIRMWAREd…G… 0000010: 0300 0000 1406 0000 b0f1 1b00 4c21 815d …L!.] 0000020: 5453 3338 4f45 4d41 4246 475f 4c49 4e55 TS38OEMABFG_LINU 0000030: 5800 0000 0000 0000 0000 0000 0000 0000 X… 0000040: 0000 0000 0000 0000 0000 0000 0000 0000 … 0000050: 0000 0000 0000 0000 0000 0000 0000 0000 … 0000060: 0000 0000 0000 0000 0000 0000 0000 0000 … 0000070: 0000 0000 0000 0000 0000 0000 0000 0000 … 0000080: 0000 0000 0000 0000 0000 0000 0000 0000 … 0000090: 0000 0000 0000 0000 0000 0000 0000 0000 …

$ binwalk firmware_TS38ABFG006-ONVIF-P2P-V2.5.0.6_20140126120110.bin DECIMAL HEX DESCRIPTION ------------------------------------------------------------------------------------------------------- 1556 0×614 uImage header, header size: 64 bytes, header CRC: 0xB21E2C9F, created: Sun Sep 22 11:07:02 2013, image size: 1831280 bytes, Data Address: 0×80008000, Entry Point: 0×80008000, data CRC: 0×1F4EFBAB, OS: Linux, CPU: ARM, image type: OS Kernel Image, compression type: none, image name: «Linux-2.6.18_pro500-davinci_IPNC» 14468 0×3884 gzip compressed data, from Unix, last modified: Sun Sep 22 11:07:02 2013, max compression 1832900 0×1BF7C4 CramFS filesystem, little endian size 13389824 version #2 sorted_dirs CRC 0xc832a8c3, edition 0, 7334 blocks, 2607 files Итак, формат его неизвестен, стартовая метка «FIRMWARE» навевает мысли о том, что это что-то своё, наличие внутри uImage ядра и cramfs файлухи подсказывает, что по факту это что-то простое. Наличие строки TS38OEMABFG_LINUX подскзывает что это что-то даже напоминает какой-то вариант архива.

Так как нам сейчас надо просто найти где искать — просто вытаскиваем оттуда файловую систему, и ищем виновный модуль:

$ dd if=firmware_TS38ABFG006-ONVIF-P2P-V2.5.0.6_20140126120110.bin bs=1832900 skip=1 of=cramfs 7+1 записей получено 7+1 записей отправлено скопировано 13389824 байта (13 MB), 0,161755 c, 82,8 MB/c $ fakeroot cramfsck -x fs cramfs $ grep LIVE555 -R fs/ Двоичный файл fs/opt/topsee/rtsp_streamer совпадает $ strings fs/opt/topsee/rtsp_streamer | grep TCP sendRTPOverTCP 12RTCPInstance sendRTPOverTCP failed, sock: %d, chn: %d is not a RTCP instance RTCPInstance: RTCPInstance error: totSessionBW parameter should not be zero! RTP/AVP/TCP %sTransport: RTP/AVP/TCP; unicast; destination=%s; source=%s; interleaved=%d-%d /TCP; unicast Failed to create RTCP socket (port %d) MediaSession: initiate (): unable to create RTP and RTCP sockets Failed to create RTCP instance Received RTCP «BYE» on » 18RTCPMemberDatabase Хохохо! «sendRTPOverTCP failed, sock: %d, chn: %d» говорит нам о том, что код оттранслирован с отладочными принтами, а значит, объём работы снижен на порядки!

Итак, у нас есть модуль, содержащий искомую ошибку, модуль с кучей отладочных строк внутри, а значит процесс раздербанивания значительно упрощается.

Локализация и фикс проблемы Грузим модуль в дизассемблер, ищем по «OverTCP» строку отладочную, от неё ищем код вызывающий распечатку => мы нашли функцию sendRTPOverTCP.Проглядывая её бегло видим два вызова функции send () — одна с 4 байтами, одна с буфером переданным на вход. Значит, нам досталась версия не самая старая, а когда уже объеденили буфер, но еще не сделали функции sendDataOverTCP (подробности о различиях реализации — в прошлом< посте.Теперь возникает вопрос, как можно поправить баг в бинарном виде, когда практически нет запаса по месту (пустого пространства внутри файла нет).Идём в функцию выше, которая вызывает sendDataOverTCP — sendPacket. Её код от версии к версии не менялся, он по сути один — foreach(streams) { sendDataOverTCP(packet, stream) }.

По счастью, код был щедро напичкан отладочными fprintf’ами, и это нас спасает! Вот как этот цикл выглядит в бинарном виде:

loop_next_5FAE4: LDR R4, [R4,#4] CMP R4, #0 BEQ loc_5FB68

loop_body_5FAF0: MOV R3, R4 MOV R1, R5 MOV R2, R7 MOV R0, R6 BL sendRTPOverTCP CMP R0, #0 BGE loop_next_5FAE4 MOV R1, #0 LDR R2, =aS_10;»%s ():» LDR R3, =aSendpacket; «sendPacket» MOV R0, #STDERR_FILENO BL fprintf_0 LDRB R12, [R4,#0xC] LDR R3, [R4,#8] MOV R1, #7 LDR R2, =aSendrtpovert_0; «sendRTPOverTCP failed, sock: %d, chn: %»… MOV R0, #STDERR_FILENO STR R12, [SP,#0×350+var_350] BL fprintf_0 … Это фактически спасение! Просто вырезка отладочного куска даёт нам место в размере 12 инструкций (у ARM все инструкции ровно по 4 байта, и это очень хорошо).Итак, у нас есть место в 12 инструкций, чтобы что-то сделать для улучшения ситуации. Но что? Полноценно впихнуть сюда код sendDataOverTCP из последней версии будет сильно затруднительно…Хотя стоп. А зачем? Я, вроде, подробно описал, что даже использование корректной формы sendDataOverTCP всё равно плохо… А если не видно разницы — почему бы просто не обернуть вызов в makeSocketBlocking ()…makeSocketNonBlocking ()?

Действительно, если место в системном буфере есть — send () выполнится моментально. Если места нет — то и их реализация sendDataOverTCP всё равно залипнет (почему залипнет, а не вывалится сразу с нулём — смотри предыдущий пост).

Отлично! Путем быстрой отмотки назад от функции fcntl находим makeSocketBlocking и makeSocketNonBlocking, после чего рисуем какой должен получиться код:

loop_next_5FAE4: LDR R4, [R4,#4] CMP R4, #0 BEQ loc_5FB68 loop_body_5FAF0: ; Сперва сделаем сокет блокирующим LDR R0, [R4,#8] BL makeSocketBlocking ; Затем остаётся прежний вызов отправки MOV R3, R4 MOV R1, R5 MOV R2, R7 MOV R0, R6 BL sendRTPOverTCP ; Сохраним результат вызова функции STMFD SP!, {R0} ; Сделаем сокет снова неблокирующим LDR R0, [R4,#8] BL makeSocketNonBlocking ; Восстановим результат вызова функции отправки LDMFD SP!, {R0} ; Зацикливание остаётся как и раньше CMP R0, #0 BGE loc_5FAE4 ; Ну и занопливаем остатки отладочной распечатки NOP NOP NOP NOP NOP NOP Для того чтобы запатчить, либо открываем документацию на арм и транслируем в уме, либо пишем код в отдельном файле и транслируем его (не забываем ORG’ами выставлять точные адреса, чтобы все переходы (BL/BGE/ИТД) пересчитаны были корректно), а я всего лишь находил подходящие инстуркции в коде и на их основе вычислял нужные опкоды (первый раз редактирую ARM, уж извините).В результате получаем rtsp_streamer с наложенным на него патчем, защищающим TCP поток от порчи.

Пайка взрывом, сборка трезвым Итак, у нас есть новый rtsp_streamer, и есть firmware…bin в который его надо встроить. Ну тут, вроде, всё просто: надо распаковать cramfs, заменить файл, запаковать обратно, заменить его внутри bin’а: $ fakeroot -s .fakeroot cramfsck -x repack cramfs $ cp rtsp_streamer repack/opt/topsee/ $ fakeroot -i .fakeroot mkcramfs repack newcramfs $ dd if=firmware_TS38ABFG006-ONVIF-P2P-V2.5.0.6_20140126120110.bin of=firmware_new.bin bs=1832900 count=1 $ cat newcramfs >> firmware_new.bin Заливаем полученный firmware_new.bin в камеру… 0 эффекта. Камера съедает прошивку, но ничего не происходит. Неприятно. Значит, надо разобраться в формате этого .bin’а.Откроем его в hex редакторе, и начнём кумекать:(0) «FIRMWARE» — 100% заголовок, 8 байт.(8) 64 81 DB 15 — 4 байта, назначение непонятно. по запаху — контрольная сумма (12) 0×00E847C4=15222724 — ага, 4 байта, размер прошивки. проверяем _new.bin — нет, размер не изменился, значит, он не при чем.(16) 0×00000003 — 4 байта хз что. версия заголовка может?(20) 0×00000614=1556 — так, а это смещение до ядра внутри (24) 0×001BF1B0=1831344 —, а это размер ядра (1831344+1556=1832900)(28) 4C 21 81 5D — хм. опять что-то на контрольную сумму похожее.(32) «TS38OEMABFG_LINUX» и куча нулей потом — 100h байт, явно место под название раздела (288) 0×001BF7C4=1832900 — ага, смещение до следующей секции (292) 0×00CC5000=13389824 — ага, размер секции (296) «TS38OEMABFG_V2.5.0.6» и куча нулей — опачки. 100h байт, явно под название раздела.Но контрольной суммы перед ней нет O_O (552–1556) — нечто неизвестной наружности.

Итак, примерно суть ясна. Размер нашей cramfs не изменился, значит, это не размеры, а значит, контрольные суммы.Извлекаем ядро, и считаем его контрольную сумму длиной 4 байта:

$ dd if=firmware_TS38ABFG006-ONVIF-P2P-V2.5.0.6_20140126120110.bin of=kernel bs=1 skip=1556 count=1831344 $ crc32 kernel 5d81214c Ну-ка ну-ка… по смещению 28 как раз 0×5D81214C. Повезло — это стандартная CRC32. Повезло потому, что по стандартная тулза умеет только его считать. Иначе пришлось бы запускать питон и считать уже «сложнее» :)

Итак, контрольные суммы у нас — crc32. А какая контрольная сумма у оригинальной cramfs?… 37499eef. Так-так-так. По смещению 552 как раз и записано 0×37499eef. Значит, для файловой системы почему-то контрольная сумма ПОСЛЕ имени раздела записана. Ну ОК, чего нам, мы не гордые. Обновляем табличку:(28) 0×5D81214C — crc32 раздела ядра (552) 0×37499eef — crc32 раздела FS (556–1556) — нечто неизвестной наружности

Пересчитываем crc32 newcramfs, hex редактором вписываем по смещению 552 его в бинарь, заливаем в камеру.И… ничего O_O. Значит, чутьё не подвело — по смещению 8 действительно crc32, но от чего? Тут действуем просто — начинаем брутфорсить.

$ python >>> from zlib import crc32 >>> d=open («firmware_TS38ABFG006-ONVIF-P2P-V2.5.0.6_20140126120110.bin», «r»).read () >>> hex (crc32(d[12:])) '-0×781aca29' # нет >>> hex (crc32(d[12:1556])) '-0×6f8f1744' # нет >>> hex (crc32(d[0:8]+d[12:1556])) '0×6d29f056' # нет >>> hex (crc32(d[0:8]+d[12:])) '-0×652ac4fd' # нет >>> hex (crc32(d[0:8]+»\0\0\0\0»+d[12:1556])) '0×15db8164' # Опа! Оно! Отлично, быстро справились. Значит, обновляем табличку:(8) 0×15DB8164 — CRC32 заголовка (первых 1556 байт), сперва занулив это поле

Итак, тут же в питоне быстро пересчитываем crc32 от заголовка firmware_new.bin и вписываем в hex редакторе его в начало.Заливаем в камеру… Она уходит в ребут. И не отвечает… не отвечает… еще кирпич… О! Пинги пошли! Фууух.

Берём cam-resync.py, и опять тыкаем палочкой нашу камеру. И… и поток не ломается! Вот прямо вот так, с первой попытки! Уииии :)

На хлеб мажется, и есть уже можно, но что-то не то Ранее упомянутый Андрей Сёмочкин тем временем собрал свою прошивку с исправленным мною rtsp_streamer’ом и залил её на одну из проблемных камер, с которых шло много разрывов. В результате тестирование показало, что поток не ломается, однако начались артефакты видео, такие же, как при потере пакетов на UDP. Так как я ничего не встраивал такого, стало любопытно, что же это было — пришлось еще раз заглянуть в код. Для начала, заглядываем в strings, у нас же куча отладочных строк. «checkBufferTimeout for %d seconds!!!», «buffered data more than %d ms, drop all the buffered data!!!».Ага! Оказывается, производители сделали защиту от переполнения! И если по какой-то причине пока синхронный send () завис на больше чем надо (по дефолту — 1 сек), он излишки дропает. Это защищает от OOM и от отставания видео, если оно не влезает в тонкий канал. Но код раньше явно не работал из-за использования неблокирующих сокетов и send ()'а.

После оборачивания в Blocking…NonBlocking — код начал работать :)Однако есть небольшая проблемка: 1 сек это мало. Если канал начинает сбоить, однако достаточно толстый, то вероятность дропа становится всё сильнее. После любого такого дропа видео восстанавливается только после кейфрейма. Обычно кейфрейм достаточно редко (раз в 5–10 сек)… И получается неприятная ситуация — если был сбой, то 5–10 сек надо ждать до следующего кейфрейма чтоб видео починилось. Если сократить частоту кейфрейма — это автоматически увеличивает объём передаваемых данных, так как кейфреймы довольно толстые, а значит увеличивает частоту помирания канала. Замкнутый круг.

В общем, я дополнительно увеличил таймаут буферизации до 10сек — этого должно быть недостаточно чтобы словить OOM, но достаточно чтобы спокойно переживать ретрансмиты и лаги на не суперстабильных каналах.

Кстати, «хитрый» алгоритм лечения Я в прошлой статье пообещал рассказать, как же легко починить ситуацию. Решение просто как валенок — так как мы отправляем RTP пакеты, у нас в каждом есть timestamp. Достаточно в sendRTPorRTCPPacketOverTCP проверять ДО отправки срок жизни пакета, и если он меньше настроенного (я всё-таки считаю что 1 сек это мало на TCP, надо именно 6–10 сек) то отправлять, иначе просто молча не отправлять его.Автоматизируем сборку-разборку Осталось дело за малым: автоматизировать сборку-разборку прошивки.unpack.sh

Скрытый текст fw=${1? Please give firmware bin as argument} if [ -e $fw.unpack ]; then echo «Already exists: $fw.unpack» exit 1 fi # Check format if [ »$(dd if=$1 bs=8 count=1 2>/dev/null)» != «FIRMWARE» ]; then echo «Wrong file» exit 1 fi mkdir -p $fw.unpack echo «Extract header…» dd if=$1 of=$fw.unpack/00header bs=1556 count=1 2>/dev/null echo «Extract kernel…» ksize=$(dd if=$1 bs=1 count=4 skip=24 2>/dev/null | perl -e 'print unpack («l», <>);') dd if=$1 of=$fw.unpack/01kernel bs=1 skip=1556 count=$ksize 2>/dev/null echo «Extract filesystem…» foff=$(dd if=$1 bs=1 count=4 skip=288 2>/dev/null | perl -e 'print unpack («l», <>);') dd if=$1 of=$fw.unpack/02cramfs bs=$foff skip=1 2>/dev/null echo «Unpack filesystem…» cd $fw.unpack fakeroot -s .fakeroot cramfsck -x root 02cramfs chmod +r -R root/ echo «Done» Так как нам неизвестно занчение куска заголовка, собирать произвольные мы не можем, чтоб не убивать камеру, поэтому все куски сохраняем как есть.Так как у нас внутри прошивки лежат блочные и прочие устройства, работать с ней от простого пользователя не получается. Но тут на помощь приходит fakeroot, который умеет сохранять состояние во внешний файл. Поэтому распаковываем используя fakeroot.Однако с ним есть микропроблема — файл в итоге должен быть доступен на чтение текущему пользователю. Если вы «настоящий» рут, то вы спокойно можете читать файл, даже если он «chmod -r». А вот fakeroot ломается на таком файле. Поэтому сразу после распаковки, я меняю права чтения для всех файлов. НО правильные права доступа сохранены в дампе состояния fakeroot’а, поэтому обратная сборка проходит на ура.В остальном распаковка не имеет никаких интересных моментов.

pack.sh

Скрытый текст dir=${1? Please give path to a directory with unpacked firmware} nfw=${2? Please give name for a newly packed firmware} if [ ! -e $dir ]; then echo «Directory not exists: $dir» exit 1 fi if [ -e $nfw ]; then echo «Firmware already exists: $nfw» exit 1 fi # repack cramfs if [ ! -e $dir/02cramfs.bak ]; then mv $dir/02cramfs $dir/02cramfs.bak 2>/dev/null fi fakeroot -i $dir/.fakeroot mkcramfs $dir/root/ $dir/02cramfs # construct new firmware dd if=$dir/00header bs=1556 of=$nfw conv=notrunc 2>/dev/null # remove old header crc32 dd if=/dev/zero bs=1 seek=8 count=4 of=$nfw conv=notrunc 2>/dev/null # save kernel size if [ $(stat -c %s $dir/01kernel) -ge 2097152 ]; then echo «WARN: size of kernel is more than 0×200000. FW probably will not flash» fi perl -e 'print pack («l», -s »'$dir/01kernel'»)' | dd bs=1 seek=24 count=4 of=$nfw conv=notrunc 2>/dev/null # save kernel crc32 crc32 $dir/01kernel | perl -e 'print pack («l», oct (»0x».<>));' | dd bs=1 seek=28 count=4 of=$nfw conv=notrunc 2>/dev/null # save fs offset perl -e 'print pack («l», 1556+(-s »'$dir/01kernel'»))' | dd bs=1 seek=288 count=4 of=$nfw conv=notrunc 2>/dev/null # save fs size if [ $(stat -c %s $dir/02cramfs) -lt 8388608 ]; then echo «WARN: size of filesystem is less than 0×800000. FW probably will not flash» fi if [ $(stat -c %s $dir/02cramfs) -ge 15728640 ]; then echo «WARN: size of filesystem is more than 0xF00000. FW probably will not flash» fi perl -e 'print pack («l», -s »'$dir/02cramfs'»)' | dd bs=1 seek=292 count=4 of=$nfw conv=notrunc 2>/dev/null # save fs crc32 crc32 $dir/02cramfs | perl -e 'print pack («l», oct (»0x».<>));' | dd bs=1 seek=552 count=4 of=$nfw conv=notrunc 2>/dev/null # save full FW size perl -e 'print pack («l», 1556+(-s »'$dir/02cramfs'»)+(-s »'$dir/01kernel'»))' | dd bs=1 seek=12 count=4 of=$nfw conv=notrunc 2>/dev/null # Update header crc32 crc32 $nfw | perl -e 'print pack («l», oct (»0x».<>));' | dd bs=1 seek=8 count=4 of=$nfw conv=notrunc 2>/dev/null # concat rest cat $dir/01kernel >> $nfw cat $dir/02cramfs >> $nfw echo «Done» А вот упаковка уже чуток сложнее. Нам надо запаковать обратно cramfs, обновить в заголовке длины отдельных файлов и общую длину; пересчитать и контрольные суммы, включая заголовок и только после этого слить всё вместе.Вообще, камера проверяет граничные значения сама, однако для удобства я добавил проверку на границы размеров файловой системы и ядра, чтобы если при сборке её размеры выползают за пределы, получить предупреждение и поудалять лишние хвосты из прошивки.Результат трудов Итак, я собрал следующие исправленные прошивки последней версии 2.5.0.6: firmware_TS38ABFG006-ONVIF-P2P-V2.5.0.6_20140126120110-TCPFIX.bin firmware_TS38CD-ONVIF-P2P-V2.5.0.6_20140126121011-TCPFIX.bin firmware_TS38HI-ONVIF-P2P-V2.5.0.6_20140126121444-TCPFIX.bin firmware_TS38LM-ONVIF-P2P-V2.5.0.6_20140126121913-TCPFIX.bin firmware_HI3518C-V4-ONVIF-V2.5.0.6_20140126124339-TCPFIX.bin Если вдруг вам потребуется фикс на какой-либо другой модуль этого же производителя — пишите в комментах, буду посмотреть по возможности.

© Habrahabr.ru