常见问题怎么查
下面是以前真实查过的问题,每条写现象、怎么查、查出来什么、结果。新问题多半能在这里找到相似的查法。卡顿、掉帧另见卡顿、掉帧怎么查。
常用的取证手段
- 线程在干什么:
top -H -p <pid>看各线程的 CPU;/proc/<pid>/task/<tid>/stat、wchan看线程状态和停在哪个内核函数;/proc/<pid>/status里的TracerPid看有没有被别人 ptrace;/proc/<pid>/syscall看卡在哪个系统调用、参数是什么。
- 线程栈:
shell su 0 debuggerd -b <pid>,不会杀进程。 - 采样和跟踪:
simpleperf record -g -t <线程 id>,也能跟指定的系统调用;- 内核 tracefs(
/sys/kernel/tracing)跟信号; - uprobe / uretprobe 看某个库函数的返回值。
- 网络:
- 模拟器控制台
emu network capture start <文件>/stop抓客体全部流量; ss -tni看每条连接的接收积压(Recv-Q)。
- 模拟器控制台
- 磁盘:
/proc/diskstats;/proc/<pid>/io的wchar、syscw;宿主上 qemu 进程的读写次数。 - 宿主:Process Explorer 看 qemu 各线程的 CPU;模拟器崩溃时拿崩溃转储,配上对应版本的符号看栈。
- 对带保护的游戏,优先被动读
/proc,不要随手 ptrace 游戏进程。
卡住、冻住
启动时好时坏,坏的时候约 20 秒后 ANR(阴阳师)
- 怎么查:ANR 时整机 CPU 只有 4.6%,主线程不是忙,是被停住了。被动读
/proc:主线程状态是t(被跟踪停住),wchan是ptrace_stop,TracerPid不是 0。顺着找到停住它的是游戏的一个子进程:WebView 的崩溃处理程序,命令行带--trace-parent-with-exception,说明它认定父进程崩了。 - 查出来:执行桥靠一次故意的段错误进入。WebView 的崩溃处理是原生代码,它装的信号处理函数抢到了最前面,把执行桥的那次段错误当成崩溃,于是去 ptrace 父进程,挂在那里。会不会碰上全看时序,所以时好时坏。
- 结果:改用 native bridge 接口本来就有的「第一处理权」(
getSignalHandler),它排在所有sigaction处理函数之前,别人装得再晚也插不到前面。
卡在「正在获取音频列表...」不动(阴阳师,约五次冷启动有一次)
- 怎么查:游戏 139 个线程全在睡,抓包 60 秒只有一个包,不是网络。主逻辑线程在读管道,等自己 fork 出来的子进程。子进程停在 futex 上,锁的地址落在转译层的内部数据里;同一把锁在父进程里是空闲的,在子进程那份里是锁着的。
- 查出来:转译层不是 fork 安全的。fork 那一瞬间,这把锁正被父进程的别的线程拿着,子进程里永远没人放。这是经典的多线程 fork 死锁。
- 结果:在伴生库里用
pthread_atfork把这几把锁带过 fork。修后连续 15 次冷启动 0 次卡住。
跑二十分钟左右画面冻住(星铁)
- 怎么查:
debuggerd -b看到渲染线程在dequeueBuffer一直等空闲缓冲;logcat 里有Too many open files。游戏进程的 fd 数随帧数线性增长,每帧一个,约 3.2 万帧用尽,时间和冻住对得上。 - 查出来:镜像自带的客体 Vulkan 驱动每帧漏一个 fd。fd 用尽后,回送缓冲的回调在应用这边解包失败,缓冲再也不归还。
- 结果:开机后设属性
debug.angle.feature_overrides_disabled=supportsSwapchainMaintenance1规避。不用改模拟器。
加载时偶发卡死(终末地,修下面那个崩溃时第一版补丁引入的)
- 怎么查:
debuggerd -b看到渲染线程在等一把锁,几个别的线程在等宿主回包,其中一个正拿着那把锁。 - 查出来:开了
VulkanQueueSubmitWithCommands时,宿主按全局序号逐条执行。渲染线程先领了序号再去等锁,拿锁的线程又在等一个序号更靠后的回包,宿主得先执行渲染线程那条,就互相等住了。 - 结果:规则是拿着锁不能跨宿主往返。第二版补丁只在锁里查表,出了锁再发给宿主。
闪退、崩溃
一进游戏 1 秒必崩(阴阳师,执行桥就是为它做的)
- 查出来:游戏的保护库把原生 x86 代码里的一些 GOT 槽改成跳进转译后的 arm64 代码。真机上这些都是 arm64,没问题;转译层只支持转译代码调原生,不支持反过来,所以必崩。
- 结果:执行桥用转译层自己导出的接口,补上「原生调转译」这条路。
- 要钩转译层自己的 import 表(实际起作用的是它的
mprotect),因为 arm64 应用的库加载走转译层自己的加载器,不经过libnativeloader; - 全程不改游戏和它的保护库。
- 要钩转译层自己的 import 表(实际起作用的是它的
不定时闪退,tombstone 是 SIGSEGV、pc 为 0、线程名每次不同(终末地)
- 怎么查:这是二次现场。用 tracefs 跟信号,链条是:游戏自己的断言 →
SIGTRAP→ 崩溃上报 SDK → 转译层 → 上报 SDK 在宿主线程上页错 → 转成 SIGSEGV。也顺带确认了反作弊没发信号。断言内容是ret == VK_SUCCESS,再用 uretprobe 抓到vkBindImageMemory返回了 -2。 - 查出来:镜像自带的客体 Vulkan 驱动里,
BindImageMemory不加锁就查句柄表。别的线程恰好让表扩容时会查漏,返回 -2。这是数据竞争,上游代码也是同一段。 - 结果:给客体驱动打「先加锁再查表」的补丁。冷启动 10 次断言 0 次,长跑 3 小时没有新 tombstone。
模拟器自己崩溃(0.36 发版回归时)
- 怎么查:升级测试里实例重启、adb 重连时 qemu 崩了。拿崩溃转储,配上那一版的符号看栈,再反汇编,定位到一条从空指针附近读数据的指令。
- 查出来:注销网络监听时,没先判断监听节点在不在。
- 结果:先判空再注销。修好以后整套回归重跑。
画面
闪屏、截图偶发全黑(NVIDIA 显卡,2%–5% 的画面)
- 怎么查:在窗口、共享内存、adb 截图、录屏四条路径上都做探针,逐帧数全黑帧,确认几条路径都受影响,问题在宿主读回这一步。
- 查出来:gfxstream 在 NVIDIA 驱动要求「专属分配」时没给专属分配,这部分画面缓冲读回来全是 0。
- 结果:修好以后各条路径全黑帧都是 0,已提交上游。
网络、下载、数据
提示「网络请求超时」,其实网络是好的(星铁)
- 怎么查:在游戏数据目录里找 0 字节的文件;用模拟器控制台抓包,确认热更新的请求和下载都正常。
- 查出来:之前一次关机没先 sync,刚写完的热更新清单变成了 0 字节。游戏解析空文件抛异常,界面上却显示成网络错误。
- 结果:游戏停着时,用同目录下
_cache的内容写回正式文件;以后关机一律先 sync。
下载很慢,不到 8 MB/s(明日方舟)
- 怎么查:
- 分三层比:宿主直连 CDN 约 100 MB/s,模拟器网卡单连接 80 多 MB/s,游戏实际只有 5.9 MB/s。
ss看到每条连接的接收积压有几 MB,说明数据已经到了客体,是游戏读得慢。/proc/<pid>/io和/proc/diskstats:写到磁盘的量是下载量的约 8 倍,都是小块写。下载线程停在jbd2_log_wait_commit,也就是边下边 fsync。- qemu 没有一条线程跑满。
- 查出来:瓶颈是数据盘每个写请求的往返成本。每次客体写在宿主上变成约 4 次写。游戏这种写法在真机闪存上没问题,到了模拟器就只剩 6 MB/s。
- 结果:还没修完。方向是降低每次写的往返成本。这类改动动的是磁盘 IO 路径,先要跑硬杀回归。
硬杀以后文件变成 0 字节或退回旧版
- 怎么查:几种写文件的方式(先删后改名、改名覆盖、直接覆盖写),在不同回写参数下写完就杀,统计多久内被杀会丢。
- 结果:就是「关机先 sync」这条规矩的来源。模拟器开机时还把客体回写周期调短,缩短丢数据的窗口,但消除不了。
系统层面
客体热重启以后卡死(WHPX)
- 怎么查:客体内核日志里有 soft lockup,一个虚拟 CPU 不响应核间中断,别的核卡在等它。
- 结果:所有需要重启客体的地方都改成冷启动(关机再开),不用
adb reboot。
内存占用比别的模拟器高
- 怎么查:分三块量,再和别的模拟器同条件对比:客体实际在用多少,宿主上的客体内存块多大,模拟器自身另占多少(约 0.65–0.75 GB)。
- 结果:
- 打开内存气球的空闲页回报,客体里释放的内存还给 Windows;
- 图形暂存缓冲改成按需增长,每台实例静置时少占约 140 MB。
冷启动慢,加核、提频都不见效(终末地)
- 怎么查:
- 先看宿主电源计划:「最大处理器状态」如果是 99%,睿频就是关着的。
- 然后把几种钉核方式交替各跑几轮,看组间差别是不是大过组内波动。
- 查出来:钉哪组核都分不出差别。睿频打开后主频高了约 20%,冷启动只快约 2%。主线程看着忙,其实在等(访存、转译缓存或别的等待),不是算力不够。
- 结果:钉核不用改;下一步要查主线程在等什么。
几条经验
- 先拿现场,再下结论。 以前把 fork 死锁当成网络慢,把硬杀弄坏的文件当成 CDN 连不上,把混着菜单的帧率当成战斗帧率,都是没取证就猜。
- 对比要交替多轮跑,看组间差别是不是大过组内波动,只跑一次比一次不算数。
- 问题在客体镜像自带的组件里时(客体 Vulkan 驱动、ANGLE),先找属性或客体侧的规避,不一定要改模拟器。
- 改到磁盘 IO 或关机流程的,先跑硬杀回归。