(大连医科大学附属第二医院 信息中心,大连 116021)
嵌入式系统远程调试工具在嵌入式开发过程中起着至关重要的作用,它直接影响开发效率。国外在嵌入式调试器方面一直领先,国内普遍采用国外的工具,价格比较昂贵,而适配自主可控的国产芯片的调试系统相对缺乏,这就对嵌入式开发环境提出了新的要求[1]。嵌入式软件开发中的远程调试主要有硬件调试和软件调试两大类,硬件调试虽然实时性强,但价格高昂,通用性差,操作复杂,这给开发者应用开发过程带来极大不便; 软件调试由于其成本低,通用性强,因而得到广泛的应用[2]。这种方式通常需要在目标系统中驻留代理软件,宿主机调试器与目标机代理软件通信,调试代理实现对被调试程序的控制[3]。
本文硬件选择了Arm架构平台,设计并实现了一款基于抢占式实时操作系统T-kernel的软件远程调试工具,T-kernel是构筑于T-Engine之上的标准化开源实时操作系统内核,由TRON(the real-time operating nucleus) 发展而来[4],而TRON是日本东京大学坂村健博士于1984年提出的计算机操作系统规范,T-kernel由于其实时性及开源性,已经被安装到了全球约40亿台家用电子产品当中,占全球微处理器操作系统市场约60%[5]的份额。同时Arm架构的微处理器更是占据了当今移动终端核心的半壁江山。因此基于两者结合的远程调试工具的设计显得更具有通用性的意义。
宿主机方面采用Linux系统,使用Arm-elf-gdb打开经过Arm交叉编译器编译出来的带有调试信息的T-kernel系统应用程序文件。宿主机端作为请求方,通过通用串行接口UART与目标机相连,向目标机远程发送调试命令,并接收相应的反馈,实现交互。整体结构概要设计如图1所示。
图1 整体结构概要设计
目标机中T-kernel系统下建立调试命令服务端监听任务,实时监听来自宿主机通过串口发送来的调试命令,一旦接收到调试请求命令,监听任务即陷入Arm exception系统状态,通过T-monitor中事先注册好的异常处理程序接管系统执行,进行相应任务的解析及具体调试命令的执行,最后将结果通过串口反馈给宿主机,完成整体调试通信。
目标机中T-kernel系统在启动之初,需要将Arm exception注册进T-monitor中,T-monitor是T-kernel的启动部分,启动系统并创建调试监听任务,这里需要注意的是要将该监听任务的优先级适当调高,以便使远程调试命令及时得到服务。任务创建成功后即可实时监听串口发来的数据字符,根据RSP(Remote Serial Protocol)协议规则进行字符解析,当解析出完整的“$
图2 目标机执行流程
本文设计的监听任务只负责解析一个远程调试启动命令。当宿主机由串口虚拟终端ttyS1发送启动调试命令“target remote/dev/ttyS1”时,监听任务可以接收到“$qSupported:qRelocInsn+#9a”字符信息,即表示远程调试已由宿主机发起,此时目标机应该进入调试模式。这里利用Arm处理器的未定义指令异常模式,在监听任务中通过内嵌预设好的Arm未定义异常指令,使系统陷入启动之初注册入T-monitor中的undefined exception程序。至此系统进入异常处理模式,并在此模式下通过串口与上位机进行通信,接收并进一步解析开发者的调试命令,将不同的命令信息反馈给上位机,完成对特定内存、特定寄存器的存取察看工作,完成断点插入和删除等一系列工作以达到开发调试的目的。
图3 Debug task结构设计
调试入口的Debug task主要完成两个工作:一是负责利用串口与宿主机进行通信,二是接收到调试启动命令后跳转到未定义异常入口。因为T-kernel是抢占式内核,任务调度由优先级决定,高优先级任务具有绝对优势,因此,为了使启动调试命令能够及时得到响应,Debug task的优先级被设计成高于一般业务task,T-kernel系统优先级数值越小级别越高,这里设置为2,因为通信数据量小,故栈空间设置为4 096。在操作系统启动之初,Debug task就得到CPU使用权,并开始监听串口,如果串口没有数据,则会因为等待资源而被系统挂起并让出CPU使用权,当宿主机启动调试,数据发送到下位机串口会引起中断而唤醒该任务,从而解析命令并顺利产生异常,进入T-monitor中预先注册好的异常处理程序中。Debug task结构设计如图3所示。
该注册函数处于T-kernel系统的T-monitor中,异常入口处首先将寄存器r8~r11全部入栈,这些寄存器后续需要使用,因此先入栈保存,然后将PC寄存器地址减8字节,并将内容取出进行判断,即为发生异常时的地址内容。此处注意,本文使用的Arm处理器采用了五级流水线机制,当第三阶段PC寄存器值取指时,第一阶段的指令执行,所以PC值为当前执行指令地址值加8个字节,也就是说,对Arm指令集来说,PC指向当前指令的下两条指令的地址,因此想要得到异常发生时的地址内容,必须要PC地址减去8个字节,此处具体还要看使用的是芯片的哪种设计方式,注意其差异。
图4 Bootloader中注册程序流程
同时,这里还可以采用另外一种方案,即取出R14寄存器减4字节的地址内容,因为在Arm体系结构中,R14即为链接寄存器,就是用来存储子程序的返回地址,因此它之前的地址内容即为产生异常的指令。取出该值后进行判断,来区分产生异常的指令是否为预设的异常指令或是其它的未定义指令异常情况,以进入不同的处理分支。如果是预设的异常指令,则继续将r0~r7通用寄存器内容保存到预设好的全局变量中,然后取出spsr,spsr寄存器用于保存cpsr的状态,根据此寄存器的最低5位可以判定出发生异常前的系统模式,因为Arm处理的不同工作模式下,寄存器R8~R14是与模式相关的,具有非通用性,所以在保存它们之前需要判断出发生异常时的工作模式,并切换到该模式下,将R8~R14这些寄存器内容取出并保存到预设的全局变量结构体中,以便异常返回后恢复异常发生时的处理器工作状态。Bootloader中注册程序流程如图4所示。
调试命令实现流程如图5所示。
图5 调试命令实现流程
进入异常结构后,对于上位机的诸多调试命令,最终经过RSP协议均被解析为图5中的一系列命令。‘g’命令读取寄存器,将r0~R15包括fps和cpsr均打包发送回主机端。‘G’命令将接收到的将要修改的数据回写到相应寄存器中;‘m/M’分别对指定内存地址进行读/写操作;‘c’命令用于使停止在某异常处理状态的程序继续执行;‘s’命令用于系统进行单步执行操作。单步执行的处理,实际上就是向下一地址插入断点命令的过程,即插入一条未定义指令,该指令就是Debug task中启动调试时预设的内嵌指令。程序维护一个数据结构,分别用于存储断点地址、断点内容以及断点标志。每次进入exception handler之后,首先判断之前是否存在已经运行过的断点,如果有,则将其地址中的内容恢复,然后再根据异常发生时保存的PC寄存器内容判断程序将要运行的下一个地址,并将其地址及内容取出保存,替换为异常指令,最后返回程序继续运行,这样程序运行到下一地址时即再次陷入异常,从而达到单步调试的目的。因此,不管在宿主机端一次插入多少个断点,实际在目标机端都是一次只恢复一个断点,再插入下一个,直到程序运行结束。宿主机只是维护了一个断点集合,只要这个断点集合不被删除,程序就会循环不断地进入异常,完成开发者的调试工作。
对于单步执行的断点插入工作,比较复杂的问题是下一地址的获取方法,Arm指令的一般编码格式如下:
3128272524 212019 1615 12 110CondOpcodeSRnRdShifter_operand
其中:Cond: 指令执行的条件编码; Opcode: 指令操作编符码;S: 决定指令的操作是否影响CPSR值;Rn: 包含第一个操作数的寄存器编码;Rd: 目标寄存器编码;Shifter_operand: 表示第二个操作数[7]。
Arm指令大致格式如此,但不同的指令有其差别,经过对与跳转有关指令寻址方式进行分析,当程序进入异常时,可以通过取出 PC寄存器内容的bit[27:25]三位进行过滤,并根据其差异对照Arm指令集中与跳转有关的指令编码结构,可以判断出指令集中可能产生跳转的命令,再由特定的指令字段中找到目标地址寄存器,从而定位实际地址,或由Rd、Rn、Shifter_operand中的一个或几个组合而生成,具体取决于不同指令的编码。另外还需要额外对条件域进行判断,Cond域中的条件编码数据决定指令的跳转逻辑。BX/B/BL/BLX均为跳转地址。B/BL跳转指令结构如下:
31 28 27262524230Cond1 0 1LSigned_immed_24
在判断Cond域的前提下,该指令的目标跳转地址是将指令中的24位带符号的立即数扩展为32位,注意是带符号位进行扩展,然后将这个32位立即左移两位,将得到的值再加到PC寄存器中即可。
BLX指令有两种结构,其一如下所示:
31 28 2726252423011111 0 1HSigned_immed_24
该指令的目标地址同样需要扩展并左移两位,但结果需要再加上H位值左移一位后的数值,再加到PC寄存器中。值得注意的是需要对该指令的最高4位进行二次确认是否为全1,以此来区分B/BL指令。
对于BLX的第二种指令结构,如下所示:
31 28 27 2019 8 7 4 30Cond00010010应为00011Rm
该指令的Rm为寄存器号,在这个寄存器中即保存着跳转的目标地址。在此之前除对bit[27:25]确认为全0后,还需要进一步确认bit[24:20],以确定是否为该指令。
BX的指令格式与BLX2类似,如下所示:
31 2827 2019 8 7 4 3 0Cond00010010应为00001Rm
需要对bit[5]进行判断以进行区分,Rm寄存器中保存着跳转的目标地址。
对于内存读取指令LDR、LDM,当PC寄存器作为其目标地址时,指令从内存中读取的字数据将被当作目标地址值,指令执行后程序将从目标地址处开始执行,也起到了跳转程序的作用。LDR指令格式如下所示:
31 28272625 24232221 201916 15 12110Cond00IPU0W1RnRdAddress
对于第一操作数,当PC作为基址寄存器Rn时,内存基地址为当前指令地址加8字节的偏移量。对于第二操作数,首先需要判断bit[25]即I位,如果为1,Address则由索引寄存器Rm,即bit[3:0]与其它移位值共同组合而成;I位如果为0,Address整体则为Rn的偏移值offset_12。无论对于索引寄存器Rm中存储的值,还是直接偏移值offset_12,Rn对其偏移的方向都由bit[23]决定,当U位为1时,加上偏移量,当U位为0时,则要减去偏移量,同时偏移量的生成也有需要Rm经过移位操作后得到的结果。
LDR指对单个内存单元向单个寄存器传送数据,而LDM则可以完成批量内存数据到一组寄存器的数据传输工作。LDM指令格式如下所示:
31 2827 2524 23 22 21 2019 1615 0Cond100PUSWLRnRegisterlist
Register list分别对应r0~r15寄存器,其中编号低的寄存器对应内存区域的低地址,编号高的对应高地址。Rn中存放连续内存区域的最低地址值。与LDR类似,也有向上偏移和向下偏移之分。这里不再赘述。
最后一类为数据处理指令,第一操作数Rn的获取方式与LDR指令获取方式相同,第二操作数需要首先判断bit[25],如果为1,那么指令的操作数为直接的立即数;如果为0,则需要索引寄存器Rm参与,即偏移值由Rm寄存器中存储的指定操作数经过相应的移位操作获得。
本文介绍了关于抢占式实时嵌入式系统的远程调试功能的设计思想,并解析了实现过程,详述了调试任务的设计及调试功能的实现方式。宿主机采用了安装在VMware8.0上的centOS 7.0操作系统以及Arm_elf_gdb工具共同完成,目标机Arm芯片采用CXD3175。图6、图7为部分已经验证过的可支持的命令以及节选的调试日志。由于内容较多,故不全部列举。
图6 可支持的调试命令节选
图7 验证日志节选
参考文献
[1] 余梓奇,章建雄,马鹏,等.基于OpenOCD和DAP的嵌入式远程调试系统研究与设计[J].电子设计工程,2017,25(11):149-153.
[2] 赵俊涛,詹瑾瑜.嵌入式内核远程调试系统的研究与应用[J].计算机应用与软件, 2015,32(8):211-214.
[3] 殷绍剑,雷航,詹瑾瑜.嵌入式远程调试原理研究与实现[J].计算机应用与软件,2014(6):240-241.
[4] 坂村健.源码开放的嵌入式实时操作系统T-Kernel[M].北京:北京航空航天大学出版社,2005.
[5] 李传煌,王伟明.T-Kernel任务调度的实时性分析[J].计算机工程,2006,32(16):58.
[6] Bill Gatliff.Embedding with GNU:the gdb Remote serial Protocol[J].Embedded System Programming,1999(11):108-113.
[7] 杜春雷.ARM体系结构与编程[M].北京:清华大学出版社,2003.
[8] 蒋龙.基于GDB的嵌入式多任务调试器的设计实现与集成[D].杭州:浙江大学,2014.
[9] 殷绍剑.嵌入式多线程远程调试器研究与实现[D].成都:电子科技大学,2013.
任玉帅(工程师),主要研究方向为嵌入式软件程序设计。