本文共 10018 字,大约阅读时间需要 33 分钟。
我们的这次实验的目标是:能在系统挂起唤醒中玩出一个白屏。为啥不搞个panic出来?panic出现和修好都太容易,不好玩。显示异常才好玩呢,没日志只有现象,hiahiahia~
好了知己知彼才能百战百胜,先看看系统挂起的简单流程。
系统挂起流程主要分为两个部分:挂起和唤醒。挂起的入口在suspend_enter
,基本流程下面这样的:
上图大概是大家能见到系统挂起流程图中最简单的一个了吧。然而在非arm、x86这种使用广泛的平台,这其中看似简单的每一步都可能暗藏杀机。不管是系统挂起还是系统休眠,信号一定是上层传进来的,假如在某个场景硬件无缘无故自己睡下去了,虽然看上去和内核很相关但锅真的不是内核的。收到上层传进来的系统睡眠信号之后,首先是冻结用户进程同步文件系统,为了避免数据不一致问题。之后的冻结设备、保存cpu现场、非boot cpu下电等等,每一步都有可能出错,非常刺激。可能某个显卡、网卡、输入设备睡眠做的有问题设备没法冻结起来,可能是cpu现场保存的位置溢出了或者数组越界了,可能是某个外设不能下电导致cpu不能断电,因为cpu在等设备;可能固件初始化完成后没法跳到内核里面来,cpu现场恢复的有问题,甚至可能进程恢复之后都访问零地址,内核把他们全都杀死了。每一个报错,都是一个加深理解内核的机会,多好玩。好了回到本文重点,想要做一个白屏出来,最直接的肯定是在驱动的唤醒流程里改动,因为cpu和平台相关的唤醒并没有显示参与,而且动唤醒架构代码容易起不来那就没办法演示啦。下面大概展开讲讲驱动睡眠或者唤醒流程,这两个流程是对称的,搞清楚一个另一个自然明白。
对内核熟悉的童鞋们肯定清楚,从核心代码执行到驱动代码的一般都要通过好几个钩子函数。正常分析,就是从入口一点点查找,然后慢慢往下分析。今天,我们就让驱动说话,告诉我们它是怎么被调用起来的。得到调用链之后,就像拿到了一篇文章的纲领,后面再填充内容就简单很多啦。首先选择一个熟悉的驱动,找到这个驱动的挂起函数。挂起函数在内核中的接口叫suspend,查找这个关键字就即可找到啦。
$ grep --color "suspend =" drivers/gpu/drm/loongson/ -rndrivers/gpu/drm/loongson/loongson_drv.c:1013: .suspend = loongson_pmops_suspend,$ git diff drivers/gpu/drm/loongson/loongson_drv.cdiff --git a/drivers/gpu/drm/loongson/loongson_drv.c b/drivers/gpu/drm/loongson/loongson_drv.cindex 5629b2d7d4ff..82fa3aa5dd80 100644--- a/drivers/gpu/drm/loongson/loongson_drv.c+++ b/drivers/gpu/drm/loongson/loongson_drv.c@@ -941,6 +941,7 @@ static int loongson_pmops_suspend(struct device *dev) struct pci_dev *pdev = to_pci_dev(dev); struct drm_device *drm_dev = pci_get_drvdata(pdev); + dump_stack(); return loongson_drm_suspend(drm_dev); }
示例中,suspend钩子上挂的是loongson_pmops_suspend
。在函数中加上栈打印,编译运行就能得到一个这样的函数栈。在更换原有的内核之前有一个小的tips,一定保证机器上有俩内核,因为每一个对内核的改动都可能会起不来。
[ 38.527113] CPU: 1 PID: 3177 Comm: kworker/u8:12 Tainted: G W 4.19.0-loongson-shiwen #1672[ 38.527117] Hardware name: HT706 TR4191/B20-3a40, BIOS V4.0 12/14/2020[ 38.527131] Workqueue: events_unbound async_run_entry_fn[ 38.527134] Stack : 0000000000000000 0000000000000001 0000000000000000 0000000000000001[ 38.527139] 0000000000000000 0000000000000000 0000000000000001 0000000000000040[ 38.527142] 0000000000000000 0000000000000000 0000000000000001 746e657665203a65[ 38.527146] ffffffff80209234 ffffffff80209224 ffffffff81460000 0000000000000000[ 38.527149] 0000000000000000 ffffffff812d0000 0000000000000000 0000000000000002[ 38.527153] 980000025c05e0f8 0000000000000000 ffffffff81264730 ffffffff812d0000[ 38.527156] 000000000000000c 9800000254e97950 0000000000006000 980000025d72c000[ 38.527160] 9800000254e94000 9800000254e97b70 980000025d9b0d80 ffffffff80e6ea24[ 38.527163] 980000025c014600 980000025c01c000 0000000000000000 0000000000000002[ 38.527167] 980000025c05e0f8 ffffffff80218f24 0000000000000001 ffffffff80e6ea24[ 38.527170] ...[ 38.527174] Call Trace:[ 38.527183] [] show_stack+0x94/0x140[ 38.527192] [ ] dump_stack+0x94/0xd0[ 38.527209] [ ] loongson_pmops_suspend+0x1c/0x38 [loongson][ 38.527219] [ ] pci_pm_suspend+0x7c/0x188[ 38.527227] [ ] dpm_run_callback.isra.5+0x20/0x70[ 38.527231] [ ] __device_suspend+0x16c/0x3a0[ 38.527235] [ ] async_suspend+0x2c/0xd8[ 38.527238] [ ] async_run_entry_fn+0x50/0x128[ 38.527243] [ ] process_one_work+0x23c/0x440[ 38.527246] [ ] worker_thread+0x164/0x5d8[ 38.527250] [ ] kthread+0x128/0x130[ 38.527254] [ ] ret_from_kernel_thread+0x14/0x1c
首先一眼看去,__device_suspend
在这个函数长得就很像驱动挂起的入口。加上打印,就可能看到其他驱动的挂起函数和他们被调起来的逻辑啦。另外栈底压得函数能看出来和kthread worker有关,说明驱动挂起内核使用工作队列这样的方式调起来的。工作队列在驱动这边用的很常见,主要分为队列初始化和队列调用两部分,工作队列的初始化一般是放在驱动的init或者probe函数,调用是在任何一个需要的位置。虽说在执行顺序是并发不可控的,但从调用的位置还是能找到点东西。工作队列的调度要求把队列所完成的功能函数当参数传递进去,从查找这个功能函数名方式接着往上查,直到能查到系统挂起的最开始入口。
__device_suspend
函数主要做很多安全检查,然后是设备掉电前的准备,最终挨个扫描调用驱动提前注册好的suspend函数。 if (dev->pm_domain) { info = "power domain "; callback = pm_op(&dev->pm_domain->ops, state); goto Run; } if (dev->type && dev->type->pm) { info = "type "; callback = pm_op(dev->type->pm, state); goto Run; } if (dev->class && dev->class->pm) { info = "class "; callback = pm_op(dev->class->pm, state); goto Run; } if (dev->bus) { if (dev->bus->pm) { info = "bus "; callback = pm_op(dev->bus->pm, state); } else if (dev->bus->suspend) { pm_dev_dbg(dev, state, "legacy bus "); error = legacy_suspend(dev, state, dev->bus->suspend, "legacy bus "); goto End; } } Run: if (!callback && dev->driver && dev->driver->pm) { info = "driver "; callback = pm_op(dev->driver->pm, state); } error = dpm_run_callback(callback, dev, state, info);
Run就是挨个调用驱动中的suspend了,看着遍历的代码大概能看出来dev是按照树或者链表的形式管理的。OK,先挨个把suspend函数打出来看看遍历的先后顺序是个什么顺序。
diff --git a/drivers/base/power/main.c b/drivers/base/power/main.cindex 4abd7c6531d9..f6151ab60b25 100644--- a/drivers/base/power/main.c+++ b/drivers/base/power/main.c@@ -1793,6 +1793,7 @@ static int __device_suspend(struct device *dev, pm_message_t state, bool async) callback = pm_op(dev->driver->pm, state); } + printk("SHIWEN MESSAGe: in %s, callback=%#x\n", __func__, callback); error = dpm_run_callback(callback, dev, state, info);
实验机器上得到的地址在system.map里面对比即可得到函数名称。遍历的先后顺序分别是:
好了,花费不到10分钟拿到了驱动挂起的整个流程,还绝对正确,为自己鼓掌!上述函数的调用也都不是线性的,是穿插多次调用的,因为像scsi、platform、pci每扫一个bus,就需要对每个扫到的device调用一下对应的suspend。
接下来就是修改代码得到一个白屏啦。无任何理论基础前提,想到两个办法:一是提前点亮屏幕或者延迟显示数据的准备;二是自己写一个白屏进去。不管采用那种,看起来都需要在显卡驱动里面做点事,先看看显卡驱动的resume流程咯。lspci查看显卡类型,这次实验是在龙芯机上做的,显卡是龙芯集显。看看龙芯集显的resume流程,然后再看看是不是能加点东西。不停的grep看钩子函数挂的是哪个,或者直接在驱动里找带resume的函数。很快就找到了龙芯显卡resume入口——loongson_drm_resume
int loongson_drm_resume(struct drm_device *dev){ u32 r; u64 gpu_addr; struct loongson_bo *lbo; struct drm_framebuffer *drm_fb; struct loongson_framebuffer *lfb; struct loongson_device *ldev = dev->dev_private; if (dev->switch_power_state == DRM_SWITCH_POWER_OFF) return 0; console_lock(); mutex_lock(&dev->mode_config.fb_lock); drm_for_each_fb (drm_fb, dev) { lfb = to_loongson_framebuffer(drm_fb); lbo = gem_to_loongson_bo(lfb->obj); r = loongson_bo_reserve(lbo, false); if (unlikely(r)) continue; loongson_bo_pin(lbo, TTM_PL_FLAG_VRAM, &gpu_addr); loongson_bo_unreserve(lbo); } mutex_unlock(&dev->mode_config.fb_lock); loongson_encoder_resume(ldev); drm_helper_resume_force_mode(dev); drm_kms_helper_poll_enable(dev); loongson_fbdev_set_suspend(ldev, 0); loongson_connector_resume(ldev); console_unlock(); return 0;}
gem和bo是显示缓冲区相关的,根据传入的dev,先获取到显示的framebuffer、gem和bo这些显示缓冲区相关变量。之后唤醒encoder,encoder是显示器解码器,把显存中的像素点解码成显示器需要的信号。随之设置显示mode相关参数,使能输出轮询唤醒fbdev,最后是唤醒显示器,backlight设备的唤醒是放在唤醒显示器里。先试试看把显示器的的唤醒放在前面看看。
diff --git a/drivers/gpu/drm/loongson/loongson_drv.c b/drivers/gpu/drm/loongson/loongson_drv.cindex 5629b2d7d4ff..cb77c482a473 100644--- a/drivers/gpu/drm/loongson/loongson_drv.c+++ b/drivers/gpu/drm/loongson/loongson_drv.c@@ -905,6 +905,7 @@ int loongson_drm_resume(struct drm_device *device console_lock(); + loongson_connector_resume(ldev); mutex_lock(&dev->mode_config.fb_lock); drm_for_each_fb (drm_fb, dev) {
实验结果证明,没有任何变化。不应该啊,难道是没有调用?直接把connector的resume注释掉试试看?
嗯,没有任何变化。没道理啊,难道是没有调用?或者真正的点亮并不是内核调用的?找到驱动设置bl的位置打印试试看。[ 3286.496606] CPU: 2 PID: 2246 Comm: backlight_helpe Tainted: G W 4.19.0-loongson-3-desktop-shiwen #1663[ 3286.496611] Hardware name: HT706 TR4191/B20-3a40, BIOS V4.0 12/14/2020[ 3286.496615] Stack : 0000000000000000 0000000000000001 0000000000000000 0000000000000001[ 3286.496623] 0000000000000000 0000000000000000 0000000000000001 0000000000000000[ 3286.496628] 0000000000000448 3230322f34312f32 0000000000000030 ffffffff81260000[ 3286.496633] 0000000000000001 ffffffff8144ba15 ffffffffffffffff 3134525420363037[ 3286.496637] ffff000000000000 ffffffff812d0000 0000000000000000 98000002557fd018[ 3286.496642] 98000002572ebe60 00000001202fedf8 000000c00003e8c8 00000001204e0000[ 3286.496647] 0000000000000001 0000000000000000 0000000000006000 9800000255e28000[ 3286.496652] 98000002572e8000 98000002572ebb70 000000c000001680 ffffffff80e6ea24[ 3286.496657] 0000000000000000 0000000000000000 0000000000000000 98000002557fd018[ 3286.496661] 98000002572ebe60 ffffffff80218f24 0000000000000001 ffffffff80e6ea24[ 3286.496666] ...[ 3286.496671] Call Trace:[ 3286.496685] [] show_stack+0x94/0x140[ 3286.496697] [ ] dump_stack+0x94/0xd0[ 3286.496718] [ ] loongson_connector_pwm_set+0x54/0xf8 [loongson][ 3286.496728] [ ] loongson_connector_backlight_update+0x44/0xa8 [loongson][ 3286.496741] [ ] backlight_device_set_brightness+0x74/0xc8[ 3286.496746] [ ] brightness_store+0x40/0x58[ 3286.496755] [ ] kernfs_fop_write+0xd0/0x1f0[ 3286.496762] [ ] __vfs_write+0x28/0x170[ 3286.496767] [ ] vfs_write+0xb4/0x1e8[ 3286.496771] [ ] ksys_write+0x60/0x100[ 3286.496778] [ ] syscall_common+0x34/0x58
从syscall调下来的,那点亮屏幕肯定是上层干的,单单改内核像做到延迟点亮屏幕看来是走不通了。那就试试看延迟显卡的resume,不让console挂起,倒是延迟了显卡数据的准备,显示的界面是挂起之前的console而且一闪而过,并不是想象中白屏之类的明显异常界面,是不是可以在其他驱动里面加延时呢?
根据dmesg能明显的看出来在drm设备之前被唤醒的是usb、wifi和sata等,wifi和sata最好不要动,数据量太大了,可能还没搞出来白屏系统就崩溃了。那最好欺负的就是你了——usb驱动,试试看在usb唤醒函数里加延迟试试看。 嗯,现在到时能看到一个明显的显示残留了,但是看起来还是不够严重。通过观察发现,显示残留看到的是系统挂起前最后显示的内容。也就是说,显存里的东西是suspend保存进去的。如果想要看上去更严重一点,我们需要把resume之后把显存清掉,或者是显卡suspend时候不要保存显存里的东西。最简单暴力的方式是,显卡不要suspend了,对于设备来说是直接断电没有任何保存的过程。 好了,这下看上去严重多了,这勉勉强强算是做出了一个概率白屏了。目前白屏的实现是把显卡suspend去掉,迫使显卡resume之后恢复出来的显存是未知数据,从而导致的显示异常。至于什么样子的异常,这点不可控制。可以尝试通过修改fb控制异常显示的内容,做一个闪烁显示出来?或者修改色深,做一个炫彩异常?尝试控制光标等等,都是很有趣。
最后献上一首,挂起调试之歌,请笑纳。 休眠调试一定要仔细,内核日志太少不要急,
拿到日志后慢慢看。
每一行的报错不要遗。
通用驱动先看设备的问题,
然后再和上游代码比一比。
上游能用回头来看自己。
碰到内存错误不要慌,
数据溢出越界先查起。
成百上千测试跑不过,
稳定测试日志要整齐。
如果显示异常调试方法,
请一定要介绍给我。
谢谢大家,欢迎补充。
转载地址:http://rsbsi.baihongyu.com/