全网首个GDB移植手册【Howto:Porting the GUN Debugger】翻译

Howto:Porting the GUN Debugger

【作者】电子科大不知名程序员
📣【说明】:本文是自己在搭建mcore架构GDB时的参考的手册,具有很强的学习指导性,因原文档(链接:https://www.embecosm.com/appnotes/ean3/embecosm-howto-gdb-porting-ean3-issue-2.html)为纯英文,因此自行将它翻译用于个人学习,现将它分享出来,供各位参考学习。

目录

第一章:介绍

本文档是对GDB([3],[4],[5])现有文档的补充。其目的是帮助首次将GDB移植到新体系结构的软件工程师。

本应用注释基于作者迄今为止的经验,将在未来版本中进行更新。欢迎提出改进建议。

1.1. 缘由

尽管GDB项目包括一个关于其内部的100页指南,但该文档主要面向那些希望开发GDB本身的人。该文档还存在三个局限性。它倾向于在详细级别上进行文档编写。单个函数描述得很好,但很难获得整体情况。它是不完整的。许多最有用的部分(例如关于帧解释的部分)尚未编写。它往往过时。例如,与UI独立输出相关的文档描述了一些不再存在的函数。因此,面对首次将GDB移植到新体系结构的工程师必须通过阅读源代码并查看其他体系结构的移植方式来了解GDB的工作原理。本应用注释的作者在将OpenRISC 1000体系结构移植到GDB时经历了这个过程。本文档捕捉了这一学习经验,旨在帮助其他人。

1.2. 目标受众

如果您即将开始将GDB移植到新体系结构,本文档适用于您。如果在您的努力结束时您有更多了解,请通过添加到本文档来提供帮助。如果您已经经历了移植过程,请通过添加到本文档来帮助其他人。

第二章:GDB内部概述

GDB主要涉及三个主要领域:

  1. 用户界面:GDB与用户的通信方式。
  2. 符号方面:对目标文件的分析,以及将其中包含的信息映射到相应源文件。
  3. 目标方面:执行程序并分析其数据。

GDB对处理器有一个非常简单的视图。它有一个内存块和一个寄存器块。执行代码将其状态保存在寄存器和内存中。GDB将该信息映射到正在调试的源级程序。

将新体系结构移植到GDB意味着提供一种读取可执行文件的方法,ABI的描述,物理体系结构的描述以及访问正在调试的目标的操作。

GDB最常见的用途可能是调试其实际运行的体系结构。这是本地调试,其中主机和目标的体系结构相同。

对于OpenRISC 1000,通常在与目标分离的主机上运行GDB(通常是工作站),通过JTAG连接到OpenRISC 1000目标,使用OpenRISC 1000远程JTAG协议。以这种方式进行远程调试是嵌入式系统中最常见的工作方法。

2.1. GDB术语

一个完整的词汇表将在本文档末尾提供。然而,值得在前期解释一些关键概念。

  • Exec或Program(执行程序或程序): 一个可执行程序,即可以独立运行的二进制文件。通常,用户文档中使用术语“程序”,而在注释和GDB内部文档中使用术语“exec”。
  • Inferior(下位): 代表已运行、正在运行或将来会运行的程序或exec的GDB实体。一个下位对应一个进程或核心转储文件。
  • 地址空间: 一个GDB实体,可以解释地址(即CORE_ADDR类型的值)。下位必须至少有一个地址空间,而下位可以共享一个地址空间。
  • Thread(线程): 下位内的单个控制线程。

对于GDB的OpenRISC 1000移植是为了进行“裸金属”调试,因此将仅具有单个地址空间和具有单个线程的下位。

2.2. 主要功能区域和数据结构

2.2.1. 二进制文件描述(BFD)

BFD是一个允许应用程序使用相同的例程在不同的目标文件格式上操作的包。通过创建一个新的BFD后端并将其添加到库中,可以支持新的目标文件格式。

BFD库后端创建了多个描述特定类型目标文件中的数据的数据结构。最终,为每个个体体系结构定义了一个唯一的枚举常量(类型为enum bfd_architecture)。然后,此常量用于访问与特定体系结构的BFD相关联的各种数据结构。

对于OpenRISC 1000的32位实现(可能是COFF或ELF二进制),枚举常量为bfd_arch_or32。

BFD是binutils软件包的一部分。任何打算支持GNU工具链的体系结构都必须提供binutils实现。

OpenRISC 1000得到了GNU工具链的支持。已经存在的BFD后端适用于在RTEMS或Linux操作系统中使用的32位OpenRISC 1000映像的ELF或COFF格式。

2.2.2. 架构描述

GDB描述要由其调试的任何体系结构都在struct gdbarch中。当要调试对象文件时,GDB将使用其BFD中捕获的关于对象文件的信息来选择正确的struct gdbarch。

struct gdbarch中的数据既有助于符号方面的处理(它还使用BFD信息),又有助于目标方面的处理(与帧和目标操作信息结合使用)。

struct gdbarch是数据值(例如整数中的字节数)和执行标准操作的函数(例如打印寄存器)的混合体。主要的功能组包括:

  • 描述硬件体系结构详细信息的数据值。例如,字节顺序、地址中的位数以及字中的位数。其中一些数据包含在BFD中,struct gdbarch中引用了BFD。还有一个结构struct gdbarch_tdep,用于捕获超出标准struct gdbarch范围的附加特定目标的数据。
  • 描述所有标准高级标量数据结构(char、int、double等)的数据值。
  • 访问和显示寄存器的函数。GDB包括“伪寄存器”概念,即在体系结构内不存在但在体系结构中具有意义的寄存器。例如,在OpenRISC 1000中,浮点寄存器实际上与通用寄存器相同。但是,可以定义一组浮点伪寄存器,以便以浮点格式显示GPR。
  • 访问堆栈帧信息的函数。这包括设置“虚拟”帧以允许GDB评估函数(例如使用call命令)。

体系结构将需要指定struct gdbarch的大多数内容,为此提供了一组函数(全部以set_gdbarch_开头)。对所有条目提供了默认值,在少数情况下,这些默认值将是合适的。

分析执行程序的堆栈帧在不同情况下需要不同的方法,与每个struct gdbarch相关联的一组函数用于识别堆栈帧并分析其内容。

提供了一组实用函数,用于访问struct gdbarch的成员。可以使用gdbarch_xyz(g,…)访问由g指向的struct gdbarch的元素xyz。这将使用gdb_assert检查g是否已定义,并在函数的情况下检查g->x是否不为NULL,并返回g->xyz的值(对于值)或调用g->xyz(…)的结果(对于函数)。这样可以使用户在每次函数调用之前测试存在性,确保任何错误都得到干净处理。

2.2.3. 目标操作

为了实现目标端功能,需要一组操作来访问使用struct gdbarch描述的目标体系结构的程序。对于任何给定的体系结构,可以使用GDB target命令指定与目标连接的多种方式。例如,在OpenRISC 1000体系结构中,连接可以直接到通过主机计算机的并行端口连接的JTAG接口,或通过TCP/IP上的OpenRISC 1000远程JTAG协议。

这些目标操作在struct target_ops中描述。与struct gdbarch一样,这包括数据和函数的混合。主要功能组包括:

  • 用于建立和关闭与目标的连接的函数。
  • 用于访问目标上的寄存器和内存的函数。
  • 用于在目标上插入和远程断点以及监视点的函数。
  • 用于启动和停止在目标上运行的程序的函数。
  • 一组描述目标特性的数据,因此可以应用哪些操作。例如,在检查核心转储时,可以检查数据,但无法执行程序。

与struct gdbarch一样,为struct target_ops的值提供了默认值。在许多情况下,这些默认值已经足够,因此不需要提供。

2.2.4. 将命令添加到GDB

GDB的命令处理旨在具有可扩展性。一组函数(在cli-decode.h中定义)提供了这种可扩展性。

GDB将其命令分组为多个命令列表(struct cmd_list_element的列表),由多个全局变量指向(在cli-cmds.h中定义)。其中,cmdlist是所有定义命令的列表。单独的列表定义各种顶级命令的子命令。例如,infolist是所有info子命令的列表。

命令还根据它们处理的领域进行分类,例如提供支持的命令、检查数据的命令、文件处理等命令。这些类别由在command.h中定义的enum command_class指定。这些类别提供了在其中给出帮助的顶级类别。

2.3. GDB体系结构规范

为了为新体系结构创建GDB描述,通常在源文件arch-tdep.c中按照约定定义了一个名为_initialize_arch_tdep的全局函数。在OpenRISC 1000的情况下,此函数称为_initialize_or1k_tdep,位于文件or1k-tdep.c中。

包含实现_initialize_arch_tdep函数的对象文件的规范在GDB configure.tgt文件中指定,该文件包括一个大型的case语句,与configure命令的–target选项进行模式匹配。

在_initialize_arch_tdep函数内部通过调用gdbarch_register创建新的struct gdbarch:

void gdbarch_register (enum bfd_architecture    architecture,
                       gdbarch_init_ftype      *init_func,
                       gdbarch_dump_tdep_ftype *tdep_dump_func);

例如,_initialize_or1k_tdep通过调用以下方式为32位OpenRISC 1000架构创建其架构:

gdbarch_register (bfd_arch_or32, or1k_gdbarch_init, or1k_dump_tdep);

架构枚举将标识此架构的唯一BFD(参见第2.2.1节)。init_func被调用以创建并返回新的struct gdbarch(参见第2.3节)。tdep_dump_func是一个将转储与该体系结构相关的目标特定详细信息的函数(也在第2.3节中描述)。

gdbarch_register调用(参见第2.2节)指定了一个函数,该函数将为特定的BFD体系结构定义struct gdbarch。

struct gdbarch  gdbarch_init_func (struct gdbarch_info  info,
                                   struct gdbarch_list *arches);

例如,在OpenRISC 1000体系结构的情况下,初始化函数是or1k_gdbarch_init。

[提示] 按照惯例,GDB中所有特定目标的函数和全局变量都以唯一于该体系结构的字符串开头。这有助于在使用C时避免命名空间污染。因此,所有特定于MIPS的函数以mips_开头,特定于ARM的函数以arm_开头,依此类推。对于OpenRISC 1000,所有特定目标的函数和全局变量都以or1k_开头。

2.3.1. 查找现有体系结构

架构初始化函数的第一个参数是一个包含有关此体系结构的所有已知信息的struct gdbarch_info(从提供给gdbarch_register的BFD枚举中推断出)。第二个参数是GDB中当前定义的体系结构列表。

查找是使用gdbarch_list_lookup_by_info完成的。它接受现有体系结构的列表和struct gdbarch_info(可能已更新)作为参数,并返回找到的第一个匹配体系结构,如果没有找到则返回NULL。如果找到体系结构,初始化函数可以完成,将找到的体系结构作为结果返回。

2.3.1.1. struct gdbarch_info

struct gdbarch_info具有以下组件:

struct gdbarch_info
{
  const struct bfd_arch_info *bfd_arch_info;
  int                         byte_order;
  bfd                        *abfd;
  struct gdbarch_tdep_info   *tdep_info;
  enum gdb_osabi              osabi;
  const struct target_desc   *target_desc;
};
  • bfd_arch_info包含有关体系结构的关键详细信息。
  • byte_order是指示字节顺序的枚举。
  • abfd是指向完整BFD的指针。
  • tdep_info是额外的自定义目标特定信息。
  • gdb_osabi是一个枚举,标识该体系结构使用的操作系统特定ABI(如果有)。
  • target_desc是一组名称-值对,提供有关在此目标中使用的寄存器的信息。

在调用struct gdbarch初始化函数时,并非所有字段都提供了——仅提供了可以从BFD推断出的字段。struct gdbarch_info用作具有现有体系结构列表的查找键(初始化函数的第二个参数),以查看是否已存在合适的体系结构。在此查找之前,可能会添加tdep_info、osabi和target_desc字段以细化搜索。

2.3.2. 创建新体系结构

如果未找到体系结构,则必须通过使用提供的struct gdbarch_info和struct gdbarch_tdep中的任何附加自定义目标特定信息来调用gdbarch_alloc来创建新体系结构。

然后,新创建的struct gdbarch必须进行填充。虽然有默认值,但在大多数情况下,它们不是所需的。对于每个元素X,都有一个相应的访问函数来设置该元素的值,即set_gdbarch_X。

以下各节标识了以这种方式应设置的主要元素。这不是完整列表,但代表了通常必须为新体系结构指定的函数和元素。许多函数在头文件gdbarch.h中进行描述,许多可以在GDB Internals文档[4]中找到。

2.3.2.1. struct gdbarch_tdep
struct gdbarch *gdbarch_alloc (const struct gdbarch_info *info,
                               struct gdbarch_tdep       *tdep);

struct gdbarch_tdep在GDB中未定义——如果需要保存标准struct gdbarch不包含的自定义目标信息,则用户必须定义此struct。例如,在OpenRISC 1000架构中,它用于保存目标中可用的匹配点数量(以及其他信息)。如果没有其他目标特定信息,可以将其设置为NULL。

2.3.3. 指定硬件数据表示

struct gdbarch中的一组值定义了在体系结构内部如何表示不同的数据类型。

  • short_bit:C/C++ short变量中的位数。默认值是2*TARGET_CHAR_BIT。TARGET_CHAR_BIT是一个已定义的常量,如果未显式设置,则默认为8。
  • int_bitlong_bitlong_long_bitfloat_bitdouble_bitlong_double_bit:这些与short类似,分别表示对应类型的C/C++变量中的位数。对于int、long和float,默认值是4TARGET_CHAR_BIT,对于long long、double和long double,默认值也是4TARGET_CHAR_BIT。
  • ptr_bit:C/C++指针中的位数。默认值是4*TARGET_CHAR_BIT。
  • addr_bit:C/C++地址中的位数。几乎总是与指针中的位数相同,但有一小部分体系结构的指针无法到达所有地址。默认值是4*TARGET_CHAR_BIT。
  • float_formatdouble_formatlong_double_format:这些指向一个C结构数组(每个都有一个字节序),定义了每个浮点类型的格式。预定义了多个这样的数组。它们又基于由库libiberty定义的一组标准类型。
  • char_signed:如果char要被视为有符号,则为1,如果char要被视为无符号,则为0。默认值为-1(未定义),因此应始终设置它。
2.3.4. 指定硬件架构和ABI

struct gdbarch的一组函数成员定义了体系结构及其ABI的各个方面。对于其中的一些函数,提供了默认值,适用于大多数体系结构。

  • return_value:此函数确定给定数据类型的返回约定。例如,在OpenRISC 1000上,结构/联合和大于32位的标量作为引用返回,而小标量则在GPR 11中返回。此函数应始终定义。

  • breakpoint_from_pc:在内存中的特定位置时,返回用于断点的指令。对于具有可变长度指令的体系结构,断点指令的选择可能取决于程序计数器处指令的长度。返回指令序列及其长度。

    默认值为NULL(未定义)。如果GDB要支持此体系结构的断点,则此函数应始终定义。

  • adjust_breakpoint_address:某些体系结构根本不允许在所有点放置断点。给定一个程序计数器,此函数返回可以放置断点的地址。默认值为NULL(未定义)。只有对于无法在所有程序计数器位置接受断点的体系结构才需要定义此函数。

  • memory_insert_breakpointmemory_remove_breakpoint:这些函数插入或删除基于内存的(也称为软件)断点。默认值default_memory_insert_breakpoint和default_memory_remove_breakpoint适用于大多数体系结构,因此在大多数情况下无需定义这些函数。

  • decr_pc_after_break:某些体系结构要求在断点后将程序计数器减小,以允许在恢复时执行断点的指令。此函数返回要减小地址的字节数。默认值为NULL(未定义),这意味着程序计数器保持不变。只有在需要此功能时才需要定义此函数。

    实际上,此函数仅对最简单的体系结构有用。它仅适用于软件断点,不适用于监视点或硬件断点。通常会在目标的to_wait和to_resume函数中根据需要调整程序计数器(参见第2.4节)。

  • single_step_through_delay:如果目标正在执行延迟槽,并且需要在指令完成之前进行进一步的单步操作,则返回1。默认值为NULL(未定义)。如果目标具有延迟槽,则应实现此函数。

  • print_insn:反汇编一条指令并打印。默认值为NULL(未定义)。如果要支持代码的反汇编,则应定义此函数。

反汇编是binutils库所需的功能。此函数在opcodes子目录中定义。如果binutils已经进行了移植,则可能已经存在一个合适的实现。

2.3.5. 指定寄存器架构

GDB将寄存器视为一组成员编号从0递增的集合。该集合的第一部分对应于真实的物理寄存器,第二部分对应于任何“伪寄存器”。伪寄存器没有独立的物理存在,但对体系结构内的信息表示很有用。例如,OpenRISC 1000体系结构最多有32个通用寄存器,通常表示为32位(或64位)整数。但是,定义一组伪寄存器可能是方便的,以显示将GPR表示为浮点寄存器的情况。

对于任何体系结构,实施者将决定从硬件到GDB寄存器编号的映射。与真实硬件对应的寄存器称为原始寄存器,其余寄存器称为伪寄存器。总的寄存器集(原始和伪)称为烹饪寄存器集。

2.3.5.1. struct gdbarch函数指定寄存器架构

这些函数指定体系结构中寄存器的数量和类型。

  • read_pcwrite_pc:读取程序计数器的函数。默认值为NULL(没有可用的函数)。但是,如果程序计数器只是一个普通寄存器,则可以在struct gdbarch中指定它(见下文的pc_regnum),并将使用标准例程来读取或写入它。因此,只有在程序计数器不是普通寄存器时,才需要指定此函数。
  • pseudo_register_readpseudo_register_write:如果有伪寄存器,则应定义这些函数(参见第2.2.2节和第2.3.5.3节,了解有关伪寄存器的更多信息)。默认值为NULL。
  • num_regsnum_pseudo_regs:定义真实和伪寄存器的数量。它们默认为-1(未定义),应始终显式定义。
  • sp_regnumpc_regnumps_regnumfp0_regnum:指定保存堆栈指针、程序计数器、处理器状态和第一个浮点寄存器的寄存器。除了第一个浮点寄存器(默认为0)外,其余寄存器默认为-1(未定义)。它们可以是真实或伪寄存器。必须始终定义sp_regnum。如果未定义pc_regnum,则必须定义函数read_pcwrite_pc(见上文)。如果未定义ps_regnum,则GDB用户将无法使用$ps变量。如果目标支持浮点运算,则无需fp0_regnum。
2.3.5.2. struct gdbarch函数提供寄存器信息

这些函数返回有关寄存器的信息。

  • register_name:此函数应将寄存器编号(原始或伪)转换为寄存器名称(作为C char *)。这既用于确定寄存器的输出名称,也用于解析输入时使用的任何寄存器名称的含义。例如,在OpenRISC 1000上,GDB寄存器0-31是通用寄存器,寄存器32是程序计数器,寄存器33是监视寄存器,它们映射到字符串"gpr00"到"gpr31"、“pc"和"sr”。这意味着GDB命令print $gpr5应打印OR1K通用寄存器5的值。此函数的默认值为NULL。应始终定义它。

    从历史上看,GDB总是有一个帧指针寄存器的概念,可以通过GDB变量 f p 访问。该概念现已弃用,认识到并非所有体系结构都有帧指针。但是,如果体系结构确实具有帧指针寄存器,并且定义了一个名为 " f p " 的寄存器或伪寄存器,则该寄存器将用作 fp访问。该概念现已弃用,认识到并非所有体系结构都有帧指针。但是,如果体系结构确实具有帧指针寄存器,并且定义了一个名为"fp"的寄存器或伪寄存器,则该寄存器将用作 fp访问。该概念现已弃用,认识到并非所有体系结构都有帧指针。但是,如果体系结构确实具有帧指针寄存器,并且定义了一个名为"fp"的寄存器或伪寄存器,则该寄存器将用作fp变量的值。

  • register_type:给定寄存器编号,此函数标识其可能包含的数据类型,指定为struct type。GDB允许创建任意类型,但还提供了一些内置类型(builtin_type_void、builtin_type_int32等),以及从这些类型派生类型的函数。通常,程序计数器将具有“指向函数”的类型(指向代码),帧指针和堆栈指针将具有“指向void”的类型(它们指向堆栈上的数据),而所有其他整数寄存器将具有32位或64位整数类型。这些信息在显示寄存器信息时引导格式化。默认值为NULL,表示在显示寄存器时没有可用于指导格式化的信息。

  • print_registers_infoprint_float_infoprint_vector_info:分别定义这些函数以为GDB info registers、info float和info vector命令提供输出。默认值为NULL(未定义),表示不提供信息。如果目标分别支持浮点或矢量操作,请定义每个函数。

  • register_reggroup_p:GDB将寄存器分组到不同的类别(通用、矢量、浮点等)。给定一个寄存器和组,如果寄存器在该组中,则返回1(true),否则返回0。默认值为函数default_register_reggroup_p,该函数将根据寄存器的类型(参见上文的register_type函数)执行合理的工作,为通用寄存器、浮点寄存器、矢量寄存器和原始(即非伪)寄存器提供不同的组。

2.3.5.3. 寄存器缓存

寄存器的缓存用于在寄存器值不可能已更改的情况下,多次访问并重新分析目标。

GDB提供了struct regcache,与特定的struct gdbarch关联,以保存原始寄存器的缓存值。提供了一组函数来访问原始寄存器(其名称中带有raw)和完整的烹饪寄存器(其名称中带有cooked)。提供了函数来确保寄存器缓存与目标中实际寄存器的值保持同步。

通过struct regcache例程访问寄存器将确保在需要时调用适当的struct gdbarch函数来访问底层的目标体系结构。通常,用户应该使用“cooked”函数,因为这些函数将根据需要自动映射到“raw”函数。

两个关键函数是regcache_cooked_readregcache_cooked_write,它们读取或将寄存器的值写入字节缓冲区(类型为gdb_byte *)。为方便起见,还提供了包装函数regcache_cooked_read_signedregcache_cooked_read_unsignedregcache_cooked_write_signedregcache_cooked_write_unsigned,它们读取或写入值,并根据需要进行值的转换。

2.3.6. 指定帧处理

GDB需要了解本地(自动)变量存储在哪个堆栈上。包含函数调用的所有本地变量的堆栈区域称为该函数的堆栈帧(或俗称为“帧”)。反过来,调用该函数的函数将具有其堆栈帧,依此类推,一直返回到已调用的函数链。

几乎所有体系结构都有一个专用于指向堆栈末尾(堆栈指针)的寄存器。许多体系结构还有第二个寄存器,指向当前活动堆栈帧的起始位置(帧指针)。体系结构的具体安排是ABI的关键部分。

以下是一个说明的图表。这里是一个计算阶乘的简单程序:

#include <stdio.h>

int fact(int n)
{
    if (0 == n)
    {
        return 1;
    }
    else
    {
        return n * fact(n - 1);
    }
}

int main()
{
    int i;

    for (i = 0; i < 10; i++)
    {
        int f = fact(i);
        printf("%d! = %d\n", i, f);
    }
}

考虑代码到达第6行时的堆栈状态,此时主程序已调用fact(3)。函数调用链将是main、fact(3)、fact(2)、fact(1)和fact(0)。在此示例中,堆栈是降序的(与OpenRISC 1000 ABI使用的方式相同)。堆栈指针(SP)位于堆栈末尾(最低地址),帧指针(FP)位于当前堆栈帧中的最高地址。图2.1显示了堆栈的外观。

image-20240229095841111

图2.1 一个示例堆栈帧

在每个堆栈帧中,相对于堆栈指针的偏移为0的位置是上一帧的帧指针,而相对于堆栈指针的偏移为4(这是一个32位架构的示例)的位置是返回地址。局部变量从帧指针开始索引,使用负索引。在函数fact中,相对于帧指针的偏移为-4的位置是参数n。在主函数中,相对于帧指针的偏移为-4的位置是局部变量i,而相对于帧指针的偏移为-8的位置是局部变量f。

[注意] 注意:这只是一个用于说明的简化示例。对于这样简单的函数,优化良好的编译器可能不会将任何内容放在堆栈上。实际上,它们可能会消除递归和对堆栈的使用!

在检查堆栈时很容易混淆。GDB在整个过程中都使用严格的术语。当前执行的函数的堆栈帧编号为零。在这个例子中,帧#0是对fact(0)的调用的堆栈帧。其调用函数的堆栈帧(在这种情况下是fact(1))的编号为#1,依此类推,一直返回到调用链中。

描述帧的主要GDB数据结构是struct frame_info。它不是直接使用的,而是通过其访问器函数使用。struct frame_info包括有关帧中寄存器的信息以及指向与该帧关联的函数代码的指针。整个堆栈被表示为struct frame_info的链表。

2.3.6.1. 栈帧处理术语

在引用栈帧时很容易感到困惑。GDB使用一些精确的术语。

  • 此栈帧(THIS frame)是当前正在考虑的栈帧。
  • 下一个栈帧(NEXT frame),有时也称为内部或较新的栈帧,是由此栈帧调用的函数的栈帧。
  • 前一个栈帧(PREVIOUS frame),有时也称为外部或较旧的栈帧,是调用此栈帧的函数的栈帧。

因此,在图2.1的示例中,如果此栈帧是#3(对fact(3)的调用),则下一个栈帧是栈帧#2(对fact(2)的调用),前一个栈帧是栈帧#4(对main()的调用)。

最内部的栈帧是当前执行函数的栈帧,或者在程序停止时,例如,在对fact(0)的调用中间。它始终编号为栈帧#0。

栈帧的基址是下一个栈帧的开始之前的地址。对于下降堆栈,这将是最低地址;对于上升堆栈,这将是栈帧中的最高地址。

通常,GDB用于分析堆栈的函数通常会提供一个指向下一个栈帧的指针,以确定有关此栈帧的信息。有关此栈帧的信息包括有关前一个栈帧的寄存器存储在此栈帧中的数据。在此示例中,前一个栈帧的帧指针存储在此栈帧的堆栈指针的偏移0处。

通过将一个函数指针传递给下一个栈帧来确定有关此栈帧的信息的过程称为展开。在此过程中涉及的GDB函数通常在其名称中包含“unwind”。

确定应在struct frame_info中放置的信息的目标分析过程称为嗅探。执行此操作的函数通常称为嗅探器,并且通常在其名称中包含“sniffer”。为了提取特定帧的所有信息,可能需要超过一个嗅探器。

由于许多函数使用下一个栈帧,存在一个关于寻址最内部栈帧的问题——它没有下一个栈帧。为解决这个问题,GDB创建了一个虚拟的栈帧“#-1”,称为哨兵栈帧。

2.3.6.2. 前言缓存

所有栈帧嗅探函数通常会检查相应函数开头的代码,以确定寄存器的状态。ABI将在每个函数的开始处保存旧值并设置关键寄存器的新值,这称为函数前言。

对于特定的栈帧,此数据不会更改,因此所有标准展开函数,除了将下一个栈帧的指针作为其第一个参数之外,还会将前言缓存的指针作为其第二个参数。这可用于存储与特定栈帧相关的值,以便在涉及相同栈帧的后续调用中重复使用。

用户需要定义所使用的结构(它是void *指针),并安排存储的分配和释放。但是,对于一般用途,GDB提供了struct trad_frame_cache,其中包含一组访问例程。此结构保存了此栈帧的堆栈和代码地址,栈帧的基址,指向下一个栈帧的struct frame_info的指针,以及前一个栈帧的寄存器在此栈帧中的位置的详细信息。

通常,第一次使用下一个栈帧调用任何嗅探器函数时,此栈帧的前言嗅探器将为NULL。嗅探器将分析栈帧,分配前言缓存结构并将其填充。随后使用相同下一个栈帧调用的调用将传递此前言缓存,以便可以在无需额外分析的情况下返回数据。

2.3.6.3. struct gdbarch用于分析帧并根据需要进行调整的函数

skip_prologue:函数的序言是函数开头的代码,用于设置堆栈帧、保存返回地址等。该函数在函数的程序计数器在函数的序言中时跳过该序言。对于现代优化编译器,这可能是一个相当棘手的任务。但是,如果所需的信息在二进制文件中作为DWARF2调试信息,则该任务可能更加容易。默认值为NULL(未定义)。此函数应始终提供,但如果可用DWARF2调试信息,可以利用它。

inner_than:给定两个帧或堆栈指针,如果第一个表示“内部”堆栈帧,则返回1(true),否则返回0(false)。这用于确定目标堆栈帧是上升还是下降。参见第2.3.6节中关于“内部”帧的解释。该函数的默认值为NULL,应始终定义。但是对于几乎所有体系结构,都可以使用内置函数之一:core_addr_lessthan(用于下降堆栈)或core_addr_greaterthan(用于上升堆栈)。

frame_align:体系结构可能对其帧的对齐方式有约束。给定堆栈指针的建议地址,此函数返回适当对齐的地址(通过扩展堆栈帧)。默认值为NULL(未定义)。对于可能发生堆栈失调的任何体系结构,应定义此函数。辅助函数align_down(用于下降堆栈)和align_up(用于上升堆栈)将有助于实现此函数。

frame_red_zone_size:某些ABI在堆栈末尾保留空间,供无序言或结尾或异常处理程序使用(OpenRISC 1000属于此类)。这称为红区(AMD术语)。默认值为0。如果体系结构具有这样的红区,则设置此字段。

2.3.6.4. struct gdbarch用于访问帧数据的函数

这些函数提供对堆栈帧中关键寄存器和参数的访问。

unwind_pcunwind_sp:给定指向“THIS”堆栈帧的指针(请参阅第2.3.6节以了解帧的表示方式),分别返回“PREVIOUS”帧中的程序计数器和堆栈指针的值(即调用此函数的函数的帧)。

frame_num_args:给定指向“THIS”堆栈帧的指针(请参阅第2.3.6节以了解帧的表示方式),返回正在传递的参数数量,如果不知道,则返回-1。默认值为NULL(未定义),在这种情况下,任何堆栈帧上传递的参数数量始终是未知的。对于许多体系结构,这将是一个合适的默认值。

2.3.6.5. struct gdbarch Functions Creating Dummy Frames

GDB可以调用目标代码中的函数(例如使用callprint命令)。这些函数可能会被断点中断,如果函数碰到断点,像backtrace这样的命令仍然能够正确工作是至关重要的。

这通过使堆栈看起来好像函数是从GDB之前停止的地方调用一样来实现。这要求GDB能够设置适用于这种函数调用的堆栈帧。

以下函数提供了设置这种“虚拟”堆栈帧的功能。

  • push_dummy_call: 此函数为即将被调用的函数设置一个虚拟堆栈帧。push_dummy_call会接收要传递的参数,并必须根据ABI将它们复制到寄存器或适当地推送到堆栈上。然后,GDB将控制传递给目标代码中函数的地址,并且它将找到堆栈和寄存器的设置,就像预期的那样。

    此函数的默认值是NULL(未定义)。如果未定义此函数,那么GDB将不允许用户在被调试的目标中调用函数。

  • unwind_dummy_id: 这是push_dummy_call的反函数,它在使用虚拟堆栈帧评估函数调用后,恢复堆栈和帧指针。默认值为NULL(未定义)。如果定义了push_dummy_call,则还应定义此函数。

  • push_dummy_code: 如果未定义此函数(其默认值为NULL),虚拟调用将使用目标的入口点作为其返回地址。在那里将设置一个临时断点,因此该位置必须是可写的并且有空间容纳断点。

    可能这个默认值不太适用。它可能不可写(可能在ROM中),或者ABI可能要求在调用返回之前执行代码以展开堆栈,然后才遇到断点。

    如果有这样的情况,那么应该定义push_dummy_code以在堆栈末尾推送指令序列,该虚拟调用应该返回到该序列。

    注意: 这要求堆栈中的代码可以执行。某些哈佛架构可能不允许这样做。

2.3.6.6. 分析堆栈:帧嗅探器

当程序停止时,GDB需要使用适当的嗅探器构建表示堆栈状态的struct frame_info链。

每个架构都需要适当的嗅探器,但它们不会形成struct gdbarch的条目,因为可能需要多个嗅探器,而且一个嗅探器可能适用于多个struct gdbarch。相反,使用以下函数将嗅探器与架构关联起来。

  • frame_unwind_append_sniffer: 用于在给定指向NEXT堆栈帧的指针时添加新的嗅探器以分析此堆栈帧。
  • frame_base_append_sniffer: 用于添加新的嗅探器,该嗅探器可以确定堆栈帧的基址的信息。
  • frame_base_set_default: 用于指定默认的基址嗅探器。

这些函数都需要struct gdbarch的引用,因此它们与特定架构相关联。通常在struct gdbarch初始化函数中调用它们,此时struct gdbarch已经设置好。除非设置了默认值,否则将首先尝试最近添加的嗅探器。

主要的帧展开嗅探器(由frame_unwind_append_sniffer设置)返回一个结构,指定一组嗅探函数:

struct frame_unwind {
  enum frame_type            type;
  frame_this_id_ftype       *this_id;
  frame_prev_register_ftype *prev_register;
  const struct frame_data   *unwind_data;
  frame_sniffer_ftype       *sniffer;
  frame_prev_pc_ftype       *prev_pc;
  frame_dealloc_cache_ftype *dealloc_cache;
};

type字段指示此嗅探器可以处理的堆栈帧的类型:正常、虚拟(参见第2.3节中的push_dummy_call)、信号处理程序或哨兵。信号处理程序有时会为了效率而有自己的简化堆栈结构,因此可能需要它们自己的处理程序。

unwind_data保存可能与特定类型的堆栈帧相关的附加信息。例如,它可能保存信号处理程序帧的附加信息。

其余字段定义了在给定指向NEXT堆栈帧的指针时产生不同类型信息的函数。不需要提供所有函数。如果一个条目为NULL,则将尝试下一个嗅探器。

  • this_id: 确定THIS堆栈帧的堆栈指针和函数(代码入口点)。
  • prev_register: 确定在THIS堆栈帧中的哪个位置存储了PREVIOUS堆栈帧的寄存器的值。
  • sniffer: 查看THIS帧的寄存器以确定这是否是适当的展开器。
  • prev_pc: 确定THIS堆栈帧的程序计数器。只在程序计数器不是普通寄存器时需要(参见第2.3节中的prev_pc)。
  • dealloc_cache: 释放与此帧的prologue缓存关联的任何附加内存(参见第2.3.6.2)。

通常只有对于自定义嗅探器,需要定义this_id和prev_register函数。

基础嗅探器要简单得多。它是一个struct frame_base,它引用相应的struct frame_unwind并提供在帧内生成各种地址的函数。

struct frame_base {
  const struct frame_unwind *unwind;
  frame_this_base_ftype     *this_base;
  frame_this_locals_ftype   *this_locals;
  frame_this_args_ftype     *this_args;
};

所有这些函数都接受指向NEXT帧的指针作为参数。this_base返回THIS帧的基址,this_locals返回THIS帧中本地变量的基址,this_args返回此帧中函数参数的基址。

值得注意的是,如果无法以其他方式确定(例如存在一个名为“fp”的寄存器),那么this_base函数的结果将用作GDB中帧指针变量$fp的值。

2.4. 目标操作

与目标的通信归结为一组目标操作。这些操作存储在struct target_ops中,与描述目标行为的标志一起。struct target_ops元素在target.h中定义和记录。以下部分描述了其中最重要的一些函数。

2.4.1. 目标层次

GDB有几种不同类型的目标:可执行文件、核心转储、正在执行的进程等。在任何时候,GDB可能有多组目标操作正在使用。例如,用于执行进程的目标操作(可以运行代码)的操作可能与检查核心转储时使用的操作不同。

GDB知道的所有目标都保存在一个堆栈中。GDB通过堆栈向下遍历,以找到适用的目标操作集。堆栈被组织为一系列递减重要性的层次结构:线程的目标操作,然后是适用于进程的目标操作,用于下载远程目标的目标操作,用于核心转储的目标操作,用于可执行文件的目标操作,最后是虚拟目标的目标操作。因此,当GDB调试正在运行的进程时,它将始终选择来自process_stratum的目标操作,如果可用的话,而不是来自file_stratum的目标操作,即使file_stratum的目标操作是最近推入堆栈的。

在任何特定时间,都有一个当前目标,保存在全局变量current_target中。这永远不会为NULL - 如果没有其他目标可用,它将指向虚拟目标。

target.h定义了一组方便的宏,用于访问current_target中的函数和值。因此,current_target->to_xyz可以被访问为target_xyz

2.4.2. 指定新目标

一些目标(struct target_ops中的目标操作集)是由GDB自动设置的 - 这些包括驱动模拟器的操作(请参见第2.6节)和驱动GDB远程串行协议(RSP)的操作(请参见第2.7节)。

其他目标必须由实现者显式设置,使用add_target函数。其中最常见的是用于本机调试的本机目标。设置非本机目标的情况较少,例如,用于OpenRISC 1000的JTAG目标[1]。

2.4.2.1. 本机目标

通过在arch-os-nat.c源文件中为体系结构,arch和操作系统os定义_initialize_arch_os_nat函数,可以创建新的本机目标。在config/arch/os.mh文件中创建用于从源文件生成二进制文件的代码片段,其中包含在config/arch/nm-os.h中的头部,该头部在构建时将链接到nm.h。_initialize_函数应创建一个新的struct target_ops并调用add_target将此目标添加到可用目标的列表中。

对于新的本机目标,有可重用的标准实现,只需进行一两个更改。例如,函数linux_trad_target返回适用于大多数Linux本机目标的struct target_ops。通常只需要更改描述字段和用于获取和存储寄存器的函数。

2.4.2.2. 远程目标

对于新的远程目标,流程稍微简单。应将源文件添加到configure.tgt中,就像对体系结构描述一样(请参见第2.3节)。在源文件内,定义一个新的函数_initialize_remote_arch以实现新的远程目标,arch。

对于新的远程目标,remote.c中用于实现RSP的定义提供了一个良好的起点。

2.4.3. struct target_ops函数和变量提供信息

这些函数和变量提供关于目标的信息。第一组标识目标的名称,并为用户提供帮助信息。

  • to_shortname。这个字符串是目标的名称,用于GDB的target命令。将to_shortname设置为foo意味着目标foo将连接到该目标,调用to_open为此目标(见下文)。
  • to_longname。给出目标类型的简要描述的字符串。这将与info target命令一起打印(另请参见下文的to_files_info)。
  • to_doc。这是此目标的帮助文本。如果目标的简称是foo,那么help target命令将打印target foo,后面是这个帮助文本的第一句话。命令help target foo将打印出完整的文本。
  • to_files_info。此函数为info target命令提供附加信息。

第二组变量提供有关目标当前状态的信息。

  • to_stratum。一个枚举常量,指示struct target_ops属于哪个stratum。
  • to_has_all_memory。布尔值,指示目标是否包含整个内存,还是只包含其中的一部分。如果只有部分内存,则可以通过堆栈中的其他目标满足失败的内存请求。
  • to_has_memory。布尔值,指示目标是否具有内存(虚拟目标没有)。
  • to_has_stack。布尔值,指示目标是否有堆栈。目标文件没有,核心转储和可执行线程/进程有。
  • to_has_registers。布尔值,指示目标是否有寄存器。目标文件没有,核心转储和可执行线程/进程有。
  • to_has_execution。布尔值,指示目标当前是否正在执行。对于某些目标,这与它们是否能够执行是相同的。但是,一些远程目标可能处于未执行状态,直到调用create_inferiorattach
2.4.4. struct target_ops函数控制目标连接

这些函数控制与目标的连接。对于远程目标,这可能意味着使用TCP/IP等协议建立和拆除连接。对于本机目标,这些函数将更关心设置描述状态的标志。

  • to_open。这个函数由GDB的target命令调用。除了调用的目标名称之外,还将任何其他参数传递给此函数。to_open应该与目标建立通信。它应该设置目标的状态(例如,它当前是否正在运行),并适当地初始化数据结构。如果目标当前没有运行,这个函数不应该启动目标运行 - 这是由GDB的run命令调用的函数(to_create_inferiorto_resume)的工作。
  • to_xcloseto_close。这两个函数都应该关闭远程连接。to_close是遗留函数。新的实现应该使用to_xclose,它还应该释放为此目标分配的任何内存。
  • to_attach。对于可以在没有连接调试器的情况下运行的目标,此函数将调试器连接到运行中的目标(它应该首先被打开)。
  • to_detach。从目标分离的函数,使其继续运行。
  • to_disconnect。这类似于to_detach,但不费力地通知目标调试器正在分离。它应该只是断开与目标的连接。
  • to_terminal_inferior。此函数将目标的终端I/O连接到本地终端。此功能在远程目标中并非总是可用。
  • to_rcmd。如果目标能够运行命令,则此函数请求在目标上运行该命令。这对于远程目标最相关。
2.4.5. struct target_ops 访问内存和寄存器的函数

这些函数在目标寄存器和内存之间传输数据。

  • to_fetch_registersto_store_registers。用于将目标的值传递给寄存器缓存,并使用寄存器缓存中的值设置目标寄存器的函数。
  • to_prepare_to_store。在存储寄存器之前调用此函数以设置所需的任何附加信息。在大多数情况下,它将是一个空函数。
  • to_load。将文件加载到目标中。对于大多数实现,通用函数 generic_load 会重用用于内存访问的其他目标操作,因此非常合适。
  • to_xfer_partial。这个函数是一个通用函数,用于在目标和目标之间传输数据。它的最重要的功能(通常是唯一实际实现的功能)是从目标内存中加载和存储数据。
2.4.6. struct target_ops 处理断点和监视点的函数

对于所有目标,GDB都可以通过在目标中插入代码来在软件中实现断点和写入访问监视点。但是,许多目标为这些功能提供了更为高效的硬件支持,而且还可能实现读取访问监视点。

struct target_ops 中的这些函数提供了一种访问此类功能(如果可用)的机制。

  • to_insert_breakpointto_remove_breakpoint。这些函数在目标上插入和删除断点。它们可以选择使用硬件或软件断点。然而,如果插入函数允许使用硬件断点,则GDB命令 set breakpoint auto-hw off 将不起作用。
  • to_can_use_hw_breakpoint。如果目标可以设置硬件断点或监视点,则此函数应返回1(true),否则返回0。该函数被传递一个枚举值,指示正在查询监视点还是断点,并应使用有关当前正在使用的硬件断点/监视点数量的信息来确定是否可以设置断点/监视点。
  • to_insert_hw_breakpointto_remove_hw_breakpoint。插入和删除硬件断点的函数。如果没有硬件断点可用,则返回失败的结果。
  • to_insert_watchpointto_remove_watchpoint。插入和删除监视点的函数。
  • to_stopped_by_watchpoint。如果最后一次停止是由监视点引起的,则此函数返回1(true)。
  • to_stopped_data_address。如果最后一次停止是由监视点引起的,则此函数返回触发监视点的数据的地址。
2.4.7. struct target_ops 控制执行的函数

对于能够执行的目标,这些函数提供了启动和停止执行的机制。

  • to_resume。告诉目标再次开始运行(或第一次运行)的函数。
  • to_wait。等待目标将控制返回给调试器的函数。通常情况下,当目标完成执行或命中断点时,控制将返回。如果连接中断(例如通过 ctrl-C),它也可能发生。
  • to_stop。停止目标的函数——每当需要中断目标时使用(例如通过 ctrl-C)。
  • to_kill。终止与目标的连接。即使与目标的连接中断,这也应该有效。
  • to_create_inferior。对于可以执行的目标,这将初始化一个准备运行的程序。它由GDB的 run 命令调用,随后将调用 to_resume 来开始执行。
  • to_mourn_inferior。在目标执行结束后整理工作(例如在退出或被终止后)。大多数实现调用通用函数 generic_mourn_inferior,但可能会进行一些额外的整理工作。

[1] 对于任何新的远程目标,建议的方法是使用标准的GDB远程串行协议(RSP),并使目标实现此接口的服务器端。仅剩下的远程目标是历史遗留接口,例如OpenRISC 1000远程JTAG协议。

2.5. 向GDB添加命令

正如在第2.2节中所述,GDB的命令处理是可扩展的。命令被分组到多个命令列表(struct cmd_list_element 类型)中,由多个全局变量(在 cli-cmds.h 中定义)指向。其中,cmdlist 是所有定义的命令的列表,还为各种顶层命令的子命令定义了单独的列表。例如,infolist 是所有 info 子命令的列表。

每个命令(或子命令)与一个回调函数相关联,该函数实现了命令的行为。对于在GDB中设置或显示值的函数,还有额外的要求。每个函数还接受一个文档字符串(由 help 命令使用)。添加命令的函数都会返回一个指向所添加命令的 struct cmd_list_element 的指针(这未必是其命令列表的头)。最有用的函数包括:

  • add_cmd。将函数添加到命令列表。
  • add_com。将函数添加到主命令列表 cmdlist。这是 add_cmd 的便捷包装。
  • add_prefix_cmd。添加新的前缀命令。此命令应该有自己的函数,以便在其自己上调用,以及专用于前缀命令的全局命令列表指针,所有其子命令都将添加到其中。如果使用未知的子命令调用前缀命令,它可以要么报错,要么调用前缀命令自身的函数。在调用 add_prefix_cmd 时,通过调用标志来指定使用这两者中的哪一个。
  • add_alias_cmd。为已定义的命令添加别名。
  • add_info。将子命令添加到 info。这是 add_cmd 的便捷包装。

通常,在定义 struct gdbarch 后,新命令会在 _initialize_arch 函数中添加。

2.6. 模拟器

GDB允许实现者将GDB链接到一个内置的模拟器,以便通过使用 target sim 命令执行模拟目标。

模拟器应该构建为一个库 libsim.a,实现标准的GDB模拟器接口。库的位置由在 configure.tgt 中设置的 gdb_sim 参数指定。

接口由一组应该被实现的函数组成。详细的规范可以在 include 目录中的头文件 remote-sim.h 中找到。

  • sim_open。初始化模拟器。
  • sim_close。销毁模拟器实例,包括释放任何内存。
  • sim_load。将程序加载到模拟器的内存中。
  • sim_create_inferior。准备运行模拟程序。在调用 sim_resume(见下文)之前不要实际运行它。
  • sim_readsim_write。从模拟器的内存中读取和写入字节。
  • sim_fetch_registersim_store_register。读取和写入模拟器的寄存器。
  • sim_info。为 info sim 命令打印信息。
  • sim_resume。恢复(或开始)模拟程序的执行。
  • sim_stop。停止模拟程序的执行。
  • sim_stop_reason。返回程序停止的原因。
  • sim_do_command。执行模拟器支持的任意命令。

2.7. 远程串行协议(RSP)

GDB远程串行协议是一种用于连接到远程目标的通用协议。它通过 target remotetarget extended-remote 命令调用。

该协议是一种简单的文本命令-响应协议。GDB会话充当协议的客户端。它向在目标上实现的服务器发出命令,该服务器必须由目标实现。通过实现RSP的服务器端,任何远程目标都可以与GDB通信。为各种体系结构提供了一些存根实现,可用作新实现的基础。该协议作为主GDB用户指南附录的完整文档 [3]。

强烈建议任何新的远程目标都应使用RSP来实现,而不是创建新的远程目标协议。

2.7.1. RSP客户端实现

客户端实现可以在 gdb 子目录中的源文件 remote.hremote.c 中找到。这些实现一组目标操作,如第2.4节所述。标准操作的每个操作都映射到与目标上的服务器进行的RSP交互的序列。

2.7.2. RSP服务器实现

RSP服务器实现是一个庞大的主题,不构成GDB实现的直接部分(因为它是目标的一部分,而不是调试器的一部分)。

Embecosm编写了一份全面的“Howto”,描述了使用OpenRISC 1000体系结构模拟器Or1ksim作为RSP目标的示例,以及描述了RSP服务器实现技术 [2]。

2.8. GDB文件组织

GDB源代码的大部分内容位于少数几个目录中。GDB的某些组件是在其他地方使用的库(例如BFD在GNU binutils中使用),这些库有它们自己的目录。主要目录包括:

  • include。包含横跨主要组件的信息的头文件。例如,主模拟器接口头文件在这里(remote-sim.h),因为它将GDB(在gdb目录中)与模拟器(在sim目录中)链接起来。其他头文件,特定于特定组件的,驻留在该组件的目录中。
  • bfd。二进制文件描述符库。如果必须识别新的目标文件类型,则应在此处添加。
  • gdb。主GDB目录。所有源文件应首先包含 defs.h,然后包含它们引用的任何其他头文件。头文件也应包含它们引用的任何头文件,但可以假定已经包含了 defs.h
  • configure.tgt 文件包含一个大型的开关语句,用于匹配指定给主 configure 命令的目标。通过在此文件中包含其模式匹配来添加新目标。
  • config 子目录包含本地目标的特定配置信息。
  • libiberty。在POSIX和glibc之前,这是一个GNU项目,旨在提供一组标准函数。它在GDB中继续存在。最有价值的是它的自由存储管理和参数解析函数。
  • opcodes。包含用于GDB(disassemble 命令)的反汇编器。由于这段代码也用于binutils中,因此它在自己的目录中。
  • sim。各种目标的模拟器。每个目标架构模拟器都构建在其自己的子目录中。

2.9. 测试GDB

运行GDB测试套件需要安装DejaGNU软件包。然后可以使用以下命令运行测试:

make check

测试运行完成后,摘要结果将在 gdb/testsuite 目录中的 gdb.sum 文件中,详细日志在 gdb.log 文件中。对于在主机和目标不同的环境中进行最全面的测试,DejaGNU需要一些额外的配置。这可以通过将 DEJAGNU 环境变量设置为引用适当的配置文件,并在 ~/boards 目录中定义自定义板配置文件来实现。这些配置文件可用于指定适当的模拟器以及在运行测试时如何连接它。

2.10. 文档

GDB的一些子目录又包含doc子目录。文档是用texinfo [9]编写的,可以生成PDF、PostScript、HTML或info文件。文档不会在 make allmake doc 中自动构建。

要创建文档,请切换到各个文档目录并使用 make htmlmake pdfmake psmake info,根据需要选择。

主要感兴趣的文档有:

  • bfd/doc/bfd.texinfo。这是BFD手册。
  • gdb/doc/gdb.texinfo。这是主要的GDB用户指南 [3]。
  • gdb/doc/gdbint.texinfo。这是内部用户指南 [4]。对于任何正在进行代码移植的开发人员来说,这是必读的。

唯一例外的是使用 make install。这将为 gdb/doc 目录中的任何文档构建info文件,并将其安装在安装目录的info子目录中。

2.11.GDB中的示例流程

了解体系结构规范功能和目标操作如何响应各种GDB命令是有益的。以下各节通过序列图说明了几个过程流。这些图显示了过程的调用链。仅显示了关键函数 - 实际调用通常涉及多个中间函数调用。

2.11.1. 初始启动

图2.2显示了GDB启动的序列图。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2.2 GDB 启动时的序列图

target 命令直接映射到当前目标的 to_open。一个典型的实现会建立与目标的物理连接(例如通过打开到远程目标的 TCP/IP 连接)。对于远程目标,通常会调用 start_remote,该函数等待目标停止(使用当前目标的 to_wait 函数),确定停止的原因(handle_inferior_event),然后标记为正常停止(normal_stop)。

handle_inferior_event 是 GDB 中的一个核心函数。每当通过目标的 to_wait 函数将控制返回给 GDB 时,它必须确定发生了什么以及应该如何处理。图 2.4 显示了在响应 target 命令时 handle_inferior_event 的行为。

image-20240229110053936

图 2.4. 对 GDB target 命令响应中的 handle_inferior_event 的时序图

handle_inferior_event 需要确定执行停止的程序计数器位置,因此调用 read_pc_pid。由于程序计数器是一个寄存器,这会触发寄存器缓存的创建,其中必须使用 gdbarch_register_type 确定每个寄存器的类型(这是一次性的操作,因为它永远不会改变)。在确定了寄存器类型后,寄存器缓存通过调用当前目标的 to_fetch_registers 函数为相关寄存器的程序计数器值进行填充。

然后,handle_inferior_event 确定停止是否由断点或监视点引起。watchpoints_triggered 函数使用目标的 to_stopped_by_watchpoint 来确定是否是监视点触发了停止。

normal_stop 的调用还会调用 struct gdbarch 函数,包括调用 gdbarch_unwind_pc 来确定当前程序计数器和帧探测器函数以建立帧探测器栈。

2.11.3. GDB的load命令

图2.5显示了GDB响应load命令的高级顺序图。这映射到当前目标的to_load函数,在大多数情况下,该函数最终会调用当前目标的to_xfer_partial函数,为加载图像的每个部分调用一次,将其加载到内存中。

image-20240229110646050

load函数将从加载的文件中获取数据,最重要的是其执行的起始地址。

2.11.4. GDB的break命令

图2.6显示了GDB响应break命令的高级顺序图。此示例是针对break目标是目标可执行文件中的符号(即函数名)的情况。

image-20240229110710446

大多数与断点相关的操作发生在程序开始运行时,任何活动断点都会被安装。但是对于任何break命令,必须在断点数据结构中设置断点的地址。

对于符号地址,可以从符号表中用于调试目的的行号信息中获取函数的开始地址(称为符号和行信息,或SAL)。对于函数,这将产生代码的起始地址。但是,断点必须设置在函数序言之后。使用gdbarch_skip_prolog来在代码中找到该地址。

2.11.5. GDB的run命令

图2.7显示了GDB响应run命令的高级顺序图。

image-20240229110729653run命令必须创建inferior,插入任何活动的断点和监视点,然后开始inferior的执行。直到目标报告停止,控制权才会返回到GDB。

实现run命令的顶级函数是run_command。这将通过调用当前目标的to_create_inferior函数创建inferior。 GDB支持能够动态描述其体系结构的目标(例如可用寄存器的数量)。这是通过当前目标的to_find_description函数(默认情况下为空函数)实现的。

执行是通过proceed开始的。这首先必须确定代码是否正在重新启动需要经过延迟槽的指令(以便代码永远不会停在延迟槽上)。如果需要此功能,则由gdbarch_single_sep_through_delay函数实现。

使用当前目标的to_insert_breakpoint函数插入活动断点。然后使用当前目标的to_resume函数运行代码。

然后,GDB调用wait_for_inferior,该函数将等待目标停止,然后确定停止的原因。最后,normal_stop将从目标代码中删除断点,并根据需要向用户报告目标的当前状态。

很多详细的处理都发生在wait_for_inferior和normal_stop函数中(请参阅它们在第2.11.2节中的使用)。这些是重要的函数,详细查看它们的行为会很有用。

图2.8显示了处理GDB run命令时wait_for_inferior的顺序图。

image-20240229110749642

再次,关键工作在handle_inferior_event中。该代码使用当前目标的to_stopped_by_watchpoint函数检查监视点。该函数还检查断点,但由于它已经知道当前程序计数器(由target_wait返回控制时设置),因此不需要对目标操作进行进一步调用。target_wait将报告是否由于断点引起而停止。handle_inferior_event然后可以查找活动断点列表中的程序计数器,以确定遇到了哪个断点。

图2.9显示了处理GDB run命令时normal_stop的顺序图。在这个例子中,停止是由目标遇到断点引起的。

image-20240229110759206

首先要执行的操作是删除断点。这确保目标可执行文件返回到其正常状态,而没有插入任何陷阱或类似的代码。

为了识别目标的帧嗅探器,使用体系结构的帧嗅探器arch_frame_sniffer。然后为用户打印当前堆栈帧。这需要使用帧嗅探器从NEXT帧(这里是arch_frame_this_id)中识别THIS帧的ID(从而识别所有其他数据)。print_stack_frame将从哨兵帧开始向内工作,直到找到包含当前堆栈指针和程序计数器的堆栈帧。

2.11.6. GDB的backtrace命令

图2.10显示了GDB响应backtrace命令的高级顺序图。此顺序图显示了在控制返回到GDB后第一次调用backtrace时的行为。

image-20240229111628664

主要的命令函数是backtrace_command,它使用print_frame_info打印堆栈上每个函数的名称及其参数。

第一个帧已知,来自停止目标的程序计数器和堆栈指针,因此由print_frame打印出来。这将最终使用当前目标的to_xfer_partial函数获取局部参数值。

由于这是程序停止后的第一次回溯,因此通过get_func_type从哨兵帧获取堆栈指针和程序计数器。然后,对于每个帧,依次调用print_frame,直到没有更多的堆栈帧。每个帧中的信息是使用体系结构的帧嗅探器构建的。

详细查看print_frame是很有用的。图2.11显示了处理GDB backtrace命令时print_frame函数的第二系列调用的顺序图,用于打印堆栈帧。

image-20240229111651365

可以从与堆栈帧相关联的程序计数器和堆栈指针中获取关于堆栈帧上函数的信息。这是通过对gdbarch_unwind_pc和gdbarch_unwind_sp函数的调用实现的。

然后,对于每个参数,必须打印其值。符号表调试数据将标识参数,并为GDB提供足够的信息,以判断值是在堆栈上还是在寄存器中。帧嗅探器函数(在此示例中是arch_frame_prev_register)用于根据需要获取寄存器的值。

具体的调用顺序取决于堆栈帧中的函数、它们具有的参数以及这些参数是在寄存器中还是在堆栈上。

2.11.7. GDB的continue命令在断点后

最后的顺序图显示了在断点后使用continue命令恢复执行时的行为。图2.12显示了GDB响应continue命令的高级顺序图。此顺序图显示了在由于断点而停止后的第一次调用continue时的行为。

image-20240229111708387

命令功能由continue_command提供,该命令在很大程度上调用proceed函数。

proceed调用当前目标的to_resume函数以恢复执行。对于这第一次调用,执行完成后未替换执行run命令后删除的断点,目标的恢复仅为单步执行。这允许在触发异常的情况下将目标步过断点。

然后,proceed使用wait_for_inferior等待控制在单步之后返回,并诊断下一步操作。等待使用当前目标的to_wait函数,然后调用handle_inferior_event来分析结果。在这种情况下,handle_inferior_event确定目标刚刚越过断点。它重新插入断点并再次调用目标的to_resume函数,这次是为了连续运行。

wait_for_inferior将再次使用当前目标的to_wait函数等待目标停止执行,然后再次调用handle_inferior_event来处理结果。这一次,控制权应该返回到GDB,因此断点被移除,handle_inferior_event和wait_for_inferior返回。proceed调用normal_stop来整理并打印关于执行停止的当前堆栈帧位置的消息(请参阅第2.11.5节)。

查看handle_inferior_event的第一次调用的行为是有用的,以查看完成单步操作并继续连续执行的顺序。图2.13显示了handle_inferior_event的第一次调用的顺序图。

image-20240229111729446

handle_inferior_event首先确定是否触发了监视点。如果不是这种情况,它会检查处理器是否现在处于指令的延迟槽中(需要立即进行另一个单步操作)。在确定连续执行是适当的情况下,它调用keep_going函数重新插入活动断点(使用当前目标的to_insert_breakpoint函数)。最后,它调用当前目标的to_resume函数,不设置单步标志,以恢复连续执行。

2.12. 总结 GDB移植摘要

将新架构移植到 GDB 中可以分为多个步骤。

  1. 确保 bfd 目录中存在目标架构可执行文件的 BFD。如果不存在,通过修改现有的类似文件创建一个。
  2. 在 opcodes 目录中为目标架构实现一个反汇编器。
  3. 在 gdb 目录中定义目标架构。在 configure.tgt 中添加新目标的模式,包括包含该架构代码的文件的名称。按照惯例,对于架构 arch 的目标架构定义将放置在 arch-tdep.c 中。
  4. 在 arch-tdep.c 中定义 _initialize_arch_tdep 函数,该函数调用 gdbarch_register 来创建新的 struct gdbarch 用于该架构。
  5. 如果需要一个新的远程目标,请考虑通过定义 _initialize_remote_arch 函数添加新的远程目标。但如果可能的话,使用远程串行协议(Remote Serial Protocol)并独立实现目标的服务器端协议。
  6. 如果需要的话,在 sim 目录中实现一个模拟器。这应该创建一个名为 libsim.a 的库,实现 remote-sim.h 中的接口(该接口在 include 目录中找到)。
  7. 构建和测试。如果需要的话,请求 GDB 主管小组将新的移植包含在主分发版中!
  8. 在主 GDB 用户指南(gdb/doc/gdb.texinfo [3])的“配置特定信息”部分中添加对新架构的描述。

本文档的其余部分展示了如何使用这个过程将 GDB 移植到 OpenRISC 1000 架构。

第3章:OpenRISC 1000 架构

OpenRISC 1000 架构定义了一系列自由、开源的 RISC 处理器核。它是一种32或64位的载荷和存储 RISC 架构,注重性能、简单性、低功耗、可扩展性和通用性。

OpenRISC 1000 的完整文档可在其《架构手册》[8]中找到。

从调试的角度来看,由指令集操作的有三个数据区域。

  1. 主内存:一个统一的地址空间,具有32或64位寻址。支持单独或统一的指令和数据以及指令缓存。支持单独或统一的、1级或2级数据和指令内存管理单元。
  2. 通用寄存器(GPRs):最多32个寄存器,长度为32或64位。
  3. 特殊寄存器(SPRs):最多32个组,每个组最多有2048个寄存器,长度为32或64位。这些寄存器提供了处理器的所有管理功能:程序计数器、处理器状态、保存的异常寄存器、调试接口、内存管理单元和缓存接口等。

特殊目的寄存器(SPRs)对于 GDB 的实现具有挑战性,因为它们既不表示可寻址的内存,也不具有寄存器集的特性(通常数量不多)。

对于 GDB 实现而言,一些 SPR 对于 GDB 的实现尤为重要。

  1. 配置寄存器:单元存在寄存器(SPR 1, UPR)、CPU 配置寄存器(SPR 2, CPUCFGR)和调试配置寄存器(SPR 7, DCFGR)标识特定 OpenRISC 1000 实现中可用的功能。这包括所使用的指令集、通用寄存器的数量以及硬件调试接口的配置。
  2. 程序计数器:上一个程序计数器(SPR 0x12, PPC)是刚刚执行的指令的地址。下一个程序计数器(SPR 0x10, NPC)是即将执行的下一条指令的地址。NPC 是 GDB 的 $pc 变量报告的值。
  3. 监督寄存器:监督寄存器(SPR 0x11, SR)表示处理器的当前状态。这是 GDB 的状态寄存器变量 $ps 报告的值。

特别重要的是控制调试单元(如果存在)的第6组 SPR。调试单元可以响应高达10个观察点中的任何一个触发陷阱异常。观察点是由组合匹配点(matchpoints)构建的逻辑表达式,这些匹配点是对特定行为(例如是否已访问指定地址)的简单点测试。

  1. 调试值和控制寄存器:有多达8对调试值(SPR 0x3000–0x3007, DVR0 到 DVR7)和调试控制寄存器(SPR 0x3008–0x300f, DCR0 到 DCR7)寄存器。每对寄存器与一个硬件匹配点相关联。每对中的调试值寄存器给出要与之比较的值。调试控制寄存器指示匹配点是否已启用,要与之比较的值的类型(指令获取地址、数据加载和/或存储地址、数据加载和/或存储值)以及要进行比较的方式(相等、不相等、小于、小于或等于、大于、大于或等于),包括有符号和无符号。如果启用了匹配点并且测试满足,则触发相应的匹配点。
  2. 调试观察点计数器:有两个16位的调试观察点计数器寄存器(SPR 0x3012–0x3013, DWCR0 和 DWCR1),与另外两个匹配点相关联。上16位是要匹配的值,下16位是计数器。在触发指定的匹配点时,计数器递增(参见调试模式寄存器1)。当计数达到匹配值时,触发相应的匹配点。

【注意】注意:存在潜在的歧义,因为计数器在响应匹配点时递增,并且还生成自己的匹配点。将计数器设置为递增自己的匹配点是不好的做法!

  1. 调试模式寄存器:有两个调试模式寄存器用于控制调试单元的行为(SPR 0x3010–0x3011, DMR1 和 DMR2)。DMR1为每个10个匹配点(与DVR/DCR对相关联的8个,与计数器相关联的2个)提供一对比特。这些指定观察点是否由相关匹配点触发,由与前一个观察点进行 AND 或 OR 运算的匹配点触发。通过构建观察点链,可以建立对硬件行为的复杂逻辑测试。

另外,DMR1中的两个位启用单步行为(每个指令完成后触发一个陷阱异常)和分支步进行为(每个分支指令完成后触发一个陷阱异常)。

DMR2 包含每个计数器的一个启用位,10个指示哪些观察点分配给哪个计数器的位,以及10个指示哪些观察点生成陷阱异常的位。它还包含10位输出,指示哪些观察点生成了陷阱异常。

  1. 调试停止和原因寄存器:在正常操作中,所有 OpenRISC 1000 异常都通过位于地址0x100至0xf00的异常向量处理。调试停止寄存器(SPR 0x3014, DSR)用于将特定异常分配给 JTAG 接口。这些异常会使处理器停止,允许通过 JTAG 接口分析机器状态。通常,调试器将为用于断点的陷阱异常启用此功能。

在异常被重定向到开发接口的情况下,调试原因寄存器(SPR 0x3021, DRR)指示导致重定向的异常。请注意,尽管单步和分支步进引发陷阱,但如果它们被分配到 JTAG 接口,它们不会在 DRR 中设置 TE 位。这允许外部调试器区分断点陷阱和单步/分支步进陷阱。

3.1.OpenRISC 1000 JTAG 接口

OpenRISC 1000 具有两个用于 JTAG 接口的变体。

  1. 原始 JTAG 接口是作为 OpenRISC SoC 项目 ORPSoC [10] 的一部分创建的。它提供三个扫描链:一个用于访问所有 SPR,一个用于访问外部存储器,一个用于控制 CPU。控制扫描链可以重置、停滞或跟踪处理器。
  2. 新的 JTAG 接口是由 Igor Mohor 在 2004 年提供的 [11]。它提供了对 SPR 和外部存储器的相同访问权限,但提供了一个更简单的控制接口,只提供了停滞或重置处理器的功能。

目前,OpenRISC 架构模拟器 Or1ksim(见第 3.4 节)支持第一种接口。

这两种接口都提供三个扫描链:

  1. RISC_DEBUG(扫描链 1),提供对 SPR 的读/写访问。
  2. REGISTER(扫描链 4),提供对 CPU 的控制。在 ORPSoC 接口中,它提供多个寄存器,用于读写以控制 CPU。其中寄存器 0,MODER,控制硬件跟踪,寄存器 4,RISC_OP,控制重置和停滞最为重要。通过设置 MODER 中的位 1 启用跟踪,通过清除禁用。通过设置 RISC_OP 中的位 1 和位 0 触发和清除重置和处理器停滞。通过读取 RISC_OP 中的停滞位,可以确定停滞状态。 在 Mohor 接口中,有一个单一的控制寄存器,其行为与原始调试接口中的 RISC_OP 相同。
  3. WISHBONE(扫描链 5),提供对主内存的读/写访问。

由于通用寄存器(GPRs)映射到 SPR 组 0,这个机制也允许对 GPR 进行读写。

3.2.OpenRISC 1000 远程 JTAG 协议

重要说明:

OpenRISC 1000 的最新版本 GDB 实现了 GDB 远程串行协议,这是连接到远程目标的首选机制 [2]。

但是,这里描述的协议保留为向后兼容性。它在这里被用作教程的载体,以说明如何在 GDB 中使用自定义调试协议。

为了方便 GDB 进行远程调试,OpenRISC 定义了一种软件协议,描述了适用于通过套接字接口经由 TCP/IP 传输的 JTAG 访问。

注意:

这个协议在 GDB 远程串行协议(见第 2.7 节)之前。在将来的某个日期,OpenRISC 1000 远程 JTAG 协议将被 RSP 替代。

OpenRISC 1000 远程 JTAG 协议是一种简单的消息发送/确认协议。JTAG 请求被封装为 32 位命令、32 位长度和一系列 32 位数据字的形式。JTAG 响应被封装为 32 位状态和可选数量的 32 位数据字。可用的命令有:

  1. OR1K_JTAG_COMMAND_READ(1)。读取单个 JTAG 寄存器。请求中提供了一个 32 位地址。响应包括 64 位读取数据。
  2. OR1K_JTAG_COMMAND_WRITE(2)。写入单个 JTAG 寄存器。请求中提供了一个 32 位地址和要写入的 64 位数据。
  3. OR1K_JTAG_COMMAND_READ_BLOCK(3)。读取多个 32 位 JTAG 寄存器。请求中提供了第一个寄存器的 32 位地址和要读取的寄存器数。响应包括读取的寄存器数和每个读取的寄存器的 32 位数据。
  4. OR1K_JTAG_COMMAND_WRITE_BLOCK(4)。写入多个 32 位 JTAG 寄存器。请求中提供了第一个寄存器的 32 位地址和要写入的寄存器数,然后是每个寄存器要写入的 32 位数据。
  5. OR1K_JTAG_COMMAND_CHAIN(5)。选择扫描链。请求中提供了一个 32 位扫描链编号。

如果使用 Mohor 版本的 JTAG 接口,将被忽略对 REGISTER 扫描链的读/写访问的地址,因为只有一个控制寄存器。

注意:

这个协议中似乎存在一个矛盾点。为了通信效率,提供了读/写单个寄存器的 64 位,而块读/写只有 32 位。

图 3.1 显示了所有五个请求及其相应(成功)响应的结构。请注意,如果请求失败,响应将只包含状态字。

image-20240229112627405

OpenRISC 1000远程JTAG协议的客户端部分(发出请求的部分)由专为OpenRISC 1000设计的GDB端口实现。

另一方面,服务器端应用程序可以实现此协议,以与物理硬件进行交互,通常通过其JTAG端口,或者与包含JTAG功能的模拟进行交互。前者的例子包括ORSoC™ AB生产的USB JTAG连接器。后者的示例是OpenRISC 1000体系结构模拟器Or1ksim(有关详细信息,请参见第3.4节)。

3.3. 应用二进制接口(ABI)

OpenRISC 1000的ABI描述在《体系结构手册》的第16章[8]中。然而,实际的GCC编译器实现与文档中描述的ABI略有不同。由于对ABI的准确理解对于GDB至关重要,这里记录了这些差异。

寄存器使用:R12被用作另一个被调用保存的寄存器。在32位体系结构上,它从不用于返回64位结果的高32位。大于32位的所有值都通过指针返回。

尽管规范要求堆栈帧应为双字对齐,但当前的GCC编译器实现了单字对齐。

大于32位(在64位体系结构上为64位)、结构和联合的整数值以指向结果位置的指针的形式返回。调用函数提供该位置,并将其作为GPR 3中的第一个参数传递。换句话说,当函数返回这种类型的结果时,函数的第一个真实参数将出现在R4中(或者在32位体系结构上是R5/R6,如果它是64位参数)。

3.4. Or1ksim:OpenRISC 1000体系结构模拟器

Or1ksim是OpenRISC 1000体系结构的指令集模拟器(ISS)。目前只模拟32位体系结构。除了对核心处理器的建模外,Or1ksim还可以模拟多个外设,以提供完整的片上系统(SoC)功能。

Or1ksim模拟OpenRISC 1000 JTAG接口,并在服务器端实现OpenRISC 1000远程JTAG协议。它被用作对GDB进行的此端口的测试平台。

JTAG接口模拟了旧的ORPSoC的行为(支持多个控制寄存器和硬件跟踪)。未来的版本将提供支持Igor Mohor JTAG接口的选项。

[注意] 注意 GDB的移植揭示了Or1ksim中的一些错误。该实现现在相当陈旧,早于当前的OpenRISC 1000规范。有一个修复这些错误的补丁(可以从www.embecosm.com/download/index.html下载)。

第4章:将OpenRISC 1000架构移植至GDB

本章描述了将OpenRISC 1000架构移植至GDB的步骤。它使用了第2章中描述的信息和数据结构。

OpenRISC 1000版本的GDB在GDB用户指南[3]中有简要文档。更全面的教程[6]位于gdb/doc子目录中的or1k.texinfo文件中。

严格来说,这不是一个新的移植。在GDB 5.3中存在一个旧的移植。然而,由于那时GDB发生了重大变化,因此几乎需要进行完整的重新实现。

[提示] 在处理任何大型代码库时,TAGS文件非常有价值。这允许在整个代码库中立即查找任何过程或变量。通常对于任何GNU项目,可以使用make tags命令来实现这一点。但是这对于GDB不起作用——在opcodes目录中的tags目标存在问题。

然而,在gdb目录中构建tags是有效的,因此可以通过以下方式在该目录中构建TAGS文件:

cd gdb
make tags
cd ..

4.1. BFD规范

OpenRISC 1000的BFD规范已经存在(它是binutils的一部分),因此无需实现这个。现有的代码被重复使用。

4.2. OpenRISC 1000体系结构规范

代码位于gdb子目录。主要的架构规范位于or1k-tdep.c中,带有OpenRISC 1000全局标头的or1k-tdep.h。对OpenRISC 1000远程JTAG接口的支持位于remote-or1k.c中,具有在or1k-jtag.c中的详细协议和在or1k-jtag.h中的协议标头。

有几个目标可以使用OpenRISC 1000体系结构。这些都以or16、or32或or32开头。configure.tgt被编辑以添加这些目标,以便从这些源文件生成的二进制文件。

or16* | or32* | or64*)
        # 目标:OpenCores OpenRISC 1000体系结构
        gdb_target_obs="or1k-tdep.o remote-or1k.o or1k-jtag.o"
        ;;

注意: configure.tgt仅指定二进制文件,因此无法显示对标头的依赖关系。为了纠正这一点,可以编辑Makefile.in,以便automake和configure生成带有正确依赖关系的Makefile。

架构定义是通过通过调用gdbarch_register从_initialize_or1k_tdep创建的。该函数还初始化了反汇编器(build_automata),并添加了两个新命令:info命令的子命令以读取SPR和新的顶级支持命令spr以设置SPR的值。

4.2.1. 创建struct gdbarch

使用初始化函数or1k_gdbarch_init和目标特定的dump函数or1k_dump_tdep,调用gdbarch_register以BFD类型bfd_arch_or32注册。将来的实现可能会通过使用相同的函数来创建64位版本的架构。

gdbarch_init接收从BFD条目创建的struct gdbarch_info以及现有架构的列表。首先使用gdbarch_list_lookup_by_info检查该列表,看看是否已经有了适合给定struct gdbarch_info的架构,如果是,则返回该架构。

否则,将创建一个新的struct gdbarch。为此,将目标依赖项保存在一个OpenRISC 1000特定的struct gdbarch_tdep中,在or1k-tdep.h中定义。

struct gdbarch_tdep
{
  unsigned int  num_matchpoints;
  unsigned int  num_gpr_regs;
  int           bytes_per_word;
  int           bytes_per_address;
};

这是除了struct gdbarch中保存的信息之外的信息。通过使用这个结构,可以使OpenRISC 1000的GDB实现足够灵活,以处理32和64位的实现以及可变数量的寄存器和匹配点。

注意: 尽管这种灵活性内置在代码中,但当前的实现仅已经在32位OpenRISC 32寄存器上进行了测试。

然后,通过gdbarch_alloc创建新的架构,传入struct gdbarch_info和struct gdbarch_tdep。使用各种set_gdbarch_函数填充struct gdbarch,并将OpenRISC 1000 Frame sniffer与架构关联起来。

在创建新的struct gdbarch时,必须提供一个函数,以将struct gdbarch_tdep中的目标特定定义转储到文件中。这是在or1k_dump_tdep中提供的。它接收一个指向struct gdbarch的指针和一个文件句柄,并简单地写出struct gdbarch_tdep中的字段,带有适当的解释性文本。

4.2.2. OpenRISC 1000硬件数据表示

struct gdbarch的第一个条目初始化所有标准数据类型的大小和格式。

set_gdbarch_short_bit             (gdbarch, 16);
set_gdbarch_int_bit               (gdbarch, 32);
set_gdbarch_long_bit              (gdbarch, 32);
set_gdbarch_long_long_bit         (gdbarch, 64);
set_gdbarch_float_bit             (gdbarch, 32);
set_gdbarch_float_format          (gdbarch, floatformats_ieee_single);
set_gdbarch_double_bit            (gdbarch, 64);
set_gdbarch_double_format         (gdbarch, floatformats_ieee_double);
set_gdbarch_long_double_bit       (gdbarch, 64);
set_gdbarch_long_double_format    (gdbarch, floatformats_ieee_double);
set_gdbarch_ptr_bit               (gdbarch, binfo->bits_per_address);
set_gdbarch_addr_bit              (gdbarch, binfo->bits_per_address);
set_gdbarch_char_signed           (gdbarch, 1);
4.2.3. OpenRISC 1000架构的信息函数

这些struct gdbarch函数提供有关架构的信息。

set_gdbarch_return_value          (gdbarch, or1k_return_value);
set_gdbarch_breakpoint_from_pc    (gdbarch, or1k_breakpoint_from_pc);
set_gdbarch_single_step_through_delay
                                  (gdbarch, or1k_single_step_through_delay);
set_gdbarch_have_nonsteppable_watchpoint
                                  (gdbarch, 1);
switch (gdbarch_byte_order (gdbarch))
  {
  case BFD_ENDIAN_BIG:
    set_gdbarch_print_insn        (gdbarch, print_insn_big_or32);
    break;

  case BFD_ENDIAN_LITTLE:
    set_gdbarch_print_insn        (gdbarch, print_insn_little_or32);
    break;

  case BFD_ENDIAN_UNKNOWN:
    error ("or1k_gdbarch_init: Unknown endianism");
    break;
    }
  • or1k_return_value: 该函数告诉GDB如何按ABI返回特定类型的值。结构/联合和大标量(> 4字节)放在内存中,并通过引用返回(RETURN_VALUE_ABI_RETURNS_ADDRESS)。较小的标量在GPR 11(RETURN_VALUE_REGISTER_CONVENTION)中返回。
  • or1k_breakpoint_from_pc: 返回在给定程序计数器地址处使用的断点函数。由于所有OpenRISC 1000指令的大小相同,因此该函数始终返回相同的值,即l.trap指令的指令序列。
  • or1k_single_step_through_delay: 此函数用于确定单步执行的指令是否实际上是执行延迟槽。如果先前执行的指令是分支或跳转,则是这种情况。
  • print_insn_big_or32和print_insn_little_or32: 有两个反汇编器的变体,具体取决于字节顺序。有关更详细的信息,请参见第4.4节。
4.2.4. OpenRISC 1000寄存器体系结构

寄存器体系结构由两组struct gdbarch函数和字段定义。第一组指定了寄存器(原始和伪造)的数量以及一些“特殊”寄存器的寄存器编号。

set_gdbarch_pseudo_register_read  (gdbarch, or1k_pseudo_register_read);
set_gdbarch_pseudo_register_write (gdbarch, or1k_pseudo_register_write);
set_gdbarch_num_regs              (gdbarch, OR1K_NUM_REGS);
set_gdbarch_num_pseudo_regs       (gdbarch, OR1K_NUM_PSEUDO_REGS);
set_gdbarch_sp_regnum             (gdbarch, OR1K_SP_REGNUM);
set_gdbarch_pc_regnum             (gdbarch, OR1K_PC_REGNUM);
set_gdbarch_ps_regnum             (gdbarch, OR1K_SR_REGNUM);
set_gdbarch_deprecated_fp_regnum  (gdbarch, OR1K_FP_REGNUM);

第二组函数提供有关寄存器的信息。

set_gdbarch_register_name         (gdbarch, or1k_register_name);
set_gdbarch_register_type         (gdbarch, or1k_register_type);
set_gdbarch_print_registers_info  (gdbarch, or1k_registers_info);
set_gdbarch_register_reggroup_p   (gdbarch, or1k_register_reggroup_p);

原始寄存器的表示(参见第2.3.5.3节)如下:寄存器0-31是相应的GPR,寄存器32是前一个程序计数器,33是下一个程序计数器(通常称为程序计数器),寄存器34是监控寄存器。为了方便起见,头文件or1k_tdep.h中定义了所有特殊寄存器的常量。

#define OR1K_SP_REGNUM         1
#define OR1K_FP_REGNUM         2
#define OR1K_FIRST_ARG_REGNUM  3
#define OR1K_LAST_ARG_REGNUM   8
#define OR1K_LR_REGNUM         9
#define OR1K_RV_REGNUM        11
#define OR1K_PC_REGNUM       (OR1K_MAX_GPR_REGS + 0)
#define OR1K_SR_REGNUM       (OR1K_MAX_GPR_REGS + 1)

在此实现中,没有伪寄存器。可以提供一个集合来表示以浮点格式表示的GPR(用于浮点指令),但尚未实现这一点。为各种总数定义了常量。

#define OR1K_MAX_GPR_REGS    32
#define OR1K_NUM_PSEUDO_REGS  0
#define OR1K_NUM_REGS        (OR1K_MAX_GPR_REGS + 3)
#define OR1K_TOTAL_NUM_REGS  (OR1K_NUM_REGS + OR1K_NUM_PSEUDO_REGS)

注意: 这些总数目前是硬编码的常数。它们实际上应该依赖于struct gdbarch_tdep中的数据,为具有少于32个寄存器的架构提供支持。这种功能将在将来的实现中提供。

不提供伪寄存器的一个结果是,GDB中的帧指针变量$fp将不具有其正确的值。将此寄存器作为GDB的固有部分提供的做法已不再受支持。如果需要,应将其定义为寄存器或伪寄存器。

但是,如果没有具有此名称的寄存器,GDB将使用struct gdbarch中deprecated_fp_regnum值的值或由帧基础嗅探器报告的当前帧基础。

目前,deprecated_fp_regnum已设置。但长期计划是将帧指针表示为伪寄存器,其取值为GPR 2。

寄存器体系结构在大多数情况下只涉及在struct gdbarch中设置所需的值。然而,定义了两个函数or1k_pseudo_register_read和or1k_pseudo_register_write,以提供对任何伪寄存器的访问。这些函数被定义为为将来提供挂钩,但在没有伪寄存器的情况下它们什么都不做。

有一组函数,提供有关寄存器名称和类型的信息,并为GDB info registers命令提供输出。

or1k_register_name: 这是一个简单的表查找,从其编号中获取寄存器名称。

or1k_register_type: 此函数必须将类型作为struct type返回。这个GDB数据结构包含有关每种类型及其与其他类型的关系的详细信息。

为此函数,预定义了许多标准类型,其中包括一些实用函数,用于从中构造其他类型。对于大多数寄存器,预定义的builtin_type_int32是合适的。堆栈指针和帧指针是指向任意数据的指针,因此需要void *的等效类型。通过将预定义的builtin_type_void应用于lookup_pointer_type函数来构造它。程序计数器是指向代码的指针,因此适用于指向void函数的指针的等效类型。通过将lookup_pointer_type和lookup_function_type应用于builtin_type,构造它。

or1k_register_info: 此函数由info registers命令使用,以显示有关一个或多个寄存器的信息。

实际上,这个函数并不是真正需要的。它只是default_print_registers_info的一个包装器,对于这个函数来说,这是默认设置。

or1k_register_reggroup_p: 这个谓词函数如果给定寄存器在特定组中,则返回1(true)。在请求特定类别的寄存器时,info registers命令使用此函数。

实际上,这个函数与默认函数(default_register_reggroup_p)几乎没有区别,因为无论如何,对于任何未知情况,都会调用它。然而,它确实利用了目标相关的数据(struct gdbarch_tdep),从而为不同的OpenRISC 1000架构提供了灵活性。

4.2.5. OpenRISC 1000帧处理

OpenRISC 1000帧结构在其ABI [8]中有描述。一些细节在当前OpenRISC实现中略有不同,这在第3.3节中有描述。

帧处理的关键是理解每个函数中负责初始化堆栈帧的前奏(和可能的尾声)。对于OpenRISC 1000,GPR 1用作堆栈指针,GPR 2用作帧指针,GPR 9用作返回地址。前奏序列如下:

l.addi  r1,r1,-frame_size
l.sw    save_loc(r1),r2
l.addi  r2,r1,frame_size
l.sw    save_loc-4(r1),r9
l.sw    x(r1),ry

OpenRISC 1000堆栈帧容纳任何局部(自动)变量和临时值,然后是返回地址,接着是旧的帧指针,最后是由该函数调用的函数的基于堆栈的参数。这最后一条规则意味着返回地址和旧帧指针不一定在堆栈帧的末尾 - 将留出足够的空间来构建必须放在堆栈上的任何被调用函数的参数。图4.1显示了前奏结束时堆栈的外观。

image-20240229134733457

并非所有字段都始终存在。函数不需要将其返回地址保存到堆栈中,可能没有需要保存的被调用者保存的寄存器(即GPR 12, 14, 16, 18, 20, 22, 24, 26, 28和30)。叶函数不需要设置新的堆栈帧。

尾声是前奏的反向。被调用者保存的寄存器被恢复,返回地址放在GPR 9中,并在跳转到GPR 9中的地址之前恢复堆栈和帧指针。

l.lwz   ry,x(r1)
l.lwz   r9,save_loc-4(r1)
l.lwz   r2,save_loc(r1)
l.jr    r9
l.addi  r1,r1,frame_size

只有与前奏相对应的尾声的那些部分实际上需要出现。OpenRISC 1000在分支指令后有一个延迟槽,因此为了效率,堆栈恢复可以放在l.jr指令之后。

4.2.5.1. OpenRISC 1000分析帧的功能

一组struct gdbarch函数和一个值提供有关当前堆栈及其如何由目标程序处理的信息。

set_gdbarch_skip_prologue         (gdbarch, or1k_skip_prologue);
set_gdbarch_inner_than            (gdbarch, core_addr_lessthan);
set_gdbarch_frame_align           (gdbarch, or1k_frame_align);
set_gdbarch_frame_red_zone_size   (gdbarch, OR1K_FRAME_RED_ZONE_SIZE);

or1k_skip_prologue: 如果程序计数器当前位于函数前奏中,则此函数返回函数前奏的末尾。

最初的方法是使用DWARF2符号和行(SAL)信息来标识函数的开始(find_pc_partial_function),因此在前奏结束时(skip_prologue_using_sal)标识。

如果此信息不可用,or1k_skip_prologue将重用帧嗅探器函数or1k_frame_unwind_cache(见第4.2.5.5节)的辅助函数,以遍历似乎是函数前奏的代码。

core_addr_lessthan: 此标准函数如果其第一个参数的地址低于第二个参数的地址则返回1(true)。它提供了struct gdbarch inner_than函数所需的功能,适用于像OpenRISC 1000这样具有降低堆栈帧的体系结构。

or1k_frame_align: 此函数接受一个堆栈指针并返回一个值(扩展帧),以满足ABI的堆栈对齐要求。由于OpenRISC 1000 ABI使用降低的堆栈,因此使用内置函数align_down。对齐度在or1k-tdep.h中定义的常量OR1K_STACK_ALIGN中指定。

OR1K_FRAME_RED_ZONE_SIZE: OpenRISC 1000为堆栈指针下方的2,560字节保留供异常处理程序和无帧函数使用的空间。这被称为红区(AMD术语)。此常量记录在struct gdbarch frame_red_zone_size字段中。任何虚构的堆栈帧(见第4.2.5.3节)将放置在此点之后。

4.2.5.2. 用于访问帧数据的OpenRISC 1000函数
set_gdbarch_unwind_pc             (gdbarch, or1k_unwind_pc);
set_gdbarch_unwind_sp             (gdbarch, or1k_unwind_sp);

只有两个在这里需要的函数,or1k_unwind_pc和or1k_unwind_sp。给定指向NEXT帧的指针,这些函数分别返回THIS帧中程序计数器和堆栈指针的值。

由于OpenRISC架构定义了标准的帧嗅探器,并且这两个寄存器都是原始寄存器,因此可以通过调用frame_unwind_register_unsigned函数来实现这些函数。

4.2.5.3. 用于创建虚构堆栈帧的OpenRISC 1000函数
set_gdbarch_push_dummy_call       (gdbarch, or1k_push_dummy_call);
set_gdbarch_unwind_dummy_id       (gdbarch, or1k_unwind_dummy_id);

or1k_push_dummy_call: 此函数创建一个虚构的堆栈帧,以便GDB可以在目标代码中评估函数(例如,在调用命令中)。输入参数包括调用的所有参数,包括返回地址和应该返回结构的地址。

该函数的函数返回地址始终被断点设置(以便GDB可以捕获返回)。使用regcache_cooked_write_unsigned将此返回地址写入链接寄存器(在寄存器缓存中)。

如果函数要返回结构,则结构应该返回的地址作为第一个参数传递,位于GPR 3中。

接下来的参数传递给剩余的参数寄存器(最多GPR 8)。结构按引用传递到它们在内存中的位置。对于传递64位参数的32位体系结构,将使用一对寄存器(3和4、5和6或7和8)。

任何剩余的参数必须推送到堆栈的末尾。这里有一个困难,因为推送每个参数可能会导致堆栈不对齐(OpenRISC 1000指定双字对齐)。因此,代码首先计算所需的空间,然后调整结果堆栈指针以正确对齐。然后可以在正确的位置将参数写入堆栈。

or1k_unwind_dummy_id: 这是or1k_push_dummy_call的反向操作。给定指向NEXT堆栈帧的指针(将是虚构调用的帧),它返回此帧的帧ID(即堆栈指针和函数入口点地址)。

这并不完全琐碎。对于虚构帧,有关此帧的NEXT帧信息未必完整,因此简单调用frame_unwind_id会无限递归回到此函数。相反,通过取消堆栈指针和程序计数器并尝试使用DWARF2符号和行(SAL)信息查找从PC到函数开头的开始的方式来构建帧信息。如果该信息不可用,则程序计数器将用作函数开始地址的代理。

4.2.5.4. OpenRISC 1000帧嗅探器

前面的函数都与struct gdbarch有一个1:1的关系。但是,对于堆栈分析(或“嗅探”),可能有多种方法是合适的,因此维护了一个函数列表。

使用frame_unwind_append_sniffer设置低级别堆栈分析函数。OpenRISC 1000具有用于查找帧的ID并获取由or1k_frame_sniffer(见第4.2.5.5节)指定的帧上寄存器值的自己的嗅探器。对于所有其他嗅探功能,都使用默认的DWARF2帧嗅探器,dwarf2_frame_sniffer。

高级别的嗅探器找到了堆栈帧的基地址。OpenRISC定义了自己的基础嗅探器,or1k_frame_base作为默认值。它提供了所需的所有功能,因此可以用作默认的基础嗅探器,使用frame_base_set_default设置。帧基是一个结构,其中的条目指向相应的帧嗅探器和提供基地址的函数,帧上的参数和帧上的局部变量。由于这三者对于OpenRISC 1000都是相同的,因此相同的函数or1k_frame_base_address用于所有三者。

4.2.5.5. OpenRISC 1000基础帧嗅探器

相同的函数or1k_frame_base_address用于提供三个基础函数:对于帧本身,局部变量和任何参数。在OpenRISC 1000中,这三者的值都相同。

cCopy codeor1k_frame_base.unwind      = or1k_frame_sniffer (NULL);
or1k_frame_base.this_base   = or1k_frame_base_address;
or1k_frame_base.this_locals = or1k_frame_base_address;
or1k_frame_base.this_args   = or1k_frame_base_address;
frame_base_set_default            (gdbarch, &or1k_frame_base);

此函数的规范需要堆栈的末尾,即堆栈指针。令人困惑的是,如果未设置deprecated_fp_regnum并且没有名为“fp”的寄存器,则此函数还用于确定$fp变量的值。然而,正如前面提到的,GDB正在摆脱对帧指针的固有理解。对于OpenRISC 1000,deprecated_fp_regnum目前已定义,尽管以后将定义一个伪寄存器,其名称为fp,映射到GPR 2。

与所有帧嗅探器一样,此函数通过使用通用寄存器解缠器frame_unwind_register_unsigned从堆栈中解缠NEXT帧的地址来传递。

4.2.5.6. OpenRISC 1000低级别帧嗅探器

函数or1k_frame_sniffer返回一个指向struct frame_unwind的指针,其中包含此嗅探器定义的函数的条目。对于OpenRISC 1000,这定义了一个自定义函数来构造给定指向NEXT帧的指针的THIS帧的帧ID的函数(or1k_frame_this_id)和一个自定义函数,给定指向NEXT帧的指针,给定指向NEXT帧的指针,此帧的帧ID的值的寄存器的函数(or1k_frame_prev_register)。

or1k_frame_this_id: 此函数的输入是指向NEXT帧的指针和此帧的前奏缓存(如果存在)。如果前奏缓存不存在,则它使用主要的OpenRISC 1000帧分析器or1k_frame_unwind_cache生成前奏缓存(见下文)。

从缓存的数据中,函数返回帧ID。这包括两个值,此帧的堆栈指针和使用此堆栈帧的函数的代码的地址(通常是入口点)

or1k_frame_prev_register: 此函数的输入是指向NEXT帧的指针,此帧的前奏缓存(如果存在)和寄存器号。如果前奏缓存不存在,则它使用主要的OpenRISC 1000帧分析器or1k_frame_unwind_cache生成前奏缓存(见下文)。

从缓存的数据中,返回一个标志,指示是否已优化掉寄存器(永远不会是这种情况),寄存器表示的l值的类型是什么(寄存器,内存还是非l值),保存在内存中的寄存器的地址(如果在内存中保存的话),保存此寄存器值的不同寄存器的编号(如果是这种情况),如果提供了缓冲区,则是从内存或寄存器缓存中获取的实际值。

由于实现使用内置的struct trad_frame_cache作为其寄存器缓存,因此代码可以使用trad_frame_get_register函数从缓存中解码所有这些信息。

OpenRISC 1000低级嗅探器依赖于or1k_frame_unwind_cache。这是嗅探器的核心。它必须确定给定指向NEXT帧的指针的THIS帧的帧ID,然后确定在THIS帧中关于PREVIOUS帧中寄存器值的信息。

所有这些数据都在前奏缓存(见第2.3.6节)中返回,将其引用作为一个参数传递。如果此缓存已经存在于此帧中,则它可以立即作为结果返回。

如果缓存尚不存在,则它将被分配(使用trad_frame_cache_zalloc)。第一步是从NEXT帧中解缠此函数的开始地址。可以使用目标文件中的DWARF2信息找到前奏的末尾(使用skip_prologue_using_sal)。

然后,代码通过每个前奏的指令来查找所需的数据。

谨慎: 分析只能考虑实际执行的前奏指令。很可能程序计数器位于前奏代码中,只能分析实际执行的指令。

通过简单地解缠NEXT帧来找到堆栈指针和程序计数器。堆栈指针是THIS帧的基础,并使用trad_frame_set_this_base将其添加到缓存数据中。

end_iaddr标记应该分析的代码的结束。只有地址小于此的指令将被考虑。

l.addi指令应该首先出现,其立即常数字段是堆栈的大小。如果缺少它,那么这是对函数的无帧调用。如果程序计数器恰好位于函数开始之前,在设置堆栈和帧指针之前,则它也将看起来像是无帧函数。

除非后来发现它已在堆栈上保存,否则PREVIOUS帧的程序计数器是THIS帧的链接寄存器,并且可以在寄存器缓存中记录。

提示: 保存寄存器数据时必须使用正确的函数。

当从THIS帧中的寄存器中获取PREVIOUS帧中的寄存器时,请使用trad_frame_set_reg_realreg。

当从THIS帧中的地址中获取PREVIOUS帧中的寄存器时,请使用trad_frame_set_reg_addr。

当在THIS帧中的特定值中获取PREVIOUS帧中的寄存器时,请使用trad_frame_set_reg_value。

每个寄存器的默认条目是PREVIOUS帧中的值是从THIS帧中的相同寄存器中获取的。

对于无帧调用,将不再找到更多的信息,因此代码分析的其余部分仅在帧大小为非零时适用。

前奏中的第二条指令是保存PREVIOUS帧的帧指针的地方。如果缺少此项,则是一个错误。保存它的地址(THIS帧的堆栈指针加上l.sw指令中的偏移)被保存在缓存中,使用trad_frame_set_reg_addr。

第三条指令应该是设置帧指针的l.addi指令。此指令中设置的帧大小应该与第一条指令中设置的帧大小相匹配。设置了此帧后,就可以使用帧指针来获取前一帧的堆栈指针。此信息记录在寄存器缓存中。

第四条指令应该是保存PREVIOUS帧的链接寄存器的l.sw指令。只有当PREVIOUS帧在堆栈上保存链接寄存器时才出现。

最后,对于实际的前奏,所有PREVIOUS帧的GPR(1到31)的保存都应该在其一开始完成。可以在缓存中记录哪些GPR已被保存,使用trad_frame_set_reg_realreg。

最后,PREVIOUS帧的实际帧大小(包括任何以字节为单位的局部变量和非l值参数)可以在缓存中记录,使用trad_frame_set_frame_size。

最终,这将返回指向缓存的指针,以供OpenRISC 1000的所有嗅探器使用。

4.3. OpenRISC 1000 JTAG 远程目标规范

OpenRISC 远程 JTAG 协议的远程目标规范代码位于 gdb 子目录中。remote-or1k.c 包含了目标定义。低级接口位于 or1k-jtag.c 中,共享头文件位于 or1k-jtagh 中。

低级接口被抽象成一组与目标行为相关的 OpenRISC 1000 特定函数。提供两种实现(在 or1k-jtag.c 中),一种用于直接连接到主机的并行端口的目标,另一种用于通过 OpenRISC 1000 远程 JTAG 协议通过 TCP/IP 连接的目标。

void      or1k_jtag_init (char *args);
void      or1k_jtag_close ();
ULONGEST  or1k_jtag_read_spr (unsigned int  sprnum);
void      or1k_jtag_write_spr (unsigned int  sprnum, ULONGEST data);
int       or1k_jtag_read_mem (CORE_ADDR  addr, gdb_byte  *bdata, int len);
int       or1k_jtag_write_mem (CORE_ADDR  addr, const gdb_byte *bdata, int len);
void      or1k_jtag_stall ();
void      or1k_jtag_unstall ();
void      or1k_jtag_wait (int  fast);

选择使用哪种实现由传递给 or1k_jtag_init 的参数确定。

or1k_jtag_init 和 or1k_jtag_close。初始化和关闭与目标的连接。or1k_jtag_init 接收一个包含目标地址(可以是本地设备或远程 TCP/IP 端口地址)的参数字符串。可提供可选的第二个参数 reset,以指示连接后应重置目标。

or1k_jtag_read_spr 和 or1k_jtag_write_spr。读取或写入特殊目的寄存器。

or1k_jtag_read_mem 和 or1k_jtag_write_mem。读取或写入一块内存。

or1k_jtag_stall 和 or1k_jtag_unstall。暂停或取消暂停目标。

or1k_jtag_wait。等待目标暂停。

远程目标接口的二进制文件(remote-or1k.o 和 or1k-jtag.o)被添加到 OpenRISC 目标的 configure.tgt 文件中。如第4.2节所述,这只指定二进制文件,因此无法捕获对头文件的依赖关系。为此,需要编辑 Makefile.in。

[提示] 提示 对于简单的端口,可以省略编辑 Makefile.in。相反,在调用 make 之前,触摸目标特定的 C 源文件以确保它们被重新构建。

4.3.1. 为 OpenRISC 1000 创建 struct target_ops

通过定义函数 _initialize_remote_or1k 创建远程目标。创建并填充一个新的 struct target_ops,or1k_jtag_target,并通过调用 add_target 将其添加为目标。

大部分目标操作对于 OpenRISC 1000 是通用的,独立于实际的低级接口。通过在第4.3节中描述的接口函数中抽象低级接口来实现这一点。

在建立了所有目标函数之后,通过调用 add_target 添加目标。

当选择了目标(使用 GDB 目标 jtag 命令),将使用在 or1k-tdep.c 中定义的全局变量 or1k_target 来引用用于 OpenRISC 1000 架构的目标操作集。

[注意] GDB 有自己的全局变量 current_target,它指的是当前的目标操作集。然而,这是不够的,因为即使通过 OpenRISC 远程接口连接了目标,它可能不是当前目标。GDB 使用的层次结构意味着在同一时间可能有另一个处于活动状态的目标。

目标接口的操作大部分涉及操作调试 SPRs。为了避免不断地将它们写入目标,它们的值的缓存保存在 or1k_dbgcache 中,在任何将取消目标暂停(导致其执行)的操作之前被清除。

4.3.2. OpenRISC 1000 目标函数和变量提供信息

一组变量和函数提供接口的名称和不同类型的信息。

to_shortname。这是在 GDB 中连接时用于目标的名称。对于 OpenRISC 1000,它是 “jtag”,因此在 GDB 中通过命令 target jtag … 建立连接。

to_longname。用于 GDB info target 命令的命令的简要描述。

to_doc。此目标的帮助文本。第一句用于关于所有目标的一般帮助,关于此目标的详细帮助的完整文本。文本解释了如何直接连接和通过 TCP/IP 连接。

or1k_files_info。此函数为 info target 提供初始信息。对于 OpenRISC 1000,它提供在目标上运行的程序的名称(如果已知)。

OpenRISC 远程目标始终是可执行的,一旦连接建立,就可以完全访问内存、堆栈、寄存器等。OpenRISC 1000 的 struct target_ops 中的一组变量记录了这一点。

to_stratum。由于 OpenRISC 1000 目标可以执行代码,因此将此字段设置为 process_stratum。

to_has_all_memory、to_has_memory、to_has_stack 和 to_has_registers。一旦连接到 OpenRISC 1000 目标,它就可以访问其所有内存、堆栈和寄存器,因此将所有这些字段设置为 1(true)。

to_has_execution。当连接初始建立时,OpenRISC 1000 处理器将处于暂停状态,因此实际上不执行。因此,此字段初始化为 0(false)。

to_have_steppable_watchpoint 和 to_have_continuable_watchpoint。这些标志指示目标是否可以在执行完监视点后立即步进,或者监视点是否可以在没有任何效果的情况下立即继续。

如果 OpenRISC 1000 触发硬件监视点,受影响的指令尚未执行完成,因此必须重新执行它(代码无法继续)。此外,在重新执行时,监视点必须暂时禁用,否则它将再次触发(无法步进)。因此,这两个标志都设置为 0(false)。

4.3.3. 控制连接的 OpenRISC 1000 目标函数

这些函数控制与目标的连接。对于远程目标,这涉及建立和关闭到驱动硬件的服务器的 TCP/IP 套接字链接。对于本地目标,这涉及打开和关闭设备。

or1k_open。通过调用 target_preopen 清理现有的连接,并通过 unpush_target 从目标堆栈中移除此目标的任何实例,来建立到目标的连接。然后,通过低级接口例程 or1k_jtag_init(如果请求则重置目标)建立连接。

连接建立后,将检查目标的 Unit Present SPR 以验证它是否有可用的调试单元。从 CPU Configuration SPR 中读取关于 GPR 数量和匹配点数量的数据,并用于更新 struct gdbarch_tdep。

然后,目标处理器被暂停,以防止进一步执行,并等待 1000 微秒以完成暂停。

调试缓存被清除,将 Debug Stop SPR 设置为在陷阱异常上触发 JTAG 接口(用于调试断点、监视点和单步执行)。在执行重新开始之前,缓存将被写入 SPRs。

建立连接后,目标被推送到堆栈上。它被标记为运行中,这将设置与运行中进程相关联的所有标志,并更新当前目标的选择(根据层次可能是此目标)。然而,OpenRISC 连接是在目标处理器暂停的情况下建立的,因此通过将宏 target_has_execution 设置为 0 来清除 to_has_execution 标志。在 or1k_resume 恢复目标暂停时,它将被设置。

作为良好管理的一部分,使用 no_shared_libraries 清除所有共享库符号。

GDB 根据其进程和线程 ID 标识所有下级可执行文件。OpenRISC 1000 的此端口用于裸金属调试,因此没有可能执行的不同进程的概念。因此,null_ptid 被用作目标的进程/线程 ID。这在 GDB 全局变量 inferior_pid 中设置。

[注意] 确保在这个早期阶段建立下级进程/线程 ID,以便始终能够唯一标识目标是很重要的。

最后,调用通用的 start_remote 来设置准备好执行的新目标。这可能失败,因此调用被包装在一个函数 or1k_start_remote 中,该函数具有使用 catch_exception 运行的正确原型。如果发生失败,则在异常被抛到顶层之前可以将目标弹出。

or1k_close。这通过调用低级接口函数 or1k_jtag_close 来关闭连接。目标已经被弹出,并且下级已经被删除(参见第4.3.6节),因此不需要这些操作。

or1k_detach。这只是从正在调试的目标分离,通过调用 or1k_close 来实现。

没有明确的重新连接到目标的函数,但通过在 GDB 中使用 target jtag 命令调用 or1k_open 可以实现相同的效果。

4.3.4. OpenRISC 1000 目标函数以访问内存和寄存器

这是一组用于访问目标寄存器和内存的函数。

or1k_fetch_registers。此函数从实际目标寄存器中填充寄存器缓存。与 OpenRISC 1000 的接口仅允许读取内存或 SPRs。然而,通用寄存器(GPRs)被映射到 SPR 空间,因此可以通过这种方式读取。

or1k_store_registers。这是 or1k_fetch_registers 的反向操作。它将寄存器缓存的内容写回目标上的物理寄存器。

or1k_prepare_to_store。GDB 允许需要在存储之前进行一些准备工作的目标,因此提供了此函数。对于 OpenRISC 1000,这不是必需的,因此只是返回。

or1k_xfer_partial。这是从目标读取和写入对象的通用函数。然而,只需要支持从内存读取和写入的对象类别。通过低级接口例程 or1k_jtag_read_mem 和 or1k_jtag_write_mem 来实现这一点。

generic_load。此通用函数用作目标操作的 to_load 函数。加载 OpenRISC 1000 映像没有什么特别之处。此函数将调用 or1k_xfer_partial 函数,以传输映像的每个部分的字节。

4.3.5. OpenRISC 1000 处理断点和监视点的目标函数

OpenRISC 1000 可以支持硬件断点和监视点,如果调试单元中有匹配点可用。

[注意] 要注意术语 “监视点” 可能会导致混淆。在 GDB 中,它用于指代正在监视读取或写入活动的位置。这可能是通过硬件或软件实现。

如果以硬件形式实现,它们可能使用 OpenRISC 1000 调试单元机制,该机制也使用术语 “监视点”。本文档在存在混淆风险时使用术语 “GDB 监视点” 和 “OpenRISC 1000 监视点”。

or1k_set_breakpoint。这是设置硬件断点的底层 OpenRISC 1000 函数,如果有可用的话。这由 Debug Value 寄存器和 Debug Control 寄存器控制。此函数由目标操作函数 or1k_insert_breakpoint 和 or1k_insert_hw_breakpoint 使用。

通过搜索 Debug Control 寄存器,找到第一个没有设置其 DVR/DCR Preset(DP)标志的寄存器,使用 or1k_first_free_matchpoint。

将 Debug Value 寄存器设置为断点的地址,将 Debug Control 寄存器设置为在取指令的无符号有效地址等于 Debug Value 寄存器时触发。在 Debug Mode 寄存器 1 中将相应的 OpenRISC 1000 监视点标记为未链接,并设置为在 Debug Mode 寄存器 2 中触发陷阱异常。

or1k_clear_breakpoint。这是 or1k_set_breakpoint 的对应部分。它由目标操作函数 remove_breakpoint 和 remove_hw_breakpoint 调用。

在 Debug Control 寄存器中搜索与给定地址匹配的条目(使用 or1k_matchpoint_equal)。如果找到寄存器,则清除其 DVR/DCR Present 标志,并在 Debug Mode 寄存器 2 中标记匹配点未使用。

or1k_insert_breakpoint。此函数插入断点。它尝试使用 or1k_set_breakpoint 插入硬件断点。如果失败,则使用通用的 memory_insert_breakpoint 设置软件断点。

or1k_remove_breakpoint。这是 or1k_insert_breakpoint 的对应部分。它尝试清除硬件断点,如果失败,则尝试使用通用的 memory_remove_breakpoint 清除软件断点。

or1k_insert_hw_breakpoint 和 or1k_remove_hw_breakpoint。这些函数类似于 or1k_insert_breakpoint 和 or1k_remove_breakpoint。然而,如果硬件断点不可用,则它们不会尝试使用软件(内存)断点。

or1k_insert_watchpoint。此函数尝试插入 GDB 硬件监视点。为此,它需要一对链接在一起的 OpenRISC 1000 监视点。第一个将检查大于或等于感兴趣的起始地址的内存访问。第二个将检查小于或等于感兴趣的结束地址的内存访问。如果两个条件都满足,则访问类型可以是加载有效地址(对于 GDB rwatch 监视点)、存储有效地址(对于 GDB watch 监视点)或两者兼而有之(对于 GDB awatch 监视点)。

OpenRISC 1000 监视点对必须是相邻的(以便可以使用 Debug Mode 寄存器 1 将它们链接在一起),但是可能连续的断点已经分散使用了 OpenRISC 1000 监视点。or1k_watchpoint_gc 用于整理可以移动的所有现有 OpenRISC 1000 监视点,以找到一对(如果可能的话)。

or1k_remove_watchpoint。这是 or1k_insert_watchpoint 的对应部分。它搜索匹配的相邻一对 OpenRISC 1000 监视点,使用 or1k_matchpoint_equal。如果找到,两者都将在其 Debug Control 寄存器中标记为未使用,并在 Debug Mode 寄存器 2 中清除。

or1k_stopped_by_watchpoint 和 or1k_stopped_data_address。这些函数用于了解可能已触发的 GDB 监视点。两者都使用实用程序函数 or1k_stopped_watchpoint_info,该函数确定是否触发了 GDB 监视点,如果触发了,则是哪个监视点以及对于什么地址。or1k_stopped_watchpoint 仅返回一个布尔值,表示是否触发了监视点。or1k_stopped_data_address 每次触发监视点时调用一次。它返回触发监视点的地址,并且必须还清除监视点(在 Debug Mode 寄存器 2 中)。

4.3.6. OpenRISC 1000 目标函数以控制执行

当使用 GDB 的 run 命令启动执行时,它需要在下级上建立可执行文件,然后开始执行。这是通过目标的 to_create_inferior 和 to_resume 函数完成的。

一旦开始执行,GDB 将等待目标的 to_wait 函数返回控制。

此外,目标提供了停止执行的操作。

or1k_resume。这是导致目标程序运行的函数。它是响应于 run、step、stepi、next 和 nexti 指令调用的。

该函数清除 Debug Reason 寄存器,在 Debug Mode 寄存器 2 中清除任何监视点状态位,然后提交调试寄存器。

如果调用方请求单步执行,将使用 Debug Mode 寄存器 1 设置这一点,否则将清除此寄存器。

最后,目标可以标记为正在执行(第一次调用 or1k_resume 时不会标记为正在执行),调试寄存器写出,并且处理器取消暂停。

or1k_wait。此函数等待目标暂停,并分析原因。关于目标暂停的原因的信息通过状态参数返回给调用方。函数返回暂停的进程/线程 ID,尽管对于 OpenRISC 1000,这将始终是相同的值。

在等待目标暂停(使用 or1k_jtag_wait)的同时,将安装一个信号处理程序,以便用户可以使用 ctrl-C 中断执行。

等待返回后,所有寄存器和帧缓存都是无效的。这通过调用 registers_changed(其反过来清除了帧缓存)来实现。

当处理器暂停时,Debug Reason 寄存器(一个 SPR)显示暂停的原因。这可能是由于 Debug Stop 寄存器中设置的任何异常(当前仅为 trap),由于单步执行,由于复位(调试器在复位时使处理器停滞)或者(当目标是体系结构模拟器 Or1ksim 时)由于执行退出 l.nop 1。

在所有情况下,先前的程序计数器 SPR 指向刚刚执行的指令,下一个程序计数器 SPR 指向即将执行的指令。然而,对于生成陷阱的监视点和断点,前一个程序计数器处的指令将不会执行完成。因此,在程序恢复执行时,应重新执行此指令,而不使断点/监视点生效。

GDB 理解这一点。将程序计数器设置为前一个程序计数器就足够了。GDB 将意识到该指令对应于刚刚遇到的断点/监视点,取消断点,单步执行此指令,然后重新启用断点。通过调用 write_pc 以前的程序计数器值实现。

在此,OpenRISC 1000 产生了一个小问题。标准 GDB 方法很好,但是如果断点在分支或跳转指令的延迟槽中,则重新执行不仅必须是前一个指令,而且必须是前一个的前一个指令(如果它是跳转并且是跳转-并链接指令,则还要恢复链接寄存器)。此外,这仅在分支确实是前面的指令的情况下才是正确的重启,而不是延迟槽是不同分支指令的目标的情况。

在没有“前一个前一个”程序计数器的情况下,在所有情况下都无法正确重启。暂时来说,不希望在延迟槽上设置断点。但是在实际情况下,几乎不可能源代码级别的调试器在延迟槽中设置断点。

将来的更完整的解决方案将使用 struct gdbarch adjust_breakpoint_address 来移动对延迟槽的任何断点的请求,以坚持断点放置在前导的跳转或分支上。这对于除最不寻常的代码之外的所有情况都适用,该代码将延迟槽用作分支目标。

解决了程序计数器的调整问题后,将任何单步操作标记为陷阱,即使单步操作不设置陷阱异常,也无需重新执行,但通过在这里设置标志,异常将正确映射到 TARGET_SIGNAL_TRAP 以返回给 GDB。

响应被标记为停止的处理器(TARGET_WAITKIND_STOPPED)。所有异常都映射到相应的 GDB 信号。如果没有引发异常,则将信号设置为默认值,除非刚刚执行的指令是 l.nop 1,这由体系结构模拟器用于指示终止。在这种情况下,响应被标记为 TARGET_WAITKIND_EXITED,并且关联值设置为退出返回代码。

现在 Debug Reason 寄存器(它是粘性的)可以清除,并且可以返回进程/线程 ID。

or1k_stop。这将停止执行处理器。为了干净地实现这一点,将处理器暂停,设置为单步模式,然后取消暂停,因此执行将在指令结束时停止。

or1k_kill。这是更激烈的终止,当 or1k_stop 未能令人满意时使用。假定与目标的通信已中断,因此将哀悼目标,这将关闭连接。

or1k_create_inferior。这在下级上设置一个要在目标上运行的程序,但实际上并未开始运行。这是响应于 GDB run 命令的调用,并传递给该命令的任何参数。然而,OpenRISC 1000 JTAG 协议无法将参数发送到目标,因此这些参数被忽略。

如果已使用 file 命令加载了可执行文件的本地符号表,调试会更加容易。这进行检查并发出警告。但是,如果不存在,可以在没有符号数据的情况下调试 OpenRISC 1000 目标上的代码。

然后通过调用 init_wait_for_inferior 在 GDB 中清除所有静态数据结构(断点列表等)。

[提示] 如果在 ddd 中使用 OpenRISC 1000 的 GDB,则经常会触发有关传递参数的警告。当 ddd 被要求在单独的执行窗口中运行程序时,它会尝试通过创建 xterm 并通过伪终端将 I/O 重定向到该 xterm 来实现这一点。重定向是传递给 GDB run 命令的参数。

OpenRISC 1000 的 GDB 不支持这一点。应该使用 ddd 禁用在单独窗口中运行的选项。

or1k_mourn_inferior。这是 or1k_create_inferior 的对应部分,在执行完成后调用。它通过调用通用函数 generic_mourn_inferior 进行整理。如果目标仍然显示为正在执行,则将其标记为已退出,这将导致选择新的当前目标。

4.3.7. OpenRISC 1000目标执行命令的函数

OpenRISC 1000目标实际上没有执行命令的直接方式。然而,将SPR访问实现为远程命令提供了一种独立于目标访问协议的访问机制。特别是GDB架构无需了解实际远程协议的细节。

SPR访问被实现,好像目标能够运行两个命令,readspr用于从SPR获取值,writespr用于在SPR中设置值。每个命令都有一个十六进制指定的第一个参数,表示SPR编号。writespr有一个第二个参数,用十六进制指定,表示要写入的值。readspr返回读取的值,以十六进制表示为一个数字。

当需要访问SPR时,通过将这些命令之一作为参数传递给目标的to_rcmd函数来实现。

or1k_rcmd。要执行的命令(readsprwritespr)作为第一个参数传递。命令的结果通过第二个参数,即独立于UI的文件句柄,写回。

readspr被映射到相应的or1k_jtag_read_spr调用。writespr被映射到相应的or1k_jtag_write_spr调用。在发生错误的情况下(例如命令格式错误),返回结果"E01"。如果writespr成功,返回结果为"OK"。如果readspr成功,返回值为十六进制形式的结果。在所有情况下,结果都被写入到指定为函数的第二个参数的UI独立文件句柄中。

4.3.8. 低级JTAG接口

OpenRISC JTAG系统的接口位于gdb/or1k-jtag.cgdb/or1k-jtag.h中。这些细节对于移植GDB并不直接相关,因此这里仅提供概述。完整的细节可在源代码的注释中找到。

接口是分层的,以最大程度地提高使用效率。特别是在目标通过TCP/IP远程连接还是直接通过连接到并行端口的JP1头连接时,很多功能是相同的。

最高层是公共函数接口,这些接口根据在GDB中可见的实体操作:打开和关闭连接,读写SPR,读写内存,暂停,取消暂停和等待处理器。这些函数始终成功,并具有or1k_jtag_的函数前缀。

接下来的层是由OR1K JTAG协议提供的抽象:读/写JTAG寄存器,读/写一块JTAG寄存器,并选择扫描链。这些函数可能会遇到错误,但否则不会返回错误结果。这些是静态函数(即本地文件中的函数),前缀是or1k_jtag_

接下来的层分为两组,一组用于本地连接的JTAG(JP1),另一组用于通过TCP/IP进行远程连接,与上一层中的函数相对应。这些函数检测到错误并返回一个错误代码,指示发生了错误。这些是静态函数,前缀分别是:jp1_jtr_

最后一层有不同的版本,用于本地连接的JTAG(用于驱动JP1接口的低级例程)和远程使用(用于构建和发送/接收TCP/IP数据包)。这些函数检测错误并返回错误代码,指示发生了错误。这是带有前缀jp1_ll_jtr_ll_的静态函数。

错误可以通过静默处理,或者(如果是致命错误)通过GDB的错误函数来处理。

[注意] 少数人现在使用JP1直接连接,并且无法确保该代码是否有效!

4.4. OpenRISC 1000反汇编器

OpenRISC 1000反汇编器是更广泛的binutils实用工具集的一部分,位于opcodes子目录中。它提供两个版本的反汇编函数,print_insn_big_or32print_insn_little_or32,用于大端和小端实现的or32-dis.c架构。

指令解码使用or32-opc.c中的有限状态自动机(FSA)。这是通过从描述指令集的表中构建的build_automata函数在启动时构建的。此函数是在在OpenRISC 1000体系结构被定义后,即在_initialize_or1k_tdep函数中立即调用的。

反汇编器利用符号表信息,以尽可能替换分支和跳转目标为符号名称。

4.5. OpenRISC 1000专用GDB命令

第2.5节描述了如何扩展GDB命令集。对于OpenRISC 1000架构,扩展了info命令以显示SPR值(info spr),并添加了一个新命令spr以设置SPR的值。

这两个命令是在架构被创建并反汇编器自动机被初始化之后,在_initialize_or1k_tdep中添加的。

4.5.1. info spr命令

使用add_info添加了新的子命令:

add_info ("spr", or1k_info_spr_command,
          "Show the value of a special purpose register");

功能由or1k_info_spr_command提供。用户可以通过名称或编号指定组(显示该组中所有寄存器的值),或者通过寄存器名称(显示该寄存器的值),或者通过组名称/编号和寄存器名称/编号(显示该组中寄存器的值)。

参数是使用or1k_parse_params从命令文本中提取的,它还处理语法或语义错误。如果成功解析参数,则使用UI独立函数ui_out_field_fmt打印结果。

SPR是使用方便函数or1k_read_spr读取的。这将访问转换为调用readspr命令,该命令可以通过其to_rcmd目标操作传递给目标(请参阅第4.3.7节)。这将允许以最适合当前目标访问方法的方式访问SPR。

4.5.2. spr命令

使用add_com添加了新的顶级命令,被分类为支持命令(class_support):“add_com("spr", class_support, or1k_spr_command);”。

功能由or1k_spr_command提供。它还使用or1k_parse_spr_params解析参数,尽管现在多了一个参数(要设置的值)。新值被写入到相关的SPR中,并使用ui_out_field_fmt记录更改。

SPR是使用方便函数or1k_write_spr写入的。这将访问转换为调用writespr命令,该命令可以通过其to_rcmd目标操作传递给目标(请参阅第4.3.7节)。这将允许以最适合当前目标访问方法的方式访问SPR。

【2】强烈建议将此作为集合的新子命令。但是,spr命令是在GDB 5.0中引入的,现在没有替代它的必要。

第5章:总结

本应用注释详细描述了将GDB移植到新体系结构所需的步骤。该过程以OpenRISC 1000架构的端口为例进行了说明。

欢迎提出更正或改进的建议。请通过jeremy.bennett@embecosm.com与作者联系。

术语表

  • ABI(Application Binary Interface)
    • 应用程序和操作系统之间的低级接口,确保程序之间的二进制兼容性。
  • big endian
    • 计算机体系结构中字节和字寻址之间关系的描述。在big endian架构中,数据字中的最低有效字节位于内存中最高的字节地址(字节中的字节)。
  • Binary File Descriptor (BFD)
    • 允许应用程序使用相同的例程操作对象文件的软件包。通过创建新的BFD后端并将其添加到库中,可以支持新的对象文件格式。
  • COFF(Common Object File Format)
    • 用于Unix系统上的可执行文件、目标代码和共享库计算机文件的格式规范。现在在很大程度上被ELF替代。
  • ELF(Executable and Linkable Format)
    • 一种用于可执行文件、目标代码、共享库和核心转储的通用标准文件格式。这是Unix和类Unix系统上x86的标准二进制文件格式,现在在很大程度上替代了COFF。曾被称为Extensible Linking Format。
  • frame pointer
    • 在基于堆栈的语言中,堆栈指针通常指的是本地框架的末尾。帧指针是第二个寄存器,它指向本地框架的开头。并非所有基于堆栈的体系结构都使用帧指针。
  • GPR(General Purpose Register)
    • 在OpenRISC 1000架构中,介于16到32个通用整数寄存器之间的一个。
  • JTAG(Joint Test Action Group)
    • IEEE 1149.1标准的通常名称,该标准为用于测试印刷电路板和使用边界扫描测试芯片的测试访问端口定义了标准测试访问端口和边界扫描架构。该标准允许在板或芯片内部读取状态,因此是调试器连接到嵌入式系统的一种自然机制。
  • little endian
    • 计算机体系结构中字节和字寻址之间关系的描述。在little endian架构中,数据字中的最低有效字节位于内存中最低的字节地址(字节中的字节)。
  • MMU(Memory Management Unit)
    • 一种硬件组件,通过页面查找表将虚拟地址引用映射到物理内存地址。当访问时,可能需要异常处理程序将不存在的内存页面从后备存储器中带入物理内存。
  • RTEMS(Real Time Executive for Multiprocessor Systems)
    • 用于实时嵌入式系统的操作系统,提供POSIX接口。它没有进程或内存管理的概念。
  • SPR(Special Purpose Register)
    • 在OpenRISC 1000架构中,控制处理器所有方面的最多65536个寄存器之一。这些寄存器按照2048个寄存器的组进行排列。当前的架构总共定义了12个组。
  • stack frame
    • 在过程化语言中,用于在执行的特定点保存过程中的本地变量值的动态数据结构。
  • SoC(System on Chip)
    • 包括一个或多个处理器核心的硅芯片。
  • 9
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值