近日在给NDB增加新功能,让它可以访问用户空间。但在ARM平台上遇到一个问题,如果CPU中断时位于内核空间,那么访问任何用户空间的地址都失败。
失败的基本症状是调试器中打印出一串串的问号。

而JTAG库打印出如下错误信息:abort occurred - dscr = 0x03047d47
其中的dscr是ARM CoreSight技术中定义的外部调试寄存器,全称为EDSCR,即外部调试状态和控制寄存器,在ARMv8架构手册中可以找到它的详细定义,结合上面的错误码,解读如下图所示。

本以为低6位的状态值部分可以给出错误原因,但是却始终给出的是“断点”含意,意思是这一次中断到调试器是因为遇到断点。
请老朋友使用ARM官方的DTRACE工具进行测试,也发现类似情况,某些情况下无法读用户空间。比如下图中,当切换到5号CPU后,再访问其它核可以访问的一段地址空间,访问失败,内存窗口显示红色背景,没有数据。

下图是正常能读的情况。

是谁在阻止强大的硬件调试器访问内存呢?根据多年的经验,可能是安全机制在搞鬼。但是这个鬼藏在哪里呢?
今日周末,又想起这个问题。想到了以前在写“猫蛇大战”系列时读过的Linux内核uaccess代码。
虽然内核空间具有高特权,从特权级别来说可以访问用户空间。但其实真的要访问的话,还是有一些复杂的。首先用户空间有很多个,用户空间的内存常常不在物理内存中,另外,内核访问用户空间也要有正当的理由,不可以“擅闯民宅”。所以内核访问用户空间时,也可谓是如履薄冰。
另外,因为这部分逻辑与CPU硬件相关,所以代码也很分散。实现时又常常用宏或者嵌入式汇编,读起来也比较难读。
想到这里,我便打开内核代码,搜索uaccess,搜索整个内核代码树得到的结果太多了,只搜索GDK8使用的arch/arm64。这样一搜,果然有收获。处理页错误的一行printk引起了我的注意。
die_kernel_fault("access to user memory outside uaccess routines",
addr, esr, regs);
这个die系列打印是内核里的一道景观。一执行到这个函数,系统就进入panic了。品味这个错误信息:“在uaccess过程外访问用户内存”,这是调用die的理由,也就是给系统判死刑的原因。
顺着这条消息思考:在uaccess之外不可以访问用户内存。NDB显然属于这种情况啊。因为NDB是用JTAG来访问内存,不是用uaccess。
那么为什么uaccess就能访问呢?
打开arm64下的uaccess代码,很快找到了一个关键函数。

__uaccess_ttbr0_disable,从这个函数名来看,是要禁止访问,“坏事”多半就是它干的。
这个函数上面有个条件编译选项,查这个选项,果然是打开的。
geduer@gdk8:~$ zcat /proc/config.gz | grep TTBR0
CONFIG_ARM64_SW_TTBR0_PAN=y
如此看来就是它在捣鬼。搜索这个宏的文档:
Emulate Privileged Access Never using TTBR0_EL1 switching
configname: CONFIG_ARM64_SW_TTBR0_PAN
Linux Kernel Configuration
└─> Kernel Features
└─> Emulate Privileged Access Never using TTBR0_EL1 switching
Enabling this option prevents the kernel from accessing
user-space memory directly by pointing TTBR0_EL1 to a reserved
zeroed area and reserved ASID. The user access routines
restore the valid TTBR0_EL1 temporarily.
写的很清楚,为了防止内核空间随便访问用户空间,故意把记录用户空间页目录的ttbr0寄存器偷梁换柱了。这招真够损的。^-^
使用ndb读ttbr0寄存器,看到的是这样一个值:
rdmsr ttbr0_el1
msr[182000] = 00000000`02503000
剧透一下,这个值就是假冒的,和后面将看到的有效值差别很大。
既然是uaccess函数做的禁止,那么它也该有方法来启用啊,诚然如此。

阅读这个函数的代码,它是从当前线程信息中读到保存的ttbr0,然后再写到物理CPU中。
如何找到保存的ttbr值呢?
有办法。在NDB中先执行!ps命令显示当前线程的task_struct地址。
task_struct:0xffffffc0f409e740 pid: 179 comm:avahi-daemon
PGD:0xffffffc0f0d2b000 CR3=0x0
state 0 flags:0x404100 stack:0xffffff800b8a8000
上面的地址指向的就是Linux下每个线程都有的task_struct结构体,内核源代码中常常使用著名的current宏来访问(我将其称为Linux内核第一霸)。这个结构体极其庞大,它的起始部分就是架构相关的thread_info子结构。
dt lk!task_struct
+0x000 thread_info : thread_info
+0x020 state : Int8B
+0x028 stack : Ptr64 Void
+0x030 usage :
+0x034 flags : Uint4B
+0x038 ptrace : Uint4B
+0x040 wake_entry : llist_node
+0x048 on_cpu : Int4B
+0x04c cpu : Uint4B
+0x050 wakee_flips : Uint4B
+0x058 wakee_flip_decay_ts : Int8B
+0x060 last_wakee : Ptr64 task_struct
【此处省略数百行】
既然thread_info就在task_struct的开头,那么task_struct的地址就是thread_info的地址。使用dt命令观察:
dt lk!thread_info 0xffffffc0f409e740
+0x000 flags : 0
+0x008 addr_limit : 549755813887
+0x010 ttbr0 : 0xf80000`f0d2b000
+0x018 preempt_count : 0
果然,真实的ttbr0现身了。
接下来,使用NDB的写寄存器命令把这个保存的ttbr0写给CPU:
wrmsr ttbr0_el1 0xf80000f0d2b000
再读回来确认:
rdmsr ttbr0_el1
msr[182000] = 00f80000`f0d2b000
确认ttbr0写成功后,再尝试访问用户空间:
dd 0000007f`82809000
0000007f`82809000 464c457f 00010102 00000000 00000000
0000007f`82809010 00b70003 00000001 000011c0 00000000
0000007f`82809020 00000040 00000000 0001e548 00000000
0000007f`82809030 00000000 00380040 00400007 0019001a
0000007f`82809040 00000001 00000005 00000000 00000000
0000007f`82809050 00000000 00000000 00000000 00000000
0000007f`82809060 0001c4d4 00000000 0001c4d4 00000000
0000007f`82809070 00010000 00000000 00000001 00000006
居然就成功了,困扰多日的问题就这么解决了,在解决的过程中,年轻的NDB调试器和挥码枪发挥了积极作用。给它们个合影吧。

我是在旅途中写的这篇文章。GDK8和挥码枪都很方便携带,所以我就把他们放在背包中,随时可以取出来,快速搭建起一个强大的调试环境。

上图中的蓝色配件叫Nano Display,它可以把GDK8的HDMI输出转为USB信号,送给笔记本电脑,Nano Code中集成了一个视频播放功能,可以把GDK8的桌面显示在主机上。

GDK8的桌面是我深爱的庐山秀峰之龙潭。很多格友曾经与我同游过。
(写文章很辛苦,恳请各位读者点击“在看”,也欢迎转发)
*************************************************
正心诚意,格物致知,以人文情怀审视软件,以软件技术改变人生
扫描下方二维码或者在微信中搜索“盛格塾”小程序,可以阅读更多文章和有声读物

也欢迎关注格友公众号

8599

被折叠的 条评论
为什么被折叠?



