天天看点

ELF函数重定位问题一、背景二、静态连接三、动态连接四、参考文献

一、背景

ld将.o连接为.so或者可执行程序,以及可执行程序使用.so时,都会遇到函数重定位的问题,本文对该问题进行分析。

二、静态连接

代码示例:

x.c:

#include <stdio.h>

void foo()
{
    printf("foo\n");
}
           

main.c:

extern void foo(void);

int main(void)
{
    foo();
    return 0;
}
           

Makefile:

all: main

main: main.o x.o
	$(CC) -m32 -o $@ $^

main.o: main.c
	$(CC) -m32 -c -o $@ $<

x.o: x.c
	$(CC) -m32 -c -o $@ $<

clean:
	rm -f main main.o x.o
           

调用make进行编译,得到x86 32bit版本的.o和可执行程序

objdump -d main.o得到:

00000000 <main>:
   0:	55                   	push   %ebp
   1:	89 e5                	mov    %esp,%ebp
   3:	83 e4 f0             	and    $0xfffffff0,%esp
   6:	e8 fc ff ff ff       	call   7 <main+0x7>
   b:	b8 00 00 00 00       	mov    $0x0,%eax
  10:	c9                   	leave
  11:	c3                   	ret
           

0xfffffffc是-4的补码, e8是相对跳转指令,e8 fc ff ff ff所跳转的位置,不和任何函数对应,是个假的位置。

这里应当填入的是foo函数的相对地址,但是我们在编译main.o时,foo是外部函数,无法得知foo的地址,所以使用了0xfffffffc这个假地址做代替,等连接时确定foo函数的地址后,再替换这个假地址。

objdump -d main得到:

08048404 <main>:
 8048404:	55                   	push   %ebp
 8048405:	89 e5                	mov    %esp,%ebp
 8048407:	83 e4 f0             	and    $0xfffffff0,%esp
 804840a:	e8 09 00 00 00       	call   8048418 <foo>
 804840f:	b8 00 00 00 00       	mov    $0x0,%eax
 8048414:	c9                   	leave
 8048415:	c3                   	ret
 8048416:	66 90                	xchg   %ax,%ax
           
08048418 <foo>:
 8048418:       55                      push   %ebp
 8048419:       89 e5                   mov    %esp,%ebp
 804841b:       83 ec 18                sub    $0x18,%esp
 804841e:       c7 04 24 00 85 04 08    movl   $0x8048500,(%esp)
 8048425:       e8 16 ff ff ff          call   8048340 <[email protected]>
 804842a:       c9                      leave
 804842b:       c3                      ret
 804842c:       8d 74 26 00             lea    0x0(%esi,%eiz,1),%esi
           

可以看到0xfffffffc这个假地址,已经被替换为0x00000009了。e8相对地址调用0x00000009,会call到0x804840f+0x00000009=0x8048418这个位置,也就是foo的地址。

那么,main.o连接为main时,到底发生了什么?

2.1 .rel.text .rel.data段

.o中有两个段:.rel.text .rel.data,用于连接时,分别处理函数和数据的重定位的问题。

这里只介绍函数的处理,数据的类似,不再赘述。

.rel.text对应的数据结构为:

typedef struct elf32_rel {
  Elf32_Addr	r_offset;
  Elf32_Word	r_info;
} Elf32_Rel;
           

r_offset,重定位入口的偏移,对于.o来说,是需要修正的位置的第一个字节相对于段起始的偏移;对于.so和可执行程序来说,是需要修正的位置的第一个字节的虚拟地址。

r_info,重定位入口的类型和符号,前三个字节是该入口的符号在符号表中的下标;后一个字节,表示重定位的类型,比如R_386_32、R_386_PC32。

readelf -r main.o得到:

Relocation section '.rel.text' at offset 0x3a0 contains 1 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00000007  00000902 R_386_PC32        00000000   foo
           

r_offset为0x00000007,我们处理的是.o,表示需要重定位的位置是0x00000007,和之前objdump -d main.o中得到的

一致,需要将0xfffffffc替换为foo的相对地址

r_info为0x00000902,0x000009表示该入口符号,在符号表中的下标为0x000009,readelf -s main.o | grep 9:

可以看到这个入口处理的是foo这个函数

0x02表示重定位的类型为R_386_PC32

2.2 指令修正

ld在将main.o,x.o连接为main时,可以获得foo的实际地址为0x08048418,然后根据.rel.text中的重定位信息,进行指令修正。

ld在处理到.rel.text中的foo时,根据r_info中的0x000009可以得知,需要处理foo这个符号,foo的实际地址为0x08048418。

根据r_info中的0x02可以得知,处理方式为R_386_PC32,R_386_PC32表示相对寻址修正S+A-P。其中

A = 保存在被修正位置的值。

被修正位置为0x00000007,这个位置的值是0xfffffffc,所以A为0xfffffffc,即A为-4。

P = 被修正的位置,(相对于段开始的位置或者虚拟地址),可以通过r_offset计算得到。

r_offset为0x00000007,当连接为可执行程序时,应该用被修正位置的虚拟地址,也就是0x0804840b(objdump -d main看到被修正位置的虚拟地址为0x0804840a + 1),所以P为0x0804840b。

S = 符号的实际地址,通过r_info中前三个字节计算得到。

r_info前三个字节为0x000009,在readelf -s main.o可以查到是foo这个符号,其实际地址为0x08048418,S为0x08048418。

S+A-P = 0x08048418 + (-4) - 0x0804840b = 0x00000009,这个就是修正后的值,用它来覆盖0x0804840b这个位置,得到

PS:

连接完成后,readelf -s main可以看到,没有了.rel.text .rel.data这两个段,说明这两个段是在连接时使用的,连接后就没有用处了。

但是多了.rel.dyn .rel.plt段,下一节进行详细介绍。

三、动态连接

上一节所说的重定位,会对二进制指令进行修改。如果我们想在多个进程中共享某一段代码的话,每一个进程中的这段代码,都需要进行重定位。

由于各个进程重定位后,函数地址的结果不同,所以每一个进程都会拷贝一份这段代码(copy on write),然后进行修改,达不到共享的目的。

有没有办法让所有进行使用同一段共享代码,不需要进行copy on write呢?

当然可以,只需要这段代码是地址无关的即可(PIC,Position-independent Code)。

我们在编译.so时,一般会添加参数-fPIC,就是为了产生地址无关的代码,用于多个进程中共享。

当然,不加-fPIC,也可以编译.so,只是代码无法共享,每一个进程都会有这个.so的私有拷贝。

怎么产生地址无关代码呢?根据函数和数据,模块内调用和模块间调用,分为四种情况:

1、模块内调用或跳转

由于调用者和被调用者处于同一模块,其相对位置是固定的,所以使用相对地址调用指令即可,能够保证代码是地址无关的,比较简单,无需赘述。

2、模块内部数据访问,本文不讨论数据重定位的问题。

3、模块间数据访问,本文不讨论数据重定位的问题。

4、模块间跳转、调用

这个比较复杂,下面将详细介绍。

先修改上一节中的Makefile:

all: main

main: main.o libx.so
	$(CC) -m32 -L. -Wl,-rpath=. -o $@ $< -lx

main.o: main.c
	$(CC) -m32 -c -o $@ $<

libx.so: x.o
	$(CC) -m32 -shared -fPIC -o $@ $<

x.o: x.c
	$(CC) -m32 -fPIC -c -o $@ $<

clean:
	rm -f main libx.so main.o x.o
           

make clean && make后,得到x86 32bit的.o .so和可执行程序

objdump -d main得到:

080484a4 <main>:
 80484a4:	55                   	push   %ebp
 80484a5:	89 e5                	mov    %esp,%ebp
 80484a7:	83 e4 f0             	and    $0xfffffff0,%esp
 80484aa:	e8 31 ff ff ff       	call   80483e0 <[email protected]>
 80484af:	b8 00 00 00 00       	mov    $0x0,%eax
 80484b4:	c9                   	leave
 80484b5:	c3                   	ret
 80484b6:	8d 76 00             	lea    0x0(%esi),%esi
 80484b9:	8d bc 27 00 00 00 00 	lea    0x0(%edi,%eiz,1),%edi
           
080483e0 <[email protected]>:
 80483e0:	ff 25 08 a0 04 08    	jmp    *0x804a008
 80483e6:	68 10 00 00 00       	push   $0x10
 80483eb:	e9 c0 ff ff ff       	jmp    80483b0 <_init+0x34>
           

调用foo函数时,跳转到的是[email protected]这个位置,这个[email protected]是啥?下面进行介绍。

3.1 .rel.plt .rel.dyn段

.rel.plt .rel.dyn和.rel.text .rel.data比较类似,分别用于处理动态连接的.so/可执行程序中的函数和数据的重定位问题,这里只介绍.rel.plt。

readelf -r main得到:

Relocation section '.rel.plt' at offset 0x364 contains 3 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
0804a000  00000107 R_386_JUMP_SLOT   00000000   __libc_start_main
0804a004  00000207 R_386_JUMP_SLOT   00000000   __gmon_start__
0804a008  00000407 R_386_JUMP_SLOT   00000000   foo
           

只有R_386_JUMP_SLOT是新鲜的东西,其他都是和.rel.text一样的。

R_386_JUMP_SLOT是另外一种修正方式,修正结果为S,比R_386_PC32要简单多了,用这个修正结果去覆盖被修正的位置即可。

还有其他的Type,可以看IA: Relocation Types中的介绍。

在实际使用可执行程序和动态库时,有很多函数是在出异常,或者很少情况下才会调用的,如果我们在ld-linux.so载入可执行程序和动态库时,处理完所有的重定位信息,会有很多无用的工作。

为了避免这些无用的工作,引入了.plt这种方式,可以在真正需要调用函数时,去处理函数的重定位问题。

3.2 .plt .got .got.plt段

main中调用foo时,实际上会调用[email protected],那么这是什么呢?

objdump -d main可以看到:

080483e0 <[email protected]>:
 80483e0:       ff 25 08 a0 04 08       jmp    *0x804a008
 80483e6:       68 10 00 00 00          push   $0x10
 80483eb:       e9 c0 ff ff ff          jmp    80483b0 <_init+0x34>
           

其实[email protected]就是一个壳,先试图跳转到*0x804a008中。*0x804a008是啥呢?readelf -S main可以看到0x804a008位于.got.plt段中:

[23] .got.plt          PROGBITS        08049ff4 000ff4 000018 00  WA  0   0  4
           

0x804a008 - (0x8049ff4 - 0x000ff4) = 0x1008,说明0x804a008在文件中的位置为0x1008,hexdump -C main | grep 1000,可以看到,*0x804a008就是0x080483e6:

00001000  c6 83 04 08 d6 83 04 08  e6 83 04 08 00 00 00 00  |................|
           

0x080483e6刚好是[email protected]中的下一条指令,也就是说,第一次执行[email protected]时,jmp    *0x804a008其实啥也没干,就是跳转到下一条指令了。那么下一条指令是干什么的呢?

push   $0x10,其中0x10为foo在.rel.plt段中的偏移量(每一项8字节,foo为第三项)。

jmp    80483b0调用的是_dl_runtime_resolve()函数,这个函数去解析符号的地址,需要一个参数,也就是之前push的0x10。

_dl_runtime_resolve根据0x10在.rel.plt中获取相关信息,得知需要重定位的是foo这个函数,重定位方式是R_386_JUMP_SLOT,重定位的结果覆盖0x0804a008这个位置,也就是说会把foo的地址放到0x0804a008中。

那么当下一次再调用[email protected]时,jmp    *0x804a008就会直接跳转到的foo函数了,不会再跳回来重新解析一遍。

PS:如果调用的是可执行程序模块内部的函数,那么会使用相对地址进行调用,不会涉及到got plt的问题。

.plt的前三项比较特殊,分别是Address of .dynamic,Module ID "Lib.so",_dl_runtime_resolve(),然后才是各种[email protected], [email protected], [email protected]等。

.got用于处理数据重定位的,本文不讨论。

.got.plt的前三项也比较特殊,也是Address of .dynamic,Module ID "Lib.so",_dl_runtime_resolve(),之后的每一项都是foo, bar, printf函数的地址(初始时,是跳回[email protected]并解析符号。函数执行一次后,经过_dl_runtime_resolve的解析,这里保存的才是正确的函数地址)。

综上所述,地址相关的部分被隔离在.got .got.plt中。

3.3 动态库

上面讨论的是可执行程序中的重定位。

.so中,情况类似,区别是不仅模块间的调用,而且模块内的调用,也会通过got plt进行。

修改上一节的x.c:

#include <stdio.h>

void foo()
{
    printf("foo\n");
}

void bar()
{
    foo();
}
           

修改上一节的main.c:

extern void foo(void);
extern void bar(void);

int main(void)
{
    foo();
    bar();
    return 0;
}
           

make clean && make后,得到x86 32bit的.o .so和可执行程序

objdump -d libx.so得到:

000004cc <foo>:
 4cc:	55                   	push   %ebp
......

000004f2 <bar>:
 4f2:	55                   	push   %ebp
 4f3:	89 e5                	mov    %esp,%ebp
 4f5:	53                   	push   %ebx
 4f6:	83 ec 04             	sub    $0x4,%esp
 4f9:	e8 c9 ff ff ff       	call   4c7 <__i686.get_pc_thunk.bx>
 4fe:	81 c3 f6 1a 00 00    	add    $0x1af6,%ebx
 504:	e8 f7 fe ff ff       	call   400 <[email protected]>
 509:	83 c4 04             	add    $0x4,%esp
 50c:	5b                   	pop    %ebx
 50d:	5d                   	pop    %ebp
 50e:	c3                   	ret
 50f:	90                   	nop
           

readelf -r libx.so得到:

0000200c  00000507 R_386_JUMP_SLOT   000004cc   foo
           

可以看到,bar调用foo时,即使他们定义在同一个模块中,但还是使用了plt got的方式去访问了。会在载入时,进行重定位。

3.4 函数符号重复定义

如果多个.so中重复定义并导出了foo这个函数,但是.got.plt中foo的地址只能保存一个,会发生什么呢?

y.c:

#include <stdio.h>

void foo()
{
    printf("foo in another .so\n");
}
           

gcc -m32 -shared -fPIC -o liby.so y.c编译得到liby.so

先连接libx.so,再连接liby.so

gcc -m32 -L. -Wl,-rpath=. -o main main.o -lx -ly
./main
foo
foo
           

先连接liby.so,再连接libx.so

gcc -m32 -L. -Wl,-rpath=. -o main main.o -ly -lx
./main
foo in another .so
foo in another .so
           

只有最先被_dl_runtime_resolve解析的foo生效了,之后的foo都被忽略了。

而_dl_runtime_resolve解析foo时,又是按照-l的顺序来的。

3.5 visibility protected

如果libx.so中的bar,必须调用libx.so中的foo,不想被其他动态库覆盖,怎么操作呢?

可以使用__attribute__ ((visibility ("protected")))将x.c中foo函数的可见性声明为protected:

Protected visibility is like default visibility except that it
indicates that references within the defining module will
bind to the definition in that module.  That is, the declared
entity cannot be overridden by another module.
           

修改上面的x.c:

#include <stdio.h>

void __attribute__ ((visibility ("protected"))) foo()
{
    printf("foo\n");
}

void bar()
{
    foo();
}
           

make clean && make

先连接libx.so,再连接liby.so

gcc -m32 -L. -Wl,-rpath=. -o main main.o -lx -ly
./main
foo
foo
           

先连接liby.so,再连接libx.so

gcc -m32 -L. -Wl,-rpath=. -o main main.o -ly -lx
./main
foo in another .so
foo
           

可以看到,无论如何操作,libx.so中bar调用的foo,就是libx.so中定义的那个foo,而不是其他.so中的foo。

protected是如何实现的呢?

objdump -d libx.so可以看到bar是使用相对地址调用的foo函数,没有通过plt got,所以调用的一定是libx.so中的foo函数:

000004ac <foo>:
 4ac:   55                      push   %ebp
 4ad:   89 e5                   mov    %esp,%ebp
 4af:   83 ec 18                sub    $0x18,%esp
 4b2:   c7 04 24 22 05 00 00    movl   $0x522,(%esp)
 4b9:   e8 fc ff ff ff          call   4ba <foo+0xe>
 4be:   c9                      leave
 4bf:   c3                      ret

000004c0 <bar>:
 4c0:   55                      push   %ebp
 4c1:   89 e5                   mov    %esp,%ebp
 4c3:   83 ec 08                sub    $0x8,%esp
 4c6:   e8 e1 ff ff ff          call   4ac <foo>
 4cb:   c9                      leave
 4cc:   c3                      ret
 4cd:   8d 76 00                lea    0x0(%esi),%esi
           

3.6 protected的一个问题

这一小节比较复杂,如果没有遇到这个编译失败,可以先不看。

使用protected时,可能会遇到编译失败:

relocation R_386_GOTOFF against protected function `%s' can not be used when making a shared object
           

这个是ld.bfd拒绝连接.o中重定位方式为R_386_GOTOFF的protected的函数,由这个patch引入,patch引入的原因,是因为一个gcc的bug,bug的表现为.so中定义的protected的函数的地址,在.so中和可执行程序中,不相同。

下载gcc的bug附件中的代码,做一些修改:

x.c:

#include <stdio.h>

void
__attribute__ ((visibility ("protected")))
foo ()
{
  printf ("shared foo: %p\n", foo);
}

void (*foo_p) () = foo;


void *
bar (void)
{
  printf ("called from shared foo: %p\n", foo);
  (*foo_p) ();
  foo ();
  printf ("called from shared foo_p: %p\n", foo_p);
  return foo;
}
           

m.c:

#include <stdio.h>

extern void (*foo_p) ();
extern void foo ();
extern void* bar ();


int
main ()
{
  void *p;
  printf ("called from main foo_p: %p\n", foo_p);
  p = bar ();
  foo ();
  (*foo_p) ();
  printf ("called from main foo: %p\n", foo);
  printf ("got from main foo: %p\n", p);
  if (p != foo)
    printf ("Function pointer `foo' are't the same in DSO and main\n");
  return 0;
}
           

Makefile:

all: foo
	./foo

x.o: x.c Makefile
	$(CC) $(CFLAGS) -fPIC -m32 -g -c -o $@ $<

libx.so: x.o
	$(CC) $(CFLAGS) -fPIC -m32 -g -shared -o $@ $<

m.o: m.c Makefile
	$(CC) $(CFLAGS) -m32 -g -c -o $@ $<

foo: m.o libx.so
	$(CC) $(CFLAGS) -L. -Wl,-rpath=. -m32 -g -o $@ $< -lx

clean:
	rm -f x.o m.o libx.so foo
           

make clean && make进行编译

3.6.1 有protected函数的动态库

objdump -d libx.so可以看到:

000005bc <foo>:
 5bc:   55                      push   %ebp
......

000005ec <bar>:
 5ec:   55                      push   %ebp
 5ed:   89 e5                   mov    %esp,%ebp
 5ef:   53                      push   %ebx
 5f0:   83 ec 04                sub    $0x4,%esp
 5f3:   e8 bd ff ff ff          call   5b5 <__x86.get_pc_thunk.bx>
 5f8:   81 c3 fc 19 00 00       add    $0x19fc,%ebx
 5fe:   83 ec 08                sub    $0x8,%esp
 601:   8d 83 c8 e5 ff ff       lea    -0x1a38(%ebx),%eax
 607:   50                      push   %eax
 608:   8d 83 7d e6 ff ff       lea    -0x1983(%ebx),%eax
 60e:   50                      push   %eax
 60f:   e8 6c fe ff ff          call   480 <[email protected]>
 614:   83 c4 10                add    $0x10,%esp
 617:   8b 83 fc ff ff ff       mov    -0x4(%ebx),%eax
 61d:   8b 00                   mov    (%eax),%eax
 61f:   ff d0                   call   *%eax
 621:   e8 96 ff ff ff          call   5bc <foo>
......
           

libx.so中,由于foo是protectd的函数,所以bar调用foo时,是通过相对地址调用的,没有通过got plt,调用的是真实的foo函数(0x5bc+虚拟地址)。

再看一下libx.so中,如何获得foo函数的地址:

__x86.get_pc_thunk.bx是将下一条指令的地址放到ebx寄存器中,执行后ebx为0x5f8+虚拟地址。

add    $0x19fc,%ebx执行后,ebx为0x1ff4+虚拟地址。

printf打印foo函数的地址时,需要两个参数,通过push %eax压入堆栈,第一次push的是foo函数的地址,第二次push的是fmt字符串的地址。-0x1a38(%ebx)得到的就是foo函数的地址,ebx - 0x1a38 = 0x5bc+虚拟地址。

libx.so中,由于foo是protected函数,所以获得foo地址时,是通过相对地址获得的,没有通过got plt,获得的是真实的foo函数地址(0x5bc+虚拟地址)。

PS:

如果x.c中去掉protected的声明,那么libx.so中调用foo,以及获得foo地址时,都是通过got plt进行的。

3.6.2 没有使用-fPIC编译的可执行程序

objdump -d foo可以看到:

080484d0 <[email protected]>:
 80484d0:       ff 25 10 a0 04 08       jmp    *0x804a010
 80484d6:       68 20 00 00 00          push   $0x20
 80484db:       e9 a0 ff ff ff          jmp    8048480 <_init+0x24>

080485dc <main>:
......
 80485fb:       e8 b0 fe ff ff          call   80484b0 <[email protected]>
 8048600:       83 c4 10                add    $0x10,%esp
 8048603:       e8 b8 fe ff ff          call   80484c0 <[email protected]>
 8048608:       89 45 f4                mov    %eax,-0xc(%ebp)
 804860b:       e8 c0 fe ff ff          call   80484d0 <[email protected]>
 8048610:       a1 2c a0 04 08          mov    0x804a02c,%eax
 8048615:       ff d0                   call   *%eax
 8048617:       83 ec 08                sub    $0x8,%esp
 804861a:       68 d0 84 04 08          push   $0x80484d0
 804861f:       68 24 87 04 08          push   $0x8048724
 8048624:       e8 87 fe ff ff          call   80484b0 <[email protected]>
......
           

可执行程序中调用foo,以及获得foo地址时,都是在编译期间确定好的值,没有通过got plt获取。

readelf -s foo | grep foo可以看到:

10: 080484d0     0 FUNC    GLOBAL DEFAULT  UND foo
           

虽然foo是一个undefine的外部符号,但是其符号地址在编译后,已经确定下来了,为0x080484d0,其实就是[email protected]的地址。

所以在运行时,_dl_runtime_resolve解析到的也是[email protected]这个地址,从而libx.so中通过got plt方式获得的也是[email protected]的地址。

./foo的输出为:

called from main foo_p: 0x5557a5bc
called from shared foo: 0x5557a5bc
shared foo: 0x5557a5bc
shared foo: 0x5557a5bc
called from shared foo_p: 0x5557a5bc
shared foo: 0x5557a5bc
shared foo: 0x5557a5bc
called from main foo: 0x80484d0
got from main foo: 0x5557a5bc
Function pointer `foo' are't the same in DSO and main
           

foo和[email protected]执行的效果相同,但是函数地址不同。ld.bfd为了防止这种情况,添加了patch,拒绝连接。

3.6.3 使用-fPIC编译的可执行程序

一般来说,编译可执行程序时,不需要-fPIC这个参数,我们只是做一个测试。编译m.o时,添加-fPIC参数。make clean && make重新编译。

objdump -d foo可以看到:

080485dc <main>:
......
 80485ee:       e8 92 00 00 00          call   8048685 <__x86.get_pc_thunk.bx>
 80485f3:       81 c3 01 1a 00 00       add    $0x1a01,%ebx
......
 804860c:       e8 9f fe ff ff          call   80484b0 <[email protected]>
 8048611:       83 c4 10                add    $0x10,%esp
 8048614:       e8 a7 fe ff ff          call   80484c0 <[email protected]>
 8048619:       89 45 f4                mov    %eax,-0xc(%ebp)
 804861c:       e8 af fe ff ff          call   80484d0 <[email protected]>
 8048621:       8b 83 f8 ff ff ff       mov    -0x8(%ebx),%eax
 8048627:       8b 00                   mov    (%eax),%eax
 8048629:       ff d0                   call   *%eax
 804862b:       83 ec 08                sub    $0x8,%esp
 804862e:       8b 83 fc ff ff ff       mov    -0x4(%ebx),%eax
 8048634:       50                      push   %eax
 8048635:       8d 83 50 e7 ff ff       lea    -0x18b0(%ebx),%eax
 804863b:       50                      push   %eax
 804863c:       e8 6f fe ff ff          call   80484b0 <printf@plt>
......
           

可执行程序中,调用foo函数时,是通过[email protected]调用的,此处代码是在编译时就确定好的。

readelf -s foo | grep foo可以看到:

10: 00000000     0 FUNC    GLOBAL DEFAULT  UND foo
           

foo是一个UND的外部符号,其符号地址未确定,需要在运行时,通过_dl_runtime_resolve函数解析,最终获得的结果为foo函数的真实地址(0x5bc+虚拟地址),而不是[email protected]的地址。libx.so中通过got plt方式获得的也是foo的真实地址,不再是[email protected]的地址。

再看一下可执行程序中,如何获得foo函数的地址。

__x86.get_pc_thunk.bx是将下一条指令的地址放到ebx寄存器中,执行后ebx为0x80485f3。

add    $0x1a01,%ebx执行后,ebx为0x8049ff4。

printf打印foo函数的地址时,需要两个参数,通过push %eax压入堆栈,第一次push的是foo函数的地址,第二次push的是fmt字符串的地址。所以-0x4(%ebx)就是foo函数的地址。

-0x4(%ebx)里面是啥呢?0x8049ff4-4=0x8049ff0,readelf -S foo可以看到:

[20] .got              PROGBITS        08049fe8 000fe8 00000c 00  WA  0   0  4
           

hexdump -C foo | grep ff0可以看到:

00000ff0  00 00 00 00 f0 9e 04 08  00 00 00 00 00 00 00 00  |................|
           

0x8049ff0在.got段中,初始值为0,程序运行后,_dl_runtime_resolve会将解析到的foo函数的地址放在这个位置,最终存放的foo函数的真实地址,而不是[email protected]的地址。

./foo的输出为:

called from main foo_p: 0x5557a5bc
called from shared foo: 0x5557a5bc
shared foo: 0x5557a5bc
shared foo: 0x5557a5bc
called from shared foo_p: 0x5557a5bc
shared foo: 0x5557a5bc
shared foo: 0x5557a5bc
called from main foo: 0x5557a5bc
got from main foo: 0x5557a5bc
           

所以,对于relocation R_386_GOTOFF against protected function的编译失败,有如下workaround方法:

1、使用不带这个patch的旧版本的ld.bfd,或者使用ld.gold,保证libx.so可以编译通过;

2、编译可执行程序的.o时,添加-fPIC参数,解决可执行程序和.so中protected的函数地址不同的问题。

四、参考文献

1、《程序员的自我修养》

2、IA: Relocation Types:https://docs.oracle.com/cd/E19455-01/816-0559/chapter6-26/index.html

3、x86 Instruction Set Reference:http://x86.renejeschke.de

4、Properly handle protected function for ia32 and x86_64:https://sourceware.org/ml/binutils/2005-01/msg00401.html

5、protected function pointer and copy relocation don't work right:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=19520

继续阅读