大致浏览了下GDB源码,分析记录如下:
1. GDB与GCC等其他GNU工具以前,构成了程序开发调试不可缺少的一环.
2. GDBSERVER源码架构如下:
a) Gdbserver代码简单,本身支持serial或tcp连接
b) 源码位于 gdb/gdbserver下
c) Server.c文件是入口文件
d) 编译配置
i. 主Makefile.in中,gdbserver依赖OBS,OBS依赖DEPFILES,DEPFILES=@GDBSERVER_DEPFILES@
ii. GDBSERVER_DEPFILES是configure传递的,configure.ac中,GDBSERVER_DEPFILES=”$srv_regobj $srv_tgt ….”
iii. $srv_regobj在configure.srv中根据—target选项生成,configure.srv被主configure包含,所以最终结果是不同的target,不同的文件列表
iv. 对于arm linux来说,configure.srv中定义如下:
srv_regobj="reg-arm.o arm-with-iwmmxt.o"
srv_regobj="${srv_regobj} arm-with-vfpv2.o"
srv_regobj="${srv_regobj} arm-with-vfpv3.o"
srv_regobj="${srv_regobj} arm-with-neon.o"
srv_tgtobj="linux-low.o linux-osdata.o linux-arm-low.o linux-procfs.o"
v. reg-arm.c是生成的,linux-low.c是原有的
vi. initialize_low接口在linux-low.c里
e) 代码流程:
i. 重要变量
1. the_target – 全局默认的调试目标
2. the_low_target – 底层目标
ii. initialize_low接口被调用,底层初始化
1. 调用set_target_ops赋值the_target
2. linux_target_ops是linux上目标的操作集合定义
iii. remote_prepare接口准备远程连接,也就是gdb这一端
iv. 之后是事件循环
1. remote_open (port);打开远程
a) 区分tcp或者serial即可
b) add_file_handler (listen_desc, handle_accept_event, NULL);这句,设置handle_accept_event接口来处理tcp或者serial的事件
2. start_event_loop ();
a) 处在等事件处理事件的循环里
f) 调试目标的启动
i. 调试目标有两种
1. 可执行文件
2. attach到现有进程
ii. 可执行文件在处理输入参数时候,直接执行的
1. start_inferior (program_argv);
iii. attach是在处理输入参数时候,直接attach的
1. attach_inferior (pid)
g) 处理远程交互过程
i. handle_accept_event接口处理远程包
ii. handle_serial_event处理连接后的事件
iii. handle_serial_event主要调用process_serial_event接口实现处理原承包
iv. process_serial_event过程如下
1. 解析命令
a) g G p P m M z Z d D等命令,详见GDB Remote Serial Protocol - RSP
2. 处理命令
a) the_target的各种接口被调用,处理命令
3. 填充own_buf
a) 以g包为例
i. g包意思是要获取所有寄存器值
ii. the_low_target的arch_setup接口对于arm-linux是arm_arch_setup接口,会在适当的实际被调用,会调用init_registers_arm初始化寄存器,init_registers_arm(arm.c – 编译时候生成的,里边的寄存器列表是根据gdb/regformats目录下的reg-arm.dat生成的)接口.
iii. init_registers_arm接口设置寄存器cache,寄存器数量等.
iv. 处理g包的时候,
1. 调用fetch_traceframe_registers获取寄存器
2. registers_to_string转换成串
v. 这里看到gdbserver回复的寄存器数量是根据目标架构的不同,arm_linux_init_hwbp_cap接口获取的设置到arm_linux_hwbp_cap变量里,不同的寄存器数量,具体是在arm_arch_setup接口中,调用arm_linux_init_hwbp_cap获取能力,根据能力注册不同的寄存器组.
4. 最后,调用putpkt (own_buf);发送返回包
3. GDB源码架构如下:
a) 名词解释
i. Interpreter – 解释,执行器,指的是console,tui,mi等UI,即用户接口,包括:
1. #define INTERP_CONSOLE "console"
2. #define INTERP_MI1 "mi1"
3. #define INTERP_MI2 "mi2"
4. #define INTERP_MI3 "mi3"
5. #define INTERP_MI "mi"
6. #define INTERP_TUI "tui"
7. #define INTERP_INSIGHT "insight"
ii. Command loop (event loop) – 命令(事件)循环,实际任何程序都是无限循环,循环的过程中,处理外接的事件输入,产生输出,GDB也是基于这样的架
事件包括很多类型:输入,网络等
iii. Target – 调试的目标,本地文件,进程,网络,串口等,调试目标是真正的读取数据,写入数据的接口.
b) 关键变量
i. current_target – 当前目标
ii. current_interpreter – 当前执行器
iii. inf – info 命令
iv. cli – command line interface
v. tui – text user interface
vi. insight – 是一个图形前端
c) 架构
i. 编译时候,根据configure --target参数,确定了目标架构需要编译的.c,根据参数生成init.c,主要是里边的initialize_all_files接口,只包含了配置需要的初始化函数,初始化函数就是类似: _initialize_arm_tdep,_initialize_xxxx这样的接口,这样的接口都会被配置生成到init.c里.
ii. 实际编译出来后,自然是针对目标架构调试的gdb了
iii. initialize_all_files接口中,会调用针对目标架构的初始化接口,如_initialize_arm_tdep,这样的接口会初始化目标架构,包括寄存器列表,特殊的命令等.
iv. 初始化另一个比较重要的事情是选择Interpreter,Interpreter有特有的事件循环,这样无论是GUI还是Console,都可以根据需要处理用户交互了.
v. 初始化完成后,进入事件(命令)循环,处理事件和命令,直到退出.
vi. GDB新版本中,加入了事件监听机制
d) 基本执行流程
i. main (gdb.c) -> 程序入口
ii. gdb_main (main.c) -> gdb入口
iii. captured_main (main.c) -> gdb入口
iv. gdb_init(top.c) -> 总初始化入口,很重要,很多重点信息都在这
v. captured_command_loop(main.c) -> 命令循环入口
vi. current_interp_command_loop(interps.c) -> 命令循环,调用到此处后,不再返回
vii. current_interp_command_loop接口根据不同的Interpreter,进入不同的循环里.
e) 初始化和init.c
i. gdb-7.2/gdb/Makefile.in中把init.c作为一个总目标的依赖,目标的生成过程就是,建立initialize_all_files函数,添加一系列的_initialize_xxxx函数,这也是_initialize_xxx函数看不到调用的原因.
ii. initialize_all_files函数被gdb_init调用,初始化目标.
iii. 命令注册
iv. gdb实际上是基于命令循环的,在用户的命令作用下,执行相应的动作
v. 命令有两个部分:
1. 公共的命令
a) gdb_init -> init_cli_cmds 初始化了一些命令,其他命令可自行查找
2. 目标特有的命令
a) _initialize_arm_tdep接口内部注册此类命令
f) 命令添加和分类
i. 分类,如下:
no_class = -1, class_run = 0, class_vars, class_stack, class_files,
class_support, class_info, class_breakpoint, class_trace,
class_alias, class_bookmark, class_obscure, class_maintenance,
class_pseudo, class_tui, class_user, class_xdb,
ii. 接口
1. add_cmd, - xxx
2. add_com, - xxx
3. add_prefix_cmd, zzz xxx yyy
4. add_info, - info xxx
5. add_setshow_integer_cmd – set xxx
g) 命令执行
i. Interpreter的resume接口,调用gdb_setup_readline初始化了call_readline和 input_handler = command_line_handler;这两个变量。最后调用 add_file_handler (input_fd, stdin_event_handler, 0);,注册了标准输入事件的回调是stdin_event_handler
ii. 主事件循环中,如果发生了输入事件,stdin_event_handler接口被调用,进一步调用call_readline,call_readline调用input_handler接口来处理,这里的input_handler就是command_line_handler
iii. command_line_handler 调用command_handler,command_handler调用execute_command执行命令.
iv. 命令回调的参数
h) 目标CPU注册
i. 比较重要的就是目标cpu的注册
ii. initialize_all_files->_initialize_arm_tdep类接口,初始化了目标
iii. 以_initialize_arm_tdep为例:
1. 调用gdbarch_register注册架构
2. 增加一些特殊命令
3. 重要的接口是:arm_gdbarch_init,这个接口真正初始化了arm架构
4. arm_gdbarch_init这个接口,不会直接调用,会根据目标文件的格式来启用执行的.
i) 目标架构初始化
i. file命令
1. add_cmd ("file", class_files, file_command….
2. file_command接口处理file命令
3. file_command -> exec_file_command -> exec_file_attach
4. exec_file_attach重要的部分是,调用set_gdbarch_from_file (exec_bfd);调用gdbarch_find_by_info初始化了目标架构.
ii. target的open操作,会引起架构初始化
j) 调试GDB
i. 可以通过pc上的gdb调试编译出来的gdb
ii. 通过设置gdbarch_debug变量,可以打开gdb本身的调试信息,实际就是打开GDBARCH_DEBUG宏,输出信息相当多.
k) Target概述
i. 分类
1. gdb下输入help target
target async -- Use a remote computer via a serial line
target child -- Win32 child process (started by the "run" command)
target core -- Use a core file as a target
target exec -- Use an executable file as a target
target extended-async -- Use a remote computer via a serial line
target extended-remote -- Use a remote computer via a serial line
target remote -- Use a remote computer via a serial line
ii. 启用
1. 通过显示的执行 target xxx yyy
a) xxx 是上边列出的名字,yyy是参数
b) 命令后,target ops相应的open接口会被打开
c) 所有的初始化都在init.c里
d) target_fetch_registers获取寄存器
e) target打开后,被调试文件应该被载入,进一步根据文件格式初始化了目标架构
iii. 默认target
1. gdb_init -> initialize_targets -> push_target (&dummy_target);设置了dummy_target为默认,名字是”None”
2. 我们直接用gdb hello,之后直接run,这时候有个默认的目标
3. 注意到initialize_all_files接口,后侧调用的_initialize_exec接口,初始化了exec目标,过程中会调用add_target增加target
4. 初始化完后,加载可执行文件的时候,exec_file_attach 调用 add_target_sections, add_target_sections会调用 push_target (&exec_ops);,把exec设置成默认目标的.
l) 寄存器的显示和获取
i. 显示
1. Info registers 和 info all-registers 可以显示寄存器
2. registers_info接口用来处理”info registers”命令, add_info ("registers", nofp_registers_info, _("\
3. registers_info调用gdbarch_print_registers_info调用gdbarch->print_registers_info,gdbarch->print_registers_info实际上是初始化架构的时候赋值的,一般都是用的默认值default_print_registers_info,是在gdbarch_alloc接口中赋值的,还有很多架构的其他默认值.
4. default_print_registers_info实际是从缓存里那结果,也就是这个寄存器的结果是别的地方拿回来的.
ii. 获取
1. 远程的情况
a) 所有的远程的情况都可以在remote.c找到答案
b) _initialize_remote->init_remote_ops接口初始化了remote_ops变量,接着调用add_target 这个接口比较特殊,也比较重要,调用 add_cmd (t->to_shortname, no_class, t->to_open, t->to_doc, &targetlist); t->to_shortname是在init_remote_ops里赋值成” remote”的,也就是”target remote xxx”的来历,同时t->to_open正是remote_open接口,就是说命令执行的时候,remote_open被调用.
c) remote_open调用remote_open_1,remote_open_1调用reopen_exec_file重开可执行文件,也意味着重新获取架构arch.调用reread_symbols重新获取符号表.
d) remote_open_1接下来调用remote_serial_open打开远程连接,
e) remote_fetch_registers接口负责获取远程寄存器,会被target_fetch_registers接口间接调用到.
f) target_fetch_registers接口是target的获取寄存器的接口,regcache_raw_read接口调用了target_fetch_registers。
g) 一定是通过某种方式:调用到了target_fetch_registers,获取一个寄存器,并且缓存起来.
h) target_preopen接口会被remote_open_1调用,target_preopen迭代了所有的观察者: iterate_over_inferiors (dispose_inferior, NULL);使用dispose_inferior接口分发,调用到switch_to_thread,switch_to_thread调用regcache_read_pc读出pc寄存器的值.
2. 本地的情况
a) 类似远程情况,由当前的target的target_fetch_register接口获取
b) File或者exec-file命令加载文件后,exec变成默认目标
c) 运行起来后,child变成默认目标
d) 所以这时需要关注child目标的获取寄存器接口
e) child 目标在i386-linux-nat.c中,_initialize_i386_linux_nat接口注册
f) i386_linux_fetch_inferior_registers接口作为child目标的获取寄存器接口
g) i386_linux_fetch_inferior_registers接口通过ptrace调用,获取了寄存器的值
m) 调试功能实现
i. Linux系统上
1. 基本都是通过ptrace系统调用实现的,无论是本地的child目标,还是gdbserver,都是通过ptrace来控制子进程完成的
2. ptrace包括了读写寄存器,读写内存
3. 单步需要硬件支持,软件做的话,要分析出下一条指令的准确地址,然后替换
4. 断点功能是通过读写内存,修改对应指令即可实现
ii. Win32系统上
1. Winapi有很多这样的接口可以实现调试功能,读写子进程内存,读写寄存器等
2. 单步一样需要硬件支持
3. 断点也是通过读写内存,修改对应指令实现的
n) gdb和gdbserver的交互
i. 一般情况下,gdb和gdbserver
1. 使用tcp连接
2. target是remote
3. Interpreter任选
4. 架构从被调试的elf文件中获取
5. 使用gdb remote serial protocol – GDB RSP
a) 简单的字符串命令
ii. gdbserver,功能需要如下,也就是需要能够响应的命令如下
1. 读写寄存器 - 必须
2. 读写内存 - 必须
3. 软硬件断点 - 可选
4. 中断执行 - 可选
5. 单步执行 - 可选
iii. RSP – 读写寄存器
1. remote_fetch_registers接口负责获取remote寄存器
a) 两种方法获取
i. fetch_registers_using_g – “g”命令
ii. fetch_register_using_p – “p”命令
iii. regcache_raw_supply负责填充返回的结果到cache里,这样后边就可以使用了
b) 寄存器的顺序和数量
i. gdbarch_num_regs接口用户获取目标架构的寄存器数量
ii. default_print_registers_info接口是默认的打印寄存器的接口,这里边设计到了缓存的reg值,reg名字,以及顺序
1. fputs_filtered (gdbarch_register_name (gdbarch, i), file);是在打印名字,其中i是寄存器索引
2. arm架构对应的gdbarch_register_name调用接口是arm_register_name(arm-tdep.c)
a) arm_register_names[i]被返回
b) arm_register_names定义为特定值,这个顺序可作为gdbserver返回g命令时候的顺序.
c) 寄存器映射
1. arm_gdbarch_init接口非常重要,初始化了很多架构相关的数据和接口
2. 本地的寄存器和remote有个对应关系
3. arm_gdbarch_init中有一部分是初始化架构描述,这部分描述了寄存器,具体见 if (tdesc_has_registers (tdesc))下部分代码,remote目标初始化调用init_remote_state接口,初始化了寄存器映射关系,之后调用 tdesc_use_registers (gdbarch, tdesc, tdesc_data),
4. tdesc_use_registers 设置了寄存器相关的名字,类型,映射关系的回调,set_gdbarch_remote_register_number (gdbarch,tdesc_remote_register_number);tdesc_remote_register_number接口,
5. init_remote_state接口,负责初始化remote状态,适当的时机被调用
6. init_remote_state接口初始化了g包的大小,调用map_regcache_remote_table接口初始化了寄存器顺序和映射关系,这个接口很重要.
7. 过滤的结果是,那些不支持的寄存器不会在远程package的数据包里.
8. process_g_packet接口处理了g数据包返回的寄存器结果包,包括了解析包的过程.
d) g包的大小
i. init_remote_state接口中,分配了rsa – Remote_arch_state,并且初始化了rsa->sizeof_g_package,调用的是map_regcache_remote_table
ii. map_regcache_remote_table接口完成了计算.
1. 首先获取架构寄存器总个数,架构初始化时候固定的
2. 遍历所有寄存器,如果寄存器大小描述不是0,则通过接口gdbarch_remote_register_number获取远程寄存器pnum,也就是g包里的偏移.寄存器大小通过register_size获取,register_size接口直接使用的descr->sizeof_register[regnum],descr->sizeof_register[regnum]是在初始化regcache描述的时候赋值的,接口是init_regcache_descr,init_regcache_descr接口中,通过descr->sizeof_register[i] = TYPE_LENGTH(descr->register_type[i]),descr->register_type[i]在稍微上边一点赋值,具体等于descr->register_type[i]=gdbarch_register_type(gdbarch,i),gdbarch_register_type接口,gdbarch_register_type最终映射到arm_register_type接口,arm_register_type根据架构支持特性,根据寄存器所在偏移,返回寄存器类型.此处需要注意:arm-tdep.h中定义了寄存器的偏移,如ARM_PC_REGNUM = 15,arm-tdep.c中定义了寄存器的列表,arm_register_names,此处判断如果不在arm_register_names范围内,就会返回bulltin_int0类型,实际这个类型大小size是0,额外的寄存器会通过xml文件的形式传递过来.xml描述由远端传递过来,如arm-with-vfpv2.xml文件,存在于gdb/features目录下,gdbserver端对应的变量是gdbserver_xmltarget.如果此时gdbserver提供的g包很大,而不在pc端g包大小范围内,就会出现”g package too long”这样的提示了.就是说,最好打开gdb的xml支持,才能不会出现类似的问题了,支持xml方法是
i. Sudo apt-get install expat libexpat1-dev
ii. ./configure –enable-werror=no –target=arm-linux-gnueabi --with-expat=yes --with-expat-prefix =/uar/local
iii. 上面那样做,不起作用,需要手动修改configure,直接修改成不需要检查,同时设置好expat.so路径即可
iv. Expat库,自己下载编译安装,会安装到/usr/local
iv. RSP - 读写内存
1. 类似读写寄存器,命令不同而已
v. RSP – 软硬件断点
vi. RSP – 中断执行
vii. RSP – 单步执行
1. 以上都是发命令和参数的过程
o) gdb的编译
i. 官网获取源码,解压
ii. 针对arm,但是在pc上运行
1. ./configure --target=arm-linux-gnueabi --enable-werror=no
2. make –j5
iii. 针对arm,在arm上运行
1. ./configure --target=arm-linux-gnueabi –host=arm-linux-gnueabi –enable-werror=no
2. 这时候需要termcap库,下载编译即可
3. make –j5
4. 嵌入式gdbserver实现思路
a) gdb保持gnu工具里的不变
b) 没有标准操作系统(linux,win32,unix),就没法运行标准的gdbserver,所以要在pc上实现一个gdbserver,之后在板子端实现gdbstub,gdbserver与gdb通信,gdbserver通过板子与pc的连接口与gdbstub通信。
c) gdbserver这块还是主要实现gdb发过来的命令,比较重要的是g获取全部寄存器和m读内存,gdb根据寄存器值,配合内存读写命令,就能得到几乎所有的内容了
i. g命令要注意寄存器顺序,数量,大小
ii. 寄存器顺序,数量,大小和架构相关,同时顺序参照gdb定义,当然自然情况是按照编号排序(r0,r1,…,r15之类).
iii. 因为嵌入式gdbserver无法获取调试文件或进程的架构,所以需要在启动gdbserver的时候传递参数,这时候已经知道需要调试的目标的信息了.
d) gdbserver和gdbstub通信,根据板子不同而不同