林广栋 黄光红 赵纪堂 郭利锋 耿 锐
(安徽芯纪元科技有限公司 安徽 合肥 230088)
计算机软件诞生之初,调试器就是软件开发人员的重要工具。最初的调试器是基于硬件直接实现的,通过硬件设备提供计算机底层的信息。直到计算机基础软件(如操作系统、编译器)发展成熟到一定程度之后,用户友好的开源或商业化调试软件才出现。调试器的工作原理是基于处理器的硬件机制,或基于操作系统封装后的异常处理机制,结合编译器生成的调试信息,将软件的状态以方便的方式提供给软件开发人员,并支持软件开发人员对正在调试的软件的状态进行修改。调试器的基本功能包括控制软件运行、查看软件运行时的信息、修改软件执行流程或状态等。先进的调试器还会添加事件分析、向后运行等功能。时至今时,应用于计算机各领域的调试系统仍然在不断发展中,各种新技术层出不穷[1-9]。近年来的创新点主要集中在针对新型架构处理器的调试技术[3]、针对区块链软件的调试技术[5]、针对虚拟仿真环境的调试技术[6]、智能调试技术[8]等。
目前,最常见开源调试器是GNU维护的GDB[10-12]。GDB是起源于Linux环境的调试器,既可以调试本机上运行的Linux进程,也可以调试远端的嵌入式处理器。GDB定义了基于字符串的RSP(Remote Serial Protocol)通信协议与嵌入式调试硬件(如在线仿真器ICE:In-Circuit Emulator)进行通信。基于RSP和ICE,GDB可以对嵌入式处理器进行调试操作,如查看/修改寄存器、查看/修改内存、暂停/运行处理器等等。结合GNU的另一个工具GDBServer,还可以调试远端Linux系统上运行的进程。GDB是开源软件,已经被很多处理器厂商移植用于嵌入式处理器的调试,是目前使用最广泛的调试工具[13-16]。但GDB对由数百片芯片组合而成的处理器阵列的调试功能支持不够完善。
LLVM基金会维护的LLDB是近年来新开发的调试器软件[17]。由于它和著名的开源编译器LLVM一起维护,也值得引起关注。LLDB的目标是解决多线程调试、C++模板、重载等问题。它采用与GDB完全不同的软件架构,其命令接口也与GDB不兼容。LLDB已经可以在macOS系统上成熟地使用。但LLDB的应用范围目前还比较窄,其在Linux系统上虽然可以使用,但仍然在改进。LLDB目前还不支持调试Windows上的程序。
另外,还有一些比较小众的商业调试器,如德国的劳特巴赫(LAUTERBACH)公司开发的TRACE32工具[18]。TRACE32是一款比较高端的调试器,价格非常昂贵。TRACE32开发工具拥有非常强大的功能,包括基本调试配置、RTOS、多核系统、虚拟目标调试、能耗分析以及强大的脚本语言等功能,可以支持市场中使用的80多种常见的微处理器架构。在TRACE32的界面中,用户可以使用菜单、鼠标完成调试操作,也可以使用命令行操作。命令行操作可以完成菜单鼠标可以实现的所有功能,而且具有更大的灵活性。TRACE32的命令接口与GDB也是不兼容的。
美国的Green Hills公司是业界领先的嵌入式软件平台和开发环境供应商,专注于研发嵌入式编译器、调试器、操作系统、开发环境[19]。该公司推出的TimeMachine调试套件是业界首个为开发人员在程序运行的时间维度上提供向前和向后完全可视性的调试器。该调试器可以在程序出错时让程序按时间顺序向后连续或单步运行,从而快速准确地找出发生错误的地方。该调试器还可以采集操作系统内核服务调用等操作系统事件的跟踪数据。该调试器提供的PathAnalyzer工具可以显示程序调用栈的历史变化记录。
TRACE32和TimeMachine都是商业收费软件,应用范围并不广泛。目前,应用最广泛、研究最集中的还是开源调试器软件GDB。本文将以GDB为参考,介绍一款自主可控的调试器软件。
本文介绍了一款新颖的调试器软件架构。该调试器软件没有在开源调试器软件的基础上进行改造,而是完全正向开发。其核心模块调试信息解析模块更是完全自主开发的。相比开源调试器软件,该调试器软件具有代码量少、对多核/多芯片/多板卡调试的支持更完备的特点。
“魂芯”系列DSP是由中国电子科技集团公司第三十八研究所自主研发的高性能多核DSP。“魂芯”系列DSP配套有完备的基础软件,包括编译器、汇编工具链、调试器、操作系统、集成开发环境等。“魂芯”配套的调试器软件称为MCCD(Multi Core Code Debugger)[20-24]。MCCD从2012年开始开发,其命令行接口基于GDB的MI接口改造而来,目前也支持与GDB基本兼容的简写命令集。与GDB相比,MCCD具有如下特点:
GDB的8.3版的代码库有4 850个文件,两百多万行代码。而MCCD目前只有111个文件,5万余行代码。MCCD目前基本满足可视化开化环境的调试需求,且支持C/C++高级语言调试。MCCD虽然没有GDB功能丰富,但是完全够用。由代码量可以看出,MCCD是一个非常简洁的调试器软件。
根据“魂芯”的硬件架构和软件环境进行深度定制化开发。针对“魂芯”的硬件架构和软件环境,MCCD在如下几个方面进行了定制化的开发:多核调试、反汇编、芯片寄存器/内存段定义、C语言系统调用(输入输出、文件操作等)、板级配置、字/字节调试信息转换等。这些功能经过多年的测试,已经成熟。
支持处理器集群调试。GDB对处理器集群调试的支持比较薄弱。GDB使用inferior的概念来管理一个被调试的进程,当需要调试多个进程时,要通过add-inferior命令添加inferior。GDB的调试命令只能发给一个inferior。如果要调试其他进程,需要通过inferior infno命令来设置infno号inferior为当前调试进程。此后GDB收到的调试命令都是针对该进程。当通过RSP协议调试嵌入式系统时,一个嵌入式设备相当于一个被调试的进程。可见,GDB同时只能对一个RSP协议管理的远程调试链接进行调试。而MCCD经过定制化开发,已经可以支持对处理器集群的调试。通过解析调试命令后把底层调试通信包分发给不同的调试终端,MCCD支持一条调试命令同时对不同嵌入式设备上的多个处理器执行多核调试操作。
GDB相比MCCD,命令行接口更为丰富,且支持对Python、FORTRAN等语言的调试。但是短期内没有在DSP上调试除C/C++语言以外的程序的需求。因此,MCCD的调试功能目前来看是足够满足“魂芯”DSP调试需求的。
GDB是由GNU开源组织维护的开源调试器软件,是Linux系列下以及大多数嵌入式芯片调试的首选工具。GDB一直被持续维护和更新,目前已经更新到8.3.1版。GDB既支持对本机运行的程序进行调试,也支持通过RSP协议对远程运行的程序进行调试。大多数嵌入式芯片的开发环境都通过移植GDB来支持调试。常见的做法是新增一个协议转换软件,该软件一方面通过RSP协议与GDB进行通信,一方面通过私有协议与嵌入式调试硬件进行通信。MCCD与GDB在调试功能上的对比结果见表1。可以看出,相对于GDB,MCCD在处理器集群调试方面具有一些独特的优势。
MCCD内部的整体架构如图1所示。MCCD以命令行接口的形式提供给用户或集成开发环境使用。MCCDMI负责接收用户输入并打印MCCD输出。无论MCCD调试多少个芯片,其命令行接口只有一个。因此,MCCDMI模块只有一个实例。MCCDDebugger负责管理一个调试终端控制的多个芯片。调试终端代表一个与MCCD进行通信,并被MCCD调试的实体,可以是一个在线仿真器,也可以是一个芯片内部的具有调试功能的MCU。一个调试终端与MCCD之间通过一个TCP/IP协议进行通信。MCCD每多管理一个调试终端,就多实例化一个MCCDDebugger。随着MCCD管理的调试终端的增多,MCCDDebugger的实例也随之增多。
图1 MCCD内部运行时软件实例架构图
每个MCCDDebugger实例负责具体的各种调试功能,其内部架构如图2所示。
图2 MCCDDebugger内部架构图
各模块的功能简介如表2所示。
表2 MCCD内部各主要模块简介
续表2
下面以查看变量为例展示上述各模块之间互相配合完成调试动作的整体流程。按存储位置划分,变量分为局部变量和全局变量两类。局部变量的位置需要分析当前活动的函数栈,找到用户当前选择的调试函数帧,结合调试信息中存储的局部变量偏移位置,得到局部变量的地址。全局变量的地址可以直接通过调试信息获取。按复杂程度划分,变量又可以分为简单变量和复杂变量。普通的int、long long、float、double型变量为简单变量,得到其地址后通过读内存就可以获取其值。struct、数组、指针类型的变量为复杂变量,得到这些变量的地址后,还需要通过更复杂的解析方法,得到其内部的struct元素、数组元素或指针指向的地址。若用户要查看的是复杂变量内部包含的更深层次的元素,则需要逐层解析,直到找到用户指定的内部元素的地址。图3展示了一般的查看变量调试功能实现流程。受篇幅限制,图中没有展示查看复杂变量的具体流程,查看复杂变量的过程可参考文献[12]。图中各序号按递增的顺序代表完成查看变量调试动作需要执行的步骤。各步骤简述如下:
(1) 用户或IDE(集成开发环境)发送查看变量命令。
(2) 根据调试命令中指定的核号或当前默认调试核号,找到管理该调试对象的MCCDDebugger类实例。
(3) 通过ThreadManager模块,得到当前活动栈的当前调试帧的位置。
(4) 通过DebugInfo模块,结合当前调试帧的位置,得到用户要查看的变量(或变量内部的元素)的地址。
(5) 向MemoryManage模块发送读取内存请求,此时的内存地址为软件看到的32位地址。
(6) 判断该地址是否属于芯片外部地址,如DDR上的地址。若属于,进行相应的地址转换。
(7) 判断该地址是否属于芯片内部的地址,如片上SRAM的地址。若属于,根据多核处理器的核内地址和全局地址的转换规则,进行相应的转换。
(8) 向TargetDebug模块发送读内存请求。此时内存地址为硬件实现上的最终物理地址。
(9) TargetDebug向嵌入式系统上的调试终端发送查看内存命令,获取该地址上的值。
(10) TargetDebug返回指定硬件地址上的值给MemoryManage。
(11) MemoryManage返回软件地址上的内存值。
(12) MCCDebugger返回用户指定的变量的值、类型。
(13) MCCDMI根据变量的值、类型等信息,将变量值转换为字符串,输出查看变量命令的返回结果。
图3 查看变量调试功能实现流程
依赖倒置原则(Dependence Inversion Principle)是面向对象编程的六大基本原则之一。该原则要求程序尽量信赖于抽象接口,而不是具体实现。该原则可以通俗地表达为“对接口编程,而不是对实现编程”。基于该原则,可以使软件模块之间的依赖关系限定到模块的接口,与模块的具体实现解耦,使软件易于扩展、修改。MCCD各子模块的设计都遵守这一原则。每个功能模块,首先设计独立的接口,MCCDDebugger只调用这些基类定义的接口。接口是一个抽象基类,该类中只定义纯虚函数,不定义成员变量和实体函数。这些纯虚函数代表一个模块对外提供的接口。每个模块的功能实现在该基类的子类中。若该模块在不同的情况下有不同的实现,则定义不同的子类,每个子类独立地实现相同的基类接口。MCCDDebugger根据实际运行时的情况,决定实例化哪个子类。MCCDDebugger中,始终维护一个基类的指针,并调用基类提供的接口。根据虚函数的性质,实际运行时,MCCDDebugger中的函数调用会自动转化为对具体子类实现的调用。
IRemoteComm是TargetDebug内部使用的与硬件仿真器或芯片模拟器通信模块接口。如图4所示,IRemoteComm是一个包含纯虚函数的基类,它有两个子类UDPRemoteComm和TCPRemoteComm。这两个子类中会定义实现具体通信功能的函数。在某些硬件仿真器中,由于片上SRAM的限制,不能使用TCP协议,只能使用UDP协议。因此,MCCD既支持TCP通信,又支持UDP通信。UDPRemoteComm负责实现UDP调试通信协议,TCPRemoteComm负责实现TCP调试通信协议。MCCDDebugger在开启一个调试会话时,根据用户的选择来决定实例化UDPRemoteComm还是TCPRemoteComm。MCCDDebugger中的实现代码,只调用IRemoteComm中定义的函数接口,具体运行过程中,会自动调用其子类中的函数。
图4 通信基类与子类关系
某些模块的所有子类有一些共同的实现,或需要相同的成员变量。这些相同的成员变量和实现可以再集中到一个类中。具体的不同实现再定义为这个类的子类。如图5所示,IDisAssemble定义了反汇编类的接口。所有反汇编类的实现都有一些共同的操作,如根据机器码中的值格式化字符串、获取机器码中的若干位等。这些相同的操作若分别在各子类中实现,则代码会非常冗余。因此,定义类CDisAssemble,该类实现了这些共同操作。针对具体不同芯片的反汇编类再继承自此子类。DisAssemble100、DisAssemble1042、DisAssemble1041分别是针对不同芯片类型的反汇编子类。这些子类只需要实现该芯片的指令集中的不同操作码即可。
DisAssemblePred代表对一种带有谓词控制功能的指令集的反汇编功能。该类中定义了对机器码中的谓词进行反汇编的常用操作。所有带有谓词控制功能的芯片的反汇编类都继承自DisAssemblePred。HXDSP1041芯片的指令集带有谓词控制功能,因此其反汇编类DisAssemble1041继承自DisAssemblePred。
图5 反汇编基类与子类关系
设计模式是伽马等[25]基于软件开发工业中常见的优秀设计方法总结提炼出的软件设计一般方法。软件按照设计模式总结的方法进行设计,会易于维护、扩展。设计模式为软件从业人员提供了一套易于交流的概念和方法。目前公认的设计模式有23条。MCCD使用C++语言实现,为简化软件架构、方便代码维护,大量使用设计模式。
1) 单件模式。单件模式确保一个类在整个程序中只有一个实例,且这个实例在整个程序中是共享的。无论调试的对象有多少个,MCCD都只有一个命令行接口。MCCD中使用MCCDMI类来解析调试命令,MCCDMI类只需要实例化一次。MCCD中使用单件模式实例化MCCDMI类。
2) 工厂方法。工厂方法定义类的一种方法,由该方法根据当程序的具体情况决定实例化哪个类。工厂方法返回一个该类的抽象接口。根据芯片类型的不同,各种调试动作的实现方法也不同,MCCDDebugger应根据不同的芯片类型分别实例化不同的调试功能类。每个调试功能类都有一个对应的工厂方法,该工厂方法的参数是芯片类型,返回该调试功能子类的抽象接口。该工厂方法内部,根据不同的芯片类型,实例化该调试功能基类的不同子类,并返回实例化的子类的指针。根据不同的芯片类型,该工厂方法“制造”出不同的功能子类,但在MCCDDebugger内部仍然以抽象接口的方式使用这些功能子类。这些抽象接口以虚函数的方式定义,会在运行时调用实例化的子类的具体的实现。
3) 状态模式。状态模式使一个对象的内部状态改变时改变其行为。当判断当前对象所处状态的表达式太过繁琐时,可以把对象的行为转移到代表不同状态的子类中。调试终端可能处于不同的状态下,如未连接状态、已连接状态,调试芯片已选择状态等。每种状态下,同样的调试操作有不同的表现,需要以不同的方式实现。若简单地让每种调试操作根据所处的状态作不同的处理,则各调试操作函数内部将会有类似的对调试对象状态对进判断的语句。如果以这种方式实现,不同状态下的调试功能实现代码将耦合在同一个类的各个方法中,难以维护和扩展。MCCD使用状态模式,定义一个实现各种调试操作的抽象接口基类MCCDDebugState。该接口基类是一个纯虚类,只定义抽象接口。再定义该接口基类在各种状态下的具体实现子类,如DisconnectedMCCDDebugger、UnselectedMCCDDebugger、SelectedMCCDDebugger等。每个子类仅仅代表一种状态下该调试操作的具体实现形式。MCCDDebugger把调试操作代理给调试操作的状态子类,由该子类完成具体调试操作。当调试对象的状态发生变化时,只需要把状态基类的指针替换为新状态子类的指针即可。通过这种方式,把不同状态下调试操作的具体实现方式分离开来,便于代码扩展与维护。
MCCD一个可执行文件可以支持调试HXDSP100、HXDSP1042、HXDSP1041、HXDSP2441等多种芯片。每种芯片的寄存器数量、名称、地址不同,内存段数量、大小、地址也不同。如果将这些信息固定在代码中,将会导致代码与芯片配置绑定,不利于代码维护,不利于运行时查错。MCCD使用两种XML文件描述与功能无关的信息:芯片配置XML文件、板级配置XML文件。芯片配置XML文件描述芯片内部的、仅仅与芯片相关的信息,如寄存器名称、读写属性、地址等。而板级配置XML文件描述芯片外部的、与不同板卡相关的信息,如FLASH类型、大小、DDR配置信息等。通过这种方式,将调试功能实现流程用代码实现,而与具体芯片、板卡相关的信息用XML文件描述。这样,MCCD软件的代码中不会出现具体的寄存器地址、FLASH大小等信息,易于代码维护。在MCCD使用过程中,根据需要选择配置不同的XML文件或修改XML文件的内容,以适应不同的芯片或板卡,方便了MCCD软件发行之后的维护。
1) 芯片配置文件。芯片描述XML文件描述芯片内部的信息,主要包括如下内容:(1) 片上SRAM的名称、地址、大小;(2) 流水线各级的名称、流水线程序地址、流水线指令寄存器地址;(3) 寄存器名称、读写属性、地址等。
图6为一个芯片描述XML文件中部分内容的示例图。这段XML文件的内容描述了4个寄存器的相关信息。其他片上寄存器的信息也以类似的方式描述。
图6 芯片描述XML文件部分内容示例
每次MCCD开启一次调试会话前,首先加载芯片描述XML文件,解析其中的内容,把相关的信息存储到RegisterManager、MemoryManager、PipelineManager等类的实例中。之后的调试功能实现时,到这些类的实例处获取这些信息即可。
2) 板级配置文件。MCCD调试器不但要支持对芯片内部资源的调试操作,还要支持对板卡上资源的调试操作。这些操作包括:烧写/擦除片外Flash、读/写DDR等。若板卡上包含多个HXDSP芯片,MCCD要支持对所有HXDSP芯片的多核调试。而片外资源对每种板卡都是不同的,且和芯片类型并没有直接关系。因此,设计板级配置描述XML文件,用来描述板卡上的资源信息。
该XML文件描述的信息包括:(1) 板上芯片的数量、每个芯片的ID等;(2) 片外Flash的种类,如SPI、并口,I2C ROM等;(3) Flash信息,包括页大小、扇区大小、块大小等;(4) DDR的物理地址、大小、配置信息等。
板级描述XML文件遵守多核联盟(MCA:Multicore Association)定义的SHIM软硬件接口标准[26-27]。多核联盟是由多家商业公司和学术机构组成的国际组织,致力于硬件描述文件、运行时API接口等软硬件接口的标准化。SHIM标准的目标是为日益多样化的多核、多处理器、多板卡硬件资源定义一种统一的描述标准,以方便上层软件以统一的方式使用不同的硬件资源。板卡描述XML文件主要定义板卡上的互联关系,互联关系信息中指定连接的器件的XML文件名称。每个器件有一个单独的XML文件来描述,如果一个器件被多个板卡使用,则只需要一份描述该器件信息的XML文件即可。图7是板级互联关系XML文件的片断示例。该示例中定义了两个DDR连接关系、一个并口Flash设备连接关系。DDR设备连接关系记录了DDR颗粒的XML描述文件名称,以及连接到芯片上的哪个DDR接口。Flash设备连接关系记录了Flash的XML描述文件以及连接到的芯片并行接口。该文件中还会记录DSP芯片的个数、种类以及芯片描述XML文件的名称。
硬件架构以及板卡配置相关信息在配置文件中描述。MCCD根据这些配置文件实现与硬件有关的调试操作。MCCD的主体代码只与调试功能实现流程、调试信息管理、硬件配置信息管理有关。若要扩展到其他架构的处理器,只需要修改硬件配置文件,并对MCCD的代码进行局部修改即可。具体需要修改的模块包括:通信协议管理模块、反汇编模块、芯片配置信息管理模块。根据不同芯片对调试时内核运行控制的方式不同,可能还需要对“运行控制模块”进行适配和改造。对这些模块的修改都不需要修改原有代码,只需要在这些模块的接口基类的基础上继承产生一个新子类即可。
本文描述了一款自主可控调试器软件的架构设计方案,并比较了该调试器软件与开源调试器软件GDB相比的特点。该调试器软件已经成功应用于“魂芯”各型号DSP,并支持基于可视化开发环境的调试功能。该调试器软件相比GDB,命令行接口不够丰富,但在集群调试等功能点上也有自己的特色。该调试器软件的代码量少,架构灵活,可以同时调试多种不同架构的芯片。通过修改芯片配置XML文件、板级配置XML文件,并对少数模块进行移植,该调试器软件可以很容易地移植到其他硬件架构的处理器。该调试器软件的架构设计值得国内自主可控调试器软件开发设计人员借鉴参考。