- GOT的功用
以gcc 內建的libc.so為例,因為你不可能用到libc.so內的所有函式,所以其實不用知道所有函式在記憶體內的絕對位置。其中GOT只列出你會用到的 fuction 或是 global variable的絕對位置。這樣會節省許多解析時間。
以下面的圖為例,圖裡面是一個簡化的例子,這和實際編譯情況不同,但適合說明GOT。
當我要從main()內去呼叫shared library中的 foo(),編譯器會先產生binary檔案,在這裡檔名我設定為 a.out,原先 main.c中的 foo()被替換為 "b @GOT+0x14",功用是會跳到GOT內所記錄的位置去,地址就是GOT表格開始地址加上0x14。內容就是 0x76fc6578,和這個地址也就是 foo()在 shared library 的絕對位置。
- PLT的功用
既然GOT已經列出需要的東西,那照理說工作就結束了,還需要PLT幹麻?
試想,當你的程式也大到跟 libc.so 一樣大時,你可能會呼叫上百個libc的函式,所以當你的程式載入記憶體時,linker 會解析你需要的函式,這也許會花上一些時間,並導致使用者認為反應很慢。為了解決這個問題,所以GCC 改為呼叫shared library的函式前,才去把絕對位置填入GOT內。而PLT的功用就是呼叫 linker去填入 GOT,這個機制就是延遲解析 (lazy binding)。
要注意 lazy binding和 lazy loading的差異。Lazy loading 是透過 dlopen()等函式將library動態載入記憶體內。GCC並沒有自動提供lazy loading的機制,所以的shared library都是一次載入到記憶體內,除非你使用dlopen()。
用下面的幾張圖來解釋:
Step 1: 呼叫 Linker
在解釋動作前,先看一下 GOT表格,其中 GOT+0x14的內容暫時填入 linker 的位置,這需要 linker 去解析然後回填到GOT+0x14。原先main()要呼叫的 foo()被替換成 "foo()@plt" 的函式,而這個函式又會轉跳到 GOT+0x14的地址去。請仔細看,這個地址是要跳去 linker,而非foo(),因為這時候 foo()的地址還沒有被解析。
Step 2: 解析 foo() 的地址
Linker "ld-2.so"會把 foo()在 shared library的絕對位址填入 GOT+0x14的記憶體內。請注意,ld代表的意思是 Linker/Loader。
Step 3: 轉跳到 foo()
接著 Linker 會轉跳到 foo(),大功告成。
- 例子 — Overview
例子是參考書本 <<程式設計師的自我修養 — 連結.載入.程式庫>> 中第 7.3.3節的例子。
範例可以從這邊下載
例子雖然很簡單,但是目的卻很有趣,一共有4個:
Type 1: Inner-module call
Type 2: Inner-module data access
Type 3: Inter-module call
Type 4: Inter-module data access
再檢視這四個目的前,先編譯並反組譯這些檔案 (實驗的環境是 arm cortex-a7 32bits、gcc 4.6.3)。首先產生 Lib_a.o 和 Lib_b.o
$ gcc -g -shared -fPIC Lib_a.c -o Lib_a.o
$ gcc -g -shared -fPIC Lib_b.c -o Lib_b.o
接著產生執行檔
$ gcc -g main.c ./Lib_a.o ./Lib_b.o
然後反組譯 Lib_a.o、Lib_b.o、a.out
$ objdump -sSdD a.out > objdump.txt
$ objdump -sSdD Lib_a.o > objdump.txt-Lib_a
$ objdump -sSdD Lib_b.o > objdump.txt-Lib_b
做完前置作業後,先看function call相關的 type 1 和 type 3的流程,也就是 inner-module call和 inter-module call。在開始檢視前,照直觀的想法,inter-module call一定會用到 GOT,而inner-module call因為不需要轉跳,所以應該不需要用到GOT。我們可以使用 "readelf -r" 這個指令去看看 relocation section。這個section的功用就是標示 GOT每個欄位的定義。
先看 main.c的GOT
$ readelf -r a.out
Relocation section '.rel.dyn' at offset 0x41c contains 1 entries:
Offset Info Type Sym.Value Sym. Name
00010708 00000115 R_ARM_GLOB_DAT 00000000 __gmon_start__
Relocation section '.rel.plt' at offset 0x424 contains 4 entries:
Offset Info Type Sym.Value Sym. Name
000106f8 00000d16 R_ARM_JUMP_SLOT 00000000 __libc_start_main
000106fc 00000116 R_ARM_JUMP_SLOT 00000000 __gmon_start__
00010700 00000516 R_ARM_JUMP_SLOT 00000000 foo
00010704 00000916 R_ARM_JUMP_SLOT 00000000 abort
其中的"Relocation section '.rel.dyn'" 表示的是資料欄位,而"Relocation section '.rel.plt'"表示的則是function欄位。這也可以從"R_ARM_GLOB_DAT" 和 "R_ARM_JUMP_SLOT" 看出來。
進一步去看各個symbol,
- __gmon_start__ : 查看效能用的,如果編譯時加上 -pg選項,這個symbol就會有作用,譬如 "gcc -pg main.c" (解說)
- __libc_start_main : 這是c程式啟動前一定會跑的程式,為的是載入需要的library。(解說)
- foo: 這是 Lib_a.o的程式
- abort: 這是 C90標準定義的預設function (參考來源)
雖然有許多沒看過的function,但是 foo()還是預期般的出現。
接著看 Lib_a.o的relocation section
$ readelf -r Lib_a.o
Relocation section '.rel.dyn' at offset 0x3bc contains 7 entries:
Offset Info Type Sym.Value Sym. Name
00008598 00000017 R_ARM_RELATIVE
0000859c 00000017 R_ARM_RELATIVE
000086b8 00000017 R_ARM_RELATIVE
000086a8 00000315 R_ARM_GLOB_DAT 00000000 __cxa_finalize
000086ac 00000415 R_ARM_GLOB_DAT 00000000 b
000086b0 00000515 R_ARM_GLOB_DAT 00000000 __gmon_start__
000086b4 00000715 R_ARM_GLOB_DAT 00000000 _Jv_RegisterClasses
Relocation section '.rel.plt' at offset 0x3f4 contains 4 entries:
Offset Info Type Sym.Value Sym. Name
00008698 00000316 R_ARM_JUMP_SLOT 00000000 __cxa_finalize
0000869c 00000a16 R_ARM_JUMP_SLOT 00000530 bar
000086a0 00000516 R_ARM_JUMP_SLOT 00000000 __gmon_start__
000086a4 00000616 R_ARM_JUMP_SLOT 00000000 ext
- __cxa_finalize : 當shared library unload時,會呼叫他。(參考資料)
- b : 這是Lib_b.o內的全域變數
- _Jv_RegisterClasses: 為了使Java能呼叫c library的stub function。記住,gcc內部有java相關的tool (參考資料)
- bar : 這是Lib_a.o內的function
- ext : 這是Lib_b.o內的function
有趣的事情可以看出來,即便 bar() 在 Lib_a.o內,也需要GOT,和之前的猜想不一樣,所以"Type 2: Inner-module data access"是需要GOT。另外,變數 "static int a" 並沒有在GOT內,非常合理。
繼續看最後一個 shared library "Lib_b.o"的relocation section:
$ readelf -r Lib_b.o
Relocation section '.rel.dyn' at offset 0x3a4 contains 7 entries:
Offset Info Type Sym.Value Sym. Name
00008594 00000017 R_ARM_RELATIVE
00008598 00000017 R_ARM_RELATIVE
000086b0 00000017 R_ARM_RELATIVE
000086a0 00000315 R_ARM_GLOB_DAT 00000000 __cxa_finalize
000086a4 00000e15 R_ARM_GLOB_DAT 000086b8 b
000086a8 00000515 R_ARM_GLOB_DAT 00000000 __gmon_start__
000086ac 00000615 R_ARM_GLOB_DAT 00000000 _Jv_RegisterClasses
Relocation section '.rel.plt' at offset 0x3dc contains 3 entries:
Offset Info Type Sym.Value Sym. Name
00008694 00000316 R_ARM_JUMP_SLOT 00000000 __cxa_finalize
00008698 00000416 R_ARM_JUMP_SLOT 00000000 printf
0000869c 00000516 R_ARM_JUMP_SLOT 00000000 __gmon_start__
裡面只有兩個有興趣的symbol
- b : Lib_b.o本身的全域變數
- printf : libc提供的function
即便 int b就在Lib_b.o內,也需要GOT來存取。
- 例子 — Trace Code ( Main.c )
實際Trace Code來看看 GOT + PLT的用途
先看 main.c的反組譯結果
513 00008540 <main>:
514 #include <stdio.h>
515 #include "Lib_a.h"
516
517 int main(int argc, char* argv[])
518 {
519 8540: e92d4800 push {fp, lr}
520 8544: e28db004 add fp, sp, #4
521 8548: e24dd008 sub sp, sp, #8
522 854c: e50b0008 str r0, [fp, #-8]
523 8550: e50b100c str r1, [fp, #-12]
524 foo();
525 8554: ebffffc8 bl 847c <foo@plt>
526 }
527 8558: e1a00003 mov r0, r3
528 855c: e24bd004 sub sp, fp, #4
529 8560: e8bd8800 pop {fp, pc}
Line 525可以看到為了呼叫foo()直接跳到0x847c的位置,但是註解寫的function名稱是foo@plt,有點奇怪。不過直接去看0x847c
450 0000847c <foo@plt>:
451 847c: e28fc600 add ip, pc, #0, 12
452 8480: e28cca08 add ip, ip, #8, 20 ; 0x8000
453 8484: e5bcf27c ldr pc, [ip, #636]! ; 0x27c
這個arm的程式碼有點煩,不過一行行解讀就行了,
Line 451: add ip, pc, #0, 12
其中pc指的是下兩行指令的位址,也就是 Line 453標注的位置 0x8484。整個指令的作用為 "ip = pc + 0x0 << 12",所以 ip = 0x8484 + 0x0 = 0x8484。
接著往下一行看
Line 452: add ip, ip, #8, 20
指令等校於 "ip = ip + 0x8 << 20",因為我使用的機器是 32bit arm cortex-a7,所以向右做circular bit shift等於是向右位移 (32-20 = 12) bit,所以指令變為 "ip = ip + 0x8 << 12 = ip + 0x8000 = 0x8484 + 0x8000 = 0x10484"
再往下一行看
Line 453: ldr pc, [ip, #636]!
pc = [ip + d'636] = [0x10484 + d'636] =[0x10700]
看一下0x10700內存的值事什麼:
126 Contents of section .got:
127 106ec ec050100 00000000 00000000 50840000 ............P...
128 106fc 50840000 50840000 50840000 00000000 P...P...P.......
所以 [0x10700]是0x8450,注意這是little endian的排列方式。所以pc會載入0x8450嗎??
記得這只是反組譯的內容,而非 linker載入程式後的結果,有可能linker會去修改GOT內的值,保險起見,還是透過 gdb去看看這個值。
$ gdb ./a.out
(gdb) list
1 #include <stdio.h>
2 #include "Lib_a.h"
3
4 int main(int argc, char* argv[])
5 {
6 foo();
7 }
反組譯 main()
(gdb) disassemble main
Dump of assembler code for function main:
0x00008540 <+0>: push {r11, lr}
0x00008544 <+4>: add r11, sp, #4
0x00008548 <+8>: sub sp, sp, #8
0x0000854c <+12>: str r0, [r11, #-8]
0x00008550 <+16>: str r1, [r11, #-12]
0x00008554 <+20>: bl 0x847c
0x00008558 <+24>: mov r0, r3
0x0000855c <+28>: sub sp, r11, #4
0x00008560 <+32>: pop {r11, pc}
End of assembler dump.
查看要執行的程式碼
(gdb) x/10wi 0x847c
0x847c: add r12, pc, #0, 12
0x8480: add r12, r12, #8, 20 ; 0x8000
0x8484: ldr pc, [r12, #636]! ; 0x27c
0x8488: add r12, pc, #0, 12
0x848c: add r12, r12, #8, 20 ; 0x8000
0x8490: ldr pc, [r12, #628]! ; 0x274
0x8494 <_start>: mov r11, #0
0x8498 <_start+4>: mov lr, #0
0x849c <_start+8>: pop {r1} ; (ldr r1, [sp], #4)
0x84a0 <_start+12>: mov r2, sp
(gdb) b *0x8484
Breakpoint 1 at 0x8484
開始 run
(gdb) r
Starting program: /home/pi/tmp/c_language/linkage_loader_library/ch7_dynamic_linkage/ch7.3.3-fPIC/a.out
Breakpoint 1, 0x00008484 in ?? ()
重新印出組與確認是否有 break在指定的位置
(gdb) x/10wi 0x847c
0x847c: add r12, pc, #0, 12
0x8480: add r12, r12, #8, 20 ; 0x8000
=> 0x8484: ldr pc, [r12, #636]! ; 0x27c
0x8488: add r12, pc, #0, 12
0x848c: add r12, r12, #8, 20 ; 0x8000
0x8490: ldr pc, [r12, #628]! ; 0x274
0x8494 <_start>: mov r11, #0
0x8498 <_start+4>: mov lr, #0
0x849c <_start+8>: pop {r1} ; (ldr r1, [sp], #4)
0x84a0 <_start+12>: mov r2, sp
列印出 GOT 內容
(gdb) x/8wx 0x106ec
0x106ec <_GLOBAL_OFFSET_TABLE_>: 0x000105ec 0x76fff958 0x76fedbe4 0x76e9470c
0x106fc <_GLOBAL_OFFSET_TABLE_+16>: 0x00008450 0x00008450 0x00008450 0x00000000
所以確認0x10700的內容還是 0x8450,回頭去看 0x8450在反組譯的內容是什麼
433 00008450 <__libc_start_main@plt-0x14>:
434 8450: e52de004 push {lr} ; (str lr, [sp, #-4]!)
435 8454: e59fe004 ldr lr, [pc, #4] ; 8460 <_init+0x1c>
436 8458: e08fe00e add lr, pc, lr
437 845c: e5bef008 ldr pc, [lr, #8]!
438 8460: 0000828c andeq r8, r0, ip, lsl #5
如果單步執行到 Line 437後可以發現會馬上轉跳到[0x106f4] = [GOT+0x8] = 0x76fedbe4,這就是Linker (ld-linux-armhf.so.3)。因為GCC Linker相關的code很多,還沒有能力去看,但可以看看foo()執行後的GOT的內容。
(gdb) list main
1 #include <stdio.h>
2 #include "Lib_a.h"
3
4 int main(int argc, char* argv[])
5 {
6 foo();
7 }
(gdb) b 7
Breakpoint 1 at 0x8558: file main.c, line 7.
(gdb) r
Starting program: /home/pi/tmp/c_language/linkage_loader_library/ch7_dynamic_linkage/ch7.3.3-fPIC/a.out
Calling from Lib_b.c: ext
Breakpoint 1, main (argc=1, argv=0x7efff794) at main.c:7
7 }
(gdb) x/8wx 0x106ec
0x106ec <_GLOBAL_OFFSET_TABLE_>: 0x000105ec 0x76fff958 0x76fedbe4 0x76e9470c
0x106fc <_GLOBAL_OFFSET_TABLE_+16>: 0x00008450 0x76fc6578 0x00008450 0x00000000
可以發現0x10700的內容從0x8450變成0x76fc6578,先看看這個記憶體附近的內容
(gdb) x/8wi 0x76fc6578
0x76fc6578 <foo>: push {r11, lr}
0x76fc657c <foo+4>: add r11, sp, #4
0x76fc6580 <foo+8>: bl 0x76fc6440
0x76fc6584 <foo+12>: bl 0x76fc6458
0x76fc6588 <foo+16>: pop {r11, pc}
0x76fc658c <_fini>: push {r3, lr}
0x76fc6590 <_fini+4>: pop {r3, pc}
0x76fc6594 <__FRAME_END__>: andeq r0, r0, r0
看起來很像一段程式碼,試著去看看是否是一個symbol
(gdb) info symbol 0x76fc6578
foo in section .text of ./Lib_a.o
的確是 foo(),這樣就算解決"Type 3: Inter-module call"的追蹤。
- 例子 — Type 1: Inner-module call ( Lib_a.c )
先看 foo()的反組譯
551 void foo()
552 {
553 578: e92d4800 push {fp, lr}
554 57c: e28db004 add fp, sp, #4
555 bar();
556 580: ebffffae bl 440 <bar@plt>
557 ext();
558 584: ebffffb3 bl 458 <ext@plt>
559 }
560 588: e8bd8800 pop {fp, pc}
可以看出呼叫 inner-module call時,是透過PLT,轉跳到 0x440的位置。
馬上反組譯 bar@plt
446 00000440 <bar@plt>:
447 440: e28fc600 add ip, pc, #0, 12
448 444: e28cca08 add ip, ip, #8, 20 ; 0x8000
449 448: e5bcf254 ldr pc, [ip, #596]! ; 0x254
可以看出馬上轉跳到 [0x448 + 0x8000 + 0x254] 的位置,也就是 [0x869c] = [GOT+0x10]
先看反組譯.got的內容
121 Contents of section .got:
122 868c a4850000 00000000 00000000 20040000 ............ ...
123 869c 20040000 20040000 20040000 00000000 ... ... .......
124 86ac 00000000 00000000 00000000 ............
再比照程式跑起後.got的內容發現除了加上0x76fc6000的offset外,指到的function一樣是
(gdb) x/11wx 0x76fce68c
0x76fce68c: 0x000085a4 0x76ffa000 0x76fedbe4 0x76fc6420
0x76fce69c: 0x76fc6420 0x76fc6420 0x76fc6420 0x76eaf104
0x76fce6ac: 0x76fc56b8 0x00000000 0x00000000
反組譯看 0x420的位置
434 00000420 <__cxa_finalize@plt-0x14>:
435 420: e52de004 push {lr} ; (str lr, [sp, #-4]!)
436 424: e59fe004 ldr lr, [pc, #4] ; 430 <_init+0x1c>
437 428: e08fe00e add lr, pc, lr
438 42c: e5bef008 ldr pc, [lr, #8]!
439 430: 0000825c andeq r8, r0, ip, asr r2
程式會跳到 [0x825c + 0x430 + 0x8] 的位置,也就是 [0x8694] = [GOT+0x8],實際跑起時的地址就是0x76fedbe4 ,查看這個位置發現沒有找到symbol name !!
(gdb) info symbol 0x76fedbe4
No symbol matches 0x76fedbe4.
單步執行程式,直到pc載入這個地址,發現會跳到 linker
(gdb) si
Cannot access memory at address 0x0
0x76fedbe4 in ?? () from /lib/ld-linux-armhf.so.3
因為追不下去了,所以直接break在 bar()開始的位置,並且看看 .got的內容
0x76fce68c: 0x000085a4 0x76ffa000 0x76fedbe4 0x76fc6420
0x76fce69c: 0x76fc6530 0x76fc6420 0x76fc6420 0x76eaf104
0x76fce6ac: 0x76fc56b8 0x00000000 0x00000000
發覺到 [GOT+0x10] 的值變了,查看symbol可以發現,變成 bar()的地址
(gdb) info symbol 0x76fc6530
bar in section .text of ./Lib_a.o
這個流程和 main.c中呼叫 foo()的流程一樣。
- 例子 — Type 2: Inner-module data access ( Lib_a.c )
525 void bar(void)
526 {
......................................
531 a = 1;
532 540: e59f3028 ldr r3, [pc, #40] ; 570 <bar+0x40>
533 544: e08f3003 add r3, pc, r3
534 548: e3a01001 mov r1, #1
535 54c: e5831000 str r1, [r3]
......................................
541 }
542 560: e28bd000 add sp, fp, #0
543 564: e8bd0800 ldmfd sp!, {fp}
544 568: e12fff1e bx lr
545 56c: 00008148 andeq r8, r0, r8, asr #2
546 570: 00008174 andeq r8, r0, r4, ror r1
547 574: 00000020 andeq r0, r0, r0, lsr #32
可以看出來存取的記憶體是 0x8174 + 0x54c = 0x86c0,而這個地址在 .bss section內,所以不需要GOT
657 Disassembly of section .bss:
.....
662 000086c0 <a>:
663 86c0: 00000000 andeq r0, r0, r0
- 例子 — Type 4: Inter-module data access ( Lib_a.c )
526 {
527 530: e52db004 push {fp} ; (str fp, [sp, #-4]!)
528 534: e28db000 add fp, sp, #0
529 538: e59f202c ldr r2, [pc, #44] ; 56c <bar+0x3c>
530 53c: e08f2002 add r2, pc, r2
...........................................
536 b = 2;
537 550: e59f301c ldr r3, [pc, #28] ; 574 <bar+0x44>
538 554: e7923003 ldr r3, [r2, r3]
539 558: e3a02002 mov r2, #2
540 55c: e5832000 str r2, [r3]
541 }
542 560: e28bd000 add sp, fp, #0
543 564: e8bd0800 ldmfd sp!, {fp}
544 568: e12fff1e bx lr
545 56c: 00008148 andeq r8, r0, r8, asr #2
546 570: 00008174 andeq r8, r0, r4, ror r1
547 574: 00000020 andeq r0, r0, r0, lsr #32
在Line 530時,r2 = 0x8148 + 0x544 = 0x868c。接著可以看出來 b的地址在 [0x868c + 0x20] = [0x86ac] = [GOT+0x20]。而實際跑起來時,發現在這不像function一樣需要lazy binding,在main.c內的.text開始跑之前,就已經知道這個地址了。
可以break在 _start去查看。
(gdb) b *0x8494
Breakpoint 1 at 0x8494
(gdb) r
Starting program: /home/pi/tmp/c_language/linkage_loader_library/ch7_dynamic_linkage/ch7.3.3-fPIC/a.out
Breakpoint 1, 0x00008494 in _start ()
(gdb) x/11wx 0x76fce68c
0x76fce68c: 0x000085a4 0x76ffa000 0x76fedbe4 0x76fc6420
0x76fce69c: 0x76fc6420 0x76fc6420 0x76fc6420 0x76eaf104
0x76fce6ac: 0x76fc56b8 0x00000000 0x00000000
(gdb) info symbol 0x76fc56b8
b in section .bss of ./Lib_b.o
- 例子 — Type 3: Inter-module call ( Lib_a.c )
551 void foo()
552 {
553 578: e92d4800 push {fp, lr}
554 57c: e28db004 add fp, sp, #4
555 bar();
556 580: ebffffae bl 440 <bar@plt>
557 ext();
558 584: ebffffb3 bl 458 <ext@plt>
559 }
接著查看ext@plt
456 00000458 <ext@plt>:
457 458: e28fc600 add ip, pc, #0, 12
458 45c: e28cca08 add ip, ip, #8, 20 ; 0x8000
459 460: e5bcf244 ldr pc, [ip, #580]! ; 0x244
121 Contents of section .got:
122 868c a4850000 00000000 00000000 20040000 ............ ...
123 869c 20040000 20040000 20040000 00000000 ... ... .......
124 86ac 00000000 00000000 00000000 ............
很熟悉的 0x420位置,確認實際run起來是否是一樣
(gdb) x/11wx 0x76fce68c
0x76fce68c: 0x000085a4 0x76ffa000 0x76fedbe4 0x76fc6420
0x76fce69c: 0x76fc6530 0x76fc6420 0x76fc6420 0x76eaf104
0x76fce6ac: 0x76fc56b8 0x00000000 0x00000000
和呼叫 bar()時候是一樣的馬上看跳到ext()時,.got section是否一樣。
(gdb) b Lib_b.c:6
No source file named Lib_b.c.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (Lib_b.c:6) pending.
(gdb) r
Starting program: /home/pi/tmp/c_language/linkage_loader_library/ch7_dynamic_linkage/ch7.3.3-fPIC/a.out
Breakpoint 1, ext () at Lib_b.c:6
6 {
(gdb) x/11wx 0x76fce68c
0x76fce68c: 0x000085a4 0x76ffa000 0x76fedbe4 0x76fc6420
0x76fce69c: 0x76fc6530 0x76fc6420 0x76fbd504 0x76eaf104
0x76fce6ac: 0x76fc56b8 0x00000000 0x00000000
(gdb) info symbol 0x76fbd504
ext in section .text of ./Lib_b.o
看來等到叫起 linker後,就會把實際的值回填到GOT+0x18內。所以Type3: inter-module call 和 Type 1: inner-module call的實作方式一樣。
沒有留言:
張貼留言