是谁不让访问用户空间

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

失败的基本症状是调试器中打印出一串串的问号。

5e373afbd2b27c3914f168315372a02b.png

而JTAG库打印出如下错误信息:abort occurred - dscr = 0x03047d47

其中的dscr是ARM CoreSight技术中定义的外部调试寄存器,全称为EDSCR,即外部调试状态和控制寄存器,在ARMv8架构手册中可以找到它的详细定义,结合上面的错误码,解读如下图所示。

ca912908a825c01ffeb8c946c23ccd5e.png

本以为低6位的状态值部分可以给出错误原因,但是却始终给出的是“断点”含意,意思是这一次中断到调试器是因为遇到断点。

请老朋友使用ARM官方的DTRACE工具进行测试,也发现类似情况,某些情况下无法读用户空间。比如下图中,当切换到5号CPU后,再访问其它核可以访问的一段地址空间,访问失败,内存窗口显示红色背景,没有数据。

874829ef7399c003b9a0a488b30b18d4.jpeg

下图是正常能读的情况。

9372ae950185268d1d2c062d7ae1208b.jpeg

是谁在阻止强大的硬件调试器访问内存呢?根据多年的经验,可能是安全机制在搞鬼。但是这个鬼藏在哪里呢?

今日周末,又想起这个问题。想到了以前在写“猫蛇大战”系列时读过的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代码,很快找到了一个关键函数。

2f447f4775456988862a1ab1d6172991.png

__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函数做的禁止,那么它也该有方法来启用啊,诚然如此。

eb33bb399918851fcc23121ce14023ad.png

阅读这个函数的代码,它是从当前线程信息中读到保存的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调试器和挥码枪发挥了积极作用。给它们个合影吧。

f86355edb2d0b4f141f31558901a2617.jpeg

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

77b6e393abdcd7e19da52ca6f6c10203.jpeg

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

2041288453690fe7ef286c63c00ec344.png

GDK8的桌面是我深爱的庐山秀峰之龙潭。很多格友曾经与我同游过。

(写文章很辛苦,恳请各位读者点击“在看”,也欢迎转发)

*************************************************

正心诚意,格物致知,以人文情怀审视软件,以软件技术改变人生

扫描下方二维码或者在微信中搜索“盛格塾”小程序,可以阅读更多文章和有声读物

778d05ab3fa1fa80dd7c97c44a77eeb7.png

也欢迎关注格友公众号

eeff9c2a66b727edade8e1f8b23406c5.jpeg

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值