
Когда-то давно мне подарили кучу VoIP-телефонов, которые списали на моей старой работе. Среди них были два Snom 360 Business, выпущенные в 2005 году. Изначально я хотел настроить АТС на основе Asterisk для всех доставшихся мне телефонов, но в процессе обновления прошивки на одном из аппаратов Snom 360 мне пришла в голову идея получше. У телефона есть экран и клавиатура... получится ли на нём запустить Doom?
1. Изначальное обновление прошивки
Эта модель была выпущена в 2005 году, поэтому первым делом я захотел загрузить на телефон новую прошивку. К счастью, компания Snom хранит архив старых образов прошивок1. Насколько я понял, последней прошивкой для серии устройств 3xx стала V08, поэтому я скачал образ Deskphone - Filearchive - V08 > snom360-8.7.3.7-SIP-f.bin.

Обновления прошивок можно устанавливать через веб-интерфейс, работающий в самом телефоне. Достаточно указать URL образа прошивки, и телефон сам скачает и установит её.
2. Исследование прошивки
На этом этапе я понятия не имел, какое ПО установлено в т��лефоне, и насколько сложно будет портировать на него Doom. Я начал своё расследование с изучения HTTP-заголовков, которые передавал веб-интерфейс:
HTTP/1.1 200 Ok Server: snom embedded Content-Type: text/html Cache-Control: no-cache Cache-Control: no-store Content-Length: 14018
Неконкретное название «snom embedded» означало, что в нём работает собственный специализированный HTTP(S)-сервер. Чтобы узнать больше, я скачал образ прошивки и пропустил его через binwalk, чтобы получить приблизительные сведения:
$ binwalk snom360-8.7.3.25.9-SIP-f.bin /tmp/snom360-8.7.3.25.9-SIP-f.bin ---------------------------------------------------------------------------------------------------------- DECIMAL HEXADECIMAL DESCRIPTION ---------------------------------------------------------------------------------------------------------- 16 0x10 JFFS2 filesystem, big endian, nodes: 2035, total size: 3377072 bytes ---------------------------------------------------------------------------------------------------------- Analyzed 1 file for 85 file signatures (187 magic patterns) in 47.0 milliseconds
Образ прошивки не был зашифрован, в нём использовалась файловая система JFFS2 — формат, предназначенный для устройств с флэш-памятью2. Я извлёк его, чтобы заглянуть внутрь:
$ binwalk -e snom360-8.7.3.25.9-SIP-f.bin -C jffs2.img /tmp/jffs2.img/snom360-8.7.3.25.9-SIP-f.bin ---------------------------------------------------------------------------------------------------------- DECIMAL HEXADECIMAL DESCRIPTION ---------------------------------------------------------------------------------------------------------- 16 0x10 JFFS2 filesystem, big endian, nodes: 2035, total size: 3377072 bytes ---------------------------------------------------------------------------------------------------------- [+] Extraction of jffs2 data at offset 0x10 completed successfully ---------------------------------------------------------------------------------------------------------- Analyzed 1 file for 85 file signatures (187 magic patterns) in 333.0 milliseconds
binwalk выдал мне и двоичный файл файловой системы, и всю извлечённую файловую систему:
$ ls jffs2.img snom360-8.7.3.25.9-SIP-f.bin snom360-8.7.3.25.9-SIP-f.bin.extracted
Извлечённая файловая система выглядела, как стандартная rootfs:
$ ls jffs2-root boot dev lost+found mnt proc sbin snomconfig tmp var
Чтобы узнать о системе больше, я проверил, на каком ядре работает телефон:
$ file boot/uImage boot/uImage: u-boot legacy uImage, MIPS Linux-2.4.31-INCAIP-4.3, Linux/MIPS, OS Kernel Image (gzip), 690926 bytes, Thu Jul 7 10:43:18 2011, Load Address: 0X80002000, Entry Point: 0X80180040, Header CRC: 0XCCC3CA0E, Data CRC: 0XE9FF8716
В телефоне был установлен Linux 2.4.31 на чипе MIPS. Одно это уже сильно повысило шансы на портирование, потому что мне не придётся писать код для совершенно неизвестной или голой платформы. Также любопытными здесь были папки /sbin и /mnt:
$ ls sbin mnt mnt: 1lid html lcs360 moh.wav snomlang sbin: init
В папке /mnt содержались файлы HTML веб-интерфейса, а также два двоичных файла: 1lid и lcs360.
$ file 1lid lcs360 1lid: ELF 32-bit MSB executable, MIPS, MIPS32 version 1 (SYSV), statically linked, for GNU/Linux 2.2.15, stripped lcs360: ELF 32-bit MSB executable, MIPS, MIPS32 version 1 (SYSV), statically linked, no section header
Оба они были статически скомпонованными двоичными файлами, скомпилированными для MIPS32. То же самое относилось и к init:
$ file init init: ELF 32-bit MSB executable, MIPS, MIPS32 version 1 (SYSV), statically linked, for GNU/Linux 2.2.15, stripped
Я поисследовал строки в двоичном файле init и нашёл интересную последовательность:
$ strings init […] forking child child alife, starting LID /mnt/1lid […]
Итак, init оказался двоичным файлом, запускающим 1lid при загрузке системы. Строки в 1lid позволили получить дополнительную информацию:
$ strings 1lid […] usage: lid --device d: set audio device name (default is /dev/audio) --host <host>: work as client (use this option for every possible host) --keyboard d: set keyboard device name (default is /dev/kbd) --display d: set display device name (default is /dev/snomdisp) --port n: use socket n for communication (default is 1298) /mnt/lcs360 --html-dir /mnt/html/ […]
Похоже было, что --html-dir не является валидным параметром для 1lid, а значит, lcs360 вызывался c --html-dir /mnt/html, то есть этот компонент отвечал за хостинг веб-интерфейса.
Находим исходный код под GPL
Теперь, когда у меня было приблизительное понимание предназначения всех двоичных файлов, я загрузил их в Ghidra. Это оказалось не особо плодотворным, потому что все символы были вырезаны, а у меня не было доступа к библиотекам, с которыми компоновались файлы.
К счастью, при изучении веб-сайта Snom я наткнулся на страницу скачивания компонентов с лицензией GPL:

По какой-то причине файлов для прошивки v8 там не было, но мне показалось, что v7 вполне к ней близка. Стоит отметить, что INCA-IP — это семейство чипов Infineon Technologies, разработанных специально для VoIP3; это название я уже встречал в образе ядра
(Linux-2.4.31-INCAIP-4.3).
Я скачал следующие файлы:
Описание и Rootfs.
Инструмент UPX.
Исходный код BusyBox.
Исходный код ядра Linux и U-Boot для v7.
Кросс-компилятор uClibc и библиотеки для v7.
Это дало мне гораздо больше, чем я надеялся: rootfs, исходники ядра, исходники Busybox, уже скомпилированный кросс-компилятор и даже инструмент для сжатия двоичных файлов (UPX).
rootfs прошивок v7
rootfs прошивок v7 оказался гораздо более полным, чем rootfs из образа прошивки:
$ ls rootfs bin boot dev etc inca_scripts lib lost+found proc sbin tmp usr var
Она была основана не на специализированном двоичном файле init, а на Busybox:
$ ls -l sbin total 0 lrwxrwxrwx 1 root root 14 Mar 3 2008 ifconfig -> ../bin/busybox lrwxrwxrwx 1 root root 14 Mar 3 2008 init -> ../bin/busybox lrwxrwxrwx 1 root root 14 Mar 3 2008 insmod -> ../bin/busybox lrwxrwxrwx 1 root root 14 Mar 3 2008 lsmod -> ../bin/busybox lrwxrwxrwx 1 root root 14 Mar 3 2008 modprobe -> ../bin/busybox lrwxrwxrwx 1 root root 14 Mar 3 2008 rmmod -> ../bin/busybox lrwxrwxrwx 1 root root 14 Mar 3 2008 route -> ../bin/busybox
Рядом с rootfs находился файл readme с инструкциями по генерации образов v7 для телефонов Snom на основе INCA-IP. В нём раскрывались следующие темы:
Конфигурирование исходников ядра.
Сборка uImage для uBoot.
Сборка образа rootfs.
Развёртывание образа по последовательной линии.
Readme заканчивался следующими строками:
"See u-boot restarting. Linux starts up. Change serial line setup to 9600 8N1. Press Enter. And you will have a shell prompt."
Честно говоря, я был счастлив от мысли о том, что получу доступ к оболочке моего телефона.
3. Собираем свою прошивку и получаем доступ к оболочке
Я извлёк тулчейн кросс-компиляции и исходники ядра, после чего начал выполнять инструкции, в том числе и мою любимую:
3. Configure for INCA-IP (ignore errors)
Я потратил какое-то время на попытки собрать ядро, но сталкивался с ошибками, которые не мог игнорировать. Меня это немного расстроило, потому что раньше компилировать ядро с нуля мне не доводилось. Хоть упакованная rootfs уже содержала uImage, я извлёк его из последнего обновления v7 (snom360-7.3.7-SIP-f.bin), чтобы гарантировать совместимость с моим устройством.
Для сборки образа я воспользовался двоичным файлом mkfs.jffs2 из списка скачиваемых файлов с лицензией GPL:
# ./bsp_4.3/tools/build_tools/mkfs.jffs2 -b -r rootfs/ -o rootfs.jffs2.img
После сборки образа (весь процесс оказался намного быстрее, чем я ожидал) оставалось лишь записать его в телефон.
Ищем последовательный порт
В readme только говорилось use serial console to deploy image via u-boot with TFTP («используйте консоль последовательной передачи данных для развёртывания образа через u-boot при помощи TFTP»), что было не особо информативно. Надеясь узнать больше, я вскрыл телефон.
У него есть две отдельные печатные платы: одна в верхней половине, другая в нижней. На нижней плате смонтированы все периферийные порты, память и чипы CPU. Верхняя плата отвечает за экран и кнопки.
Хоть я и не особо хорошо разбираюсь в проектировании печатных плат, мне быстро удалось найти в верхней части эти тестовые точки:

Маркировка GND, TxD и RxD выглядела многообещающе. Я припаял к площадкам провода и подключил их к адаптеру Serial-to-USB. К сожалению, никакого вывода не было. Однако на нижней плате было ещё несколько тестовых точек:

Любопытно, что в корпусе телефона есть три отверстия, расположенные непосредственно под этими тестовыми точками; отверстия были закрыты большим стикером. Так как они не были промаркированы, я произвёл измерения и обнаружил заземление, а затем снова припаял провода и подключил их к адаптеру. Не зная, где находятся Rx и Tx, я просто попробовал оба сочетания. К счастью, на этот раз вывод был:
# screen /dev/ttyUSB0 115200 U-Boot 1.1.3-m jffs2 (Apr 17 2007 - 12:29:17) Board: INCA-IP Standard Version, Chip V1.4, CPU Speed 150 MHz Watchdog aware version DRAM: 16 MB Flash: 4 MB In: serial Out: serial Err: serial Net: INCA-IP Switch Hit any key to stop autoboot: 1 DISPLAY fd = 0 0 INCA-IP-ROM #
Записываем образ
Имея доступ к консоли U-Boot, я мог записать созданный образ. Для начала нужно было настроить сетевое соединение для TFTP. В инструкциях было чётко написано:
"setup the network for TFTP respectively:" setenv netmask 255.255.0.0 setenv serverip 192.168.0.x setenv gatewayip 192.168.0.y setenv ipaddr 192.168.0.z "If you want to see any linux output (the side car isn't working with this!):" run console_on "Switch of the output with:" run console_off "If you want to reduce the initial time the bootloader waits for a keypress:" setenv bootdelay 1 "Finally write your changes to the flash." saveenv
Я сконфигурировал параметры сети и включил console_on, чтобы видеть вывод Linux. Затем я запустил на своей машине простой TFTP-сервер:
# sudo uftpd -n -o ftp=0,tftp=69 img/
и загрузил образ в телефон:
INCA-IP-ROM # tftpboot 80400000 rootfs.jffs2.img Using INCA-IP Switch device TFTP from server 10.20.30.40; our IP address is 10.20.30.50 Filename 'rootfs.jffs2.img'. Load address: 0x80400000 Loading: ################################################################# ################################################################# ################################################################# ################################################################# ################################################################# ################################################################# ############# done Bytes transferred = 1713156 (1a2404 hex)
Затем я прошил его в память. У всех телефонов серии 3xx, за исключением 370, 4 МБ флэш-памяти:
INCA-IP-ROM # erase b0040000 b03fffff ........................... done Erased 60 sectors INCA-IP-ROM # cp.b 80400000 b0040000 1a2404 Copy to Flash... done INCA-IP-ROM # reset
4. Из чего состоит телефон?
После перезапуска телефона я снова подключился к нему через последовательный интерфейс:
# screen /dev/ttyUSB0 9600 #
Так как теперь у меня был доступ к оболочке, мне хотелось выяснить, на каком «железе» работает телефон. Я снова проверил ядро:
# uname -a Linux 10.20.30.50 2.4.31-INCAIP-4.3 #1 Wed Feb 20 00:41:41 CET 2008 mips unknown
Телефон и в самом деле работал на специализированном ядре INCAIP 2.4.31, извлечённом мной из образа прошивки.
# df Filesystem 1k-blocks Used Available Use% Mounted on /dev/mtdblock2 3840 1996 1844 52% / tmpfs 7100 0 7100 0% /tmp
# busybox BusyBox v1.2.2 (2007.02.22-17:43+0000) multi-call binary Usage: busybox [function] [arguments]... or: [function] [arguments]... BusyBox is a multi-call binary that combines many common Unix utilities into a single executable. Most people will create a link to busybox for each function they wish to use and BusyBox will act like whatever it was invoked as! Currently defined functions: [, [[, ash, busybox, cat, chgrp, chmod, chown, chroot, cksum, cp, df, du, echo, egrep, false, fgrep, free, grep, halt, ifconfig, init, insmod, kill, linuxrc, ln, ls, lsmod, mkdir, mknod, modprobe, mount, mv, ping, pivot_root, poweroff, ps, pwd, reboot, rm, rmmod, route, sh, sleep, stty, sync, tar, test, true, tty, umount, uname, uptime, yes
Busybox из прошивки содержал не так много функций, как бы мне хотелось. Похоже, образы прошивки 7x используют предоставляемую busybox функцию init, которая при запуске выполняет чтение из /etc/inittab:
# cat /etc/inittab # This is run first except when booting in single-user mode. ::sysinit:/etc/rc.sh # /bin/sh invocations on selected ttys # # Must be first 'respawn' entries to avoid ^C problem # Start a shell on the console ::respawn:-/bin/busybox sh # Start an "askfirst" shell on /dev/ttyS1 #ttyS1::askfirst:-/bin/sh # # Start internet super daemon; do NOT background! #::respawn:/usr/sbin/xinetd -stayalive -reuse -pidfile /tmp/xinetd.pid # Start user application ::sysinit:/bin/stty -F /dev/ttyS0 -icanon ::sysinit:/bin/stty -F /dev/ttyS0 -echo ::sysinit:/bin/stty -F /dev/ttyS0 -icrnl ::sysinit:/bin/stty -F /dev/ttyS0 clocal ::sysinit:/bin/stty -F /dev/ttyS0 hupcl ::sysinit:/bin/stty -F /dev/ttyS0 9600 #::sysinit:/bin/stty -F /dev/ttyS0 115200
Собираем Busybox получше
Так как комплектный busybox был слишком ограниченным для моих задумок, я решил собрать собственный на основе исходного кода, предоставленного Snom. Раньше я никогда не собирал busybox с нуля, но всё оказалось очень просто. Я добавил в него всё из исходного образа плюс всякие полезности и FTP-клиент для упрощения передачи файлов. Для удобства я скомпилировал его, как статический двоичный файл, что сильно увеличило размер файла:
$ du -s busybox 1544 busybox
К счастью, в файлах GPL был двоичный файл upx4 для сжатия:
$ ../upx-3.03-i386_linux/upx -9 busybox Ultimate Packer for eXecutables Copyright (C) 1996 - 2008 UPX 3.03 Markus Oberhumer, Laszlo Molnar & John Reiser Apr 27th 2008 File size Ratio Format Name -------------------- ------ ----------- ----------- 1579596 -> 480008 30.39% linux/mipseb busybox Packed 1 file.
Он снизил размер файла до гораздо более удобного. Я скопировал двоичный файл в /bin папки rootfs, а затем пересобрал и снова прошил образ. Новый busybox обладал гораздо большей функциональностью:
~ $ busybox BusyBox v1.2.2 (2026.01.29-21:33+0000) multi-call binary Usage: busybox [function] [arguments]... or: [function] [arguments]... BusyBox is a multi-call binary that combines many common Unix utilities into a single executable. Most people will create a link to busybox for each function they wish to use and BusyBox will act like whatever it was invoked as! Currently defined functions: [, [[, addgroup, adduser, adjtimex, arping, ash, awk, basename, bbconfig, busybox, cal, cat, catv, chattr, chgrp, chmod, chown, chroot, chvt, cksum, clear, cmp, comm, cp, crond, crontab, cut, date, dc, dd, deallocvt, delgroup, deluser, df, diff, dirname, dmesg, dnsd, dos2unix, du, dumpkmap, dumpleases, e2fsck, e2label, echo, ed, egrep, eject, env, ether-wake, expr, fakeidentd, false, fbset, fgrep, find, findfs, fold, free, freeramdisk, fsck, fsck.ext2, fsck.ext3, fsck.minix, ftpget, ftpput, fuser, getopt, getty, grep, halt, hdparm, head, hexdump, hostid, hostname, httpd, hush, hwclock, id, ifconfig, ifdown, ifup, inetd, init, insmod, install, ip, ipcalc, ipcrm, ipcs, kill, killall, klogd, lash, last, length, less, linux32, linux64, ln, loadfont, loadkmap, logger, login, logname, losetup, ls, lsattr, lsmod, makedevs, md5sum, mdev, mesg, mkdir, mke2fs, mkfifo, mkfs.ext2, mkfs.ext3, mkfs.minix, mknod, mkswap, mktemp, modprobe, more, mount, mountpoint, msh, mt, mv, nameif, nc, netstat, nice, nohup, nslookup, od, openvt, passwd, patch, pidof, ping, pipe_progress, pivot_root, poweroff, printenv, printf, ps, pwd, rdate, readlink, readprofile, realpath, reboot, renice, reset, rm, rmdir, rmmod, route, run-parts, runlevel, rx, sed, seq, setarch, setconsole, setkeycodes, setlogcons, setsid, sh, sha1sum, sleep, sort, start-stop-daemon, stat, strings, stty, su, sulogin, sum, swapoff, swapon, switch_root, sync, sysctl, syslogd, tail, tee, telnet, telnetd, test, tftp, time, top, touch, tr, traceroute, true, tty, tune2fs, udhcpc, udhcpd, umount, uname, uniq, unix2dos, uptime, usleep, uudecode, uuencode, vconfig, vi, vlock, watch, watchdog, wc, wget, which, who, whoami, xargs, yes, zcip
5. Подготовка к портированию Doom
Имея доступ к оболочке и улучшенный busybox, я стал ещё на один шаг ближе к запуску Doom. На этом этапе я задумался, как же приступить к портированию игры. При портировании люди обычно просто модифицируют исходный код оригинала? Портируют ли они движок? В процессе исследований я обнаружил doomgeneric5 — форк fbDOOM, нацеленный на простоту портирования. Для его портирования необходимо реализовать только малое количество функций:
Функции | Описание |
|---|---|
DG_Init | И��ициализация платформы (создание окна, буфера кадров и так далее…). |
DG_DrawFrame | Кадр уже готов в DG_ScreenBuffer. Нужно скопировать его на экран платформы. |
DG_SleepMs | Sleep в миллисекундах. |
DG_GetTicksMs | Количество тактов в миллисекундах, прошедших после запуска. |
DG_GetKey | Клавиатурные события. |
DG_SetWindowTitle | Не требуется. Функция нужна для указания заголовка окна, а Doom задаёт его из файла WAD. |
Так как мы работаем в системе на основе Linux, можно было использовать SleepMS и GetTicksMs из эталонной реализации для Linux. Здесь мне нужно было разобраться в двух аспектах:
Как выполнять отрисовку на экран.
Как получать ввод с клавиатуры.
6. Реверс-инжиниринг драйвера
Чтобы понять, как работают экран и клавиатура, необходимо было глубже исследовать прошивку.
Примечание: этот раздел очень технический и содержит большой объём декомпилированного кода. В нём подробно описывается процесс реверс-инжиниринга мной драйверов экрана, светодиодов и клавиатуры. Если вам это не интересно, то можете спокойно переходить к разделу 7.
Двоичные файлы v7 не были урезаны полностью и содержали довольно много внешних символов:
nm -gD 1lid | wc -l 860
Я снова загрузил их в Ghidra и на этот указал все библиотеки, предоставленные тулчейном по адресу opt/uclibc-toolchain/gcc-3.3.6/toolchain-mips/lib. Затем я запустил автоматический анализатор и начал анализировать декомпилированные исходники. Учтите, что я буду упоминать только тот код, который относится к решению задачи. Кроме того, я уже переименовал запутанные переменные и функции для повышения читаемости.
Отрисовка на экран
Я начал с исследования функции main, присваивающей переменной значение /dev/inca-port:
port_name = "/dev/inca-port"; bDisplayOn = 1; sleep(1);
Далее с переменной выполнялись операции, зависевшие от переданных исполняемому файлу аргументов. Если аргументов не было, она сохраняла своё значение по умолчанию. Чуть позже она использовалась в вызове функции, который как будто был связан с экраном:
SetDevice__13MatrixDisplayPCc(theDisplay,port_name);
Так как экспортированные символы сохранились, разбираться в предназначении каждой функции было гораздо проще.
void SetDevice__13MatrixDisplayPCc(int display_fd,char *name)
Функция начиналась с последовательности вызовов, настраивающих файловый дескриптор экрана:
Clear__11RasterImage(); uVar1 = open_device__3TLDPCc(dev); *(undefined4 *)(display + 0x18) = 0xf; init_snom_display__3TLDv(); port_set_fd__Fi(uVar1);
Путь к устройству (char *dev) передавался open_device, которая просто открывала файл без разрешений:
display_fd = open(path,0);
Стоит отметить, что display_fd — это глобальная переменная, используемая на протяжении всего кода. После открытия файлового дескриптора /dev/inca-port экран инициализировался:
printf("DISPLAY fd = %d\n",display_fd); setup_display(1); hide_arrows_snom_display__Fv(); DAT_10015fd4 = 0;
Я внимательнее изучил функцию setup. Она ��остояла из большого количества кода, поэтому я разбирал его шаг за шагом. Оказалось, что функция отвечает за инициализацию порта для общения с экраном.
if ((DAT_10015fd0 != 0) || (param_1 != 0)) { DAT_10015fd0 = 0; port_write__Fiiii(display_fd,2,10,1); port_write__Fiiii(display_fd,2,0xb,1); port_write__Fiiii(display_fd,2,6,1); port_write__Fiiii(display_fd,2,9,1); port_write__Fiiii(display_fd,2,8,1); if (param_1 != 0) { port_write__Fiiii(display_fd,2,8,0); iVar2 = 1; do { bVar1 = iVar2 < 100000; iVar2 = iVar2 + 1; } while (bVar1); port_write__Fiiii(display_fd,2,8,1); goto LAB_00453a44; } } inca_port_write_display_cmd(0xe2);
Выполняется множество операций записи в порт по адресу display_fd (это глобальная переменная, о которой говорилось выше). Я посмотрел, как происходили эти операции записи:
void port_write__Fiiii(int param_1,undefined4 param_2,undefined4 param_3,undefined4 param_4) { parm = param_2; DAT_1003f578 = param_3; DAT_1003f57c = param_4; ioctl(param_1,0x800cbf05,&parm); return; }
Весь обмен данными выполнялся через вызовы ioctl! Для их расшифровки я сделал шаг назад. Вместе с файлами GPL находились исходники ядра, содержавшие заголовки модуля ядра INCAIP:
$ ls bsp_4.3/source/kernel/ifx/bsp/include/asm-mips/incaip dma.h inca-ip.h incaip_iom2_api.h incaip_ssc.h incaip_switch_api.h irq.h keypad.h ledmatrix.h model.h mux.h port.h pwm.h serial.h
В файле port.h находилось множество определений ioctl:
#define INCA_PORT_IOC_MAGIC 0xbf #define INCA_PORT_IOCOD _IOW(INCA_PORT_IOC_MAGIC,0,struct inca_port_ioctl_parm) #define INCA_PORT_IOCPUDSEL _IOW(INCA_PORT_IOC_MAGIC,1,struct inca_port_ioctl_parm) #define INCA_PORT_IOCPUDEN _IOW(INCA_PORT_IOC_MAGIC,2,struct inca_port_ioctl_parm) #define INCA_PORT_IOCSTOFF _IOW(INCA_PORT_IOC_MAGIC,3,struct inca_port_ioctl_parm) #define INCA_PORT_IOCDIR _IOW(INCA_PORT_IOC_MAGIC,4,struct inca_port_ioctl_parm) #define INCA_PORT_IOCOUTPUT _IOW(INCA_PORT_IOC_MAGIC,5,struct inca_port_ioctl_parm) #define INCA_PORT_IOCINPUT _IOWR(INCA_PORT_IOC_MAGIC,6,struct inca_port_ioctl_parm) #define INCA_PORT_DISPCMD _IOWR(INCA_PORT_IOC_MAGIC,7,struct inca_port_ioctl_parm) #define INCA_PORT_DISPDATA _IOWR(INCA_PORT_IOC_MAGIC,8,struct inca_port_ioctl_parm) #define INCA_PORT_GS_DISPCMD _IOWR(INCA_PORT_IOC_MAGIC,9,struct inca_port_ioctl_parm) #define INCA_PORT_GS_DISPDATA _IOWR(INCA_PORT_IOC_MAGIC,10,struct inca_port_ioctl_parm) #define INCA_PORT_GS_MDISPDATA _IOWR(INCA_PORT_IOC_MAGIC,11,struct inca_port_ioctl_parm) #define INCA_PORT_GS_MDISPDATA_2B3P _IOWR(INCA_PORT_IOC_MAGIC,12,struct inca_port_ioctl_parm) #define INCA_PORT_GS_DISPBYTES _IOWR(INCA_PORT_IOC_MAGIC,13,struct inca_port_ioctl_parm)
Определения макросов icotl находятся в linux-2.4.31/include/asm-mips/ioctl.h:
#define _IOC(dir,type,nr,size) \ (((dir) << _IOC_DIRSHIFT) | \ ((type) << _IOC_TYPESHIFT) | \ ((nr) << _IOC_NRSHIFT) | \ ((size) << _IOC_SIZESHIFT)) /* used to create numbers */ #define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0) #define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),sizeof(size)) #define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),sizeof(size)) #define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))
Если не вдаваться в подробности, всё это сводится к следующему:
Биты | Смысл |
|---|---|
31-29 | Направление: 001: IOCNONE |
28-16 | Размер параметра |
15-8 | Тип (магическое число) |
7-0 | Номер (функция) |
Следовательно, номер ICOTL из вызова функции (0x800cbf05) определяется следующим образом:
_IOW(0xbf,5,0xc)
что эквивалентно
#define INCA_PORT_IOCOUTPUT _IOW(INCA_PORT_IOC_MAGIC,5,struct inca_port_ioctl_parm)
Структура inca_port_ioctl_parm определяется следующим образом:
#define GS_DISP_DATA_SIZE 1024 struct inca_port_ioctl_parm { int port; int pin; int value; #ifdef DISP_GS_240x128 unsigned char disp_bytes[GS_DISP_DATA_SIZE]; #endif };
Так как поле size в декомпилированном вызове ioctl было равно 0xc = 12, это означало, что оно состояло только из полей port, pin и value. В декомпилированной программе вызов ioctl выполняется с глобальной переменной parm. До вызова также задаются следующие переменные:
parm = param_2; DAT_1003f578 = param_3; DAT_1003f57c = param_4;
Две безымянные переменные находились в памяти сразу после parm, из чего могло следовать, что это ошибочно определённая одна целочисленная переменная, а не структура, состоящая из трёх int. После добавления структуры inca_port_ioctl_parm в Ghidra и переписывания parm, всё стало выглядеть гораздо лучше:
void port_dir__Fiiii(int port_fd,int port,int pin,int value) { parm.port = port; parm.pin = pin; parm.value = value; ioctl(port_fd,INCA_PORT_IOCOUTPUT,&parm); return; }
Вернувшись на один уровень наверх, я изучил команды, отправляемые через порт для инициализации:
port_write__Fiiii(display_fd,2,10,1); port_write__Fiiii(display_fd,2,0xb,1); port_write__Fiiii(display_fd,2,6,1); port_write__Fiiii(display_fd,2,9,1); port_write__Fiiii(display_fd,2,8,1); if (param_1 != 0) { port_write__Fiiii(display_fd,2,8,0); iVar2 = 1; do { bVar1 = iVar2 < 100000; iVar2 = iVar2 + 1; } while (bVar1); port_write__Fiiii(display_fd,2,8,1); goto LAB_00453a44; }
После записи в порт этих команд инициализации шли другие вызовы функций:
LAB_00453a44: inca_port_busy_wait(); inca_port_write_display_cmd(0xae); inca_port_busy_wait(); inca_port_write_display_cmd(0xa0); inca_port_busy_wait(); inca_port_write_display_cmd(0xc0); inca_port_busy_wait(); inca_port_write_display_cmd(0x23); inca_port_busy_wait(); inca_port_write_display_cmd(0x81); inca_port_busy_wait(); inca_port_write_display_cmd(0x30); inca_port_busy_wait(); inca_port_write_display_cmd(0xa3); inca_port_busy_wait(); inca_port_write_display_cmd(0x2f); inca_port_busy_wait(); inca_port_write_display_cmd(0xaf); inca_port_write_display_cmd(0xa2); inca_port_busy_wait();
Смысл inca_port_busy_wait понятен из имени:
void inca_port_busy_wait(void) { bool bVar1; int iVar2; iVar2 = 0x270e; do { bVar1 = -1 < iVar2; iVar2 = iVar2 + -1; } while (bVar1); iVar2 = 1; do { bVar1 = iVar2 < 100000; iVar2 = iVar2 + 1; } while (bVar1); return; }
Она просто выполняет цикл 100000 раз, ничего не делая, чтобы CPU простаивал достаточное количество времени для исполнения следующей операции в порте. inca_port_write_display_cmd отправляет экрану и команды, и данные:
void serial_disp_write__Fiibi(int fd,int port,int data,int value) { parm.pin = 0; parm.port = port; parm.value = value; if (data == 0) { ioctl(fd,0xc00cbf08,&parm); } else { ioctl(fd,0xc00cbf07,&parm); } return; }
Повторная дешифровка номеров ioctl дала мне следующие два вызова:
#define INCA_PORT_DISPCMD _IOWR(INCA_PORT_IOC_MAGIC,7,struct inca_port_ioctl_parm) #define INCA_PORT_DISPDATA _IOWR(INCA_PORT_IOC_MAGIC,8,struct inca_port_ioctl_parm)
Эта функция отправляет экрану и команды, и данные. Ждущий цикл выполняется между командами. В порт отправляются следующие команды:
Вызовы ioctl при настройке экрана
Последовательно записываются следующие значения:
ioctl | порт | контакт | значение |
|---|---|---|---|
INCA_PORT_IOCOUTPUT | 2 | 0xA | 1 |
INCA_PORT_IOCOUTPUT | 2 | 0xb | 1 |
INCA_PORT_IOCOUTPUT | 2 | 0x6 | 1 |
INCA_PORT_IOCOUTPUT | 2 | 0x9 | 1 |
INCA_PORT_IOCOUTPUT | 2 | 0x8 | 1 |
INCA_PORT_IOCOUTPUT | 2 | 0x8 | 0 |
INCA_PORT_IOCOUTPUT | 2 | 0x8 | 1 |
INCA_PORT_DISPCMD | 2 | 0x1 | 0xe2 |
INCA_PORT_DISPCMD | 2 | 0x1 | 0xae |
INCA_PORT_DISPCMD | 2 | 0x1 | 0xa0 |
INCA_PORT_DISPCMD | 2 | 0x1 | 0xc0 |
INCA_PORT_DISPCMD | 2 | 0x1 | 0x23 |
INCA_PORT_DISPCMD | 2 | 0x1 | 0x81 |
INCA_PORT_DISPCMD | 2 | 0x1 | 0x30 |
INCA_PORT_DISPCMD | 2 | 0x1 | 0xa3 |
INCA_PORT_DISPCMD | 2 | 0x1 | 0x2f |
INCA_PORT_DISPCMD | 2 | 0x1 | 0xaf |
INCA_PORT_DISPCMD | 2 | 0x1 | 0xa2 |
Стоит отметить, что ждущий цикл необходим перед последним вызовом INCA_PORT_IOCOUTPUT и между всеми вызовами INCA_PORT_DISPCMD.
Последняя часть функции setup состояла из вызова функции очистки экрана:
void clear_snom_display__3TLDv(void) { inca_display_clear_row(0); inca_display_clear_row(1); inca_display_clear_row(2); DAT_10015fcc = 0; DAT_10015fc8 = 0; return; }
inca_display_clear_row очищает одну строку экрана:
void clear_display(uint param_1) { bool bVar1; int i; i = 0x84; inca_port_write_display_cmd(param_1 | 0xb0); inca_port_write_display_cmd(0); inca_port_write_display_cmd(0x10); do { inca_port_write_display_data(0); bVar1 = 0 < i; i = i + -1; } while (bVar1); return; }
Сначала выполняется множество вызовов INCA_PORT_DISPCMD. Строка не передаётся напрямую, а выполняется её OR с 0xb0, из чего можно сделать вывод, что строки начинаются с 0xb0. Затем функция отправляет 132 команд DISPDATA со значением 0; это (предположительно) означает, что каждая строка состоит из 132 столбцов. При запуске запись выполняется только в первые три строки, а другие остаются неизменными. После полной инициализации экрана программа скрывает стрелки рядом с каждой строкой экрана.
void hide_arrows_snom_display__Fv(void) { uint uVar1; uVar1 = 0x10; do { if (uVar1 < 0x17) { inca_port_write_display_cmd(0); inca_port_write_display_cmd(0x10); } else { inca_port_write_display_cmd(1); inca_port_write_display_cmd(0x18); } inca_port_write_display_cmd((int)(&DAT_004797f4)[uVar1 & 0xf]); write_display_data(0); uVar1 = uVar1 + 1 & 0xff; } while (uVar1 < 0x1e); return; }
Я не совсем понимал, что конкретно делают эти команды (потому что они отличались от очистки экрана), но используемый массив содержал следующие значения:
Дамп памяти
DAT_004797f5 XREF[2]: hide_arrows_snom_display__Fv:004 FUN_00454030:004540dc(R) 004797f5 b1 undefined1 B1h 004797f6 b2 ?? B2h 004797f7 b4 ?? B4h 004797f8 b5 ?? B5h 004797f9 b6 ?? B6h 004797fa b7 ?? B7h 004797fb b0 ?? B0h 004797fc b1 ?? B1h 004797fd b2 ?? B2h 004797fe b4 ?? B4h 004797ff b5 ?? B5h 00479800 b6 ?? B6h 00479801 b7 ?? B7h 00479802 00 ?? 00h 00479803 00 ?? 00h 00479804 00 ?? 00h 00479805 00 ?? 00h 00479806 00 ?? 00h 00479807 00 ?? 00h 00479808 00 ?? 00h 00479809 00 ?? 00h 0047980a 00 ?? 00h 0047980b 00 ?? 00h 0047980c 00 ?? 00h 0047980d 00 ?? 00h 0047980e 00 ?? 00h 0047980f 00 ?? 00h
На этом этапе я уже знал, как инициализировать и очищать экран, но ещё не было понятно, как выполнять отрисовку. Судя по дальнейшим вызовам функций, внутри программы используются растровые изображения:
__11RasterImageiiRC3str(auStack_40,5,8,auStack_30); __as__11RasterImageRC11RasterImage(iVar5,auStack_40); _$_11RasterImage(auStack_40,2);
Это походило на декомпилированный код на C++, который становился довольно запутанным. Я решил пойти по другому пути: написать собственный драйвер и поэкспериментировать с ним.
Пишем собственный драйвер
Для начала я просто портировал все функции, в которых разобрался:
Настройки порта.
Записи в порт.
Отправки команд/данных экрану.
Очистки строк.
После реализации всего этого я скомпилировал программу при помощи тулчейна кросс-компиляции и сжал её при помощи UPX. Сама программа пока почти ничего не делала, но очистка первых трёх строк вроде бы работала! Теперь мне нужно было разобраться, как выполнять отрисовку на экран и сколько на нём вообще строк. Для начала я попробовал заполнить верхнюю строку (0) пикселями.
Начав с кода очистки экрана, я попробовал для каждого столбца отправлять 1 вместо 0:
inca_display_write_serial(DISP_CMD, 0 | 0xb0); inca_display_write_serial(DISP_CMD, 0); inca_display_write_serial(DISP_CMD, 0x10); for (int i = 0; i < 132; ++i) { inca_display_write_serial(DISP_DATA, 0x1); }
И это действительно сработало! Однако экран, похоже, заполнялся снизу вверх, то есть 0 (0xb0) была самой нижней строкой.

Я обернул код в цикл for, чтобы попробовать разобраться, сколько строк у экрана, и получил следующий результат:

Так как я увеличивал счётчик цикла инкрементно, то пришёл к выводу, что всего есть 8 строк, а самая нижняя строка имеет номер 0. Однако было любопытно, что каждая строка содержала одну линию пикселей. Я попробовал отправлять значения, отличающиеся от 0x1; при отправке 0xf и 0xff обнаружилось следующее:

Я подозревал, что каждый отправляемый байт отвечает за задание нескольких пикселей по вертикали, потому что отправка 0xff для строки 0 отрисовывала 8 линий пикселей внизу. Это означало бы, что дисплей имеет 64 строк, а значит, разрешение 132x32 пикселя. Я не совсем понимал, почему заполнение строк также одновременно отображало стрелки (но только некоторые), однако на этом этапе я ещё не реализовал код сокрытия стрелок.
Я ещё поэкспериментировал с драйвером и в результате написал маленькую программу, которая преобразует изображения (и видео!) в данные, которые можно записывать непосредственно на экран; это подтвердило мою теорию о работе дисплея.

Включаем подсветку
Из опыта обычного использования этого телефона я знал, что у экрана есть встроенная подсветка, которая определённо может повысить видимость изображения. Пока я не находил никаких отсылок на это в коде, поэтому начал искать «light» в дереве символов. Я обнаружил следующую функцию, которая показалась мне весьма многообещающей:
void Light__13MatrixDisplayb(undefined4 param_1,int param_2) { inca360_write_led__Fib(0,param_2); return; }
inca360_write_led содержит массив, сопоставляющий каждое значение в интервале 0-16 с другим значением в интервале 0-16. Она получает индекс светодиода (от 0 до 16) и значение, которое ему нужно присвоить (0 или 1) и создаёт битовую маску. Оказалось, что подсветка — это один из 16 управляемых светодиодов телефона.
void inca360_write_led__Fib(int led,int value) { ushort bitmask; short leds [16]; leds[0] = 0xf; leds[1] = 0; leds[2] = 4; leds[3] = 8; leds[4] = 0xb; leds[5] = 1; leds[6] = 5; leds[7] = 2; leds[8] = 6; leds[9] = 10; leds[10] = 0xe; leds[0xb] = 3; leds[0xc] = 7; leds[0xd] = 9; bitmask = 1; if ((uint)led < 0xe) { bitmask = (ushort)(1 << ((int)leds[led] & 0x1fU)); } if (led == 0) { value = value ^ 1; } if (value == 0) { inca360_led = bitmask | inca360_led; } else { inca360_led = inca360_led & ~bitmask; } write_led__FUs(inca360_led); return; }
inca360_led — это глобальная переменная, содержащая битовый массив, который используется для записи физических состояний светодиодов в write_led:
void write_led__FUs(uint leds) { … leds = leds & 0xffff; … inca_port_setup(); iVar4 = inca_port_write_bit(0); iVar5 = 0; if (iVar4 == 0) { LAB_0042501c: inca_port_close(); _$_3str(&local_38,2); _$_3str(auStack_48,2); } else { do { iVar4 = inca_port_write_bit(leds >> 0xf); if (iVar4 == 0) goto LAB_0042501c; leds = (leds & 0x7fff) << 1; iVar5 = iVar5 + 1; } while (iVar5 < 0x10); inca_port_close(); _$_3str(&local_38,2); _$_3str(auStack_48,2); } return;
Сначала в порт передаются общие команды подготовки. Затем функция записывает в порт один бит (0) и проверяет его возвращаемое значение. Если он возвращает 0, функция прекращает выполнение и отправляет общие команды закрытия. Запись и чтение одного бита реализованы в функции inca_port_write_bit:
int inca_port_write_bit(int value) { int ret; inca_port_dir__Fiii(2,10,1); if (value == 0) { inca_port_write__Fiii(2,10,0); } else { inca_port_write__Fiii(2,10,1); } inca_port_write__Fiii(2,6,0); ret = inca_port_read_wait(0); if (ret != 0) { inca_port_dir__Fiii(2,10,0); inca_port_write__Fiii(2,6,1); ret = inca_port_read_wait(1); if (ret != 0) { return 1; } } inca_port_dir__Fiii(2,10,0); return 0; }
Она отправляет в порт множество команд ioctl и вызывает inca_port_read_wait:
int inca_port_read_wait(int value) { int value_read; int iVar1; iVar1 = 0; do { value_read = inca_port_read__Fii(2,0xb); iVar1 = iVar1 + 1; if (value_read == value) { return 1; } } while (iVar1 < 3000); return 0; }
Эта функция получает значение в качестве параметра, а затем выполняет чтение из порта 3000 раз. Если считанное значение соответствует ожидаемому, то она возвращает 1, в противном случае — 0.
Затем снова при помощи inca_port_write_bit записываются все отдельные значения светодиодов.
do { iVar4 = inca_port_write_bit(leds >> 0xf); if (iVar4 == 0) goto LAB_0042501c; leds = (leds & 0x7fff) << 1; iVar5 = iVar5 + 1; } while (iVar5 < 0x10);
Биты считываются по одному, начиная с самого старшего, и передаются по одному.
Вызовы ioctl при записи состояний светодиодов
Последовательно записываются следующие значения:
Настройка:
ioctl | порт | контакт | значение |
|---|---|---|---|
INCA_PORT_IOCDIR | 2 | 0x7 | 1 |
INCA_PORT_IOCDIR | 2 | 0x6 | 1 |
INCA_PORT_IOCDIR | 2 | 0xb | 0 |
INCA_PORT_IOCDIR | 2 | 0xa | 0 |
INCA_PORT_IOCOUTPUT | 2 | 0x6 | 1 |
INCA_PORT_IOCOUTPUT | 2 | 0x7 | 0 |
Запись нуля:
ioctl | порт | контакт | значение |
|---|---|---|---|
INCA_PORT_IOCDIR | 2 | 0xa | 1 |
INCA_PORT_IOCOUTPUT | 2 | 0xa | 0 |
INCA_PORT_IOCOUTPUT | 2 | 0x6 | 0 |
INCA_PORT_IOCINPUT | 2 | 0xb | |
INCA_PORT_IOCDIR | 2 | 0xa | 0 |
INCA_PORT_IOCOUTPUT | 2 | 0x6 | 1 |
INCA_PORT_IOCINPUT | 2 | 0xb |
Запись значений светодиодов (16 раз):
ioctl | порт | контакт | значение | |
|---|---|---|---|---|
INCA_PORT_IOCDIR | 2 | 0xa | 1 | |
INCA_PORT_IOCOUTPUT | 2 | 0xa | value | |
INCA_PORT_IOCOUTPUT | 2 | 0x6 | 0 | |
INCA_PORT_IOCINPUT | 2 | 0xb | чтение, =0? | |
INCA_PORT_IOCDIR | 2 | 0xa | 0 | |
INCA_PORT_IOCOUTPUT | 2 | 0x6 | 1 | |
INCA_PORT_IOCINPUT | 2 | 0xb | чтение, =1? |
Закрытие:
ioctl | порт | контакт | значение |
|---|---|---|---|
INCA_PORT_IOCOUTPUT | 2 | 0xa | 1 |
INCA_PORT_IOCOUTPUT | 2 | 0x7 | 1 |
INCA_PORT_IOCDIR | 2 | 0xb | 1 |
INCA_PORT_IOCDIR | 2 | 0xa | 1 |
Добавление в драйвер поддержки светодиодов
Теперь, когда у меня были все вызовы ioctl для настройки состояния светодиодов, я мог снова дополнить свой драйвер. Я реализовал всё, что было в дизассемблированном драйвере, но не знал точно, какой индекс каким светодиодом управляет. Эту проблему я решил простым перебором каждого индекса, то есть заданием значения 1 только одного светодиода за раз. Любопытно, что все светодиоды, за исключением подсветки, похоже, активируются при записи в них 0, а подсветка активируется при записи 1. Я определил следующие соответствия:
typedef enum { SNOM_LED_BACKLIGHT = 0, SNOM_LED_MESSAGE = 6, SNOM_LED_ONE = 13, SNOM_LED_TWO = 15, SNOM_LED_THREE = 9, SNOM_LED_FOUR = 11, SNOM_LED_FIVE = 5, SNOM_LED_SIX = 7, SNOM_LED_SEVEN = 1, SNOM_LED_EIGHT = 4, SNOM_LED_NINE = 12, SNOM_LED_TEN = 14, SNOM_LED_ELEVEN = 8, SNOM_LED_TWELVE = 10 } SnomLed;

Получаем ввод с клавиатуры
Теперь осталось только научиться получать ввод с клавиатуры. Я снова начал с поиска по дереву символов. Похоже, клавиатура считывается из функции inca360_read_kb:
void inca360_read_kb__FRUcRb(byte *kb_data,undefined4 *kb_event) { int res; int i; int data_bit [2]; byte tmp; data_bit[0] = 0; *kb_event = 0; *kb_data = 0; inca_port_setup(); res = inca_port_write_bit(1); i = 0; if (res != 0) { res = inca_poll_kbd(data_bit); if ((res != 0) && (data_bit[0] != 0)) { *kb_event = 1; while (res = inca_poll_kbd(data_bit), res != 0) { tmp = *kb_data; *kb_data = tmp << 1; if (data_bit[0] != 0) { *kb_data = tmp << 1 | 1; } i = i + 1; if (7 < i) { port_finish(); return; } } } } port_finish(); return; }
Эта функция получает два параметра: один байт, содержащий код нажатой клавиши, и int, задаваемый на основании того, считано ли клавиатурное событие. Сначала порт получает ту же команду настройки, что и для светодиодов, и в него записывается 1. Затем происходит опрос клавиатуры при помощи inca_poll_kbd:
int inca_poll_kbd(uint *data) { int ret; inca_port_write__Fiii(2,6,0); ret = inca_port_read_wait(0); if (ret != 0) { ret = inca_port_read__Fii(2,10); *data = (uint)(ret != 0); inca_port_write__Fiii(2,6,1); ret = inca_port_read_wait(1); if (ret != 0) { return 1; } } return 0; }
Эта функция выполняет повторную запись в порт, а затем считывает из него один бит. Этот бит передаётся функции считывания клавиатуры. Если и возвр��щаемое значение inca_poll_kbd, и считываемый бит равны 1, драйвер регистрирует нажатие клавиши (задавая *kbd_event = 1), а затем начинает считывать код клавиши по биту за раз. Сначала считывается старший бит и сдвигается на каждой итерации. После считывания кода клавиши в порт передаются завершающие команды (такие же, как и в случае светодиодов).
Вызовы ioctl вводе с клавиатуры
Последовательно записываются следующие значения:
Настройка:
ioctl | порт | контакт | значение |
|---|---|---|---|
INCA_PORT_IOCDIR | 2 | 0x7 | 1 |
INCA_PORT_IOCDIR | 2 | 0x6 | 1 |
INCA_PORT_IOCDIR | 2 | 0xb | 0 |
INCA_PORT_IOCDIR | 2 | 0xa | 0 |
INCA_PORT_IOCOUTPUT | 2 | 0x6 | 1 |
INCA_PORT_IOCOUTPUT | 2 | 0x7 | 0 |
Запись нуля:
ioctl | порт | контакт | значение | |
|---|---|---|---|---|
INCA_PORT_IOCDIR | 2 | 0xa | 1 | |
INCA_PORT_IOCOUTPUT | 2 | 0xa | 1 | |
INCA_PORT_IOCOUTPUT | 2 | 0x6 | 0 | |
INCA_PORT_IOCINPUT | 2 | 0xb | чтение, =0? | |
INCA_PORT_IOCDIR | 2 | 0xa | 0 | |
INCA_PORT_IOCOUTPUT | 2 | 0x6 | 1 | |
INCA_PORT_IOCINPUT | 2 | 0xb | чтение, =1? |
Опрос:
ioctl | порт | контакт | значение | |
|---|---|---|---|---|
INCA_PORT_IOCOUTPUT | 2 | 0x6 | 0 | |
INCA_PORT_IOCINPUT | 2 | 0xb | чт��ние, =0? | |
INCA_PORT_IOCINPUT | 2 | 0xa | чтение, бит | |
INCA_PORT_IOCOUTPUT | 2 | 0x6 | 1 | |
INCA_PORT_IOCINPUT | 2 | 0xb | чтение, =1? |
Чтение бита (8 раз):
ioctl | порт | контакт | значение | |
|---|---|---|---|---|
INCA_PORT_IOCOUTPUT | 2 | 0x6 | 0 | |
INCA_PORT_IOCINPUT | 2 | 0xb | чтение, =0? | |
INCA_PORT_IOCINPUT | 2 | 0xa | чтение, бит | |
INCA_PORT_IOCOUTPUT | 2 | 0x6 | 1 | |
INCA_PORT_IOCINPUT | 2 | 0xb | чтение, =1? |
Закрытие:
ioctl | порт | контакт | значение |
|---|---|---|---|
INCA_PORT_IOCOUTPUT | 2 | 0xa | 1 |
INCA_PORT_IOCOUTPUT | 2 | 0x7 | 1 |
INCA_PORT_IOCDIR | 2 | 0xb | 1 |
INCA_PORT_IOCDIR | 2 | 0xa | 1 |
Добавляем в драйвер нажатия клавиш
Я снова дополнил драйвер полученной информацией. К сожалению, сопоставление нажатий всех возможных клавиш отсутствовало, поэтому мне самому пришлось выяснять коды клавиш. Я сделал это, запустив цикл опроса клавиатуры, который при обнаружении нажатия выводил код клавиши в stdout. Событие регистрируется и при нажатии, и при отпускании клавиши. Поначалу я не понимал, действительно ли это коды отдельных клавиш, но оказалось, что тип события хранится в старшем бите, то есть код клавиши занимает 7 бит. Обнаруженное мной сопоставление было таким:
typedef enum { SNOM_KEY_HASH = 35, SNOM_KEY_STAR = 42, SNOM_KEY_ZERO = 48, SNOM_KEY_ONE = 49, SNOM_KEY_TWO = 50, SNOM_KEY_THREE = 51, SNOM_KEY_FOUR = 52, SNOM_KEY_FIVE = 53, SNOM_KEY_SIX = 54, SNOM_KEY_SEVEN = 55, SNOM_KEY_EIGHT = 56, SNOM_KEY_NINE = 57, SNOM_KEY_TRANSFER = 65, SNOM_KEY_HOLD = 66, SNOM_KEY_DND = 67, SNOM_KEY_DIAL_ONE = 74, SNOM_KEY_DIAL_TWO = 68, SNOM_KEY_DIAL_THREE = 75, SNOM_KEY_DIAL_FOUR = 69, SNOM_KEY_DIAL_FIVE = 76, SNOM_KEY_DIAL_SIX = 70, SNOM_KEY_DIAL_SEVEN = 77, SNOM_KEY_DIAL_EIGHT = 71, SNOM_KEY_DIAL_NINE = 78, SNOM_KEY_DIAL_TEN = 72, SNOM_KEY_DIAL_ELEVEN = 78, SNOM_KEY_DIAL_TWELVE = 73, SNOM_KEY_QUICK_ONE = 97, SNOM_KEY_QUICK_TWO = 98, SNOM_KEY_QUICK_THREE = 99, SNOM_KEY_QUICK_FOUR = 100, SNOM_KEY_VOL_DOWN = 101, SNOM_KEY_VOL_UP = 102, SNOM_KEY_CANCEL = 103, SNOM_KEY_ACCEPT = 104, SNOM_KEY_LEFT = 105, SNOM_KEY_RIGHT = 106, SNOM_KEY_UP = 107, SNOM_KEY_DOWN = 108, SNOM_KEY_RECORD = 109, SNOM_KEY_RETRIEVE = 110, SNOM_KEY_MUTE = 111, SNOM_KEY_SPEAKER = 112, SNOM_KEY_HEADSET = 113, SNOM_KEY_REDIAL = 114, SNOM_KEY_SETTINGS = 115, SNOM_KEY_DIRECTORY = 116, SNOM_KEY_HELP = 117, SNOM_KEY_MENU = 118, SNOM_KEY_SNOM = 119, SNOM_KEY_CONVERENCE = 120, } SnomKey; typedef enum { SNOM_LED_BACKLIGHT = 0, SNOM_LED_MESSAGE = 6, SNOM_LED_ONE = 13, SNOM_LED_TWO = 15, SNOM_LED_THREE = 9, SNOM_LED_FOUR = 11, SNOM_LED_FIVE = 5, SNOM_LED_SIX = 7, SNOM_LED_SEVEN = 1, SNOM_LED_EIGHT = 4, SNOM_LED_NINE = 12, SNOM_LED_TEN = 14, SNOM_LED_ELEVEN = 8, SNOM_LED_TWELVE = 10 } SnomLed; #define SNOM_KEY_PRESSED(x) (x & 0x80) #define SNOM_KEY_RELEASED(x) !(x & 0x80) #define SNOM_KEY_CODE(x) (x & 0x7f)
7. Портирование Doom
Закончив с драйверами, я мог приступить к исходной цели: портированию Doom! В разделе 5 я перечислил функции, необходимые для портирования doomgeneric на новую платформу, и сейчас разберу их по порядку.
DG_Init
Инициализация платформы (создание окна, буфера кадров и так далее…).
Для Snom 360 инициализация была очень простой:
void DG_Init() { snom360_setup(); snom360_set_led(SNOM_LED_BACKLIGHT, 1); }
Я всего лишь настроил саму платформу и активировал подсветку экрана. Подробности настройки приведены в разделе 6; для этого порта я использовал свой драйвер.
DG_SleepMs
Sleep в миллисекундах.
Я просто использовал код из эталонной реализации в doomgeneric_linuxvt.c:
void DG_SleepMs(uint32_t ms) { usleep(ms * 1000); }
DG_GetTicksMs
Количество тактов в миллисекундах, прошедших после запуска.
Эта функция тоже была взята из эталонной реализации:
uint32_t DG_GetTicksMs() { struct timeval cur; long seconds, usec; gettimeofday(&cur, NULL); seconds = cur.tv_sec - start.tv_sec; usec = cur.tv_usec - start.tv_usec; return (seconds * 1000) + (usec / 1000); }
где struct timeval start задаётся в main:
gettimeofday(&start, NULL);
DG_SetWindowTitle
Не требуется. Функция нужна для указания заголовка окна, а Doom задаёт его из файла WAD.
Я не стал реализовывать эту функцию, потому что у телефона нет концепции окон.
DG_GetKey
Клавиатурные события.
Это была одна из тех функций, которую пришлось реализовывать самостоятельно. Изучив эталонные реализации, я узнал, что Doom использует свои коды клавиш, которые нужно преобразовать из нативных кодов платформы. Сама функция проста:
int DG_GetKey(int* pressed, unsigned char* key) { if (s_KeyQueueReadIndex == s_KeyQueueWriteIndex) { // очередь клавиш пуста return 0; } else { unsigned short keyCode = s_KeyQueue[s_KeyQueueReadIndex]; s_KeyQueueReadIndex++; s_KeyQueueReadIndex %= KEYQUEUE_SIZE; *pressed = SNOM_KEY_PRESSED(keyCode); *key = convertToDoomKey(SNOM_KEY_CODE(keyCode)); return 1; } }
Для передачи событий клавиш игре я использовал простой кольцевой буфер. Клавиатура опрашивается каждый раз при отрисовке нового кадра (см. DG_DrawFrame), и если событие обнаружено, оно добавляется в очередь. Коды клавиш платформы преобразуются при помощи convertToDoomKey. Когда игре нужно считать нажатие клавиши, она предоставляет два указателя: один для типа события, другой для кода клавиши. Так как состояние уже хранится в кодах клавиш, организовать его передачу было просто.
DG_DrawFrame
Кадр уже готов в DG_ScreenBuffer. Нужно скопировать его на экран платформы.
Это была самая важная часть всего проекта. Буфер Doom хранится в глобальной переменной DG_ScreenBuffer:
pixel_t* DG_ScreenBuffer = NULL;
где pixel_t — это 32-битный беззнаковый integer, задающий 8-битные RGB-компоненты. Наименьшее разрешение, в котором мне удалось заставить рендериться игру, было 320×200, что не очень подходило для экрана размером 132×64. Вместо этого я решил рендерить всё в 640×400 и усреднять по четыре пикселя для уменьшения разрешения. Ещё одна проблема заключалась в том, что дисплей на светодиодной матрице поддерживает только однобитные состояния (вкл��чено/выключено), а Doom передаёт полноцветные данные. Я решил усреднять RGB-компоненты четырёх пикселей, преобразовывать их в градации серого и использовать значение отсечки для конвертации значений 0-255 в 0/1. Мне пришлось довольно долго экспериментировать с отсечкой (контрастностью), поэтому я добавил её в качестве параметра командной строки.
for (int y = 0; y < IMG_HEIGHT; y++) { for (int x = 0; x < IMG_WIDTH; x++) { // Сэмплирование из источника (простой алгоритм ближайших соседей) int src_x = (x * 640) / IMG_WIDTH; int src_y = (y * 400) / (IMG_HEIGHT); uint32_t p1 = DG_ScreenBuffer[src_y * 640 + src_x]; uint32_t p2 = DG_ScreenBuffer[src_y * 640 + src_x + 1]; uint32_t p3 = DG_ScreenBuffer[(src_y + 1) * 640 + src_x]; uint32_t p4 = DG_ScreenBuffer[(src_y + 1) * 640 + src_x + 1]; // Усреднение RGB-компонентов unsigned char r = (((p1>>16)&0xFF) + ((p2>>16)&0xFF) + ((p3>>16)&0xFF) + ((p4>>16)&0xFF)) >> 2; unsigned char g = (((p1>>8)&0xFF) + ((p2>>8)&0xFF) + ((p3>>8)&0xFF) + ((p4>>8)&0xFF)) >> 2; unsigned char b = ((p1&0xFF) + (p2&0xFF) + (p3&0xFF) + (p4&0xFF)) >> 2; // Преобразование в градации серого unsigned char gray = (r * 76 + g * 150 + b * 29) >> 8; greyscale[y * IMG_WIDTH + x] = gray > contrast; } }
Сначала кадр уменьшается до 132x64, а однобитные значения сохраняются в массив greyscale. Хотя этот код довольно непонятный, лежащий в его основе механизм достаточно прост. Дальше массив градаций серого упаковывается:
copy
for (int i = 0; i < DISPLAY_ROWS; ++i) { for (int j = 0; j < DISPLAY_COLS; ++j) { int pb = i*DISPLAY_COLS + j; int pr = (DISPLAY_ROWS-1-i)*DISPLAY_COLS*8 + j; int o = DISPLAY_COLS; buf[pb] = greyscale[pr+o*0] << 7 | greyscale[pr+o*1] << 6 | greyscale[pr+o*2] << 5 | greyscale[pr+o*3] << 4 | greyscale[pr+o*4] << 3 | greyscale[pr+o*5] << 2 | greyscale[pr+o*6] << 1 | greyscale[pr+o*7] << 0; } }
Так как каждый байт управляет 8 вертикальными пикселями, строки в градациях серого нужно упаковать в однобайтные битовые массивы. Кроме того, мне нужно было начинать упаковку пикселей с низа изображения, потому что строка 0 начинается с низа экрана. В конце буфер отрисовывается на экране:
snom360_display_draw(buf);
Этот процесс повторяется в каждом кадре.
Сборка двоичного файла
После реализации всех функций нам нужно скомпилировать программу. Я начал с копирования стандартного Makefile и изменил его так, чтобы в нём использовались мои исходники, а также тулчейн кросс-компиляции для сборки. Всё это было довольно просто, поэтому мне удалось без проблем всё скомпилировать. Стоит отметить, что я выполнял компиляцию в статический двоичный файл. Хоть это и сильно увеличило размер файла, основную часть оверхеда снова можно устранить при помощи инструмента UPX. После сборки и сжатия двоичного файла я скачал его на телефон.
Doom Generic 0.1 Z_Init: Init zone memory allocation daemon. zone memory: 0x2aba3008, 600000 allocated for zone Using . for configuration and saves V_Init: allocate screens. M_LoadDefaults: Load system defaults. saving config in .default.cfg -iwad not specified, trying a few iwad names Trying IWAD file:doom2.wad Trying IWAD file:plutonia.wad Trying IWAD file:tnt.wad Trying IWAD file:doom.wad Trying IWAD file:doom1.wad Trying IWAD file:chex.wad Trying IWAD file:hacx.wad Trying IWAD file:freedm.wad Trying IWAD file:freedoom2.wad Trying IWAD file:freedoom1.wad Game mode indeterminate. No IWAD file was found. Try specifying one with the '-iwad' command line parameter.
И он без проблем запустился! Теперь нам нужен файл IWAD для запуска игры. Я начал искать минимальный играбельный IWAD и обнаружил squashware IWAD6, в котором находился файл IWAD из одного уровня размером 660 КБ (!). Я тоже загрузил его на телефон, и...
W_Init: Init WADfiles. adding doom.wad Z_Malloc: failed on allocation of 1882193944 bytes
Загрузка не удалась. IWAD работает на моей локальной машине, а 1882193944 байт — это ужасно много, поэтому я предположил, что проблема в endianness. Оказалось, что кросс-компилятор некорректно задал макрос __BYTE_ORDER__, что привело к ошибкам с обработкой endianness. Я добавил простое обходное решение:
#ifdef SNOM360 #define SYS_BIG_ENDIAN static inline unsigned short swapLE16(unsigned short val) { return ((val << 8) | (val >> 8)); } static inline unsigned long swapLE32(unsigned long val) { return ((val << 24) | ((val << 8) & 0x00FF0000) | ((val >> 8) & 0x0000FF00) | (val >> 24)); } #define SHORT(x) ((signed short) swapLE16(x)) #define LONG(x) ((signed int) swapLE32(x)) #else // SNOM360
и скомпилировал всё заново. На этот раз всё заработало без каких-либо проблем:
Doom Generic 0.1 Z_Init: Init zone memory allocation daemon. zone memory: 0x2aba3008, 600000 allocated for zone Using . for configuration and saves V_Init: allocate screens. M_LoadDefaults: Load system defaults. saving config in .default.cfg -iwad not specified, trying a few iwad names Trying IWAD file:doom2.wad Trying IWAD file:plutonia.wad Trying IWAD file:tnt.wad Trying IWAD file:doom.wad W_Init: Init WADfiles. adding doom.wad Using ./.savegame/ for savegames =========================================================================== DOOM Shareware =========================================================================== Doom Generic is free software, covered by the GNU General Public License. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. You are welcome to change and distribute copies under certain conditions. See the source for more information. =========================================================================== I_Init: Setting up machine state. M_Init: Init miscellaneous info. R_Init: Init DOOM refresh daemon - ............... P_Init: Init Playloop state. S_Init: Setting up sound. D_CheckNetGame: Checking network game status. startskill 2 deathmatch: 0 startmap: 1 startepisode: 1 player 1 of 1 (1 nodes) Emulating the behavior of the 'Doom 1.9' executable. HU_Init: Setting up heads up display. ST_Init: Init status bar. I_InitGraphics: framebuffer: x_res: 640, y_res: 400, x_virtual: 640, y_virtual: 400, bpp: 32 I_InitGraphics: framebuffer: RGBA: 8888, red_off: 16, green_off: 8, blue_off: 0, transp_off: 24 I_InitGraphics: DOOM screen size: w x h: 320 x 200 I_InitGraphics: Auto-scaling factor: 2
Теперь осталось только сыграть в игру! При настройках по умолчанию разобрать происходящее на экране было довольно сложно. Выяснилось, что подходящий уровень контрастности примерно равен 50.
Готовый продукт
Хоть получившийся порт и неидеален (есть артефакты, отсутствует звук, а текст практически нечитаем), это всё равно замечательное достижение. Я никогда раньше не занимался «настоящим» хакинг-проектом, поэтому это стало прекрасной возможностью научиться чему-то новому.
Возможно, в будущем я добавлю звук, но, судя по собранной информации, это будет гораздо сложнее, чем уже реализованные части.
Скачиваемые файлы
Скачиваемые файлы будут доступны, когда я доберусь до подчистки всего кода.
Я выложу следующие файлы:
Мой драйвер.
Инструмент управления дисплеем (для отображения изображений/видео на телефоне).

