一、背景
ld将.o连接为.so或者可执行程序,以及可执行程序使用.so时,都会遇到函数重定位的问题,本文对该问题进行分析。
二、静态连接
代码示例:
x.c:
#include
void foo()
{
printf("foo
");
}
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 :
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
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 :
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
804840f: b8 00 00 00 00 mov $0x0,%eax
8048414: c9 leave
8048415: c3 ret
8048416: 66 90 xchg %ax,%ax
08048418 :
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
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中得到的
6: e8 fc ff ff ff
一致,需要将0xfffffffc替换为foo的相对地址
r_info为0x00000902,0x000009表示该入口符号,在符号表中的下标为0x000009,readelf -s main.o | grep 9:
9: 00000000 0 NOTYPE GLOBAL DEFAULT UND foo
可以看到这个入口处理的是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这个位置,得到
804840a: e8 09 00 00 00 call 8048418
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 :
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 <foo@plt>
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 :
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函数时,跳转到的是foo@plt这个位置,这个foo@plt是啥?下面进行介绍。
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时,实际上会调用foo@plt,那么这是什么呢?
objdump -d main可以看到:
080483e0 :
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@plt就是一个壳,先试图跳转到*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刚好是foo@plt中的下一条指令,也就是说,第一次执行foot@plt时,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中。
那么当下一次再调用foo@plt时,jmp *0x804a008就会直接跳转到的foo函数了,不会再跳回来重新解析一遍。
PS:如果调用的是可执行程序模块内部的函数,那么会使用相对地址进行调用,不会涉及到got plt的问题。
.plt的前三项比较特殊,分别是Address of .dynamic,Module ID "Lib.so",_dl_runtime_resolve(),然后才是各种foo@plt, bar@plt, printf@plt等。
.got用于处理数据重定位的,本文不讨论。
.got.plt的前三项也比较特殊,也是Address of .dynamic,Module ID "Lib.so",_dl_runtime_resolve(),之后的每一项都是foo, bar, printf函数的地址(初始时,是跳回xxx@plt并解析符号。函数执行一次后,经过_dl_runtime_resolve的解析,这里保存的才是正确的函数地址)。
综上所述,地址相关的部分被隔离在.got .got.plt中。
3.3 动态库
上面讨论的是可执行程序中的重定位。
.so中,情况类似,区别是不仅模块间的调用,而且模块内的调用,也会通过got plt进行。
修改上一节的x.c:
#include
void foo()
{
printf("foo
");
}
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 :
4cc: 55 push %ebp
......
000004f2 :
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 <foo@plt>
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
void foo()
{
printf("foo in another .so
");
}
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
void __attribute__ ((visibility ("protected"))) foo()
{
printf("foo
");
}
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 :
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
4be: c9 leave
4bf: c3 ret
000004c0 :
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
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
void
__attribute__ ((visibility ("protected")))
foo ()
{
printf ("shared foo: %p
", foo);
}
void (*foo_p) () = foo;
void *
bar (void)
{
printf ("called from shared foo: %p
", foo);
(*foo_p) ();
foo ();
printf ("called from shared foo_p: %p
", foo_p);
return foo;
}
m.c:
#include
extern void (*foo_p) ();
extern void foo ();
extern void* bar ();
int
main ()
{
void *p;
printf ("called from main foo_p: %p
", foo_p);
p = bar ();
foo ();
(*foo_p) ();
printf ("called from main foo: %p
", foo);
printf ("got from main foo: %p
", p);
if (p != foo)
printf ("Function pointer `foo' are't the same in DSO and main
");
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 :
5bc: 55 push %ebp
......
000005ec :
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
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
......
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 :
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 :
......
80485fb: e8 b0 fe ff ff call 80484b0
8048600: 83 c4 10 add $0x10,%esp
8048603: e8 b8 fe ff ff call 80484c0
8048608: 89 45 f4 mov %eax,-0xc(%ebp)
804860b: e8 c0 fe ff ff call 80484d0
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
......
可执行程序中调用foo,以及获得foo地址时,都是在编译期间确定好的值,没有通过got plt获取。
readelf -s foo | grep foo可以看到:
10: 080484d0 0 FUNC GLOBAL DEFAULT UND foo
虽然foo是一个undefine的外部符号,但是其符号地址在编译后,已经确定下来了,为0x080484d0,其实就是foo@plt的地址。
所以在运行时,_dl_runtime_resolve解析到的也是foo@plt这个地址,从而libx.so中通过got plt方式获得的也是foo@plt的地址。
./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和foo@plt执行的效果相同,但是函数地址不同。ld.bfd为了防止这种情况,添加了patch,拒绝连接。
3.6.3 使用-fPIC编译的可执行程序
一般来说,编译可执行程序时,不需要-fPIC这个参数,我们只是做一个测试。编译m.o时,添加-fPIC参数。make clean && make重新编译。
objdump -d foo可以看到:
080485dc :
......
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
8048611: 83 c4 10 add $0x10,%esp
8048614: e8 a7 fe ff ff call 80484c0
8048619: 89 45 f4 mov %eax,-0xc(%ebp)
804861c: e8 af fe ff ff call 80484d0
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函数时,是通过foo@plt调用的,此处代码是在编译时就确定好的。
readelf -s foo | grep foo可以看到:
10: 00000000 0 FUNC GLOBAL DEFAULT UND foo
foo是一个UND的外部符号,其符号地址未确定,需要在运行时,通过_dl_runtime_resolve函数解析,最终获得的结果为foo函数的真实地址(0x5bc+虚拟地址),而不是foo@plt的地址。libx.so中通过got plt方式获得的也是foo的真实地址,不再是foo@plt的地址。
再看一下可执行程序中,如何获得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函数的真实地址,而不是foo@plt的地址。
./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