学习BIOS近三个月了,遇到了不少难缠的问题,特别是最近这个,和操作系统、硬件都有很大关系。但是解bug的过程中学到了不少东西,特别是像windbg,IDA这些工具,以前一直没有机会用,现在都用上了。
问题是这样的,测试那边测试主板时发现使用某些CPU时,在BIOS下开启了过热关机的选项,发现在WinXP下,CPU温度超过了设定的关机温度时,操作系统不能正常关机,而在Win2k下则可以正常关机。导师(当然就是带我入门的前辈啦)让我解这个问题。本来以为问题很简单的,现实查看SuperIO的datasheet,因为系统和CPU的温度都由SuperIO控制器里的Hardware Mornitor或者environment Manager来控制,当然SuperIO芯片集成了很多功能,特别是有些人可能很感兴趣的键盘控制器。但是看了几遍,没有发现这里有什么问题,又用RU.exe比较了正常系统和异常系统下的SuperIO寄存器值,也没发现什么不对劲的地方。试来试去也试不出个所以然来。
想起一个多月前学的ACPI规范,好像里面也有讲有温度监测。于是翻出来看,果然里面说有,大意就是当有Hardware Mornitor会监测CPU温度,当超过某个设定值时,就会向CPU申请SCI(System Control Interrupt)中断(与之对应的是SMI中断,SMI中断会促使CPU进入SMM模式,据说在这个模式下执行的代码拥有比操作系统更高的权限,比ring0还高,不是不是真的,请高手指教),操作系统接到这个中断请求之后,会去执行DSDT里面的_TMP对象来获取CPU温度Tt,再拿这个温度与_CRT对象返回的值Tc(Critical Temperature)相比较, 如果Tt>Tc,操作系统马上就会关机。
这下子总算有点明白了,我第一感觉就是读错了(其实没有仔细考虑),于是又继续苦读ACPI Spec(干这行就是有成山的Spec看不完,这一本就有800多页)。找到了个方法,就是把温度数值抛到 IOport 0x80,然后再用Debug卡去读抛出来的数值。把Dump出来的DSDT修改一下,然后再用Asl.exe导入到注册表里(在这里又要嗦一下,据说有一种rootkit,叫做ACPI rootkit,不知道是不是用ASL语言实现的,但是我用试着在DSDT里写一些代码访问了一下一些敏感部位的内存,发现操作系统禁止了。不知道是为什么)。完了之后重启电脑,操作系统就会去读取注册表里面的DSDT,而不会去读取BIOS里的DSDT了。完了之后发现一个很怪异的情况,就是在操作系统初始化的时候(就是在跑滚动调的时候,不知是不是这么理解),有数据是不断的抛出来的(虽然是一样的数据,Debug卡上的LED会闪烁),等到看到桌面以后,系统停止抛出数据了,用SE.exe手工设置寄存器的值,让CPU温度高于预设的关机温度(当然是假的了),这时候按照datasheet说的,superio控制器会产生SCI中断,操作系统就会去执行_TMP对象,然后就会抛出温度数据(我写的代码就在里面)。这不得不让人抓狂。还是不死心,觉得肯定是某个地方读数据读错了。到网上搜了一下关于ACPI的资料,发现Windbg提供了AMLI工具,专门用来调试ACPI代码的。于是奋斗了一天下午,终于把Windbg双机调试模式弄好了(期间碰到的最大问题是Windbg老是提示没有找到symbol,我明明千辛万苦的下载了最新的symbol,设置好了路径,就是说没有找到。后来设置成然他到symbol服务器上自己去找,它终于很开心的工作起来了)
Ctrl+Break马上让windbg把系统断下来,!amli find _TMP找到_TMP对象的路径\_TZ.THRM._TMP,然后!amli bp \_TZ.THRM._TMP下个断点,然后!amli debugger启动ACPI.sys里内置的调试器。然后g,没有断下来,系统欢快的运行了起来。重启,在系统初始化的时候Ctrl+Break,然后如法炮制下了个断点,等了一会儿,终于断下来了,然后单步了几下,终于发现所有函数都执行得很正常。终于死心了。
没有办法,暂时没有了思路,还是看Spec,ACPI Spec又埋头继续看,终于又有了发现,原来SCI中断是由很多器件共享的(也就是很多器件都会触发SCI中断),器件如果需要触发这个中断,就需要连一根线到南桥的GPEx_STS寄存器的某一位上,GPEx_TST寄存器一共有两组共128位,表示系统接受128中不同的SCI中断。外设需要申请SCI中断的时候,就去置位GPEx_STS寄存器中相应的位,然而,这个中断能不能够传到CPU,还要由GPEx_EN寄存器来决定,GPEx_EN寄存器中相应的位值1,中断信号才能传到CPU。于是猜想是不是GPEx_EN被清零了呢,用SE.exe一看,果然被清零了。这时候更加让人觉得迷惑了,为什么系统初始化的过程中,ACPI中的事件处理程序代码能够被处理呢(这说明GPEx_EN绝对是没有被清零的)。于是在操作系统还没有开始显示滚动条的时候,断下来。Ib 42c(GPEx_EN寄存器组分配在IO空间),果然发现GPEx_EN寄存器组不是零。输入g回车,系统又继续向前行,进入桌面之后,再次读GPEx_EN寄存器,发现已经被清零了(当然这时候外设发出什么SCI中断,系统都是不闻不问了)。换了几个CPU,发现系统正常运行时,GPEx_EN寄存器不是全为零的,而且始终如此。
没有办法,现在就是特别想看看源码,看看系统在开机的过程中究竟做了些什么。找了NT4的源码,发现NT4没有实现ACPI,又看了WIN2K的源码(其实只是部分),发现泄露出来那部分里面没有ACPI子系统。WRK就更加没有了。听说ReactOS完全仿照windows,于是去下载了ReactOS的源码,发现ACPI部分的代码怎么就那么向linux内核的源码呢?(以前曾经看过N本linux内核分析,不过都是水过鸭背,一点不留,不过代码的形状却是记得的)
不如看看linux下面是怎么实现的。看linux内核源码,网上推荐说用sourceInsight,下下来用了一下,发现真的相当不错。看了linux里面的SCI中断处理例程acpi_ev_sci_xrupt_handler发现里面的处理流程非常简单,就是看看传进来的事件是fixed event还是general perpose event,如果是前者,就直接执行处程序,如果是后者,就直接把任务查到任务队列里,由内核工作线程去执行,内核线程再执行任务结构里的回调函数。要出问题,也就出在回调函数里,不过回调函数头绪很多,不想一一查看了,况且两个操作系统的实现也可能不一样。
又回到windows下,再看看windows internal 4th,在windbg下,断下系统,!idt查看系统的中断处理函数,赫然发现ACPI!ACPIInterruptServiceRoutine,这是我要找的吗?Bp ACPI!ACPIInterruptServiceRoutine下了个断点,g然后就断下来了。函数还挺长0x328个字节(在本菜鸟看来)。不厌其烦的在正常的和异常的系统上各跟了几遍,看不出个所以然来,只是发现这个例程执行完之后,io口0x42c就被清零了。函数里面跳转很多,依赖的莫名其妙的结构也很多。但是路貌似只有这一条了,只有把它仔细的理清了,才能知道问题究竟在不在这里。终于下定决心把它仔细读完,并且翻译成C代码,其中的数据结构也练猜带蒙的蒙出来了,最后的结论是,问题不在这里。这个例程例行公事的做一下PM Timer的累计,然后看看有没有GPE事件,如果有的话就将GPEx_EN清零(意思是,处理中断期间,不再接受中断),然后将任务KeInsertQueueDpc查到任务队列里,再由ACPI!ACPIInterruptDispatchEventDpc例程进行分派。于是 u ACPI!ACPIInterruptDispatchEventDpc L150 看一看,又是数不清的调转和未知的数据结构。看来得试一试IDA了,IDA之前没有用过多少,最多就是看看它画出来的流程图。网上查了一下,发现有人在讨论他的F5功能,然后发现是一个插件,可以将二进制逆成C代码,很高兴,于是装上插件,找ACPI!ACPIInterruptDispatchEventDpc这个函数还真难找,于是想了一个笨办法,在windbg里面得到函数的长度,然后再去IDA里面找,果然快很多。找到以后,F5一下,果然爽的很。虽然出来的代码还是很难懂,但是比汇编已经好很多了,配合windbg里面提供的符号,很快弄清楚了这个函数的流程。注意到寄存器是不是被清零,和ACPI!GpeIsLevel这个数组有很大关系。
于是重启系统,还是在没有出现滚动条之前将系统断下来,然后ba w 4 ACPI!GpeIsLevel下一个硬件断点,看看究竟是什么东西修改了ACPI!GpeIsLevel这个数组。一下子断到了ACPI!ACPICallBackLoad。kb一下看看栈回溯,里面的函数都是巨长无比。还是先g一下看看吧。一共断了9次,每次断下来,都看一看ACPI!GpeIsLevel,发现了一些有意思的事情。
ACPI!GpeIsLevel每断两次,被清零和重建各一次,最后到第9次的时候,被清零了。第10次没有来,ACPI!GpeIsLevel没有被恢复。在正常的系统上,下同样的断点,只断下来两次,ACPI!GpeIsLevel被清零和重建各一次。Kb看一下栈回溯,终于发现有意思的事情,在异常的系统上,最后三次断下来的时候,栈回溯里面有些函数包含在名叫intelppm的模块里,系统正常运行的时候,就不会出现这些函数。Google一下intelppm,发现都是一些蓝屏死机的内容,问题很可能就是这个模块引起的。命令行下Sc config intelppm start= disabled,将这个服务关掉,一切都正常了。
那么intelppm究竟是什么呢,它是微软为intel的CPU写的CPU电源管理模块。CPU进入IDLE状态时,为了节省CPU功耗,CPU会进入C1或者C2、C3状态,其中C1进入状态就是用一条HLT指令实现的。
问题到了这里,依然没有彻底弄明白究竟是谁的错,秘密应该就藏在intelppm.Sys里面,可惜我太菜了,没有能力再深入追究下去了,希望高手们能支点招。但是这次确实是大大的练了一次手,Windbg和IDA玩了一把,操作系统的一些工作方式也有所了解了。