天天看點

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

繼續閱讀