查看原文
其他

Android 大话binder通信 (下)

牛晓伟 牛晓伟
2024-08-24

戳蓝字“牛晓伟”关注我哦!

用心坚持输出易读、有趣、有深度、高质量、体系化的技术文章


前情提要

Android 大话binder通信 (上)主要介绍了矮挫丑进程一直暗恋白富美进程,遂发送情书给她,以表达对她的爱慕之情,而情书顺利的到达了binder驱动 (binder驱动位于内核),并且在binder驱动的帮助下找到了白富美对应的binder_proc,那我们就接着来看下情书如何走完后面的路程到达白富美进程,关于白富美的回复就不在赘述了,因为整个流程其实基本上一样的。

情书到达内核

Android 大话binder通信 (上)提过在内核空间用户空间的数据要想到达目的进程可以分为查找目标复制数据激活目标这三步。

查找目标

查找白富美的总体思路如下:

  1. 调用ioctl方法的时候,根据传递的mDriverFD会拿到对应的file数据结构
  2. fileprivate_data属性中拿到矮挫丑进程的binder_proc
  3. 使用用户空间传递的handlebinder_proc中查找对应的binder_ref
  4. binder_refnode属性指向binder_node数据结构
  5. binder_nodeproc属性就是白富美进程对应的binder_proc

上面找到的binder_nodecookie指向用户空间的JavaBBinder对象 (它是BBinder的子类),而JavaBBinder对象是持有BeautifuLGirl对象的引用的,BeautifuLGirl是一个binder server,它位于白富美进程,它的receiveLoveLetter方法就是用来真正接收情书,然后交给白富美来阅览的。

情书恍然大悟说到:“哦!原来如此啊,BinderProxy对象到了native层是对应BpBinder对象,而BpBinder对象的handle属性到了binder驱动层,首先会根据mDriverFD找到对应进程的binder_proc,在根据handlebinder_proc找到对应的binder_ref,而binder_refnode属性是指向binder_node对象,而它的cookie属性又指向用户空间的JavaBBinder对象,JavaBBinder对象又持有BeautifuLGirl对象。”

“对,就是这样的,那咱们就进入下一步吧。”

复制数据

binder驱动情书说:“咱们既然找到了目标白富美在binder驱动层的binder_procbinder_node,那我就把情书你复制给目标。而你的其他小伙伴就直接交给目标吧。”

还没等binder驱动接着往下说,情书急忙问到:“不好意思啊打扰下,我的理解是在binder驱动层,不同的binder_proc之间以及其他的数据结构之间是可以互相引用的,而不像用户空间进程之间是隔离的,那为啥还要用复制而不是直接交给目标呢?还有为啥其他小伙伴是直接交给目标,而只有我是复制给目标呢?还有其他小伙伴都有哪些?”

为啥要复制数据

binder驱动:“你这性子也太急了吧,你刚刚问的这些问题我也正要说呢,那我就来介绍下吧。先来介绍下为啥要复制数据,Android系统分为用户空间内核空间,当一个系统调用进入内核空间后,在内核空间是不能直接使用用户空间传递过来的数据的,原因如下:”

  1. 内存隔离和权限管理:现代操作系统如Linux通过引入SMEP(Supervisor Mode Execution Protection,即监管模式执行保护)和SMAP(Supervisor Mode Access Prevention,即监管模式访问防止)等机制,确保内核空间与用户空间是隔离的。这意味着内核态的指令无法直接存取用户态的数据,从而保证了系统的安全性和稳定性。
  2. 安全性考虑:如果内核态可以直接存取用户态的数据,将带来极大的安全风险。因为用户态的数据更容易被劫持或篡改,如果这些数据能够直接传递到内核态,可能会被恶意利用,导致系统被攻击或数据泄露。
  3. 内存管理差异:用户空间的内存数据是分页的,并且可能不在物理内存中,而是在交换空间(swap space)或其他存储设备上。当内核试图直接访问用户空间的数据时,如果这些数据不在物理内存中,将会导致页面错误(page fault),这是内核所不允许的,并可能导致进程崩溃。
  4. 驱动程序架构和内核配置:不同的驱动程序架构或内核配置可能导致用户空间数据指针在内核模式下无效。例如,用户空间数据指针可能没有对应的虚拟地址到物理地址的映射,或者可能直接指向一些无效的内存地址。
  5. 防止内核后门:如果内核代码可以直接访问用户内存指针,这将给内核留下后门,使得用户程序可以利用这一点来访问和操作整个地址空间,从而破坏系统的安全性。

“也就是说为了内核的安全,用户空间和内核空间的数据是隔离的。用户空间传递数据给内核空间时,需要把数据复制到内核空间,而copy_from_userget_user函数可以安全的复制用户空间数据到内核。而内核空间传递数据给用户空间,同样也需要把数据复制到用户空间,copy_to_userput_user函数可以帮助复制数据到用户空间。情书对于复制还有不明白的吗?”

情书:“有的,copy_from_userget_user这俩函数有啥区别。”

“前者用于复制整个数据块,而后者用于复制单个值,比如像上面提到的int类型的cmd。同理copy_to_userput_user这俩函数的区别也在于此。”

情书:“这个我明白了,谢谢。”

“那来解答你的第二个问题,情书你还记得和你一起来到内核的小伙伴吗,你们都是被包裹在了binder_transaction_data对象中,而binder_transaction_dataBC_TRANSACTION又是被包裹在了binder_write_read对象的write_buffer属性中 (如下图) ”

binder驱动停顿一下接着说:“在上一节查找目标的时候,其实我已经调用了copy_from_userget_user方法把binder_write_readBC_TRANSACTIONbinder_transaction_data都已经复制到内核空间了。在Android 大话binder通信 (上)介绍过binder_transaction_data的作用,它的target.handle属性作用就是找到目标binder_node,因此它的价值已经发挥完毕是不需要复制给目标的。而binder_transaction_data的属性codeflags已经复制到内核空间了,因此就剩下情书你处于用户空间,因此只需要把你复制到内核空间。”

情书:“我明白了,可不可以这样理解,因为receiveLoveLetter这个方法只有我一个参数,如果调用方法的有很多的参数,那这些参数其实也都还处于用户空间。”

binder驱动:“你说的非常正确,点赞。那我来问你个问题,如果你作为一个binder server,如果只把codeflags、方法参数传递给你,你是否觉得足够了呢?”

情书低着头思索着这个问题,突然眼前一亮说到:“不够啊,我总得知道是谁给我发的吧。”

“你说的非常对,而这么多信息,那肯定需要一个数据结构来承载了,因此binder_transaction就诞生了,它的作用可以理解为像一个汇款单,比如在银行给某人回款的时候,回款单上要有汇款人、汇款人账号、汇款银行、收款人、收款人账号、收款银行、钱数、时间等。binder_transaction它也有发送者信息、接受者信息、方法code值、参数等这些数据,把它交给目标,目标就能从这些属性中得到想要的信息,那就结合下面的数据结构来介绍下它吧。”

struct binder_work {
 struct list_head entry;
    //定义了各种枚举类型
 enum binder_work_type {
  BINDER_WORK_TRANSACTION = 1,
  BINDER_WORK_TRANSACTION_COMPLETE,
  BINDER_WORK_RETURN_ERROR,
  BINDER_WORK_NODE,
  BINDER_WORK_DEAD_BINDER,
  BINDER_WORK_DEAD_BINDER_AND_CLEAR,
  BINDER_WORK_CLEAR_DEATH_NOTIFICATION,
 } type;
};

struct binder_transaction {
 省略代码......
    
 //它的主要作用是用来标记数据类型
 struct binder_work work;
 //用于标记是哪个线程发送的请求
 struct binder_thread *from;
 struct binder_transaction *from_parent;
 //它的主要作用是用来标记处理请求的binder_proc
 struct binder_proc *to_proc;
 //它的主要作用是用来标记处理请求的binder_thread
 struct binder_thread *to_thread;
 struct binder_transaction *to_parent;
 //为1则是代表请求需要回复
 unsigned need_reply:1;
 /* unsigned is_dead:1; */ /* not used at the moment */
    
 //请求的参数、binder_node都是存放在这
 struct binder_buffer *buffer;
 //请求方法对应的code值
 unsigned int code;
 unsigned int flags;
 省略代码......
};

复制

binder驱动:“我会初始化binder_transaction对象,情书你还记得发送你是在哪个线程吗?”

情书有些不确定的回答到:“好像是主线程吧。”

“没关系,我已经把矮挫丑进程调用你的线程的信息封装到binder_thread对象,而它就作为binder_transaction对象的from属性的值,而在上一节查找目标的时候已经查找到了binder_node存放在binder_transaction对象的buffer->target_node属性,而binder_transaction对象的codeflags值也都指向了TRANSACTION_receiveLoveLetterflags,那就该把情书复制到内核空间了。”

一次复制

因为方法参数只有情书,因此调用copy_from_user方法从binder_transaction_data.data.ptr.buffer复制到binder_transaction对象的buffer属性,而该属性的值是在一块共享内存上,这也就是binder通信一次复制的体现,只要把方法参数从用户空间复制到binder_transaction对象的buffer属性,则在从内核空间进入用户空间时,用户空间就可以直接使用方法参数了。

情书:“那我问个问题,这片共享内存是啥时候打开的?”

binder驱动:“像java进程,在zygote进程fork子进程成功后,就会打开binder驱动,打开binder驱动后会接着调用mmap方法,进而调用到binder驱动的binder_mmap,而该方法中会打开一片匿名共享内存,这样对应的binder_proc就可以与对应用户空间进程共享一片内存了。而对于native层需要binder通信的进程,也是和上面步骤一样打开binder驱动,调用mmap方法进行匿名共享内存。”

参数转换

因为当前方法只有情书这一个参数,假如方法的参数中有BinderProxy或者Binder或者ParcelFileDescriptor (文件描述符java层对象)这三种类型的参数,则会对它们进行转换 ,比如调用startActivity方法时候,Intent参数里面放入上面这几个参数,则在该环节会对这几个特殊类型的参数进行转换,转换规则如下:

而对于ParcelFileDescriptor的转换,需要在目标进程根据ParcelFileDescriptor对应的fd来生成自己进程对应的fd。

以上就是针对参数是这三种类型的转换,大家可以想想,如果不转化,会有啥问题?比如不对ParcelFileDescriptor进行转换,若目标进程与调用进程不是同一进程,则目标进程会使用调用进程的fd,在目标进程肯定不能使用的,这时候肯定会出错。

交给目标

binder_transaction对象内的数据都已经准备好了,万事俱备只欠交给目标了,那如何交给目标呢?

先来回顾下如果线程之间要进行通信的话方法有很多,其中就有共享内存,其实现例子如下: 其中一个A线程拥有一个todo队列,A线程会检测todo队列是否有数据,没有的话则会调用wait方法进入等待状态,有的话则会从todo队列拿出数据进行处理,进而再次检测todo队列。 而其他线程往A线程的todo队列放入数据后,需要把A线程唤醒,这样A线程就检测到todo队列有数据了,进而处理它。

恰恰在binder驱动层,binder_procbinder_thread与其他binder_procbinder_thread进行通信和上面线程之间实现通信的方法一样,也使用了共享内存。对于参数为mDriverFDBINDER_WRITE_READbinder_write_read引用的ioctl函数调用,binder驱动层把用户空间传递的数据处理完毕后 (binder_write_read对象的write_xxx属性是用户空间传递给binder驱动的),会进入等待状态,这也导致用户空间处于等待状态。

而其他内核空间线程生产的数据 (如binder_transaction),要想交给目标binder_proc的话,需要先从binder_proc中选取空闲的binder_thread,若存在则把数据放到binder_thread的类型为list_headtodo的队列中,否则把数据放到binder_proc的类型为list_headtodo的队列中,进而在把对应的内核态线程唤醒。

因此把binder_transaction对象交给白富美目标的话,需要先从白富美binder_proc中选取空闲的binder_thread,如果找到则把binder_transaction对象的类型为binder_workwork属性放入binder_thread的todo队列中,否则放入binder_proc的todo队列中,最后在把对应的内核线程唤醒即可。

等待恢复数据

binder驱动情书说:“还有最重要的一点,在矮挫丑进程中调用你的线程在binder驱动层进入等待状态,它会等待白富美的回复,如果白富美的回复到达binder驱动层,我也同样会把回复数据及其他相关数据封装到一个binder_transaction对象中,同时把它放入等待状态线程对应的binder_threadtodo队列中,并且把等待线程唤醒,这样等待线程就会把回复数据复制到用户空间,进而用户空间就能收到回复数据。”

激活目标

白富美对应的内核态线程被唤醒后,会从该线程对应的binder_threadbinder_proctodo队列中把类型为binder_workwork取出来,并且把它转换为binder_transaction对象。根据binder_transaction对象的属性构造binder_transaction_data对象 (如下图)

image

这时候binder_transaction_data对象的codeTRANSACTION_receiveLoveLettercookietarget.ptr分别指向目标binder_node的对应属性,data属性中则存放了情书。

为了让回复数据能正确的回复,被唤醒的内核态线程对应的binder_threadtransaction_stack属性会把binder_transaction对象保存起来,这样这个内核态线程再次收到用户空间发送的回复数据后,保存的binder_transaction对象可就起非常大的作用。

还记得在Android 大话binder通信 (上)介绍过用户空间发送给binder驱动的cmd是以BC开头的,而binder驱动返回给用户空间的cmd是以BR开头的,因此会把BR_TRANSACTION cmd使用put_user方法把它从内核空间复制到用户空间的,会调用copy_to_user方法把binder_transaction_data对象从内核空间复制到用户空间。

情书:“binder通信不是一直在强调,一次通信只需要一次拷贝吗?但是我发现不是这样的,我记得我和我的小伙伴是使用copy_from_user方法把我们从用户空间拷贝到了内核空间,而现在使用copy_to_user方法把我们从内核空间拷贝到用户空间,难道你们binder通信是虚假宣传吗?”

binder驱动:“我个人觉得binder通信的一次通信只需要一次拷贝,这里的拷贝指的是方法参数的拷贝,而对于像codeflagscmd这些值它们是两次拷贝,这些值非常的小所以即使两次拷贝也并无大碍,而整个binder通信中方法参数才是大头 (当然如果参数非常简单就另说了),因此只需要保证方法参数只拷贝一次就可以加快整个binder通信的效率。”

情书:“我还是不明白,调用copy_to_user方法把binder_transaction_data对象从内核空间复制到用户空间,而方法参数是在binder_transaction_data对象内的,那这时候不是又拷贝了吗?”

binder驱动:“哈哈,我明白你的困惑点了,方法参数是在binder_transaction_data对象的data.ptr.buffer属性中,而它是一个指向共享内存的地址,调用copy_to_user方法只是把该地址拷贝了一下,方法参数内容是没有拷贝的。”

情书:“恍然大悟,我明白了,谢谢你的指导。”

binder驱动有些伤感的对情书说:“我把binder_transaction_dataBR_TRANSACTION从内核空间复制到用户空间,内核空间的工作就完毕了,对应的线程会从内核态进入用户态。同时你在binder驱动的行程就结束了,咱们就此告别吧。”

情书:“我也非常的不舍,谢谢你一路上对我的关照,后会有期。”

还是老规矩,用一张图来展示情书传递过程中方法和参数的变化

image

情书送达

情书自言自语到:“虽然离开内核心情确实有些伤感,但是好在我离目的地越来越近了,因此心情不好的时候,可以换个角度考虑问题,那样心情会不一样啊。”

突然一个声音的出现吵醒了我:“情书你好啊,欢迎来到白富美进程,来到这就先由我来接待你吧,我的名字叫IPCThreadState。”

情书惊诧的看着IPCThreadState说:“我记得你啊,离开矮挫丑进程的时候还是你送的我。”

IPCThreadState:“你搞混了吧,每个进程都有自己的IPCThreadState,我可是第一次见你哦。刚刚我的一个binder线程告诉我从binder驱动发送上来了一些数据,这个binder线程获取到cmd为BR_TRANSACTION,因此它根据这个cmd,就把binder_transaction_data对象读取出来了,并且调用ParcelipcSetDataReference方法把binder_transaction_data对象中的data相关的信息放入Parcel中,这样方法的参数就可以从Parce对象中获取到了。”

情书急忙插了一句:“也就是说我现在是在Parcel对象中吧。”

“是的,并且这个binder线程还发现binder_transaction_data对象的target.ptr属性是有值的,因此它非常坚信这一定是在调用某个BBinder对象的transact方法。因此在和我商量后,决定把binder_transaction_data对象的cookie属性转换为BBinder对象,并且调用它的transact方法,把binder_transaction_data对象的code属性、Parcel对象、类型为Parcel的reply、binder_transaction_data对象的flags属性传递给BBinder。”

IPCThreadState情书说:“这么快就要和你告别了,我就把你和你的小伙伴交给了BBinder,还有当白富美给了回复后,刚刚解析你们的binder线程会把回复发送到binder驱动。”

老规矩,用一张图来展示情书传递过程中方法和参数的变化

image

BBinder

BBinder情书说:“欢迎你来到我这,你可是要去往白富美BeautifulGirl。”

情书:“你好,我是一封情书,你咋知道我要去往那里呢?”

BBinder自信的答到:“哈哈,首先我也是在白富美进程;其次白富美进程也只存在BeautifulGirl这样一个binder server,并且它是java层的;最后这段时间可是有非常非常多的情书从别的进程发送过来。因此我可以断定你一定是去往BeautifulGirl的。”

情书:“你的推断非常的正确,那还需要你帮我下,如何才能到达目的地。我先提前谢谢你了。”

BBinder:“因为BeautifulGirl是一个java层的binder server,我会通过onTransact方法,把情书和你的小伙伴交给JavaBBinder,它是我的子类,它丰富了我的onTransact方法。”

JavaBBinder

JavaBBinder情书说:“欢迎你来到我这,我这可是离目的地最近最近了,我虽然处于native层,但是我持有BeautifulGirl对象,我会把你和你的小伙伴们通过JNIEnvCallBooleanMethod方法发送到java层,在java层接待你们的是IBeautifulGirl.Stub。”

情书:“你刚刚不是说持有BeautifulGirl对象吗?为啥没有把我们发送到BeautifulGirl,而是发送到IBeautifulGirl.Stub呢?”

JavaBBinder:“啊!这个吗?你到了java层,问下IBeautifulGirl.Stub吧。”

就这样情书带着问题来到了java层。

老规矩,用一张图来展示情书传递过程中方法和参数的变化

image

IBeautifulGirl.Stub

IBeautifulGirl.Stub情书说:“亲爱的欢迎来到java层,”

还没等IBeautifulGirl.Stub说完话呢,情书问到:“你好,我能问下,为啥我没有直接到达BeautifulGirl,而是先到您这呢?”

“是这样的,BeautifulGirl它是我的子类,而JavaBBinder是通过调用我的onTransact方法把code、类型为Parceldata、类型为Parcelreplyflags带到了我这,而我的子类BeautifulGirl是没有重写该方法的,因此你们需要先到我这,不过先别急,我先看看和你一起到达的小伙伴code,它的值是TRANSACTION_receiveLoveLetter,那我就明白了,你们是要去往BeautifulGirlreceiveLoveLetter方法,对了该方法只需要一个参数就是字符串的情书,那我就调用datareadString方法把情书你的内容解析出来,好了现在我就调用BeautifulGirlreceiveLoveLetter方法只把情书交给BeautifulGirl,你的小伙伴reply会把白富美的回复保存起来交给它的上级,而另外的小伙伴code根据它已经找到了要调用的方法,因此它的价值也发挥完毕了。”

“恭喜你情书到达目的地,也祝福你能有一个好的结果,确实有很多很多的进程发送情书过来,都被白富美拒绝了,说实话我也搞不懂她的标准是啥,咱们就此告别吧,我可等着白富美的回复呢。”

老规矩,用一张图来展示情书传递过程中方法和参数的变化

image

BeautifulGirl

BeautifulGirl:“你好啊,欢迎来到情书大世界,那我就把你交给白富美吧。”

情书终于见到了白富美,它的心情非常的激动,但是发现高富美和它想象的完全不一样,它认为的白富美应该是端庄大气、温文尔雅、穿着得体,而见到的白富美却是一副高高在上、时尚潮流的感觉,一点也不接地气。

白富美情书说:“你好啊,来我这的情书可是千千万,要想赢得我的芳心,先来朗读下你的内容吧。”

情书心想为了我的主人矮挫丑,我一定要用最优美的声音把情书朗读给白富美听。

亲爱的 白富美

在这个星光璀璨的夜晚,我提笔写下这封情书,只为向你倾诉我内心深处的情感。你的出现,如同晨曦中的第一缕阳光,温暖而耀眼,让我为之倾心。

你是一位才华横溢的女子,你的智慧与美丽并存,让我为之倾倒。你的眼神中闪烁着智慧的光芒,每一次与你交谈,我都能感受到你独特的见解和深邃的思考。你的笑容如春风拂面,总能在我心中激起层层涟漪。

我欣赏你的才华,更被你的温柔与善良所吸引。你总是那么细心地照顾着周围的人,用你的善良和爱心温暖着每一个人。你的存在,让我的世界变得更加美好。

我喜欢和你一起探索生活的奥秘,喜欢与你一起分享彼此的喜怒哀乐。每一次与你相处,我都感到无比幸福和满足。你的话语总能让我茅塞顿开,你的笑容总能让我忘却烦恼。

亲爱的,我愿意为你付出一切,只希望你能感受到我深深的爱意。你的存在是我生命中最美好的礼物,我会珍惜你,爱护你,直到永远。

愿我们的爱情如同这繁星点点的夜空,璀璨而美丽,愿我们的未来如同这无尽的宇宙,广阔而深邃。

仰慕你的矮挫丑

白富美听了情书的朗读,内心有了些许的感动,这真的是她收到的情书中最有文采、最发自内心的,同时情书也朗读的非常的有感情不像别的情书一点情感都没有。

她突然想到一个问题:“情书你刚刚说你的主人叫啥名字?”

情书有些不好意思,低声的说:“矮挫丑”

白富美:“啊!咋能叫这样的名字呢,不行我得调用BindergetCallingPid方法来验证下。果不其然,情书的主人果然是矮挫丑。”

白富美深深的陷入了犹豫中,她不知道该咋办了,虽然矮挫丑的情书是最棒的,但是他的长相却令她难以接受,最终还是她的感性战胜了理性,她选择了拒绝。

下面这种图,展示了情书从矮挫丑进程到达白富美进程所经历的方法和参数的变化

矮挫丑

白富美的回复信息被IPCThreadState的处于等待状态的binder线程发送到了binder驱动,经过binder驱动,回复信息最终到达了矮挫丑

矮挫丑收到白富美的回复信息是拒绝,但是矮挫丑却没有一丝的伤感、气馁,他坚信自己是不会放弃的,还要继续给白富美发送情书,直到她同意为止。





个人观点,仅供参考
继续滑动看下一个
牛晓伟
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存