APP下载

Android系统动态调试技术

2019-07-12张恩勤姜德军程雯吴海全

电子技术与软件工程 2019年9期
关键词:内核日志代码

文/张恩勤 姜德军 程雯 吴海全

1 简介

Android是目前最为广泛应用的一个嵌入式操作系统,手机、平板电脑和汽车等电子等各种设备中皆可见其身影。根据IDC的统计,2018年Android在移动平台的市场占有率为85%,而且预期未来5年内还将保持1.7%的年增长率,越来越多的Android的设备以及应用程序在被开发出来。如何在Android这么一个大的系统中调试这些新的设备驱动或者应用程序一直是开发过程中的一个重要问题。一个优秀的方法将有效节约我们的开发时间和开发费用。

这是一个典型的传统的软件调试过程:

(1)在App1中发现一个问题。

(2)调整App1的源程序,添加调试代码。

(3)重新编译App1并加载到设备中。

(4)复现App1问题,取得对应的调试信息。

(5)分析发现App1的问题是由于App1中使用了DriverA。

(6)修过DriverA的源程序,加入打开调试信息。

(7)重新编译DriverA并加载到设备中。

(8)复现DriverA问题,取得对应的调试信息。

(9)发现DriverA的问题又是由其调用DriverB引起...

这个过程我们发现很多时间被浪费在修改代码增加日志、重新编译,加载目标程序到设备等操作中。这里会有这样一个疑问,为什么我们不一次打开所有涉及模块的调试信息呢?如果我们用这种方法,我们会一次获得非常大量的信息,对于我们分析问题会不方便,而且过度的调试信息可能会改变程序的运行时序,使得问题没法重现。

是否可以在不修改程序的情况下动态打开关闭调试日志呢,其实Android内核有一种动态调试技术。pr_debug()或者dev_debug()来代替传统的printk()输出日志。内核通过他们可以在运行期动态地打开关闭调试信息。

这是一个典型的基于动态调试技术的调试软件过程:

(1)在App1中发现一个问题。

(2)运行命令打开App1的调试信息。

(3)复现App1问题,取得对应的调试信息。

(4)分析发现App1的问题是由于App1中使用了DriverA。

(5)运行命令打开DriverA的调试信息,同时如果需要可以关闭App1的的调试信息。

(6)复现DriverA问题,取得对应的调试信息。

(7)发现DriverA的问题又是由其调用DriverB引起。

(8)运行命令打开DriverB的调试信息...

显而易见,通过这种方法可以使我们的调试过程更加有效率。我们下面了解一下动态调试技术如何在Android内核中使用,然后拓展这个技术到用户态程序。

2 动态调试在Andoird核心模块中的使用方法

Android内核模块中使用的动态调试技术基于Linux DebugFS。DebugFS是一个虚拟的内存文件系统,和procFS以及sysFS类似.可以用来在用户空间和内核模块之间交换数据。

首先,编译时需要确定内核配置文件中的DebugFS和Dynamic debug是打开的。

接着,就可以在开发内核模块的过程中使用 pr_debug()或者dev_debug()代替printk(),预先在关键点加上各种打印日志。

运行调试时,首先确认debugFS被加载,加载方式可以通过为init.rc脚本增加命令,或者在启动以后直接运行如下命令:

mount -t debugfs debugfs /sys/kernel/debug

当执行过以上命令后,我们可以在调试目标的文件系统中发现如下文件:

/sys/kernel/debug/dynamic_debug/control

这时pr_debug()或者dev_debug()的行为就会为这个文件所控制,开发者可以通过修改这个文件来控制调试日志是否输出。默认情况下所有的调试开关是关闭的,只有CONFIG_DYNAMIC_DΕBUG打开时才会建立这一文件,对于非调试阶段程序运行效率的影响非常小。

每一个日志都可以被单独控制或者从更高层统一控制。例如打开一个模块中的所有打印日志、一个文件中的所有打印日志、一个函数所有打印日志或者仅有指定的一行的日志。

例如:

通过这种方法我们可以在程序运行期打开我们需要的信息,同样,通过把以上命令中的“+p”变为“-p”。我们可以动态关掉调试信息。

比如关svc_process()函数的所有信息

# echo -n 'func svc_process -p' >/sys/kernel/debug/dynamic_debug/control

3 动态调试如何运行

这个章节我们走进Android内核代码,具体分析动态调试技术是如何运行的。dev_dbg()和pr_debug()运行基本一样,我们使用pr_debug()作为例子。

如果DΕBUG被定义,我们就会继续使用printk。如果DΕBUG和CONFIG_DYNAMIC_DΕBUG都没有被定义,目标代码就不保护任何调试信息。这儿我们关注dynamic_pr_debug()。

在dynamic_debug.h里面:

Unlikely ()在这里的使用是为了在调试没有打开时获得更好的运行效率。GCC编译器会根据unlikely()做优化,把调试相关代码放到跳转语句中,因为更多的情况是调试不打开的情况。

在对应模块的makef i le文件中定义了DΕBUG_HASH 和 DΕBUG_HASH2。 使 用djb2和r5哈希算法。输入参数为代码的路径,模块名称。这两个哈希值用了加速判断某一个模块的调试是打开还是关闭。

debug_f l ags =

-D"DΕBUG_HASH=$(shell ./scripts/basic/hash djb2 $(@D)$(modname))"

-D"DΕBUG_HASH2=$(shell ./scripts/basic/hash r5 $(@D)$(modname))"

可以发现当源程序被编译后,每一个使用pr_debug()语句的地方,目标代码中会插入一个_ddebug的结构体,名为descriptor,放入__verbose数据段。它包含所有调试相关信息,比如模块名称函数名称、文件名、DΕBUG_HASH DΕBUG_HASH2和调试标志。换一种说法,每一使用pr_debug()的地方,可执行代码中都会这么一段。

更加深入一些,我们解释开一段二进制代码,就会发现__verbose数据段有这样的数据:

0e38 72020000 00000000 7b020000 b8020000 0d0f0000 26010000

0e50 72020000 1c000000 7b020000 b8020000 0d0f0000 5f010000

可以发现其实这就是_ddebug数据。

同时Android内核中有个dynamic_debug模块,当系统启动起来的时候会被调用。创建一个名为control的debugFS。

在dynamic_debug.c里面:

dynamic_debug用于创建和维持一个名为debug_tables的链表。同时创建了两个哈希表,用于加速查询过程。(和前面介绍的一样,使用路径和模块名称作为key)。

当使用pr_debug()的被调试程序装载的时候,动态调试模块会装载位于__verbose数据段的数据,分析并存到链表中。从这张表里面可以看出多少pr_debug()是出于打开状况的。

在module.c里面,当一个内核模块安装时候以下函数会被调用。所有驱动模块的_verbose数据段都会被用来初始化debug_tables。

任何用户对debugFS “control”的写操作都会导致ddebug_change ()被调用。相应的 debug_tables、 dynamic_debug_enabled 和dynamic_debug_enabled2会被改动。

以打开调试为例,这三个变量会赋值为True,从而前文提到的pr_debug()里面的__dynamic_dbg_enabled()函数中所有判断条件都满足,__dynamic_dbg_enabled()返回真,调试消息被打印出。

4 用户空间的动态技术

在用户空间里面没有内核里面的这个预先定义的动态调试模块,但是当我们理解了其运行模式,我们可以运用相似的方法来进行动态调试。下面是一个简单的例子。

对应每一个debug语句,我们也加入一个__debug_desc结构到__debug数据段。

提供给用户一个函数用来打开或者关闭调试:test_debug()可以被设计成写文件后者写控制台。

在需要使用动态调试的应用程序里面加入如下代码来初始化动态调试。

这样这段程序在正常运行是不会输出“test dynamic debug”这行日志的,当我们需要打开调试日志的时候,我们通过控制台发送一个USR2信号到对应的程序:

Kill -USR2

在接收到这个信号(Signal)以后,该程序中使用DBG()输出的日志就会被打印出来。

5 总结

动态调试技术已经被广泛应用于Android内核模块开发中,越来越多的应用程序和驱动程序也在使用这样的技术,它以极少的资源消耗加速了程序开发调试的进程。

猜你喜欢

内核日志代码
一名老党员的工作日志
强化『高新』内核 打造农业『硅谷』
扶贫日志
基于嵌入式Linux内核的自恢复设计
Linux内核mmap保护机制研究
创世代码
创世代码
创世代码
创世代码
游学日志