移植GDB(1) arch和frame v0.4
teawater<teawater@gmail.com>
修改记录:
v0.4
2006-06-24,增加5.1中落下的内容。
2006-06-25,修改5.3中的错误。
2006-06-25,修改5.4中的错误。
2006-06-25,修改5.5中的错误。
2006-06-29,修改7.2中的一些语句。
2006-06-29,修改7.4中的错误。
2006-07-01,修改7.8中的一些语句。
2006-07-01,修改7.9中的一些语句。
2006-07-03,修改8.1中的错误。
2006-07-03,修改8.2中的一些语句。
2006-07-06,修改8.3中的一些语句。
2006-07-09,修改9.3.2中的一些语句。
2006-07-10,修改9.3.3中的一些语句。
v0.3
2006-04-01,增加了第9章sigtramp frame。
2006-03-22,修改了对STEP_SKIPS_DELAY (pc)和STEP_SKIPS_DELAY_P的介绍。
2006-03-21,修改了set_gdbarch_software_single_step的介绍。
2006-02-25,修改了第1章中的一些错误。
v0.2
2006-02-20,增加了set_gdbarch_regset_from_core_section的介绍。
2006-02-12,增加了set_gdbarch_cannot_fetch_register和set_gdbarch_cannot_store_register的介绍。
sigtramp frame相关部分没有进行介绍。
v0.1
2006-01-23,增加第7章frame的介绍。增加第8章dummy frame。
v0.0
2006-01-09,v0.0版本编写完成,dummy frame相关部分没有进行介绍。
2005-11-19,文档创建。
目录
1.写在前面
2.GDB/config.sub
3.GDB/gdb/configure.tgt
4.GDB/gdb/config/ARCH/TARGET.mt
5.GDB/gdb/ARCH-tdep.c GDB/gdb/ARCH-TARGET-tdep.c
5.1.综述
5.2.GDB/gdb/ARCH-tdep.c
5.3.GDB/gdb/ARCH-TARGET-tdep.c
5.4.gdbarch_register_osabi_sniffer
5.5.gdbarch设置函数
6.GDB/gdb/config/ARCH/tm-ARCH.h GDB/gdb/config/ARCH/tm-TARGET.h
6.1.综述
6.2.宏定义
7.frame
7.1.概述
7.2.struct frame_info
7.3.struct frame_id
7.4.取得当前指令对应函数frame_info信息
7.5.取得指定frame_info的frame_id
7.6.取得指定frame_info对应的函数返回地址
7.7.取得指定frame的高一级frame的函数地址
7.8.跟frame相关的gdbarch设置函数
7.9.struct frame_unwind
8.dummy frame
8.1.概述
8.2.struct value *call_function_by_hand (struct value *function, int nargs, struct value **args)
8.3.跟dummy frame相关的gdbarch设置函数
9.sigtramp frame
9.1.概述
9.2.普通分析法
9.3.tramp_frame_prepend_unwinder法
9.3.1.void tramp_frame_prepend_unwinder (struct gdbarch *gdbarch, const struct tramp_frame *tramp_frame)
9.3.2.struct tramp_frame
9.3.3.static int tramp_frame_sniffer (const struct frame_unwind *self, struct frame_info *next_frame, void **this_cache)
9.3.4.static struct trad_frame_cache *tramp_frame_cache (struct frame_info *next_frame, void **this_cache)
1.写在前面
本文针对GDB-6.3进行编写。
LIBBFD是一套用来分析二进制文件的库,GDB用其来对二进制文件进行分析,具体关于LIBBFD可以参见http://www.gnu.org/software/binutils/manual/bfd-2.9.1/bfd.html。
在GDB中,用来表示一个arch最核心部分就是gdbarch结构,其中包含着很多函数指针和变量。
在GDB初始化以及读入新的可执行文件的时候,将调用相应代码(个人认为编写这部分代码以及用来给gdbarch结构中函数指针赋值是arch移植的主要工作),产生gdbarch结构的变量,将相应arch相关的函数地址和变量初始化在其中,并将其地址存在current_gdbarch这个指针中。
在GDB进行调试的时候,可以通过访问current_gdbarch中的函数指针和变量来达到进行arch相关调试的目的。
还有一部分跟ARCH有关的设置,在GDB/gdb/config/ARCH/tm-TARGET.h进行一些宏定义来进行设置GDB的运行行为。
有一部分宏定义会在GDB/gdb/gdbarch.h定义成current_gdbarch定义在一起,GDB调用这部分宏实际上就调用了current_gdbarch中的内容,所以这部分的宏既可以通过设置gdbarch结构来实现也可以通过设置宏的方式来实现。
frame是stack frame,也就是栈结构,个人觉得跟frame相关的部分是gdbarch结构中最重要的部分,其在调试进行中起比较重要的作用,但是在GDB Internals中相关章节是空着的,因此我决定介绍一下frame,这样也对编写初始化gdbarch相关代码有帮助。
下面是本文将使用的缩写:
GDBINT GDB Internals Manual的缩写。
GDB 指GDB源文件目录。
ARCH 体系结构名称。
TARGET 体系结构下的调试目标,一般是一种操作系统,比如Linux。
bfd_architecture 定义在LIBBFD中用来描述ARCH的枚举类型。
gdb_osabi 定义在GDB中用来描述当前操作系统的ABI的枚举类型,其具体分类可以见GDBINT 9.1节。
2.GDB/config.sub
这是一个shell文件,其作用是在运行configure的时候将用户指定的target和host转化成标准的字符串格式。
这个文件可以以后面跟一个要转化的字符串的形式被调用,输出转化后的字符串,可以很方便的进行测试。
这个文件主要分为以下几个部分:
第一部分,将用户输入的结构参数分成前$basic_machine和后$os两部分。
第二部分,对$os也就是后面的操作系统相关的字段进行分析和处理。
第三部分,对$basic_machine进行分析和处理,如果是要增加一个芯片的支持,就在这里增加分析和处理,有时候也对$os进行处理。一般来说增加一个如果在ARCH名称后面没跟任何文件类型的则给$basic_machine增加一个"-unknown"。
第四部分,对$os进行进一步的分析和处理。
第五部分,根据$basic_machine对$os进行设置。
第六部分,如果$basic_machine是*-unknown的形式,则根据$os得到其$vendor,也就是制造商,然后将$basic_machine中的unknown替换成$vendor。
第七部分,打印$basic_machine$os。
3.GDB/gdb/configure.tgt
这个文件跟运行configure时设置的target有关。这个文件主要作用是根据上面文件得到的字符串得到编译需要的一些信息取得编译需要的信息。
其是在GDB/gdb/configure中被调用,在GDB/gdb/configure中先将从GDB/config.sub得到的信息存在$target中,然后将其中三部分存放在$target_cpu、$target_vendor、$target_os中。然后在其包含的文件GDB/configure.tgt进行分析。
这个文件首先根据$target_cpu来取得$gdb_target_cpu的值。如果你的CPU不象mips那样含有mipsel类的值,就不用对这段进行修改。这个值将作为ARCH的值。
后面一段根据$target来取得$gdb_target的值,其就是TARGET的值。一般来说这个值使用操作系统的名称。
最后一段根据$target来取得$gdb_osabi的值,这个值是系统ABI的值,这个值最终将被设置为GDB_OSABI_DEFAULT,也就是系统ABI的默认值。
4.GDB/gdb/config/ARCH/TARGET.mt
这个文件是设置一些编译GDB需要的跟当前TARGET有关的文件。
TDEPFILES=
这个是指定要编译的.o文件,后面介绍的跟TARGET有关的.c文件编译成的.o文件需要在这里指出,而那个.c文件使用的一些函数相关的.o文件也需要在这里指定。
DEPRECATED_TM_FILE=
这个是指定描述TARGET的头文件,一般来说这个头文件的写法为tm-TARGET.h存放在GDB/gdb/config/ARCH/目录中,具体内容将在后面介绍。注意这里跟GDBINT写的不同,这里的写法是对的,用GDBINT中介绍的方法无法正常编译。
5.GDB/gdb/ARCH-tdep.c GDB/gdb/ARCH-TARGET-tdep.c
5.1.综述
这两个文件的主要作用就是初始化gdbarch结构,第一个是跟ARCH相关的,第二个是跟这个TARGET相关的。他们的.o文件都将在TARGET.mt的TDEPFILES=中被指定。
个人认为将这两个文件分开的好处是,一个ARCH可能需要支持若干个TARGET,也可以说是一种芯片支持若干个操作系统;而这些设置中有一部分是跟ARCH有关,另一部分则跟这个ARCH下的TARGET有关;当同一个ARCH的多个TARGET时,第一个文件ARCH-tdep.c可以重复使用。
在不需要针对TARGET作ARCH以外的设置的时候,可以不包含GDB/gdb/ARCH-TARGET-tdep.c文件。例如arm和mips的embed.mt都是这种情况。
在GDB中很多ARCH的这两个文件都对应了GDB/gdb/ARCH-tdep.h和GDB/gdb/ARCH-TARGET-tdep.h形式的头文件,不过这两个头文件主要是保存一些跟这个ARCH相关的私用数据,所以对其的使用可以灵活掌握。
5.2.GDB/gdb/ARCH-tdep.c
GDB/gdb/ARCH-tdep.c文件的初始化函数是void _initialize_ARCH_tdep (void),这个函数在GDB启动的时候将被调用,这里最主要的就是对gdbarch_register函数的调用,当然如果需要也可以在这个函数中增加一些命令。
void gdbarch_register (enum bfd_architecture bfd_architecture, gdbarch_init_ftype *init, gdbarch_dump_tdep_ftype *dump_tdep)
这个函数的作用就是将init函数指针和dump_tdep函数指针标记为bfd_architecture注册到全局链表gdbarch_registry上。这样当GDB读入类型为bfd_architecture的可执行文件的时候,init和dump_tdep就将被调用。
注册的init函数的作用就是初始化并设置一个gdbarch结构,并将其地址返回。编写这个函数基本上可以参考GDB/gdb/目录中其他ARCH的编写,下面我来基本介绍一下其的编写方法。
基本上头一步就是给gdbarch结构分配空间,使用gdbarch_alloc函数,其第一个参数是init函数指针的参数info,第二个参数tdep是一个gdbarch_tdep结构指针,其主要是存储了一些跟这个ARCH相关的私有数据,这个gdbarch_tdep结构要定义在ARCH相关的几个文件里,可以根据需要随意增加内容。在gdbarch_alloc函数中,其先分配空间,然后将tdep以及其他默认的gdbarch参数初始化到gdbarch中,然后返回。
然后就使用一些函数将这个ARCH相关的函数和数据设置到这个gdbarch上,这个将在“5.5.gdbarch设置函数”进行详细的介绍。
最后调用“gdbarch_init_osabi (info, gdbarch);”,这个函数的作用是调用后面介绍的GDB/gdb/ARCH-TARGET-tdep.c通过gdbarch_register_osabi注册跟TARGET相关的初始化函数。
注册的dump_tdep函数主要是用来显示这个TARGET中一些私有的例如tdep的信息,其被GDB/gdb/gdbarch.c:gdbarch_dump函数调用,这个函数当执行GDB命令“maintenance print architecture”的时候被调用到,主要就是用来显示TARGET的信息。
5.3.GDB/gdb/ARCH-TARGET-tdep.c
GDB/gdb/ARCH-TARGET-tdep.c文件初始化函数是void _initialize_ARCH_TARGET_tdep (void),这个函数在GDB启动的时候将被调用,这里最主要的就是对gdbarch_register_osabi函数的调用,当然如果需要也可以在这个函数中增加一些命令。
void gdbarch_register_osabi (enum bfd_architecture arch, unsigned long machine, enum gdb_osabi osabi, void (*init_osabi)(struct gdbarch_info, struct gdbarch *))
这个函数的作用是将init_osabi函数指针标记为arch和osabi注册到全局链表gdb_osabi_handler_list上。这个函数将被前面提到过的gdbarch_init_osabi函数调用。
注册的init_osabi函数的作用就是根据操作系统的特性对前面init中分配和设置的gdbarch进行进一步的初始化。
5.4.gdbarch_register_osabi_sniffer
void gdbarch_register_osabi_sniffer (enum bfd_architecture arch, enum bfd_flavour flavour, enum gdb_osabi (*sniffer)(bfd *abfd))
这个函数一般在_initialize_ARCH_tdep或者_initialize_ARCH_TARGET_tdep中调用,其主要目的是因为二进制可执行文件的格式中对OS ABI的描述并不一定能跟gdb_osabi对应的上,所以这里就是注册一个函数来帮助GDB判断当前文件使用的gdb_osabi类型。
在这个函数中arch表示ARCH,flavour是定义在LIBBFD中的对可执行文件的描述,sniffer是要注册的函数指针。
注册的sniffer函数要根据传递来的LIBBFD的文件指针对当前读入的二进制可执行文件进行分析,并将得到的gdb_osabi返回。
5.5.gdbarch设置函数
因为GDB/gdb/ARCH-tdep.c中注册的init和GDB/gdb/ARCH-TARGET-tdep.c中注册的init_osabi设置gdbarch的函数属于同一系列,所以将一起在下面进行介绍,这些函数的大部分都在GDB/gdb/gdbarch.c中。有一部分函数是跟frame有关的,将在后面关于frame的章节再进行介绍。
在GDBINT 9.10介绍了一些对TARGET进行设置的宏定义,而在GDB/gdb/gdbarch.h中可以看到,下面这些函数设置在gdbarch结构中的函数和变量最终将被定义在相关的宏中来实现调用,所以GDBINT 9.10中关于宏的介绍可以作为相关gdbarch初始化函数的介绍。
void set_gdbarch_num_regs (struct gdbarch *gdbarch, int num_regs)
设置ARCH的寄存器数量num_regs到gdbarch中,调用其的宏是NUM_REGS。
void set_gdbarch_register_type (struct gdbarch *gdbarch, gdbarch_register_type_ftype *register_type)
设置一个gdbarch_register_type_ftype类型的函数指针到gdbarch中。
注册的这个函数的作用是根据其参数reg_nr指定的寄存器号码确定寄存器,将这个寄存器对应的类型返回,这些类型定义在GDB/gdb/gdbtypes.c中。
void set_gdbarch_register_name (struct gdbarch *gdbarch, gdbarch_register_name_ftype *register_name)
设置一个gdbarch_register_name_ftype类型的函数指针到gdbarch中,调用其的宏是REGISTER_NAME。
注册的这个函数的作用是根据其参数regnr指出的寄存器序号返回相应的寄存器名称。
void set_gdbarch_pc_regnum (struct gdbarch *gdbarch, int pc_regnum)
设置ARCH的PC寄存器序号pc_regnum到gdbarch中,调用其的宏是PC_REGNUM。
void set_gdbarch_sp_regnum (struct gdbarch *gdbarch, int sp_regnum)
设置ARCH的SP寄存器序号sp_regnum到gdbarch中,调用其的宏是SP_REGNUM。
void set_gdbarch_cannot_fetch_register (struct gdbarch *gdbarch, gdbarch_cannot_fetch_register_ftype *cannot_fetch_register);
设置一个gdbarch_cannot_fetch_register_ftype类型的函数指针到gdbarch中,调用其的宏是CANNOT_FETCH_REGISTER。
注册的这个函数的作用是判断其参数指定的寄存器在本地调试ptrace(关于本地调试见移植GDB(2))的时候是否是不能读取的,如果是则返回真。
因为这个项目跟本地调试的关系更密切,所以也有将宏CANNOT_FETCH_REGISTER定义到本地调试目标头文件GDB/gdb/config/ARCH/nm-TARGET.h中的方式。
void set_gdbarch_cannot_store_register (struct gdbarch *gdbarch, gdbarch_cannot_store_register_ftype *cannot_store_register)
设置一个gdbarch_cannot_store_register_ftype类型的函数指针到gdbarch中,调用其的宏是CANNOT_STORE_REGISTER。
注册的这个函数的作用是判断其参数指定的寄存器在本地调试ptrace(关于本地调试见移植GDB(2))的时候是否是不能设置的,如果是则返回真。
因为这个项目跟本地调试的关系更密切,所以也有将宏CANNOT_FETCH_REGISTER定义到本地调试目标头文件GDB/gdb/config/ARCH/nm-TARGET.h中的方式。
void set_gdbarch_fp0_regnum (struct gdbarch *gdbarch, int fp0_regnum)
设置ARCH的FP0寄存器序号fp0_regnum到gdbarch中,调用其的宏是FP0_REGNUM。
void set_gdbarch_read_pc (struct gdbarch *gdbarch, gdbarch_read_pc_ftype *read_pc)
设置一个gdbarch_read_pc_ftype类型的函数指针到gdbarch中,调用其的宏是TARGET_READ_PC和TARGET_READ_PC_P。
注册的这个函数的作用是通过用户自定义的方式来对调试进程进行PC寄存器的读取,如果已经用set_gdbarch_pc_regnum设置了PC寄存器的序号,而且PC寄存器可以通过普通方式读取到,则不需要注册这个函数。
void set_gdbarch_write_pc (struct gdbarch *gdbarch, gdbarch_write_pc_ftype *write_pc)
设置一个gdbarch_write_pc_ftype类型的函数指针到gdbarch中,调用其的宏是TARGET_WRITE_PC。
注册的这个函数的作用是通过用户自定义的方式来对调试进程进行PC寄存器的设置,如果已经用set_gdbarch_pc_regnum设置了PC寄存器的序号,而且PC寄存器可以通过普通方式设置,则不需要注册这个函数。
void set_gdbarch_read_sp (struct gdbarch *gdbarch, gdbarch_read_sp_ftype *read_sp)
设置一个gdbarch_read_sp_ftype类型的函数指针到gdbarch中,调用其的宏是TARGET_READ_SP和TARGET_READ_SP_P。
注册的这个函数的作用是通过用户自定义的方式来对调试进程进行SP寄存器的读取,如果已经用set_gdbarch_sp_regnum设置了SP寄存器的序号,而且SP寄存器可以通过普通方式读取到,则不需要注册这个函数。
void set_gdbarch_breakpoint_from_pc (struct gdbarch *gdbarch, gdbarch_breakpoint_from_pc_ftype *breakpoint_from_pc)
设置一个gdbarch_breakpoint_from_pc_ftype类型的函数指针到gdbarch中,调用其的宏是BREAKPOINT_FROM_PC。
注册的这个函数的作用是根据参数中的要插入的断点的地址pcptr,选择合适的断点指令,并且设置这个断点指令的长度在lenptr中,如果需要的话地址也需要进行修改。例如ARM就需要判断插入一个ARM断点指令还是插入一个THUMB断点指令。
void set_gdbarch_inner_than (struct gdbarch *gdbarch, gdbarch_inner_than_ftype *inner_than)
设置一个gdbarch_inner_than_ftype类型的函数指针到gdbarch中,调用其的宏是INNER_THAN。
注册的这个函数是用来比较栈地址。如果ARCH的栈是向下增长(向内存低地址)的,则注册core_addr_lessthan(lhs小于rhs返回真)。如果ARCH的栈是向上增长(向内存高地址)的,则注册core_addr_greaterthan(lhs大于rhs返回真)。
void set_gdbarch_skip_prologue (struct gdbarch *gdbarch, gdbarch_skip_prologue_ftype *skip_prologue)
设置一个gdbarch_skip_prologue_ftype类型的函数指针到gdbarch中,调用其的宏是SKIP_PROLOGUE。
注册的这个函数的参数pc是一个函数的起始地址,函数将返回这个地址对应的函数的prologue的结束地址。prologue指函数开始分配栈空间、保存寄存器等那部分代码。一般来说这个函数的编写都是先扫描符号表,一般可以通过符号表找到结束地址,如果符号表查找失败就需要反汇编来分析代码取得结束地址(个人觉得靠反汇编来来分析这部分可有可无)。具体的过程可以参见GDB/gdb目录中ARCH-tedp.c类型文件中的相关代码。
void set_gdbarch_print_insn (struct gdbarch *gdbarch, gdbarch_print_insn_ftype *print_insn)
设置一个gdbarch_print_insn_ftype类型的函数指针到gdbarch中,调用其的宏是TARGET_PRINT_INSN。
注册的这个函数将在运行GDB的disassemble命令的时候被调用,用来对指定的地址进行反汇编。整个这个函数的结构与binutils中的一个ARCH的结构完全一样,可以直接将其目录中文件拷贝到GDB目录中使用。
void set_gdbarch_return_value (struct gdbarch *gdbarch, gdbarch_return_value_ftype *return_value)
void set_gdbarch_extract_return_value (struct gdbarch *gdbarch, gdbarch_extract_return_value_ftype *extract_return_value)
void set_gdbarch_store_return_value (struct gdbarch *gdbarch, gdbarch_store_return_value_ftype *store_return_value)
这三个函数分别设置gdbarch_return_value_ftype、gdbarch_extract_return_value_ftype和gdbarch_store_return_value_ftype类型的函数指针到gdbarch中,调用后面两个注册的函数指针的宏分别是EXTRACT_RETURN_VALUE和STORE_RETURN_VALUE。
注册的第一个函数用来取得函数的返回值以及这个返回值的传递方式。
取得返回值的传递方式的方法为通过gdbarch_return_value_ftype类型函数参数valtype用宏TYPE_CODE取得函数返回值类型,这些类型是定义在GDB/gdb/gdbtypes.h的枚举类型type_code中,例如TYPE_CODE_INT代表返回值是int。根据这个类型就可以确定返回值的传递方式,这些方式定义在GDB/defs.h中的枚举类型return_value_convention中。这些类型是:
RETURN_VALUE_REGISTER_CONVENTION表示返回值通过一个或者多个寄存器返回。
RETURN_VALUE_STRUCT_CONVENTION表示调用函数会传递一个隐含的参数传递一个用来传递返回值的指针。
RETURN_VALUE_ABI_RETURNS_ADDRESS类似RETURN_VALUE_STRUCT_CONVENTION,ABI保证被调用函数将返回值放到一个定义明确的位置。
RETURN_VALUE_ABI_PRESERVES_ADDRESS也类似RETURN_VALUE_STRUCT_CONVENTION,ABI保证返回值的地址放到一个定义明确的位置。
一般情况下TYPE_CODE_STRUCT、TYPE_CODE_UNION和TYPE_CODE_ARRAY使用RETURN_VALUE_STRUCT_CONVENTION,其他用RETURN_VALUE_REGISTER_CONVENTION,当然具体情况要由ABI决定。
在要取得函数返回值的时候gdbarch_return_value_ftype类型函数的参数readbuf为非空,通过将regcache中的值存入这个参数就可以得到返回值。要设置函数的返回值,则参数writebuf为非空,通过将writebuf中的值存入regcache中设置返回值。
如果编写一个gdbarch_return_value_ftype函数完成了所有的功能,则不需要设置gdbarch_extract_return_value_ftype和gdbarch_store_return_value_ftype函数。
gdbarch_return_value_ftype函数指针在gdbarch初始化的时候设置的默认值是函数legacy_return_value,这个函数将调用宏EXTRACT_RETURN_VALUE和STORE_RETURN_VALUE,也就是将调用后两个注册的函数。如果gdbarch_return_value_ftype函数使用的默认值,则需要设置gdbarch_extract_return_value_ftype和gdbarch_store_return_value_ftype函数。
这几个函数的编写可以参照GDB/gdb/arch-utils.c中的第一个参数的默认值legacy_return_value,以及GDB/gdb/目录中ARCH-tdep.c类似文件中同样类型的函数。
从这里开始的介绍的函数一般来说是TARGET相关的,所以一般情况下他们在GDB/gdb/ARCH-TARGET-tdep.c中进行注册,当然要具体情况具体分析。
void set_gdbarch_software_single_step (struct gdbarch *gdbarch, gdbarch_software_single_step_ftype *software_single_step)
设置一个gdbarch_software_single_step_ftype类型的函数指针到gdbarch中,调用其的宏是SOFTWARE_SINGLE_STEP和SOFTWARE_SINGLE_STEP_P。
设置这个函数就表明当前TARGET不支持单步执行,需要通过这个函数提供的软单步功能来实现单步。
注册的这个函数的作用在当前指令执行的下一条指令插入(参数非0)和删除(参数为0)断点。
注意如果是跳转类的指令则还需要先对指令进行反汇编,对条件跳转还要根据将要执行指令的条件判断出其是否真要进行跳转,判断出下一条指令的地址。
void set_gdbarch_get_longjmp_target (struct gdbarch *gdbarch, gdbarch_get_longjmp_target_ftype *get_longjmp_target)
设置一个gdbarch_get_longjmp_target_ftype类型的函数指针到gdbarch中,调用其的宏是GET_LONGJMP_TARGET和GET_LONGJMP_TARGET_P。
注册的这个函数的作用是计算出longjmp要跳转到的地址并存在函数的参数pc中,如果成功返回0,失败返回非0。一般来说这个函数的编写方法是分析存放longjump跳转目标的缓存,将目标地址取出就可以。具体的过程可以参见GDB/gdb目录中ARCH-tedp.c类型文件中的相关代码。
void set_gdbarch_skip_solib_resolver (struct gdbarch *gdbarch, gdbarch_skip_solib_resolver_ftype *skip_solib_resolver)
设置一个gdbarch_skip_solib_resolver_ftype类型的函数指针到gdbarch中,调用其的宏是SKIP_SOLIB_RESOLVER,不过这个宏好像已经被从GDB的代码中去掉了,现在都是通过gdbarch_skip_solib_resolver进行调用。
注册的这个函数是用来跳过参数pc后面的动态连接器符号定义函数,如果这个函数的参数pc在动态连接器符号定义函数中的时候,返回动态连接器符号定义函数后面的地址,如果不是则返回0。一般来说这个工作需要对符号表进行分析来实现。
如果TARGET使用的是GLIBC则可以直接注册glibc_skip_solib_resolver函数,如果使用这个函数的话需要在文件中包含glibc-tdep.h头文件,同时在前面提到的GDB/gdb/config/ARCH/TARGET.mt中的TDEPFILES=项目中包含glibc-tdep.o。
void set_gdbarch_skip_trampoline_code (struct gdbarch *gdbarch, gdbarch_skip_trampoline_code_ftype *skip_trampoline_code)
设置一个gdbarch_skip_trampoline_code_ftype类型的函数指针到gdbarch中,调用其的宏是SKIP_TRAMPOLINE_CODE。
如果TARGET在调用函数和被调用函数之间有trampolines代码(跳板代码,对其介绍可以见http://www.science.uva.nl/~mes/jargon/t/trampoline.html),则可以注册这个函数用来跳过trampolines代码。这个函数的参数pc就是当前的PC寄存器的值,如果这个函数返回非0则就是trampolines代码后面普通函数的地址,如果返回0则表示执行不成功。
如果trampolines代码是跟动态链接库有关的,可以注册标准函数GDB/gdb/minsyms.c:find_solib_trampoline_target来实现需要的功能。
void set_gdbarch_in_solib_return_trampoline (struct gdbarch *gdbarch, gdbarch_in_solib_return_trampoline_ftype *in_solib_return_trampoline)
设置一个gdbarch_in_solib_return_trampoline_ftype类型的函数指针到gdbarch中,调用其的宏是IN_SOLIB_RETURN_TRAMPOLINE。
注册的这个函数是用来确定参数pc指定的地址是否在trampolines代码中,如果是就返回非0,不是就返回0,其参数name表示当前所在的函数名称。
其实还有一个IN_SOLIB_CALL_TRAMPOLINE(设置函数是set_gdbarch_in_solib_call_trampoline),但是其实际已经没有再在GDB中被调用,所以不进行介绍,也不建议使用那个选项。
void set_solib_svr4_fetch_link_map_offsets (struct gdbarch *gdbarch, struct link_map_offsets *(*func) (void))
设置一个struct link_map_offsets *(*func) (void)类型的函数到gdbarch中。调用其的宏是SVR4_FETCH_LINK_MAP_OFFSETS。如果要使用设置函数set_solib_svr4_fetch_link_map_offsets则要在文件中solib-svr4.h头文件,同时在前面提到的GDB/gdb/config/ARCH/TARGET.mt中的TDEPFILES=项目中包含solib-svr4.o。
当你的TARGET支持SVR4共享库的时候,需要注册的这个函数用来初始化SVR4共享库的link_map_offsets结构。
如果你的TARGET是标准的ILP32结构(代表integer/long/pointer是 32位)可以直接注册GDB/gdb/solib-svr4.c:svr4_ilp32_fetch_link_map_offsets函数,如果是标准的LP64结构(代表long/pointer是64位)可以直接注册GDB/gdb/solib-svr4.c:svr4_lp64_fetch_link_map_offsets函数。
void set_gdbarch_regset_from_core_section (struct gdbarch *gdbarch, gdbarch_regset_from_core_section_ftype regset_from_core_section)
设置一个gdbarch_regset_from_core_section_ftype类型的函数到gdbarch中。
注册的这个函数会根据某个core文件的section的sect_name和sect_size返回合适的regset结构,如果没有合适的就返回NULL。这个regset结构可以是用GDB/gdb/regset.c:regset_alloc函数分配,也可以使用事先设置好的全局变量。
具体到要注册的函数的代码,主要就是根据sect_name和sect_size确定返回分析普通寄存器的regset结构、浮点寄存器的regset结构、扩展浮点寄存器(SSE,i386系列处理器特有)的 regset结构还有NULL。一般来说sect_name为".reg"返回普通寄存器的regset结构,sect_name为".reg2"返回浮点寄存器的regset结构,ARCH为i386系列的同时sect_name为".reg-xfp"返回扩展浮点寄存器的regset结构,其他返回NULL。当然还要根据实际情况具体问题具体分析。
regset结构定义在GDB/gdb/regset.h文件中,这个结构主要是存储了对core文件中数据进行处理的函数的指针。这两个指针分别是supply_regset_ftype类型的函数指针supply_regset和collect_regset_ftype类型的函数指针collect_regset。
supply_regset的作用是将参数regnum指定的寄存器(如果-1代表是全部寄存器)从gregs参数中指定的core文件中的寄存器信息进行转化存储到参数regcache中(一般用存储使用函数regcache_raw_supply)。
collect_regset的作用是将参数regnum指定的寄存器(如果-1代表是全部寄存器)从参数regcache中读出进行转化然后存储到gregs参数中指定的core文件中的寄存器信息中(一般用函数regcache_raw_collect从regcache进行读取)。
因为这个函数跟本地调试中的core文件分析关系比较密切,所以对注册的这个函数的使用将在“移植GDB(2)”中进行详细介绍。
6.GDB/gdb/config/ARCH/tm-ARCH.h GDB/gdb/config/ARCH/tm-TARGET.h
6.1.综述
这两个文件后面的GDB/gdb/config/ARCH/tm-TARGET.h文件中是TARGET相关的信息,一般其将在TARGET.mt的DEPRECATED_TM_FILE=中被指定。configure以后,定义在DEPRECATED_TM_FILE=中的文件将被软连接到GDB/gdb/tm.h文件,这个文件将被GDB/gdb/defs.h文件包含,而这个文件将被很多文件包含,所以在在这个文件中设置的宏以及其他的定义最终将被相关的文件包含,达到了设置这些文件的目的。
GDB/gdb/config/ARCH/tm-ARCH.h文件中是ARCH相关的信息,一般其是被ARCH的所有TARGET的tm-TARGET.h文件包含的,不过如果TARGET除了ARCH相关的定义不需要自己再进行什么定义的时候,也可以直接被在TARGET.mt的DEPRECATED_TM_FILE=中指定tm-ARCH.h,这个是可以灵活掌握的。
6.2.宏定义
在5.5节介绍gdbarch设置函数的时候,同时就介绍了调用注册上的变量或者函数的宏,这些宏跟下面将要介绍的宏一样,都可以在tm-ARCH.h和tm-TARGET.h中进行设置。
要注意的是很多宏都是跟“宏_P”名称的宏一起使用的,这个“宏_P”形式的宏是用来判断名称中的那个宏是否使用,所以使用设置这个宏一定要同时设置“宏_P”形式的宏为1。
这些宏在GDBINT 9.10进行了一些介绍,可以作为参考。
STEP_SKIPS_DELAY (pc)和STEP_SKIPS_DELAY_P
这个宏用来确定pc地址上的指令是否是一条缓冲槽指令,如果是则返回真。
GDB用这个宏来判断当前指令是不是缓冲槽指令,然后就会去判断下一条指令也就是缓冲槽中的指令是不是断点,如果是则表明当前指令是一条断点指令,因为如果缓冲槽中的是断点指令,发生断点一般的处理是这样,芯片进入管理模式,在芯片返回的时候回到缓冲槽指令而不是回到缓冲槽的地址中,所以就等于是缓冲槽指令发生了断点。
对这个宏的调用是在GDB/gdb/infrun.c:proceed函数中,其在判断出会有断点的时候,就会设置oneproc为1,用来标明这次执行只会运行一条指令,在下面的处理中如果oneproc为1则就不需要在进程继续运行开始前插入预先设置好的断点。
这里有一个问题,这样表明那个在缓冲槽中的断点指令必须是能被执行的吗?因为如果是条件缓冲槽指令,如果条件不符合,这条断点指令不会被调用,也就不会只执行一条指令,而且没插入断点,这样显然是不对的。但是我们又可以看到,MIPS的判断函数中将条件缓冲槽指令也包含进来,并且也并没有通过判断当前芯片情况的形式来确定是否反正真,这是怎么回事?原来在下面,将oneproc也跟step与在一起传递给了调用实际的启动函数GDB/gdb/infrun.c:resume,作为其的参数step,所以在设置oneproc为1后不论任何情况,这里只会单步执行,所以不会对正常调试有什么影响。
在本文的以前版本中,我曾经提到过“在现在GDB源码中只有MIPS用了这个宏,没有别的ARCH有缓冲槽指令?难道缓冲槽是被MIPS申请了的专利?”这里的GDB源码指的是GDB-6.3,最近我看了一点GDB-6.4,其中将这两个宏替换成了gdbarch_single_step_through_delay和gdbarch_single_step_through_delay_p,当然整体结构变化不大,只是把判断缓冲槽中指令是否是断点指令的工作也放到了这个ARCH相关的函数中进行了。而且gdbarch支持这个函数的不光有MIPS,还有CRIS。具体GDB-6.3和GDB-6.4的差异,我将在本文的后续版本中进行详细的介绍。
IN_SOLIB_DYNSYM_RESOLVE_CODE (pc)
这个宏用来确定pc地址是否在动态连接器符号定义函数中,如果是则返回真。
如果TARGET使用的动态链接库已经被GDB支持(GDB/gdb/solib-库名称.c),则可以不设置这个宏,而是在tm-TARGET.h中包含solib.h头文件,同时在TARGET.mt的TDEPFILES=中包含solib.o和solib-库名称.o来对动态库进行支持。
7.frame
7.1.概述
frame是stack frame,也就是栈结构,个人认为其主要的功能就是用来确定一条指令所在的函数位置,一个函数中的所有指令的frame结构都是一样的(用来帮助取得frame结构的-1级结构是不同的,后面会详细介绍)。
比如GDB使用指令next和nexti的时候,在执行前先会取得当前函数的frame信息,然后在每执行一条指令后取得跟这条指令对应的函数frame信息的上一层函数的frame信息,如果这两个frame信息相同则表明执行进入了一个函数,将在前一个函数现在所在函数返回位置设置断点,然后使用连续执行,达到快速通过单步代码执行中函数调用的目的。其中比较以及后面功能的代码在GDB/gdb/infrun.c:handle_inferior_event:2285行开始。
类似的还有finish指令通过frame信息取得当前函数调用函数位置等。
frame分为四种类型,分别是普通frame结构的NORMAL_FRAME,在通过GDB调用程序中某个函数的时候使用的DUMMY_FRAME,用在信号处理代码中的SIGTRAMP_FRAME,用来记录当前指令所在位置(普通frame记录的是当前指令所在函数的信息)信息的SENTINEL_FRAME,他们定义在GDB/gdb/frame.h的枚举类型frame_type中,后面会在对frame介绍的时候介绍到他们。
7.2.struct frame_info
这个结构中保存了一个frame的各种信息。其被定义在GDB/gdb/frame.c中,在这个文件之外对这个结构的调用只限于指针,所以这个结构定义在这里就可以。
其主要的元素有:
int level
这个变量代表frame的等级,当前函数的frame的等级是0,这个函数的调用函数的等级是1,依此类推。下面都将按照这个方式,按照level值的高低来区别frame,称呼他们为高一级和低一级。
有一个特殊的level是-1,一般来说这个levle的frame就是SENTINEL_FRAME类型的。其是通过当前指令的信息取得,每条指令一个。因为GDB总是需要一个frame,所以就需要有这个特殊的frame表示比当前函数frame低一级的当前指令的frame。
const struct frame_unwind *unwind;
这个结构指针是对frame_info进行控制的重要元素,每个TARGET都是通过将函数指针和变量设置到其上来达到控制frame的目的,具体的这个结构和设置过程将在后面详细介绍。
注意其中的const是为了保证这个变量只在初始化设置一次,其他时候设置将报错。
void *prologue_cache;
这个指针是给unwind函数互相传递数据使用,具体使用方式将在后面详细介绍。
struct {
int p;
CORE_ADDR value;
} prev_pc;
这个结构中的value存储了这个frame相关的函数的返回地址,因为这个地址就是比其高一级的frame的函数的地址,所以称为prev_pc。p代表这个值是否有效。
struct
{
CORE_ADDR addr;
int p;
} prev_func;
这个结构中的addr存储了比当前frame高一级的frame相关的函数的起始地址。p代表这个值是否有效。
struct
{
int p;
struct frame_id value;
} this_id;
这个结构是当前frame的ID,这是用来分辨frame的核心数据,在2个frame进行比较的时候,将用这个结构中的数据进行比较。这个结构中的p代表整个这个结构是否有效,frame_id结构将在下面介绍。
struct frame_info *next;
这个指针指向比这个frame低一级的frame的结构。
struct frame_info *prev;
这个指针指向比这个frame高一级的frame的结构。
int prev_p;
这个变量用来标明prev指针是否被赋值,在访问prev以前需要先检查这个变量,如果为1可直接访问,如果为0则需要先取得prev。
更多的信息可以看相关代码的注释,写的比较详细。
7.3.struct frame_id
这个结构是frame的ID,用来区分frame,定义在GDB/gdb/frame.h中。
其主要的元素有:
CORE_ADDR stack_addr;
这个frame对应函数的栈地址,一般来说就是调用这个函数后,函数进行栈空间分配之前的栈地址。
unsigned int stack_addr_p : 1;
stack_addr只有在stack_addr_p为真的时候有效。
CORE_ADDR code_addr;
这个frame对应函数的的起始地址。
unsigned int code_addr_p : 1;
code_addr只有在code_addr_p为真的时候有效。
CORE_ADDR special_addr;
有一些ARCH的某部分frame的stack_addr不频繁变化,但是有一个其他地址的变化可以作为标识的补充,则将这个地址设置到special_addr中。
unsigned int special_addr_p : 1;
special_addr只有在special_addr_p为真的时候有效。
7.4.取得当前指令对应函数frame_info信息
使用GDB/gdb/frame.c:get_current_frame函数。
其的执行过程主要是:
第一,检查当前target(跟前面提到的TARGET不是一个东西,其是被调试目标,介绍可以见GDBINT 10)的寄存器、栈和内存进行检查,没有任何一个都将出错。
第一,判断全局变量GDB/gdb/frame.c:current_frame是否为空,如果不为空则返回。
第二,调用函数GDB/gdb/frame.c:create_sentinel_frame取得sentinel_frame,sentinel_frame就是7.2提到level为-1类型是SENTINEL_FRAME的frame。
在函数create_sentinel_frame中,首先给frame_info结构分配空间,然后将level设置为-1。接着是初始化prologue_cache和unwind,其中prologue_cache将当前寄存器信息存储其中,而unwind初始化为GDB/gdb/sentinel-frame.c:sentinel_frame_unwind。设置next为当前初始化的这个frame_info结构,保证当需要访问比这个frame还低一级的frame的时候可以访问到这个frame自身。设置frame_id为GDB/gdb/frame.c:null_frame_id,设置这个值保证每次frame_id的比较都不相等。最后将frame返回。
第三,通过GDB/gdb/top.c:catch_exceptions函数调用GDB/gdb/frame.c:unwind_to_current_frame函数,这个函数会调用函数GDB/gdb/frame.c:get_prev_frame。
在函数get_prev_frame中,先作一些检查,然后调用函数GDB/gdb/frame.c:get_prev_frame_1进行实际的工作。
在函数get_prev_frame_1中,这里先检查参数this_frame->prev_p是否为真,如果为真表明this_frame->prev已经存在,则直接返回this_frame->prev。设置this_frame->prev_p为1,因为现在要开始设置this_frame->prev了。对this_frame进行一些检查,如果检查没问题则给prev_frame分配空间,设置prev_frame->level比this_frame->level大1,表明比this_frame高一个级别。最后互相注册到prev和next上建立连接,返回prev_frame。
这样就取得了比sentinel_frame高一级的frame,也就是对应当前函数的frame。将取得的这个frame设置到current_frame上。可以注意到这个新取得的current_frame上很多元素都是空的,这是因为当在使用这个frame_info中的一部分元素的时候,会先自动检查其是否存在,如果不存在则调用相应函数取得其。
第四,整个get_current_frame函数返回current_frame。
7.5.取得指定frame_info的frame_id
使用GDB/gdb/frame.c:get_frame_id函数。
其的执行过程主要是:
第一,检查frame_info结构的this_id.p,如果为真表明this_id.value存在,直接返回。
第二,检查frame_info结构中的unwind是否为空,如果为空则调用GDB/gdb/frame-unwind.c:frame_unwind_find_by_frame函数根据当前frame低一级的next取得unwind。
在frame_unwind_find_by_frame中,首先通过gdbarch取得frame_unwind结构的列表。然后循环依次先调用表中每个元素在其注册的时候注册的sniffer函数,然后调用frame_unwind结构上自带的sniffer函数,如果成功则返回frame_unwind结构,失败则报错。关于将frame_unwind结构注册到gdbarch中的方式将在介绍跟frame相关的gdbarch初始化函数的时候,再进行介绍。
第三,调用frame_unwind结构中的this_id函数指针,取得this_id.value,设置this_id.p为1,然后返回。
取得指定frame_info的寄存器信息的函数GDB/gdb/frame.c:frame_register_unwind跟这个函数结构类似,只是将调用this_id函数指针换成prev_register函数指针,所以不作详细的介绍。
7.6.取得指定frame_info对应的函数返回地址
使用函数GDB/gdb/frame.c:frame_pc_unwind,因为取得frame_info对应的函数返回地址是取得比其高一级的frame的frame_info信息的关键,因为取得了这个地址就意味着取得了高一级的frame的地址。
这个函数的执行过程主要是:
第一,检查这个frame的prev_pc.p是否为真,如果为真则表明prev_pc.value存在,直接将其返回。
第二,判断gdbarch是否设置了unwind_pc函数(这个函数的设置和编写将在跟frame相关的gdbarch设置函数章节进行介绍),如果设置了则运行其来取得相关值,然后设置到prev_pc.value返回。
第三,判断当前frame的级别是否小于0,如果小于0就表明是前面提到的sentinel_frame,则直接通过read_pc取得当前pc值设置到prev_pc.value并返回。
第四,如果都不存在则报错。
个人认为frame_pc_unwind以及gdbarch中的unwind_pc实在是函数起名的错误典型,明明是取得这个frame高一级的frame的pc,却偏偏叫frame_pc_unwind,很容易让人误以为是取得当前frame相关函数起始之用。确实很多TARGET之中注册到unwind_pc上的函数是靠取得frame寄存器中pc值来实现的,但是这是因为这个TARGET的这个值正好是函数返回地址。以这样的函数名称,再加上很多TARGET中的相关代码,很容易让人错误理解这个函数的作用。所以个人认为其应该叫frame_prev_pc_unwind和unwind_prev_pc。
7.7.取得指定frame的高一级frame的函数起始地址
使用函数GDB/gdb/frame.c:frame_func_unwind。
这个函数的执行过程主要是:
第一,检查这个frame的prev_func.p是否为真,如果为真则表明prev_func.addr存在,直接将其返回。
第二,调用GDB/gdb/frame.c:frame_unwind_address_in_block函数,这个函数主要就是调用frame_pc_unwind取得指定frame的函数返回地址,也就是比这个frame高一级的frame函数中的一个地址。
第三,调用函数GDB/gdb/blockframe.c:get_pc_function_start通过分析符号信息,用比这个frame高一级的frame函数中的一个地址取得这个函数的起始地址,设置到prev_func.addr中,然后返回。
个人觉得这个函数起名跟frame_pc_unwind有类似的问题。
7.8.跟frame相关的gdbarch设置函数
void frame_unwind_append_sniffer (struct gdbarch *gdbarch, frame_unwind_sniffer_ftype *sniffer)
设置一个frame_unwind_sniffer_ftype类型的函数指针到gdbarch中。注意这个函数可以在初始化的代码中多次调用来注册多个函数。
每个注册的函数都跟一个frame_unwind结构指针相关,其主要作用是在前面介绍过的frame_unwind_find_by_frame函数中依照注册的先后被调用。
一般这个函数会通过GDB/gdb/frame.c:frame_pc_unwind函数取得next_frame也就是比其低一级的frame的函数返回地址,也就是当前函数的一个地址,通过这个地址进行分析跟注册的这个函数相关的frame_unwind结构是否符合这个地址相关的frame(一般来说是执行文件调试信息有这个地址相关的调试信息),如果符合则返回这个frame_unwind结构指针,如果不符合则返回NULL。
如果TARGET的可执行文件支持DWARF2调试格式,可以直接注册GDB/gdb/dwarf2-frame.c:dwarf2_frame_sniffer函数,使用GDB中的DWARF2格式分析结构GDB/gdb/dwarf2-frame.c:dwarf2_frame_unwind对相关信息进行分析。
在函数frame_unwind_find_by_frame中,如果最后一个sniffer函数失败,则函数frame_unwind_find_by_frame的执行将出错,GDB的执行也会退出,所以建议最后用frame_unwind_append_sniffer注册一个不会进行任何检查而直接返回frame_unwind结构指针sniffer函数。而跟这个函数相关的frame_unwind结构指针中的函数相对也是最基础的frame分析方式,主要是通过对代码反汇编取得函数行为来确定,将在下面介绍frame_unwind结构的时候进行详细的介绍。
void set_gdbarch_unwind_pc (struct gdbarch *gdbarch, gdbarch_unwind_pc_ftype *unwind_pc)
设置一个gdbarch_unwind_pc_ftype类型的函数指针到gdbarch中。
注册的这个函数将被7.6介绍过的函数GDB/gdb/frame.c:frame_pc_unwind调用,其将返回参数next_frame相关的函数的返回地址。
一般在这个函数中调用GDB/gdb/frame.c:frame_unwind_register_unsigned 、get_frame_register_signed等函数(这些函数将调用7.5介绍过的函数GDB/gdb/frame.c:frame_register_unwind)来取得相应序号寄存器的在栈中的信息,具体取哪个寄存器的数据则需要根据frame_register_unwind调用的frame_unwind结构中的函数的情况来决定。比如有一些ARCH中是取PC寄存器的值,因为存储在栈中PC寄存器位置的值就是这个函数的返回地址。
7.9.struct frame_unwind
这个结构定义在GDB/gdb/frame-unwind.h中,其在前面已经被多次提到,其中的指针就是对frame信息进行分析的核心函数。下面将介绍一下这个结构中主要函数指针的作用。
enum frame_type type;
这是本章开头提到的frame的类型,一般用前面介绍的frame_unwind_append_sniffer函数注册的frame_unwind结构都使用NORMAL_FRAME类型。
因为下面要介绍frame_unwind结构中的两个函数的编写方法一般来说都是从栈中取得寄存器信息,然后进行处理,所以一般都是先调用一个独立的函数,这个函数是对栈进行分析,然后将结果保存在参数this_cache中。
这个this_cache指针就是frame_info结构中的prologue_cache,所以这个指针可以用来保存分析出来的寄存器信息。因为每个ARCH的结构差异很大,所以一般使用例如struct mips_frame_cache(定义在GDB/gdb/mips-tdep.c中)这样的结构来定义*this_cache指向的内存,一般来说这个结构包含一个CORE_ADDR类型的值存储当前frame对应函数的栈地址,一个GDB/gdb/trad-frame.h:trad_frame_saved_reg结构的指针用来保存其他分析出的寄存器的信息。
下面是栈分析函数的一般执行结构。
第一步,判断*this_cache是否为NULL,如果不为NULL,则表明已经被分析过,不需要再进行分析,可以直接使用其中的值。
第二步,调用GDB/gdb/frame.h:FRAME_OBSTACK_ZALLOC分配内存,参数为这个ARCH的frame结构类型的长度。
第三步,如果当前的结构中定义了trad_frame_saved_reg类型结构指针,则需要调用GDB/gdb/trad-frame.c:trad_frame_alloc_saved_regs以next_frame为参数对这个结构指针进行初始化。
第四步,分析栈信息最主要的就是分析出当前frame对应的函数栈地址,就是函数进行栈空间分配之前的栈地址。
如果当前的TARGET中有BP寄存器,因为BP寄存器存储函数栈地址,在函数调用的时候BP寄存器将被存入下一个函数的栈中,所以可以通过取得参数中next_frame(也就是比这个frame低一级的frame)的BP值来取得函数栈地址。
如果没有BP寄存器,则取得参数next_frame相关函数的栈地址信息(一般来说是SP寄存器信息),然后通过对当前函数的分析得到这个函数分配的栈空间长度(这里是栈分析的最主要工作,使用编译中产生的调试信息,如果没有则只有反汇编代码,在取得长度的同时一般也会分析出哪些寄存器被存在了栈中以及他们的位置),将这两个值相加(如果栈是递减形式分配)就可以得到当前函数栈地址信息。
第五步,栈地址以及对栈中信息分析得到的寄存器信息都要存到前面提到的*this_cache中。如果使用了trad_frame_saved_reg类型结构指针存储寄存器信息,则将每个分析到的寄存器用GDB/gdb/trad-frame.c:trad_frame_set_value来设置其中寄存器的值。
这里要注意如果你在前面介绍过的unwind_pc函数中设置的取用某个寄存器的值作用函数的返回地址,则在这里一定要将这个寄存器的值设置为存在栈中的函数返回值。
frame_this_id_ftype *this_id;
取得参数next_frame的高一级的frame的frame_id。
函数的编写方式是首先调用前面提到的栈分析函数得到栈信息,如果你的栈结构比较普通,可以调用GDB/gdb/frame.c:frame_id_build函数取得frame_id,其的第一个参数使用前面分析得到的栈地址,第二个参数用前面介绍过的frame_func_unwind函数取得frame相关函数的起始地址就可以。
frame_prev_register_ftype *prev_register;
分解(unwind)出当前这个frame的寄存器信息(被函数存入栈的寄存器信息)。
函数的编写方式是首先调用前面提到的栈分析函数得到栈信息,如果使用了trad_frame_saved_reg类型结构指针存储寄存器信息,则将每个分析到的寄存器用GDB/gdb/trad-frame.c:trad_frame_get_prev_register来设置寄存器的值。
8.dummy frame
8.1.概述
在GDB中可以用命令call或者命令print调用被调试的程序中的函数,调用的方式是先创建一个dummy frame(就是前面介绍过的DUMMY_FRAME),然后以这个dummy frame为基础(跟前面提到过的一样,GDB任何时候都需要一个frame)调用函数。跟dummy frame相关的一些函数等将在下面进行介绍。
8.2.struct value *call_function_by_hand (struct value *function, int nargs, struct value **args)
这个函数的定义在GDB/gdb/infcall.c中,当用户用命令调用某个函数的时候,先会调用GDB/gdb/printcmd.c文件中的函数,这个函数调用GDB/gdb/eval.c中的函数,最终就会调用call_function_by_hand函数。这个函数上完成了前面提到的创建dummy frame以及调用函数的工作,理解这个函数对理解dummy frame非常有帮助,所以要对这个函数进行介绍。
这个函数的第一个参数function是要调用函数的描述结构,nargs是函数参数的数量,args是函数参数的值。
下面来介绍一下这个函数的主要执行过程:
第一步,调用target_has_execution检查current_target是否处于执行中状态,没有则报错返回,因为只有处于执行的中的状态才能调用函数。
第二步,保存一些当前程序的寄存器等信息以及给一些后面需要的指针分配空间。
第三步,调用GDB/gdb/regcache.c:read_sp取得当前栈指针(一般来说是SP寄存器的信息)。
调用gdbarch_frame_align_p函数判断gdbarch是否设置了frame_align函数指针(8.3介绍),如果TARGET没有设置这个函数指针则跳过下面下栈指针进行处理的代码,直接到第四步,因为这个函数指针将在下面的处理中保证栈仍然按照ARCH要求对齐。
处理栈指针首先调用INNER_THAN(5.5介绍过)用来判断栈是递减分配还是递增分配,然后根据情况从栈中分配gdbarch_frame_red_zone_size(8.3介绍)长度的值,red zone是一部分的ARCH的ABI令函数使用栈空间底部以外的空间的称呼,这部分空间有一个特点是如果在当前函数中再进行函数调用,则这部分空间讲被分配为被被调用函数的空间,则其中数据将被破坏,解决办法是调用函数以前先将red zone的空间分配到当前函数空间中。现在是我们自己调用函数,所以需要先分配red zone。没有red zone的ARCH的gdbarch_frame_red_zone_size默认被设置为0,也就不进行分配工作。
然后检查空间是否仍然按照要求对齐,如果不对齐则报错退出。
现在检查新取得的栈指针跟原来的栈指针是否相同,如果相同则需要再对栈指针进行先分配1字节然后对齐的操作,这样作的目的在那段代码上面的注释上有比较详细的介绍,我按照我的理解来介绍一下:在很多RISC ARCH中,没有参数没有返回值的函数往往形成没有栈变化的函数(因为RISC ARCH好像倾向于把函数返回地址存到一个特定寄存器,相比较CISC比如X86是把函数返回地址放到栈中则不会出现这种问题),前面我们介绍过栈指针是产生用来比较栈的frame_id的重要值,栈指针相同就表明可能产生相同的frame_id,这样GDB/gdb/dummy-frame.c:dummy_frame_sniffer在搜索dummy_frame_stack列表的时候,将只能返回相同frame_id的第一个dummy frame,这样将产生错误。所以这里进行分配空间的操作防止产生相同的frame_id的dummy frame。
如果TARGET没有设置frame_align函数指针,后面将直接使用前面取得的当前栈指针,这里有一些注释说没有对齐的栈指针是个很严重的问题,其实也没那么严重,没有设置frame_align的TARGET可以在下面调用的push_dummy_call函数指针中来完成对栈指针进行设置的工作,当然设置frame_align我觉得是相对来说更清晰一点。
第四步,对第一个参数function进行一些分析取得将被调用的地址funaddr以及这个函数的返回值是否是struct的标志struct_return。
第五步,判断CALL_DUMMY_LOCATION的值进行不同操作,这个宏一般定义为调用gdbarch中设置的变量call_dummy_location,这个变量用来定义调用函数将返回的位置,在这个位置将设置断点。有三个位置可以选择,ON_STACK,AT_ENTRY_POINT,AT_SYMBOL。
ON_STACK,将返回地址设置在栈上,这里要调用GDB/gdb/infcall.c:push_dummy_code这个函数的作用是在栈中分配给返回地址上的断点需要的空间。
AT_ENTRY_POINT,这是TARGET中使用最多的一种形式,将返回地址设置在symfile_objfile->ei.entry_point的值对应的地址上。
AT_SYMBOL,这个位置好像只有MIPS使用,将返回地址设置在符号__CALL_DUMMY_ADDRESS的地址上。
第六步,对参数和返回地址进行处理。
第七步,如果gdbarch没有设置push_dummy_call函数指针,则提示用户当前的gdbarch不支持相关功能,然后退出。调用gdbarch_push_dummy_call将当前的栈和寄存器的状态设置到执行完调用函数指令后的状态。
第八步,调用frame_id_build取得dummy_id,这是比将被运行的函数的frame高一级的frame的ID。在前面指定好的位置设置断点,这样要被调用的函数在返回后将可以被中断,这样GDB就能返回正常状态。调用GDB/gdb/dummy-frame.c:dummy_frame_push将dummy_id添加到列表dummy_frame_stack中(这么作的目的在后面介绍)。
第九步,作一些初始化的工作,最后调用GDB/gdb/infrun.c:proceed函数,其的作用是在GDB中执行被调试程序,其中real_pc执行的地址就是其开始执行的地址。当这个函数返回的时候就表明对参数中指定的要调用的函数执行完毕。然后调用函数返回最开始存储的当前被调试程序的状态。最后函数返回。
8.3.跟dummy frame相关的gdbarch设置函数
void set_gdbarch_unwind_dummy_id (struct gdbarch *gdbarch, gdbarch_unwind_dummy_id_ftype *unwind_dummy_id)
设置一个gdbarch_unwind_dummy_id_ftype类型的函数指针到gdbarch中。
注册函数的编写方法一般是用frame_id_build函数取得当前frame的frame_id,一般来说调用其使用的的第一个参数是next_frame存储的SP寄存器,第二个参数是next_frame存储的PC寄存器,这两个值需要用frame_unwind_register_unsigned或者frame_pc_unwind等来取得。
在前面提到过的函数dummy_frame_sniffer中,将调用注册的这个函数。函数dummy_frame_sniffer的大概被调用过程是这样:包含dummy_frame_sniffer的frame_unwind结构dummy_frame_unwinder已经被frame_unwind_init注册在表frame_unwind_data上。而frame_unwind_init被_initialize_frame_unwind调用。所以在GDB每次调用前面提到过的函数frame_unwind_find_by_frame来取得某frame的frame_unwind结构的时候,dummy_frame_unwinder结构都被第一个被比较,则函数dummy_frame_sniffer第一个被调用。
这个函数检查当前要取得的frame是不是dummy frame。在dummy_frame_sniffer函数中,先通过调用注册的unwind_dummy_id函数指针来取得当前的frame_id,然后跟用dummy_frame_push存到列表dummy_frame_stack中的每个dummy_id进行比较,如果相同则frame_unwind_find_by_frame将返回dummy_frame_unwinder。这么作的目的是因为dummy frame需要自己特殊的frame_unwind结构才能正常使用。
由上面对调用unwind_dummy_id函数指针的过程也可以注意到,不管当前TARGET是否需要在GDB中支持dummy frame也就是call命令,都必须用set_gdbarch_unwind_dummy_id注册unwind_dummy_id函数,否则GDB将不能运行。
void set_gdbarch_frame_align (struct gdbarch *gdbarch, gdbarch_frame_align_ftype *frame_align)
设置一个gdbarch_frame_align_ftype类型的函数指针到gdbarch中。
注册的这个函数的作用在前面介绍过,其作用对参数指定的地址进行对齐操作。
如果当前TARGET不支持dummy frame或者栈指针通过set_gdbarch_push_dummy_call注册的函数来保证对齐,而且这个ARCH在调用函数的时候不会出现相同的栈指针,还不支持red zone,则可以不设置这个函数。
void set_gdbarch_frame_red_zone_size (struct gdbarch *gdbarch, int frame_red_zone_size)
设置变量frame_red_zone_size到gdbarch中,调用其的宏是FRAME_RED_ZONE_SIZE。
如果当前ARCH支持red zone,则需要设置这个变量。不支持可以设置为0。
void set_gdbarch_call_dummy_location (struct gdbarch *gdbarch, int call_dummy_location)
设置变量call_dummy_location到gdbarch中,调用其的宏是CALL_DUMMY_LOCATION。
当TARGET支持dummy frame的时候,需要设置这个变量来确定调用函数将返回的位置,具体设置什么值前面已经介绍过了,一般情况设置为AT_SYMBOL。
void set_gdbarch_push_dummy_code (struct gdbarch *gdbarch, gdbarch_push_dummy_code_ftype *push_dummy_code)
设置一个gdbarch_push_dummy_code_ftype类型的函数指针到gdbarch中。
当上面的call_dummy_location设置为ON_STACK的时候,需要调用push_dummy_code这个函数在栈中分配给返回地址上的断点需要的空间,如果设置了push_dummy_code则会调用这个函数,如果没设置则会调用GDB/gdb/infcall.c:generic_push_dummy_code。
从上面的分析可以看出这个函数只有在call_dummy_location设置为ON_STACK会被调用,而且如果需要进行的处理比较普通,可以不设置,直接让GDB调用generic_push_dummy_code函数。
void set_gdbarch_push_dummy_call (struct gdbarch *gdbarch, gdbarch_push_dummy_call_ftype *push_dummy_call)
设置一个gdbarch_push_dummy_call_ftype类型的函数指针到gdbarch中。
注册的这个函数的作用是将当前的栈和寄存器的状态设置到执行完调用函数指令后的状态,这样在调用完这个函数后,GDB可以直接从被调用函数的第一条指令开始执行。
参数function是要调用的函数的描述结构。参数regcache是当前的寄存器,可以通过regcache_cooked_write_unsigned等函数设置寄存器。参数bp_addr是被调用函数返回的地址。参数nargs和args是函数的参数数量和值。参数struct_return如果为真则表明返回值是struct类型,则参数struct_addr就是用来返回返回值的地址。
一般情况下会将返回地址bp_addr设置在RA寄存器(risc寄存器常见)或者压入栈中(例如x86);然后检查参数struct_return是否为真,如果为真则将参数struct_addr存入函数的一个位置中,有时候是作为函数的第一个参数;最后依次存储参数nargs和args中指定的每个参数到寄存器或者栈中。总体来说这个函数的编写跟TARGET相关的ABI的具体实现关系密切。
如果不设置这个函数指针,则TARGET不能够支持dummy frame。
9.sigtramp frame
9.1.概述
在某些系统中程序注册的信号处理函数返回的时候会返回到一个称为信号跳板(signal trampoline)的代码,因为在调试信号处理函数的时候,会需要对这部分代码进行frame分析,而且这部分代码结构又比较特殊,所以有了单独的类型为SIGTRAMP_FRAME的sigtramp frame。
在GDB中根据确定判断当前PC是否在signal trampoline中的方法使用了两种不同的分析方法,这两种分析方法将在下面进行详细的介绍。
9.2.普通分析法
在大部分TARGET中,有比较明确的方式根据当前PC确定当前是否在信号跳板中,比如通过已知的信号跳板代码范围,或者是解析出相应函数名称来确定当前在信号跳板中,这种情况的TARGET使用了跟一般frame一样的分析方式,这种方式在GDB中比较常用。
下面介绍一下编写方法:在初始化函数中,象注册普通frame的sniffer函数一样,用前面介绍过的frame_unwind_append_sniffer函数注册一个sniffer函数。在这个sniffer函数会根据已知方式判断取得的PC是否是在信号跳板代码中,如果是则返回一个相应的struct frame_unwind结构,如果不是则返回NULL。关于这个frame_unwind结构前面也介绍过,跟上面介绍不同的是,这里的type要设置为SIGTRAMP_FRAME。后面两个分析函数的编写跟普通分析函数编写都一样,也需要通过取得存储寄存器的栈的地址,进而取得寄存器的信息。
9.3.tramp_frame_prepend_unwinder法
另一些TARGET没有明确的用来确定当前PC是否在sigtramp frame的方法,只能通过分析代码的方法,因为其注册分析结构struct tramp_frame使用了tramp-frame.c:tramp_frame_prepend_unwinder函数,所以称为tramp_frame_prepend_unwinder方法。
9.3.1.void tramp_frame_prepend_unwinder (struct gdbarch *gdbarch, const struct tramp_frame *tramp_frame)
这个函数中首先做的就是分配一个frame_unwind结构的空间,然后将tramp_frame结构以及其他几个变量和函数注册在上面,然后调用frame-unwind.c:frame_unwind_prepend_unwinder将设置好的frame_unwind结构注册到frame_unwind列表中。
这里使用的frame_unwind_prepend_unwinder函数跟介绍过的frame_unwind_append_sniffer函数功能是基本一样的,只不过frame_unwind_prepend_unwinder函数是将参数中的frame_unwind结构经过处理加到列表最前面,而frame_unwind_append_sniffer是将参数中的sniffer函数经过处理加到列表中最后面。
frame_unwind_prepend_unwinder函数只被tramp_frame_prepend_unwinder函数调用。
9.3.2.struct tramp_frame
这个结构定义在tramp-frame.h中,下面对其中元素进行介绍。
enum frame_type frame_type;
frame的类型。
int insn_size;
当前ARCH的指令字节长度。
struct
{
ULONGEST bytes;
ULONGEST mask;
} insn[8];
这个结构在tramp-frame.c:tramp_frame_sniffer函数(这个函数将在9.3.3中介绍)中使用,这其中存储了用来跟取得的指令进行比较的指令序列,最后一个的bytes一定是TRAMP_SENTINEL_INSN,这个不参与比较,而是表示指令序列结束,具体的使用方法将在介绍tramp_frame_sniffer函数的时候进行详细的介绍。
void (*init) (const struct tramp_frame *self, struct frame_info *next_frame, struct trad_frame_cache *this_cache, CORE_ADDR func);
这个函数指针在tramp-frame.c:tramp_frame_cache函数(这个函数将在9.3.4中介绍)中使用,这个函数指针的作用就是分析存储的寄存器的信息,将这些信息设置到trad_frame_cache结构上,具体这个函数指针对应函数的编写方法将在介绍tramp_frame_cache函数的时候进行介绍。
9.3.3.static int tramp_frame_sniffer (const struct frame_unwind *self, struct frame_info *next_frame, void **this_cache)
这个函数就是前面tramp_frame_prepend_unwinder函数中介绍的注册到frame_unwind结构上的函数中的一个,其将在frame_unwind_find_by_frame被调用,用来判断当前frame是否是sigtramp frame。其主要过程是先通过next_frame取得当前PC,然后进行一些基本检查,最后调用tramp_frame_start。
tramp_frame_start用代码和insn中存储指令序列进行多次比较,代码的开始地址是从当前PC到PC减去frame_type中insn实际指令序列的长度(不包括TRAMP_SENTINEL_INSN)。每次比较都是从指令列表的第一个地址一直比较到TRAMP_SENTINEL_INSN之前的指令,比较方法是将取得的实际指令跟insn中的mask进行与操作,然后跟bytes中进行比较(这样每个insn中的元素可以匹配一系列的指令),如果相同就继续比较下一个,如果全相同就标明当前PC就在信号跳板中,比较的开始地址就是信号跳板起始地址,将这个地址返回。
9.3.4.static struct trad_frame_cache *tramp_frame_cache (struct frame_info *next_frame, void **this_cache)
这个函数被tramp-frame.c:tramp_frame_this_id和tramp-frame.c:tramp_frame_prev_register两个函数调用,这两个函数都是在tramp_frame_prepend_unwinder注册到新分配的frame_unwind上。跟7.9介绍的初始化this_cache的函数一样,tramp_frame_cache也是起同样作用的。
这里的this_cache实际使用的结构是tramp-frame.c:tramp_frame_cache,这个结构中的func就是前面tramp_frame_sniffer取得的信号跳板起始地址,tramp_frame就是最开始注册的结构,trad_cache结构是实际上存储寄存器分析结果的位置。
在函数中首先给trad_cache分配空间,然后对tramp_frame中的init函数指针进行调用。
init函数指针上注册的函数一般都是先根据func取得存储寄存器信息的地址(具体位置的要根据TARGET的情况),然后用trad-frame.c:trad_frame_set_reg_addr将每个寄存器的地址存到trad_frame_cache结构中,最后用trad-frame.c:trad_frame_set_id将frame_id存到trad_frame_cache结构中。frame_id用frame_id_build取得,一般来说frame_id_build的第一个参数栈地址用分析得到的存储寄存器的开始地址,第二个参数用func。然后函数返回。
这些存到trad_frame_cache结构中的值最后会被tramp_frame_this_id和tramp_frame_prev_register两个函数使用。