Mai on Arm

2025–10–26

尝试在我唯一带触摸屏的 Windows 设备(Windows on Arm)上安装 maimaiDX,在折腾了整整两天之后成功启动,在这里简单写一下修复的过程和学到的知识。新手爱记录

从报错定位到代码

开始没有报错,但是无法启动,游戏提示:

请检查 amdaemon.exe 是否正常运行

但发现任务管理器中没有这个进程,于是在 start.bat 中查看 amdaemon.exe 启动代码。

start /min inject -d -k mai2hook.dll amdaemon.exe -f -c config_common.json config_server.json config_client.json

意为使用当前目录下的 inject.exemai2hook.dll 注入到新创建的 amdaemon.exe 进程中。本着从小到大排查问题的原则,打算尝试不注入的情况下,能否运行 amdaemon.exe ,于是运行

amdaemon.exe -f -c config_common.json config_server.json config_client.json

生成了报错文件 amdaemon.exe.log ,内容如下

<< 2025/10/26 00:29:18 >>
Runtime exception occurred.
File: D:\Jenkins\workspace\amdaemon_all_build\libs\libamw\src\amw_platform_manager.cpp
Line: 142
Function: void __cdecl am::platform::Manager::initialize(void)
Message: amPlatformGetPlatformInformation(). ErrCode 0.

在经过了 n 个小时的搜索和尝试,重装了 n 次 DX9 和 VC 运行库之后,放弃了从网友建议中找到解决方案的想法,打算直接分析二进制。

首先在 IDA 里搜索字符串 amPlatformGetPlatformInformation ,搜不到,扔进 HybridAnalysis 就能看到这个字符串,想到 Windows 下的编码一般是 UTF16 LE 的,所以在 010Editor 里指定 Unicode 搜索,然后在 IDA 中寻找,果然找到了。

amPlatformGetPlatformInformation 等函数伪代码

__int64 __fastcall sub_14020E6A0(__int64 a1)
{
  unsigned int v2; // eax
  unsigned int v3; // eax
  wchar_t *v5; // rax
  __int64 v6; // rax
  const wchar_t *v7; // rax
  char Destination[16]; // [rsp+30h] [rbp-E38h] BYREF
  int v9; // [rsp+40h] [rbp-E28h] BYREF

  *(_QWORD *)Destination = 0;
  if ( (unsigned int)amPlatformGetPlatformInformation(Destination) || !Destination[0] )
  {
    if ( dword_1407E77D4 >= 1 )
    {
      v5 = (wchar_t *)sub_1401F0E00((wchar_t *)L"amPlatformGetPlatformInformation(). ErrCode %d.");
      v6 = sub_1401DDE30(
             (int)&v9,
             4,
             (int)L"D:\\Jenkins\\workspace\\amdaemon_all_build\\libs\\libamw\\src\\amw_platform_manager.cpp",
             142,
             (wchar_t *)L"void __cdecl am::platform::Manager::initialize(void)",
             v5);
      v7 = (const wchar_t *)sub_1401DDF50(v6);
      sub_140283540("%S", v7);
    }
    *(_DWORD *)(a1 + 300) = 3;
    return sub_1401EC450(a1, 901);
  }
  else
  {
    *(_QWORD *)(a1 + 308) = *(_QWORD *)Destination;
    *(_BYTE *)(a1 + 484) = (unsigned int)sub_14028C160() != 0;
    v2 = sub_140227BA0(a1 + 320);
    if ( v2 )
    {
      *(_DWORD *)(a1 + 300) = 3;
      v3 = sub_140228040(a1 + 320, v2);
      return sub_1401EC430(a1, v3);
    }
    else
    {
      return sub_14020E940(a1);
    }
  }
}

__int64 __fastcall amPlatformGetPlatformInformation(char *Destination)
{
  __int64 n4; // rax
  bool v4; // zf
  int C_1; // eax
  char Source[3]; // [rsp+20h] [rbp-228h] BYREF
  char C; // [rsp+23h] [rbp-225h]
  WCHAR platform_id[256]; // [rsp+30h] [rbp-218h] BYREF

  nullsub_48(0);
  if ( !Destination )
    return 0xFFFFFFFELL;
  if ( (~((unsigned int)read_platform_id((__int64)platform_id, 512) >> 31) & 1) != 0 )
  {
    n4 = -1;
    do
      ++n4;
    while ( platform_id[n4] );
    if ( n4 != 4 || (int)WideCharToMultiByte(platform_id, Source, 5u) < 0 || !isgraph(C) )
      return 0xFFFFFFFDLL;
    v4 = isdigit(C) == 0;
    C_1 = C;
    if ( !v4 )
      C_1 = C - 48;
    *((_DWORD *)Destination + 1) = C_1;
    strncpy_s(Destination, 4u, Source, 3u);
  }
  else
  {
    *((_DWORD *)Destination + 1) = 0;
    strcpy_s(Destination, 4u, Block);
  }
  return 0;
}

__int64 __fastcall read_platform_id(__int64 a1, int a2)
{
  int v4; // eax
  unsigned int v6; // eax
  _DWORD v7[136]; // [rsp+30h] [rbp-238h] BYREF

  if ( a1 && a2 )
  {
    memset(v7, 0, sizeof(v7));
    v4 = sub_1402D14D0((__int64)v7, HKEY_LOCAL_MACHINE, L"System\\SEGA\\SystemProperty\\Static", 131097, 1);
    if ( v4 >= 0 )
    {
      sub_1402D17C0(v7, 5);
      v6 = sub_1402D14B0((__int64)v7, (__int64)L"PlatformId", a1, a2);
      if ( (~(v6 >> 31) & 1) != 0 )
      {
        sub_1402D0EE0(v7);
        return 0;
      }
      else
      {
        if ( dword_140936BCC >= 1 )
          sub_1402982E0(L"amsSystemPropertyGetPlatformId", 101, L"Error: get. ErrCode %08x.\n", v6);
        sub_1402D0EE0(v7);
        return 4294967287LL;
      }
    }
    else
    {
      if ( dword_140936BCC >= 1 )
        sub_1402982E0(
          L"amsSystemPropertyStaticInit",
          449,
          L"Error: system property open. ErrCode %08x.\n",
          (unsigned int)v4);
      return 0xFFFFFFFFLL;
    }
  }
  else
  {
    if ( dword_140936BCC >= 1 )
      sub_1402982E0(L"amsSystemPropertyGetPlatformId", 84, L"Error: invalid arg (null pointer).\n");
    return 0xFFFFFFFDLL;
  }
}

逻辑是从注册表 HKEY_LOCAL_MACHINE\System\SEGA\SystemProperty\StaticPlatformId 中读取字符串,必须是 4 个字符,我查看了系统的注册表,确实没有这个键,于是手动创建文件 .reg

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\System\SEGA\SystemProperty\Static]
"PlatformId"="SDEZ"

再次运行,确实没有这个报错了,但是产生了新的 7 个报错(挠头)。难道说注入 dll 的作用之一就是处理注册表读取?在检索 segatools 仓库之后,果然找到了处理注册表部分的源码

说明这条路走错了,报错的原因是没注入成功,所以应该关注注入的代码。注入程序启动代码为:

inject -d -k mai2hook.dll amdaemon.exe -f -c config_common.json config_server.json config_client.json

报错:

mai2hook.dll: DLL failed to load inside target process

这个报错的方便查找原因了,因为有源代码,将 segatools 克隆到本地,在其子模块 capnhook 中找到报错文本,报错原因是在远程(即 amdaemon.exe)线程执行 LoadLibraryA("mai2hook.dll") 的时候发生了错误。

这个错误该如何查找呢?如果再远程执行一次 GetLastError,那时候 Error 早就被覆盖了。观察到 mai2hook源码 中进行了 dprintfOutputDebugMessageA 操作,所以可以用 DebugView 来查看 Debug 日志。但是源代码并没有打印太多日志,在尝试打印各种变量,跟踪执行流的时候,发现了端倪。

执行流程大致如下:

inject.exe
+-- main()
    +-- CreateProcessA(..., CREATE_SUSPENDED)
    +-- inject_dll()
        +-- CreateRemoteThread(..., LoadLibraryA, ...)
            +-- LoadLibraryA("mai2hook.dll")


amdaemon.exe
+-- mai2hook.dll
    +-- DllMain()
        +-- process_hijack_startup()
            +-- Thread32First()
            +-- Thread32Next() // 遍历当前进程的线程
            +-- process_hijack_try_thread(thread)
                +-- GetThreadContext(thread, &ctx)
                +-- GetProcAddress(ntdll, "RtlUserThreadStart")
                +-- thread_match_startup(&ctx, ntstart)
                    +-- [ctx->Rip == ntstart]
                        +-- patch thread

RtlUserThreadStart所有线程的起点,寻找处于起点的线程,dll 加载失败的原因是找不到任何满足条件的线程。

打印所有线程的 pc,对比 C:\Windows\System32\ntdll.dll (使用 procmon 监控进程信息,能看到程序读取了这个),发现 ctx->Ripntstart 都指向函数 RtlUserThreadStart ,只不过前者指向了带下划线的函数,后者指向不带下划线的。

同一个函数,怎么会指向两个不同的地方呢?

在 Windows on Arm 下,系统二进制均被编译为 Arm64X PE 文件,也就是同一个文件,既可以被 x64 应用调用,也可以 arm64 应用调用,原理暂未深究。当模拟的 x64 程序调用 GetProcAddress 时,获取到的是一个跳板函数:

+0x0  488bc4          mov     rax, rsp
+0x3  48895820        mov     [rax+0x20], rbx
+0x7  55              push    rbp
+0x8  5d              pop     rbp
+0x9  e9xxxxxxxx      jmp     rel32 ; +0xa = displacement */

这里的 rel32 偏移指向 _RtlUserThreadStart

mai-on-arm-Arm64X

那解决方法就很简单了,将 ntstart 替换成跳转的目标地址,重新编译和覆盖 mai2hook.dll

Patch

commit

2026–08–11 更新

PR Merge 了!

碎碎念

上一次是折腾 WoA in Xiaomi Pad5,这一次是 Mai on WoA,和 Windows on Arm 之间有一些缘分。