HowIHackedMyCar 2021款 现代IONIQ (四)Creating Custom Firmware
HowIHackedMyCar 2021款 现代IONIQ (五)CAN Bus分析
HowIHackedMyCar 2021款 现代IONIQ (六)How I Hacked my Car Again
本合集共7部分,本篇为第七部分
来源:programmingwithstyle.com,感谢greenluigi1
The port
利用第三部分中可以找到的信息。我为DAudio2开发设置了QTCreator IDE,并使用我的DAudio2 Gui Template Application作为起始模板。
有许多Doom及其类似游戏的版本,以及许多可以作为新移植起点的移植版本。我决定使用doomgeneric,这是一个特别容易移植的Doom版本。
doomgeneric声称我所要做的就是创建一个doomgeneric_myPlatform.cpp文件,并实现5个简单的函数,就这样,Doom就被移植了。
“All I had to do” Joke #5
是的,它没那么简单,我沿途遇到了一些障碍和困难。
但现在,回到开始:
我有5/6个可以实施的函数:
Function |
Description |
DG_Init |
Platform-specific initialization (Creating window, allocating buffers, etc..) |
DG_DrawFrame |
Draw the frame from the framebuffer to the window |
DG_SleepMs |
Sleeping in milliseconds |
DG_GetTicksMs |
Getting the ticks that passed since the launch in milliseconds |
DG_GetKey |
Provide keyboard inputs to Doom |
DG_SetWindowTitle |
Set the window title (Not applicable in this case) |
doomgeneric很贴心地提供了一些示例移植。其中之一是X11/xlib移植。移植具有相当通用的DG_SleepMs(),DG_GetTicksMs()实现,以及一些不错的帮助方法,使DG_GetKey()更容易。我决定复制这些用于我的移植。
这让我需要实现3个函数,DG_Init(),DG_DrawFrame(),以及一些DG_GetKey()逻辑。
我在doomgeneric_daudio.cpp文件中创建了我的doomgeneric_daudio.cpp文件,并开始了初始化逻辑。
为了保持简单,我在该文件中创建了我的main()函数,并复制了初始化我的TestGuiApplication的代码。我还把我所有的“测试”占位符文本重命名为“Doom”。
在这一点上,我决定我不需要一个单独的DG_Init()函数来初始化事情,因为我有一个main()函数。我删除了它,并将其中的几个东西移动到了main()中。按照doomgeneric的Readme.md中的指示,我添加了对doomgeneric_Create();
在创建这个main()函数时,我遇到了一些复杂情况,即使它非常简单。其中主要的是参数处理。
DAudio2 GUI应用程序的工作方式有些奇怪。它们不能像大多数Linux发行版那样从命令行启动。它们的启动由一个名为Helix的窗口管理器处理,参数的格式标准化,以允许启动应用程序中的特定AppViews或AppServices。
因此,我不能像通常那样将参数传递给doomgeneric_Create(),因为这可能会产生意想不到的副作用。我简单地硬编码了自己的参数并传递了它们。
漂亮的图片?
下一步是在屏幕上绘制帧
经过一些研究,我发现我可以使用一个QLabel通过这个过程显示一个帧缓冲区:
1 使用帧缓冲区生成一个QImage 2 使用QImage创建一个QPixmap 3 使用QPixmap作为QLabel的背景图像 doomgeneric写入的缓冲区称为DG_ScreenBuffer。我在MainWindow上创建了一个名为bufferQImage的字段,并设置它引用DG_ScreenBuffer。然后我用它来创建一个QPixmap,用作我的displayLabel的背景图像。MainWindow构造函数开始
我还需要在有新帧要渲染时更新这个QLabel。我创建了一个名为refreshDrawingBuffer的函数,它调用displayLabel上的更新函数。根据我的研究,这应该会使得QLabel重新绘制自身。
我将DG_DrawFrame()函数连接到调用refreshDrawingBuffer()函数,并设置了一个QTimer来反复调用doomgeneric_Tick()函数,这应该会使得游戏运行。
它奏效了吗?
我现在应该有了绘制和运行Doom演示所需的代码。
为了测试这个,我需要一个WAD文件。WAD文件是Doom引擎的游戏数据文件。它们包含游戏中使用的水平、地图、敌人和纹理。我找到了原始的DOOM.WAD文件,并将其下载到我的闪存驱动器中。
为了使Doom引擎能够加载这个WAD文件,我在参数中硬编码了一个路径,告诉Doom读取“/appdata/DOOM.WAD”。
我编译了应用程序,然后进入了我的车辆。
为了让我的应用被Helix注册,我必须在/etc/appmanager/appconf文件夹中为它添加一个.appconf文件。我创建了一个DAudio2Doom.appconf文件,并复制到文件夹中,内容如下:
[Application]
Name=com.greenluigi1.doom
Exec=/appdata/DAudio2Doom
[DoomAppView]
Type=AppView
然后我运行了以下命令来复制我的DAudio2Doom编译后的二进制文件,将其标记为可执行文件,重启系统以便它可以读取新的.appconf文件,最后启动应用程序。
cp /run/media/B208-FF9A/DAudio2Doom /appdata
chmod +x /appdata/DAudio2Doom
reboot
appctl startAppView com.greenluigi1.doom.DoomAppView
在运行最后一个命令后,应用程序启动了,很快就变得明显它没有工作。
我期望看到Doom的第一帧,但屏幕上完全空白。
然后整个车载主机重新启动了……
在这个平台上调试有点困难,因为Helix启动应用程序的方式。所以我目前调试的方法是到处放置日志语句。
所以为了弄清楚出了什么问题,是时候记录一切了。
我创建了几个帮助日志的方法,然后从Doom的日志函数中调用它们
void DG_Log(const char* logMessage)
{
__android_log_print(ANDROID_LOG_DEBUG, "DAudio2Doom", logMessage);
}
int DG_Log_printf(const char *__restrict __format, ...)
{
int result;
va_list args;
va_start(args, __format);
result = __android_log_vprint(ANDROID_LOG_DEBUG, "DAudio2Doom", __format, args);
va_end(args);
return result;
}
int DG_Log_vprintf(const char *__restrict __format, va_list ap)
{
return __android_log_vprint(ANDROID_LOG_DEBUG, "DAudio2Doom", __format, ap);
}
再次运行后,我提取了日志,发现了几个问题,比如我忘了复制DOOM.WAD文件。我还发现当Doom遇到错误,比如找不到有效的WAD文件时,它会调用abort()函数。
应用程序中止后,车载主机的应用程序监视器发现应用程序崩溃了,并在几秒钟后重新启动了车载主机。
我禁用了abort()调用并复制了WAD文件。我更新了二进制文件,并启动了应用程序,结果发现又是一片空白。它仍然找不到WAD文件。我尝试了一些事情,比如更改参数(使用“-iwad”而不是“-file”),但什么都不起作用。所以,我更新了D_FindIWAD()函数,让它总是返回硬编码的路径“/appdata/DOOM.WAD”,这消除了错误。
但是空白屏幕仍然存在。我仍然错过了一些东西。
我猜测有些事情可能与帧缓冲区或我读取它的方式有关。我检查的第一件事是帧缓冲区的格式。
帧缓冲区格式
帧缓冲区只是形成图像的一堆颜色数据。在存储颜色信息时,您必须选择一个格式。该格式决定了每个像素占用的数据量、存储的颜色信息类型,以及颜色的存储顺序。
我当时使用的是QImage::Format_ARGB32,这意味着QT期望数据以以下格式存储:
– Alpha (Transparency): 1 byte
这使得每个像素共有4字节(或32位)的颜色数据。
但我甚至不确定这是否正确,当时只是猜测。
所以我看了看doomgeneric提供的示例,并看到了对SDL格式RGB888的引用。
texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGB888, SDL_TEXTUREACCESS_TARGET, DOOMGENERIC_RESX, DOOMGENERIC_RESY);
我更新了我的代码,使用QImage::Format_RGB888格式并运行它。
我尝试了几种其他的QImage::Formats,但似乎没有什么起作用。我还查看了Doom的代码,看起来每个像素应该是ARGB,但正如我之前看到的那样,那不起作用。
为了确定格式,我添加了一个“Dump”按钮,该按钮会将DG_ScreenBuffer的内容转储到我的闪存驱动器上。
再次加载应用程序后,我转储了帧缓冲区,并使用一个名为RAW pixels viewer的工具来查看数据。
经过调整参数,我最终得到了一个工作的图像。车载主机一直在运行Doom,只是没有正确显示!它还清楚地运行了游戏演示,这意味着游戏tick函数运行正确。
图像的格式是BGRA,忽略了alpha颜色通道。
如果我们不忽略Alpha通道,就是这样:
Doom不处理任何透明度数据,所以alpha通道总是设置为0。这意味着我的程序将其读为完全透明。这就是为什么我在某些格式上有空白屏幕。
它“正确”地显示了图像,只是它是看不见的。:|
有了这些信息,很明显我的代码有两个问题。一个是格式问题,另一个是为什么图像没有在屏幕上更新。
我将格式设置为QImage::RGB32,它应该只读取RGB并忽略Alpha通道。格式不是写成QImage::BGR32的原因是车载主机使用小端格式。实际上,这意味着通道存储和读取的顺序是相反的,即BGR而不是RGB,这正是我想要的。
接下来,我必须弄清楚为什么图像没有在屏幕上更新。经过几个小时的谷歌搜索和测试各种东西,我弄清楚了,除非QLabel的QPixmap本身更新,否则更新调用不会刷新QLabel的图像。然后我更改了函数,反复将QLabel的Pixmap设置为我早先制作的bufferQImage。
It is… Beautiful
在启动新的二进制文件后,我看到了Doom在汽车中运行的美丽景象。
The Fun Part – Inputs
我决定尽可能地使这个端口有趣,通过结合连接到车载主机的一些输入,而不是仅仅连接一个无聊的老式键盘。
正如我在以前的帖子中提到的,我在旧的固件更新中找到了大量的头文件,这些文件让我可以访问许多与车辆交互的API。不幸的是,这些头文件不再在最新的固件更新中提供。但幸运的是,现代汽车并没有真正更新任何有价值的东西,所以旧的头文件仍然有效。
(嘘。不要告诉现代汽车,但你仍然可以在最新的韩国版本的固件中找到这些文件,只需点击带有“DN8”文本的深蓝色按钮。更新甚至没有加密,所以你可以直接提取zip文件并直接获取system.img文件。请注意,更新是旧的,所以它们的工作效果可能会有所不同。)
我浏览了许多文件,并记录下一些可以在游戏中用作输入的文件。以下是最吸引我的一些:
HChassis::getSteeringAngle(); // Or IHChassisListener::onSteeringAngleChanged();
HChassis::getAcceleratorPedalState(); // Or IHChassisListener::onAcceleratorPedalStateChanged();
HBody::isTurnSignalSwitchOn(); // Or IHBodyListener::onTurnSignalStateChanged()
IHModeChangeListener::onKeyEvent();
HSeat::isSeatBeltBuckleLatched();
我最初尝试了IHChassisListener和IHBodyListener回调函数。不幸的是,我无法让它们工作,所以我直接检查了get()函数。
使用我早些时候制作的Dump按钮,我连接到每个函数并检查它们是否有效。
Function |
Result |
HChassis::getSteeringAngle() |
Received number between 0-6553.5 indicating steering wheel position. |
HChassis::getAcceleratorPedalState() |
Always returned 0. |
HBody::isTurnSignalSwitchOn() |
Received seemingly random values with no bearing on the turn signal switches. |
IHModeChangeListener::onKeyEvent() |
Received key numbers indicating what button on the head unit or steering wheel was pressed and a state value indicating if it was pressed, released, long pressed, or long released. |
HSeat::isSeatBeltBuckleLatched() |
Recevied a True/False value if the specified seat belt buckle was latched. |
Vehicle Input Location |
Vehicle Input |
Doom Input |
Steering Wheel |
Turning wheel Left |
Left Arrow Key |
Steering Wheel |
Turning wheel Right |
Right Arrow Key |
Steering Wheel |
Seek Down Key (Which is actually the up key) |
Up Arrow Key |
Steering Wheel |
Seek Up Key (Which is actually the down key) |
Down Arrow Key |
Steering Wheel |
Mute Button |
Use |
Steering Wheel |
End Call Button |
Fire |
Head Unit |
Volume Knob Press |
Enter |
Head Unit |
Tune Knob Press |
Escape |
然后我使用doomgeneric的X11示例代码中提供的键队列,将这些输入连接到Doom。
Can it run Doom?
Yes It Can!
就这样,我在汽车上有了Doom的运行安装。
它完美吗?不,首先没有音频,我必须承认输入不是最好的。我使用的某些键也被后台应用程序接收,如果你试图向前移动,它们会执行比如换歌的操作。只要没有媒体源在播放(通过按下音量旋钮)它就足够好用。
哦,当你转动方向盘时,轮胎确实会动,所以最好不要进行长时间的游戏,否则你会磨损轮胎。:p
但对于这个小演示来说,它确实很有趣!
原文始发于微信公众号(安全脉脉):HowIHackedMyCar 2021款 现代IONIQ (七)Nothing to it but to Doom it