龙芯3B1500开核记 - Reverse engineering PMON Bootloader on MIPS/Loongson

前言

最近有一批龙芯3B1500主板低价处理,型号是Lemote-A1310,我也买了块,美中不足的是由于芯片一个硬件设计Bug,默认由Bootloader屏蔽了2个核心,也就是说只有6个核心开放给用户使用。经过一些调查,我认为通过软件绕过硬件的限制,将那两个核心开放是可行的,也因为群里呼声巨大,所以我开始着手于解放这些核。

关于这个Bug

在接受Lemote的Huacai Chen指点之后,我意识到这个Bug和I/O DMA一致性有关。 从胡伟武老师的文章《我们的龙芯三号》中,我找到了关于这个Bug的详细描述:

这个问题是从3B1000到3B1500改版过程中引进的,为了提高性能,处理器核收到多个维护Cache一致性的无效请求时,原来每两拍才能处理一个,改成可以连续处理,导致清除LL/SC同步指令的同步位llbit时错了一拍,误把IO DMA引起的Cache无效请求当作0号处理器核的Cache无效请求(IO DMA的编号刚好为0,与0号处理器核区分不开),通过软件调整可以规避此问题。经过批量测试,原不稳定现象消失。

龙芯架构下,DMA的一致性是可配置的,也就是说可以从Cached地址空间进行DMA操作,硬件维护DMA一致性;也可以不由硬件控制一致性而从Uncached地址空间操作。出现问题的环节是I/O DMA的一致性维持,所以我们可以采取不让硬件维持I/O DMA,走Uncached DMA来在牺牲一些I/O DMA性能的前提下绕过这个Bug。Lemote在这些主板出货的时候选择牺牲两个Node的0号核的方法来保证I/O DMA性能顺便降低一些功耗(没错,3B1500是个NUMA架构的处理器,全片八个核心分成两个Node,每四个核心组成一个SMP Group)。另外,即使不解决DMA的问题,虽然稳定性不高,但是对于日常折腾也已足矣。更何况我们部分人拿到的3B1500G已经解决了这个Bug,但是还是被关核,实在是不甘心。

SMP/NUMA启动流程

要找到开核方案,明白除了Bootloader初始运行的那颗核心之外剩余的核心是如何被Bootloader初始化,控制权如何被移交给内核,是个很重要的过程。根据我对龙芯多核架构的理解,多核启动的主要流程是,每个核心被复位的时候,其PC共同指向NMI Exception的入口,也就是0xbfc00000,Kseg1被map到Boot SPI Flash的头部的位置。之后由Bootloader初始化所有核心的私有缓存,TLB等组件。然后软件通过读取CP0 Ebase的0-10bit CPUNum位判断自己的核编号,如果自己是Bootloader将要下一步使用的Bootcore,就继续运行Bootloader核心代码,如果自己是从核,那么就跳转到一段循环代码,等待Bootcore的控制权移交给内核之后,内核通过向各个核心的MailBox寄存器发送这个核心将要跑的代码的PC地址来唤醒这个核心。

内核侧的实现我大致读了读,觉得和开核关系不大,就不在这里过多的做阐述了。

让我们来看看PMON下具体的代码实现,这些代码均摘录自龙芯开源的PMON Target bonito.3c780e的start.S。 .set mips64 mfc0 t0, $15, 1 /* 取CP0 Ebase / .set mips3 andi t0, t0, 0x3ff / 取出t0中的CPUNum / dli a0, 0x9800000000000000 /取NUMA Base/ andi t1, t0, 0x3 dsll t2, t1, 18
or a0, t2, a0 /
256KB offset for the each core / andi t2, t0, 0xc / node id / dsll t2, 42 or a0, t2, a0 / get the L2 cache address / dsll t1, t1, 8 or t1, t2, t1 dli t2, NODE0_CORE0_BUF0 or t1, t2, t1 / 上面主要在判断自己是哪个NUMA Node / dli a0, BOOTCORE_ID / 把BOOTCORE ID加载进a0 / bne t0, a0, slave_main / 如果t0(当前核的CPUNum) != a0(BOOTCORE) 那么跳转到slave_main / nop bal initserial / 在BOOTCORE上初始化串口 */ nop

slave_main的代码就不贴了,也是做一些初始化的活,最后是一个等待内核发mailbox的loop。

一个Naive的尝试

PMON的Start.S读的我醉生梦死,大致从头看到尾,并没有什么去关闭那两个核的代码,按照我的理解,这两个核在启动之后也在运行slave_main的代码,只不过因为通过“龙芯EFI标准”传过去的Reserved Core Mask中将那俩核Mask掉了,所以内核没有去初始化他们,那么直接在内核层面覆盖掉传参就好了嘛。事实证明我还是太Naive,改完内核一跑,屏幕上还是6只小企鹅,串口上的输出也是那么直白,CPU0 Failed to Boot, CPU4 Failed to Boot。

不甘心的我手动在内核的Head.S里加了一段汇编喂给CPU0一个Mailbox PC,指望能把它带回来,然而它根本不鸟我一下。

看来这两颗核心是真的被关掉了。

此路不通矣。

小插曲

联系到一位Lemote的员工愿意帮忙编译一份开核版本的昆仑固件,但是失败了。。。。。不过还是非常感谢他。。。 Lemote方面也表示不是很方便开放他们的PMON私有代码。 看来还是要自己动手丰衣足食。

Reverse Engineering

说起逆向工程,那必是会想到Hex-Rays的IDA工具。IDA有对MIPS架构提供支持,那是再好不过了(其实我本来想用开源的Radare2,但是当时手边只有一台Windows环境的电脑,于是心安理得的开始使用盗版软件)。

手上的PMON是一个bin文件,不要说debug symbol和lable了,就连个Entrypoint都没有。。好在Bootloader总是从头开始跑,也没什么库之类的东西。 省略一堆设置IDA的过程。 加载后: a1310-pmon-1.png (截图是后来补上的,刚载入进去肯定是没有注释,lable什么的。)

扑面而来一堆汇编,我一脸懵逼,好吧,挑灯苦读。窗外满天星斗,一叹星摇摇,长夜殊未央。

而且,不得不说反汇编得到的代码和实际Start.S汇编差距很大,一是因为assembler进行了预处理,二是因为Lemote PMON的代码基似乎和龙芯开源的版本也有一些差距。总之,对照着开源版本的Start.S又是一番痛苦的分析。同时发现了一个定位PMON代码用途的小技巧,PMON源代码里的PIRINTSTR宏实际上是用stringserial函数配上往.data section写入要打印字符实现的,也就说,只要找到stringserial的地址,就能找到在哪里call的这个函数,以及传给函数的.data section 地址/offset,进而得到PRINTSTR出来的内容。所以我得以标注出start.S中各个函数的入口点lable。

然而,重点检查了判断CPUNum的代码和slave_main里执行的代码,并没有发现更多和核心判断相关的内容。 郁闷至极,望向窗外,此时天已拂晓,第二天还有很多事情要干,不得不郁郁而眠。

又到了第二天晚上,继续去钻进这些代码里,再次检查代码,还是没有线索。郁闷至极,随手翻开3B1500的用户手册寻找灵感。 一页页翻过去,突然看到 “2.6 芯片配置、采样及 PLL 相关寄存器”,猛地一想,是不是单个核的时钟被关闭了。。。仔细读了读这一张,果然,后面有提到 “芯片处理器核软件分频设置寄存器” ,这些寄存器可以控制单个核时钟的使能与否。他的物理地址是0x1fe001d0,换算到Start.S运行的kseg1里就是0xbfe001d0。全局搜索了一下这个地址,果然,在SHUT_SLAVE和WAKE_CORES两个阶段都被写了一下,对照手册看了看写入的值,果然。CPU0和CPU4的时钟都被直接关掉了。 a1310_pmon_shut_slave.PNG

a1310-pmon-wake_cores.PNG (这里截图的已经是修改后了) 之前之所以忽视了这里,是因为龙芯开源的PMON考虑到了6核版本和8和版本共用同一固件的兼容性,并没有关掉这两个核的时钟,所以在逆向的时候,找到了这段代码的用途,就误以为作用和开源版本的一致,疏忽了。

然后梳理了一下需要修改的地方,主要是把Bootcore改成0,在SHUT_SLAVE的时候把一号核关掉,0号核打开,WAKE_CORES的时候 启用所有八个核,就好了,PMON的代码几乎不动,只是改动几个写入寄存器的值。

pmon-warn.PNG

好吧,刚想改就给我刺激的,好在对MIPS的指令编码我也还算有些了解,直接做HexEdit级别的Patch也没什么问题。 改完已经是凌晨一点了,又XJB一改内核,编程器烧写了一波,刺激的点亮时间到。

pmon-8cores.jpg

八只小企鹅!瞬间泪流满面。 pmon-patches.PNG 回头看看对PMON做的修改,也就那么7个byte而已,但是其中的艰辛,也算是只有自己明白。

就想起一个小故事:

某工厂一台关键设备出了毛病,找来找去也找不到因素。所以老板发话,能找出毛病者给一万美金的奖赏。这在当时但是重赏。所以有个工程师来了,他在出毛病的设备这儿听听,那里看看,这儿摸摸,那里敲敲,然后用粉笔在上面画了一条线,让从那里拆开。毛病公然在那里,很快就修好了。他也拿到了一万美元的奖金。过后有人谈论:只是画一条线,就得万元奖金,也太轻盈了。工程师听后回应道:画那一条线,只值1美元;而知道在哪里画线,则值9999美元。那些谈论者都闭上了嘴。

也想起里根的演讲里有这么一句话"Give me a challenge and I’ll meet it with joy.",在接受这个Challenge的时候,当这个Challenge被我攻下的时候,的确,心里只有那份Joy。

自恋的差不多了,该想想内核里要怎么办了。

Kernel

于是,这个两个核被解放了出来,接下来就是要让内核初始化并且使用上这两个核,还要让内核在适当的情况下决定自己的DMA一致性。之前我粗暴无礼的硬编码了bootcpu编号和reserved core mask,这并不是一个优雅的实现,也会让内核失去通用性。 [gitwidget type=‘github’ url=‘flygoat/linux-gs-tweak’]

Licensed under CC BY 4.0
comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy