一个关于重定位的例子

xmj@hellogcc

参考文档:SYSTEM V APPLICATION BINARY INTERFACE

重定位(relocation),可以理解为有些内容在编译和汇编的时候不能确定下来,需要在连接成可执行程序时才能够计算求得。最常见的就是跳转指令中的目标地址。

ELF文件中会有一些重定位段,比如对应于.text段的.rel.text段。重定位段中的每一项,即重定位项,描述了其对应的需要被重定位的地方,以及如何进行求值。

1、重定位项有两种结构类型,

1
2
3
4
5
6
7
8
9
10
typedef struct {
  Elf32_Addr r_offset;
  Elf32_Word r_info;
} Elf32_Rel;
 
typedef struct {
  Elf32_Addr r_offset;
  Elf32_Word r_info;
  Elf32_Sword r_addend;
} Elf32_Rela;

r_offset,需要进行重定位的地方,到其所在段开始处的字节偏移量;

r_info,有两部分组成,一部分是所对应的符号表中的索引,一部分是重定位类型;它们的组合方式如下:

1
2
3
#define ELF32_R_SYM(i) ((i)>>8)
#define ELF32_R_TYPE(i) ((unsigned char)(i))
#define ELF32_R_INFO(s,t) (((s)<<8)+(unsigned char)(t))

r_addend,计算求值时的常量加数。

MIPS的重定位项是使用了第一种方式,这种方式是将r_addend的值存放在了将要被修改的位置,也就是目标文件中需要被重定位的地方的初始内容。

2、执行如下命令,


$ mipsel-linux-readelf -r sysconf.o

Relocation section ‘.rel.rodata’ at offset 0xb74 contains 132 entries:
Offset Info Type Sym.Value Sym. Name
00000000 0000020c R_MIPS_GPREL32 00000000 .text
00000004 0000020c R_MIPS_GPREL32 00000000 .text
00000008 0000020c R_MIPS_GPREL32 00000000 .text
0000000c 0000020c R_MIPS_GPREL32 00000000 .text
00000010 0000020c R_MIPS_GPREL32 00000000 .text

这里,打印出了每个重定位项的offset和info。从info中的内容,解析出了type和sym索引,进一步解析出sym.name和sym.value。可以看到,除了.text段中有需要进行重定位的地方以外,.rodata中也有一些,注意,这些offset是对应于.rodata段。

3、执行如下命令,


$ mipsel-linux-objdump -Dr sysconf.o

Disassembly of section .rodata:

00000000 <.rodata>:
0: ffffc064 0xffffc064
0: R_MIPS_GPREL32 .text
4: ffffc06c 0xffffc06c
4: R_MIPS_GPREL32 .text
8: ffffc074 0xffffc074
8: R_MIPS_GPREL32 .text
c: ffffc07c 0xffffc07c
c: R_MIPS_GPREL32 .text
10: ffffc084 0xffffc084
10: R_MIPS_GPREL32 .text

这里将.rodata中需要重定位的地方和对应的重定位项结合在一起打印出来。可以看出,对于.rodata中的第一个字,其保存的addend值为0xffffc064。

4、MIPS ABI文档如下描述R_MIPS_GPREL_32:

Name Value Field Symbol Calculation
R_MIPS_GPREL_32 12 T-word32 local A + S + GP0 - GP

A
Represents the addend used to compute the value of the relocatable
field.

S
Represents the value of the symbol whose index resides in the relocation entry, unless the the symbol is STB_LOCAL and is of type
STT_SECTION in which case S represents the original sh_addr minus
the final sh_addr.

GP
Represents the final gp value to be used for the relocatable, executable, or shared object file being produced.

GP0
Represents the gp value used to create the relocatable object.

这里的“A + S + GP0 – GP”便是连接器进行重定位时的求值公式。

5、执行如下命令

$ mipsel-linux-objdump -Dr sysconf.o

Disassembly of section .reginfo:

00000000 <.reginfo>:
0: b200001e 0xb200001e

14: 00004000 sll t0,zero,0x0

MIPS通过reginfo保存一些寄存器信息,其结构体为:

1
2
3
4
5
typedef struct {
  Elf32_Word ri_gprmask;
  Elf32_Word ri_cprmask[4];
  Elf32_SWord ri_gp_value;
} ELF_RegInfo;

其中最后一个域是记录了GP0的值。可以看到,这里GP0的值为0×4000。

6、当链接成可执行程序时,依照同样的方法,我们可以找到


00400094 <.reginfo>:
400094: b20000f4 0xb20000f4
...
4000a8: 10008a70 b 3e2a6c <__start-0x1d6a4>

所以,这里GP的值为0x10008a70,


00425c70 <__sysconf>:
425c70: 3c1c0fbe lui gp,0xfbe

所以,S的值为0x425c70,

7、根据公式“A + S + GP0 – GP”,可以计算出.rodata中需要重定位的地方的最终内容。比如,对于.rodata中的第一个字,


0xffffc064 + 0x425c70 + 0x4000 - 0x10008a70 = 0xf041d264

8、附注

如果查看一下binutils/bfd/elf32-mips.c,就可以找到R_MIPS_GPREL32相应的HOWTO数据结构,以及处理函数。

gdb测试框架dejagnu的介绍

前些天有人问到如何测试交叉编译的gdb,和有如何与板子一起工作进行测试。 其实答案相对简单,就是提供一个board file,根据自己的板子和环境,进行 一些定制。但是,好像很多人对觉得测试交叉编译的gdb或者gcc都挺神秘的, 我觉得有必要写一点点来介绍一下dejagnu。

dejagnu的测试框架

GNU toolchain都在使用dejagnu作为自己的测试框架,所有的测试用例都是用 Tcl/Expect编写的。我们不去介绍dejagnu内部是怎样做的,而是从外部来观察 如何使用dejagnu来为GNU toolchain编写测试用例。我们可以把GNU toolchain 的测试代码分为三个部分,

  • 公共的基础函数。比如在gdb测试中,我们需要一个公共函数来把测试代码编译为可执行文件,或者,一个公共函数来启动gdb等等。
  • 板子级别的配置文件(board file)。这些文件和具体的测试场景有关系,比如,
    • natively build的gdb测试过程中,就不需要gdbserver。cross build的gdb测试过程就需要gdbserver。
    • 不同的测试场景需要不同的host gcc,
    • 远程调试需要知道远程机器的host name和端口号,等等
  • 测试用例。比如给gdb发送一个命令,通过输出来判断是否正确。在gdb的所有测试代码中,这一类占了绝大部分。

测试用例

测试用例一般都是针对gdb的某个功能或者某个bug来设计的一系列gdb的操作,和对应这些操作后,应该得到的输出。简单的说,就是让gdb 执行一个或者多个命令,然后从gdb的输出来判断这个命令的执行正确与否。GDBTestcaseCookbook 详细介绍了如何在写测试用例。我们在 这里也不介绍了。

板子级别的配置文件(board file)

board file 听起来有些玄妙,我们先看看它在什么时候需要,

  • 需要一个特殊的编译器,比如非gcc的编译器,来编译测试代码,
  • 需要测试一个非gdb的调试器,或者没有在安装缺省路径下的调试器,
  • 需要测试一个交叉调试器,在远程调试的环境下,利用gdbserver或者别的stub得到测试结果
  • 需要在测试的时候,知道远程gdbserver或者stub的机器地址和端口信息,
  • 对于某些特殊的环境,需要在启动测试的时候,启动别的程序,
  • 等等

从上边的列表我们能够看出,凡是和定制gdb测试过程的逻辑大多和board file有关。在我们每次运行gdb的测试用例的时候,所使用的board file绝对不止一个。往往会用到好几个board file,他们之间的关系类似面向对象中的类的继承关系,下层(孩子)的board file可以覆盖 上层(父亲)的board file中的一些函数,以实现定制功能。比如,基本的board file,就定义了测试的一般流程,比如 gdb\_start 启动 gdb,gdb\_load 加载。如果是用gdbserver进行远程调试,board file中就可以覆盖缺省的gdb\_start动作,加入一些启动gdbserver的 逻辑。就是利用这样的方式,dejagnu可以支持gdb的各种不同的环境下进行所有的测试,而测试用例本身不用十分在意当前的运行环境是怎样的。

这里给一个例子:

# gdbserver running over ssh.
load_generic_config "gdbserver"
process_multilib_options ""
 
# 设置编译测试用例的gcc
set_board_info compiler "/home/yao/toolchain/bin/arm-unknown-linux-gnueabi-gcc"
 
set_board_info rsh_prog /usr/bin/ssh
set_board_info rcp_prog /usr/bin/scp
set_board_info protocol standard
# 这里进行远程调试,目标机器的hostname或者ip地址
set_board_info hostname your.target.host.name or ip address
set_board_info username yao
 
# gdbserver's location on your target board.
set_board_info gdb_server_prog /home/yao/gdbserver
# We will be using the standard GDB remote protocol
set_board_info gdb_protocol "remote"
# Use techniques appropriate to a stub
set_board_info use_gdb_stub 1
# This gdbserver can only run a process once per session.
set_board_info gdb,do_reload_on_run 1
# There's no support for argument-passing (yet).
set_board_info noargs 1
# Can't do input (or output) in the current gdbserver.
set_board_info gdb,noinferiorio 1
# Can't do hardware watchpoints, in general
set_board_info gdb,no_hardware_watchpoints 1

Debugger Not In Depth: signal trampoline frame

在这个部分,我们要介绍signal trampoline frame对调试过程的影响,以及调试器如何处理signal trampoline frame。

1. signal trampoline

当程序被信号中断,它的状态会被保存,当信号处理函数执行完毕以后,程序可以恢复到它之前被中断的点上继续执行。这就意味着从信号处理函数返回要比从一般的函数返回复杂。Linux内核安排信号处理函数在返回的时候,跳转到一小段代码,这段代码会最终执行sigreturn或者rt sigreturn, 来执行到程序之前被中断的点。这一小段代码就叫做signal trampoline。

这样说听着有些玄妙,我们看看内核是如何使用signal trampoline, 并保证信号处理函数返回到signal trampoline。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/*
* handle the actual delivery of a signal to userspace
*/
static int handle_signal(int sig, siginfo_t *info, struct k_sigaction *ka,
                         sigset_t *oldset, struct pt_regs *regs,
int syscall)
{
/* ... */
ret = setup_frame(sig, ka, oldset, regs);
}
 
static int setup_frame(int signr, struct k_sigaction *ka,
sigset_t *set, struct pt_regs *regs)
{
  /* ... */
  retcode = (unsigned long *) frame->;retcode;
  put_user(0x00003BAAUL, retcode++); /* MVK 139,B0 ; __NR_rt_sigreturn in B0 */
  put_user(0x10000000UL, retcode++); /* SWE */
  put_user(0x00006000UL, retcode++); /* NOP 4 */
  put_user(0x00006000UL, retcode++); /* NOP 4 */
  put_user(0x00006000UL, retcode++); /* NOP 4 */
  put_user(0x00006000UL, retcode++); /* NOP 4 */
  put_user(0x00006000UL, retcode++); /* NOP 4 */
 
  /* Change user context to branch to signal handler */
  regs->sp = (unsigned long) frame - 8;
  regs->b3 = (unsigned long) retcode;
  regs->pc = (unsigned long) ka->sa.sa_handler;
 
  /* Give the signal number to the handler */
  regs->a4 = signr;
  regs->b4 = (unsigned long) &amp;frame->sc;
 
  /* ... */
}

我们看到函数handle signal调用了setup frame,其中setup frame设置了使得信号处理函数返回到signal trampoline (函数返回值的地址在寄存器B3)。这样当信号处理函数执行完毕,就会自动的转到signaltrampoline,最后调用sigreturn返回。这里可以看出,signal trampoline完全处于正常程序和信号处理函数之间,如果调试器不能正确识别signal trampoline,很多调式功能在信号处理函数上,就会有问题。

2. signal trampoline给调试带来的问题

在知道了signal trampoline是什么以后,我们来描述一下signal trampoline给调式带来的问题。例如我们现在有个正常函数func,在执行到一半的时候,受到信号, 进而执行信号处理函数handler, 假如我们在handler上设置了断点,有一些调试操作是不能正确执行的,

    1. 查看函数调用堆栈。实际上,当前的函数调用栈包括:正常函数func,signaltrampoline的一些信息,信号处理函数handler。其中func和handler都是一般的函数 ,所以它们的frame都可以容易处理,但是signal trampoline是很特殊的一段代码,它的frame不能按照通常的frame来处理。
    2. 命令next。命令next的意思是执行程序到下一行,如果程序当前已经在函数最后一行,那么这个命令应该使得程序执行一段停止在调用这个函数的下一句。这些是我们在调试器手册看到的解释,但是调试器内部怎么理解这样的条件呢?调试器会根据不同frame之间的关系(inner than 或者 outter than),来判断这些条件(调试器内部的frame管理,是一个很复杂的模块,对于不熟悉frame和unwinding的读者,可以暂时忽略frame和unwinding的内部原理)。如果调试器不能识别signal trampoline,这些命令在信号处理函数的尾部,就无法使用。

3 支持signal trampoline

在上边分析完signal trampoline给调试带来的问题后,我们把支持signal trampoline总结为两个部分

  1. 识别signal trampoline。调试器首先能够在代码里边识别signal trampoline。因为signal trampoline都是很特别而且很短的代码,所以调试器可以匹配代码指令,就能找到signal trampoline。
  2.  unwind signal trampoline frame。我们上边说过,signal trampoline的frame联系着正常函数和信号处理函数,如果调试器希望在堆栈上正确的找到他们各自的内容,就需要正确分析signal trampoline的frame(对于不清楚frame unwinding的读者,可以暂时认为frame unwinding就是递归地找到当前函数的caller的寄存器保存的位置和值)。所以,对于signal trampoline frame,我们也需要找到保存它的caller(由于从signal trampoline后就进入sigreturn,进而回到原来正常函数,我们可以认为signal trampoline的caller就是原来的正常函数)的寄存器的位置。

识别signal trampoline就是一个对指令进行模式匹配的过程,如果指令匹配上了一个模式,这个几条指令就是signal trampoline。如所示,我们可以把指令模式定义如下,

我这里不打算具体去讲这个结构的定义和含义(结构tramp_frame也是GDB内部的数据结构),只展示如何定义自己的指令模式,以识别signal trampoline。在定义完这个指令模式后,第一个问题就解决了,下来我们看第二个问题,如何找到caller的寄存器的位置和值
。当函数执行收到信号的时候,内核会把进程当前状态保存在堆栈上某个位置,进而在恢复的,从堆栈上得到。我们这里的工作就是,查看内核源代码,得到寄存器的保存位置,然后我们就可以得到这些寄存器的值。在这里,“获得寄存器的值”还是有些模糊的,在这个环境中,我们可以把获得寄存器分为两个部分:sp,fp和其他寄存器。在函数调用的过程中,寄存器都是保存在堆栈上,所以基本都是基于sp或者fp的寻址。

我们结合代码来看看如何得到signal trampoline frame上保存的寄存器。前提是我们已经知道sp,下来就是寻找保存寄存器的位置(在堆栈上)对sp的偏移。

1
2
3
4
5
6
7
asmlinkage int do_rt_sigreturn(struct pt_regs *regs)
{
  struct rt_sigframe *frame;</code>
 
  frame = (struct rt_sigframe *) ((unsigned long) regs->sp + 8);
/* ... */
}

从上边的代码中我们能看到,rt_sigframe的起始地址距离sp为8,我们接着看rt_sigframe的结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct rt_sigframe
{
  struct siginfo *pinfo;
  void *puc;
  struct siginfo info;
  struct ucontext uc;
  unsigned long retcode[RETCODE_SIZE >> 2];
};
 
struct ucontext {
  unsigned long uc_flags;
  struct ucontext *uc_link;
  stack_t uc_stack;
  struct sigcontext uc_mcontext;
  sigset_t uc_sigmask; /* mask last for extensibility */
};

最后,我们发现寄存器都保存在结构struct sigcontext中。这样我们就可以计算出保存每个寄存器距离sp的偏移,然后从这些地址读出寄存器的值,填写每个frame,

trad_frame_set_reg_addr (this_cache, reg_num,
base + foo_register_sigcontext_offset (reg_num));

这样,一个完整的signal trampoline frame unwinding就做好了。