iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 12
0
Software Development

從 Rust 往程式底層前進系列 第 22

位置無關程式碼與 GOT 和 PLT

前一篇提到了動態函式庫,而動態函式庫有個很重要的特性是它必須載入至其它程式的空間來執行,因為載入的位置並不是固定的 (不過為了實作 ALSR 實際上沒有程式的載入位置是固定的) ,所以動態函式庫都是使用位置無關程式碼 (PIC, position independent code) ,而為了要實作 PIC ,必須都以相對位置來呼叫函式,或是存取全域變數,相對於目前程式碼執行的位置,在之前 32 位元的系統下,為了要取得目前執行的位置,還必須呼叫一個函式,再讓那個函式把返回位置傳回來,因為那時並不支援直接存取程式執行到的位置 eip ,但在現在執行的 64 位元的程式就能直接存取 rip

如果看個簡單程式的反組譯結果,其中的 foo 是個全域變數

  printf("%d\n", foo);
 64e:   8b 05 bc 09 20 00       mov    eax,DWORD PTR [rip+0x2009bc]        # 201010 <foo>
 654:   89 c6                   mov    esi,eax
 656:   48 8d 3d 97 00 00 00    lea    rdi,[rip+0x97]        # 6f4 <_IO_stdin_used+0x4>
 65d:   b8 00 00 00 00          mov    eax,0x0
 662:   e8 b9 fe ff ff          call   520 <printf@plt>

可以看到上面的第一行就使用了 rip 來定址取得變數的值,透過這樣的方式使得程式中不會出現絕對位址,而能保證程式不管載入到哪個位置都能正常執行,只是這個方式僅限於程式自己內部的變數或是函式,如果是由動態函式庫所提供的變數或是函式呢?

GOT 與 PLT

為了能在不知道實際位址的情況下使用動態函式庫中的函式或變數,勢必要把重定址的資訊留在執行檔中,並在執行時才由動態連結器來做,不過這邊又有些不太一樣,一般程式在連結的過程中,如果碰到不知道位址的情況,都是把程式碼中的位置部份留空,再記錄需要修正的程式碼的位置,但如果在連結動態函式庫時,程式中大量的使用到了動態函式庫的內容,那不就會有一大堆的地方需要修正了嗎?為了解決這個問題,當初的人們又在這中間加入了一層中間層,那就是 GOT

之前在一本書上看到,「要解決程式上的問題只要加上一層中間層」,這句話原本是來自於 FTSE ,不過有趣的是那本書後面又補上了一句話,要解決社會的問題是減少一層中間層

GOT 是一個記錄記憶體位置的表格,只要讓程式中需要使用到外部的變數時都先到這個表格來取實際位置,然後之後再用這個位置來取實際的值的話,動態連結器需要修正的值就只剩下 GOT 中的內容了,而且這更能確保程式碼的部份可以維持不變,使得程式碼所在的分頁可以共用在不同的處理緒之間

這邊直接用個簡單的程式來看看:

extern "C" {
  // 來自動態函式庫的變數
  static FOO: i32;
}

// 這邊因為 println 展開後的內容會讓組語變亂,所以把它移到另一個函式去
fn print(n: i32) {
  println!("{}", n);
}

fn main() {
  unsafe {
    print(FOO);
  }
}

然後直接用 objdump 反組譯:

fn main() {
    4270:	50                   	push   rax
    4271:	48 8b 05 80 da 22 00 	mov    rax,QWORD PTR [rip+0x22da80]        # 231cf8 <FOO>
    unsafe {
        print(FOO);
    4278:	8b 38                	mov    edi,DWORD PTR [rax]
    427a:	e8 61 ff ff ff       	call   41e0 <demo::print>
    }
}
    427f:	58                   	pop    rax
    4280:	c3                   	ret

可以看到程式先從 rip + 0x22da80 的位置取得一個另一個位置,再透過這個位置取值給 edi 做為給 demo::print 的參數,那如果是呼叫動態函式庫的函式又會一樣嗎?一樣來個簡單的程式

extern "C" {
    pub fn foo();
}

fn main() {
    unsafe {
        foo();
    }
}

反組譯後

fn main() {
    3f50:	50                   	push   rax
    unsafe {
        foo();
    3f51:	ff 15 c1 be 22 00    	call   QWORD PTR [rip+0x22bec1]        # 22fe18 <foo>
    }
}
    3f57:	58                   	pop    rax
    3f58:	c3                   	ret

它直接用 got 簡接的呼叫到了目標函式,於是動態載入器只要能對這個表完成重定址,由程式到動態函式庫,或動態函式庫之類的呼叫與變數的取用都能正常運作了

不過標題還有提到一個叫 plt 的東西又是什麼,根據網路上的說明, plt 是要讓函式的位置被延遲到第一次呼叫時才做解析而使用的,而做解析的是一個叫 _dl_runtime_resolve 的函式,不過我在我自己的電腦上試著用 gdb 去追蹤程式碼,都沒看到 plt 中的程式有做延遲載入的動作,位置一樣都是從 got 取得的,另外我也試著從 ld.so 中找 _dl_runtime_resolve 這個函式也找不到,只是我在網路上實在沒有找到它可能被移除掉之類的資訊,這部份可能就自己上網看看了

LD_PRELOAD

這部份算是個下集預告,如果我們準備另一個 .so 檔,並且有個名字一樣是 foo 的函式,然後用像下面這樣的方法執行

$ LD_PRELOAD="./libbar.so" ./demo
Hello from libbar

我們可以蓋掉另一個動態函式庫的函式呢,下一篇再來聊這個吧

參考資料


上一篇
動態連結
下一篇
LD_PRELOAD hook 與 dlsym
系列文
從 Rust 往程式底層前進26
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言