Когда-то давно мне подарили кучу 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.

Web Interface - Update

Обновления прошивок можно устанавливать через веб-интерфейс, работающий в самом телефоне. Достаточно указать 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:

Snom Website - GPL Downloads

По какой-то причине файлов для прошивки 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. В нём раскрывались следующие темы:

  1. Конфигурирование исходников ядра.

  2. Сборка uImage для uBoot.

  3. Сборка образа rootfs.

  4. Развёртывание образа по последовательной линии.

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. Верхняя плата отвечает за экран и кнопки.

Хоть я и не особо хорошо разбираюсь в проектировании печатных плат, мне быстро удалось найти в верхней части эти тестовые точки:

Маркировка GNDTxD и 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
010: IOCREAD
100: IOCWRITE
110: IOCREAD|_IOC_WRITE

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, это означало, что оно состояло только из полей portpin и 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) была самой нижней строкой.

Single line of pixels at the bottom

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

Several lines of pixels with spacing

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

Several lines of pixels with spacing and differing thickness

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

Я ещё поэкспериментировал с драйвером и в результате написал маленькую программу, которая преобразует изображения (и видео!) в данные, которые можно записывать непосредственно на экран; это подтвердило мою теорию о работе дисплея.

An image of garfield smoking a pipe
Гарфилд с трубкой

Включаем подсветку

Из опыта обычного использования этого телефона я знал, что у экрана есть встроенная подсветка, которая определённо может повысить видимость изображения. Пока я не находил никаких отсылок на это в коде, поэтому начал искать «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;
The same picture of garfield again, but now with backlight
Та же картинка с Гарфилдом, но уже с подсветкой

Получаем ввод с клавиатуры

Теперь осталось только научиться получать ввод с клавиатуры. Я снова начал с поиска по дереву символов. Похоже, клавиатура считывается из функции 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.

Готовый продукт

Хоть получившийся порт и неидеален (есть артефакты, отсутствует звук, а текст практически нечитаем), это всё равно замечательное достижение. Я никогда раньше не занимался «настоящим» хакинг-проектом, поэтому это стало прекрасной возможностью научиться чему-то новому.

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

Скачиваемые файлы 

Скачиваемые файлы будут доступны, когда я доберусь до подчистки всего кода.

Я выложу следующие файлы:

  • Мой драйвер.

  • Инструмент управления дисплеем (для отображения изображений/видео на телефоне).

  • Патч doomgeneric.

Ссылки

  1. https://service.snom.com/spaces/wiki/pages/234331549/Archive+-+Deskphones

  2. https://en.wikipedia.org/wiki/JFFS2

  3. https://media.digikey.com/pdf/Data%20Sheets/Infineon%20PDFs/PSB21653_PB_Rev2+0.pdf

  4. https://github.com/upx/upx

  5. https://github.com/ozkl/doomgeneric

  6. https://github.com/fragglet/squashware