PowerPC下连接器--relax选项实现
一、问题
在编译一些大的工程的可执行文件的时候,可以发现如果使用DEBUG版本,代码段加上和代码段放在一起的只读数据(字符串或者常量全局变量等)数量将会比较庞大,而在glibc的crti中会有一个对GLABAL_OFF_TABLE(GOT)表的重定向,这个重定向类型为为,从网络上可以看到这种错误类型提示信息(来自http://meld.org/discussion/linking-error-cge-51-p2020-rdb-tool-chain):
oot@Desktop # ppc_85xx-gcc -o sampleprog sampleprog.c
/opt/cge5.1p2020rdb/montavista/cge/devkit/ppc/85xx/bin/../target/usr/lib/crt1.o: In function `_start':
(.text+0x20): relocation truncated to fit: R_PPC_REL24 against symbol `__libc_start_main@@GLIBC_2.0' defined in .plt section in /opt/cge5.1p2020rdb/montavista/cge/devkit/ppc/85xx/bin/../target/usr/lib/crt1.o
/opt/cge5.1p2020rdb/montavista/cge/devkit/ppc/85xx/bin/../target/usr/lib/crti.o: In function `_init':
/home/build/BUILD/glibc-2.5.90/objdir/csu/crti.S:18: relocation truncated to fit: R_PPC_LOCAL24PC against symbol `_GLOBAL_OFFSET_TABLE_' defined in .got section in /opt/cge5.1p2020rdb/montavista/cge/devkit/ppc/85xx/bin/../target/usr/lib/crt1.o+fffffffc
/tmp/cckOiWe5.o: In function `main':
sampleprog.c:(.text+0x28): relocation truncated to fit: R_PPC_REL24 against symbol `printf@@GLIBC_2.4' defined in .plt section in /opt/cge5.1p2020rdb/montavista/cge/devkit/ppc/85xx/bin/../target/usr/lib/crt1.o
collect2: ld returned 1 exit status
二、出现原因
PowerPC属于RISC机型,RISC的一个重要特定就是指令定长,在32bits系统中每条指令就是32bits。而一般一个指令都是由操作码部分和操作数部分两部分组成(当然也有例外,就是没有操作数,例如系统调用指令sc,中断返回指令rfi等),其中的操作码表示不同的动作,而操作数则表明了操作的内容,例如 mr r1,r2,就是将r2寄存器的值复制到r1中,由于系统中通常有32个寄存器,32个寄存器需要5bits来进行表示不同寄存器编号,所以这个指令中需要有10bits来作操作数,可以有22bits作操作码,当然由于RISC中操作指令一般不多,事实上不会用这么多bit作操作码的。所以这个例子中操作数限制没有体现出来。
现在看一下跳转指令,大部分机型中跳转更喜欢相对跳转,也就是相对于当前指令运行时PC加上一个偏移量的目的去执行(可能是函数,也可能是if else的无条件跳转),原因是因为这种代码长度比较短,并且便于重定位,整个代码段起始位置变化之后,相对位移不变,所以不用重定位。所以在386中也有这种相对跳转指令。
一个PowerPC跳转指令的32bits大致分为两部分
010110 000000000000000000000000
也就是高6bits用来作为操作码,表示为一个跳转指令,之后的剩余26bits在相对跳转的时候作为相对于当前PC的偏移值。在编译的时候,一个.o文件不知道自己引用的代码在整个可执行文件中的位置,所以它大胆假设和此处的差异在整个范围之内。但是如果不巧的时候,这目标地址偏偏就真的和起跳处相差过大,此时连接器在整个24bits中无法填充这个偏移值,此时就会提示上面的错误。
三、连接器--relax的实现方法
powerpc的工具链应该为自己的错误假设买单,它就有必要解决自己错误假设引入的问题。注意:连接器一般是不会修改操作码的(也就是不会把一个无条件b指令转换为条件bc跳转指令)。所以连接器引入了--relax来解决这个问题,看一下链接器是如何解决这个问题的。
BinUtils\binutils-2.21.1\bfd\elf32-ppc.c:ppc_elf_relax_section函数是ppc解决这个问题的主题,看一下它的主要相关代码:
static const int stub_entry[] =
{
0x3d800000, /* lis 12,xxx@ha */
0x398c0000, /* addi 12,12,xxx@l */
0x7d8903a6, /* mtctr 12 */
0x4e800420, /* bctr */
};
static bfd_boolean
ppc_elf_relax_section (bfd *abfd,
asection *isec,
struct bfd_link_info *link_info,
bfd_boolean *again)
{
……
for (irel = internal_relocs; irel < irelend; irel++)这里遍历一个目标文件中所有的重定位项,找到其中的重定位类型,然后将他们收集到fixups指向的链表结构中,这个链表的作用是为了删除冗余。例如,同一个模块可能有多处bl target,对于相同的target,它们可以共用一个“跳板”。
{
……
/* Look for an existing fixup to this address. */
for (f = fixups; f ; f = f->next)
if (f->tsec == tsec && f->toff == toff)
break;
if (f == NULL)
{
……
stub_rtype = R_PPC_RELAX;
if (tsec == htab->plt
|| tsec == htab->glink)
{
stub_rtype = R_PPC_RELAX_PLT;
if (r_type == R_PPC_PLTREL24)
stub_rtype = R_PPC_RELAX_PLTREL24;
}
/* Hijack the old relocation. Since we need two
relocations for this use a "composite" reloc. */由于后面将会修改跳板,而跳板内需要重定向,并且需要两处重定向,一个是目标32bits地址的高16bits,一个是低16bits,而原始的重定向只有一个重定向,所以此处引入了一个新的重定向类型R_PPC_RELAX,这一个重定位项要求链接器进行两处重定向。
irel->r_info = ELF32_R_INFO (ELF32_R_SYM (irel->r_info),
stub_rtype);
irel->r_offset = trampoff + insn_offset;
if (r_type == R_PPC_PLTREL24
&& stub_rtype != R_PPC_RELAX_PLTREL24)
irel->r_addend = 0;
/* Record the fixup so we don't do it again this section. */
f = bfd_malloc (sizeof (*f));
f->next = fixups;
f->tsec = tsec;
f->toff = toff;
f->trampoff = trampoff;
fixups = f;
trampoff += size;
changes++;将新的修补项纪录起来。
}
……
/* Write out the trampolines. */
if (fixups != NULL)
{
const int *stub;
bfd_byte *dest;
int i, size;
do
{
struct one_fixup *f = fixups;
fixups = fixups->next;
free (f);由于上面说过,这个fixups主要是用来查重的,所以这里就可以删除了。
}
while (fixups);
contents = bfd_realloc_or_free (contents, trampoff);这里的trampoff指向所有跳板(stub桩)汇集在一起之后原始代码段扩充后大小。
if (contents == NULL)
goto error_return;
isec->size = (isec->size + 3) & (bfd_vma) -4;
dest = contents + isec->size;
/* Branch around the trampolines. */
if (maybe_pasted)
{
bfd_vma val = B + trampoff - isec->size;
bfd_put_32 (abfd, val, dest);
dest += 4;
}
isec->size = trampoff;
if (link_info->shared)
{
stub = shared_stub_entry;
size = ARRAY_SIZE (shared_stub_entry);
}
else
{
stub = stub_entry;
size = ARRAY_SIZE (stub_entry);
}
i = 0;
while (dest < contents + trampoff)
{
bfd_put_32 (abfd, stub[i], dest);这里开始对每一个桩函数进行拷贝,桩代码的内容就是stub_entry中四条指令,每个目标项对应一个。
i++;
if (i == size)
i = 0;
dest += 4;
}
BFD_ASSERT (i == 0);
}
在ppc_elf_relocate_section函数中,对这个R_PPC_RELAX重定位类型的处理
case R_PPC_RELAX:
if (info->shared)
relocation -= (input_section->output_section->vma
+ input_section->output_offset
+ rel->r_offset - 4);
{
unsigned long t0;
unsigned long t1;
t0 = bfd_get_32 (output_bfd, contents + rel->r_offset);
t1 = bfd_get_32 (output_bfd, contents + rel->r_offset + 4);
/* We're clearing the bits for R_PPC_ADDR16_HA
and R_PPC_ADDR16_LO here. */
t0 &= ~0xffff;
t1 &= ~0xffff;
/* t0 is HA, t1 is LO */
relocation += addend;
t0 |= ((relocation + 0x8000) >> 16) & 0xffff;
t1 |= relocation & 0xffff;
bfd_put_32 (output_bfd, t0, contents + rel->r_offset);
bfd_put_32 (output_bfd, t1, contents + rel->r_offset + 4);
/* Rewrite the reloc and convert one of the trailing nop
relocs to describe this relocation. */
BFD_ASSERT (ELF32_R_TYPE (relend[-1].r_info) == R_PPC_NONE);
/* The relocs are at the bottom 2 bytes */
rel[0].r_offset += 2;
memmove (rel + 1, rel, (relend - rel - 1) * sizeof (*rel));
rel[0].r_info = ELF32_R_INFO (r_symndx, R_PPC_ADDR16_HA);
rel[1].r_offset += 4;
rel[1].r_info = ELF32_R_INFO (r_symndx, R_PPC_ADDR16_LO);
rel++;
}
continue;
四、思路总结
当距离太长的时候,链接器首先在这个跳转起点(bl指令)所在代码节的最后(注意这个桩代码的放置位置,因为一个节内地址相对于节结束位置是连接是确定的,不依赖其它节)为每个跳转补一块跳板(也就是代码中所说的桩stub),修改这个bl指令的操作数,让这个bl指令跳转到这个桩代码处,跳转过来之后执行下面注释中代码
static const int stub_entry[] =
{
0x3d800000, /* lis 12,xxx@ha */
0x398c0000, /* addi 12,12,xxx@l */
0x7d8903a6, /* mtctr 12 */
0x4e800420, /* bctr */
};
可以看到,其中的xxx也就是真正的跳转目标,这四条指令首先把目标的高16位放r12寄存器的高16bits中,然后目标地址(绝对地址)的低16位放入r12的低16位,从而组成一个32bits的目标绝对地址,然后将这个地址放入ctr寄存器,之后通过bctr跳转到目的地址。从这个复杂的指令也可以看出为什么powerpc大胆假设是在24bits范围之内而是用相对跳转,原始的一个指令在这里要转换为4条指令。
五、一点精确计算
那么我们可以精确计算一下这个跳转的范围,其中有26bits作为操作数,所以范围为64M范围之内,但是要考虑到前向跳转和后像跳转,所以减去一个符号位,总共可以前后在32M范围内。当起跳点和目标之间的距离大于这个区间的时候,就会提示连接错误,此时需要在连接器中添加--relax选项。由于crti是在链接器输入非常靠前的节,所以一个大工程的DEBUG版本是有可能超过这个值的。