摘要:不同的進程,調用同樣的,各自里面指向最終加載的動態鏈接庫里面的虛擬內存地址是不同的。實際上,在進行程序開發,一直會用到各種各樣的動態鏈接庫。通過動態鏈接這個方式,可以說徹底解決了這個問題。參考深入淺出計算機組成原理
把對應的不同文件內的代碼段,合并到一起,成為最后的可執行文件
鏈接的方式,讓我們在寫代碼的時候做到了“復用”。
同樣的功能代碼只要寫一次,然后提供給很多不同的程序進行鏈接就行了。
“鏈接”其實有點兒像我們日常生活中的標準化、模塊化生產。
有一個可以生產標準螺帽的生產線,就可生產很多不同的螺帽。
只要需要螺帽,都可以通過鏈接的方式,去復制一個出來,放到需要的地方
但是,如果我們有很多個程序都要通過裝載器裝載到內存里面,那里面鏈接好的同樣的功能代碼,也都需要再裝載一遍,再占一遍內存空間。
這就好比,假設每個人都有騎自行車的需要,那我們給每個人都生產一輛自行車帶在身邊,固然大家都有自行車用了,但是馬路上肯定會特別擁擠。
1 鏈接可以分動、靜,共享運行省內存我們上一節解決程序裝載到內存的時候,講了很多方法。說起來,最根本的問題其實就是內存空間不夠用。
如果能夠讓同樣功能的代碼,在不同的程序里面,不需要各占一份內存空間,那該有多好?。?/p>
就好比,現在馬路上的共享單車,我們并不需要給每個人都造一輛自行車,只要馬路上有這些單車,誰需要的時候,直接通過手機掃碼,都可以解鎖騎行。
這個思路就引入一種新的鏈接方法,叫作動態鏈接(Dynamic Link)
相應的,我們之前說的合并代碼段的方法,就是靜態鏈接(Static Link)
在動態鏈接的過程中,我們想要“鏈接”的,不是存儲在硬盤上的目標文件代碼,而是加載到內存中的共享庫(Shared Libraries)
這個加載到內存中的共享庫會被很多個程序的指令調用到。
在Windows下,這些共享庫文件就是.dll文件,也就是Dynamic-Link Libary(DLL,動態鏈接庫)
用了“動態鏈接”的意思
在Linux下,這些共享庫文件就是.so文件,也就是Shared Object(一般我們也稱之為動態鏈接庫)。
用了“共享”的意思
正好覆蓋了兩方面的含義。
2 地址無關很重要,相對地址解煩惱要在程序運行的時候共享代碼,這些機器碼必須“地址無關”
也就是說,我們編譯出來的共享庫文件的指令代碼,是地址無關碼(Position-Independent Code)
換句話說就是,這段代碼,無論加載在哪個內存地址,都能夠正常執行
如果還不明白,我給你舉一個生活中的例子
如果我們有一個騎自行車的程序,要“前進500米,左轉進入天安門廣場,再前進500米”。
它在500米之后要到天安門廣場了,這就是地址相關的。
如果程序是“前進500米,左轉,再前進500米”,無論你在哪里都可以騎車走這1000米,沒有具體地點的限制,這就是地址無關的。
大部分函數庫其實都可以做到地址無關,因為它們都接受特定的輸入,進行確定的操作,然后給出返回結果就好了。
無論是實現一個向量加法,還是實現一個打印的函數,這些代碼邏輯和輸入的數據在內存里面的位置并不重要。
而常見的地址相關的代碼,比如絕對地址代碼(Absolute Code)、利用重定位表的代碼等等,都是地址相關的代碼
回想一下我們之前講過的重定位表。在程序鏈接的時候,我們就把函數調用后要跳轉訪問的地址確定下來了,這意味著,如果這個函數加載到一個不同的內存地址,跳轉就會失敗。
對于所有動態鏈接共享庫的程序來講,雖然我們的共享庫用的都是同一段物理內存地址,但是在不同的應用程序里,它所在的虛擬內存地址是不同的。
沒辦法、也不應該要求動態鏈接同一個共享庫的不同程序,必須把這個共享庫所使用的虛擬內存地址變成一致。
如果這樣的話,我們寫的程序就必須明確地知道內部的內存地址分配。
那么問題來了,我們要怎么樣才能做到,動態共享庫編譯出來的代碼指令,都是地址無關碼呢?
動態代碼庫內部的變量和函數調用都很容易解決,我們只需要使用相對地址(Relative Address)
各種指令中使用到的內存地址,給出的不是一個絕對的地址空間,而是一個相對于當前指令偏移量的內存地址
因為 整個共享庫是放在一段連續的虛擬內存地址中的,無論裝載到哪一段地址,不同指令之間的相對地址都是不變的。
3 動態鏈接的解決方案PLT和GOT
要實現動態鏈接共享庫,也并不困難,和前面的靜態鏈接里的符號表和重定向表類似
拿出一小段代碼來看一看。
lib.h
定義了動態鏈接庫的一個函數 show_me_the_money
lib.c
包含了lib.h的實際實現
show_me_poor.c
調用了 lib 里面的函數
把 lib.c 編譯成了一個動態鏈接庫,也就是 .so 文件
最終生成文件集
在編譯的過程中,指定了一個 -fPIC 的參數
其實就是Position Independent Code意,也就是要把這個編譯成一個地址無關代碼
然后,我們再通過gcc編譯 show_me_poor 動態鏈接了 lib.so 的可執行文件
在這些操作都完成了之后,我們把 show_me_poor 這個文件通過objdump出來看一下。
0000000000400540: 400540: ff 35 12 05 20 00 push QWORD PTR [rip+0x200512] # 600a58 <_GLOBAL_OFFSET_TABLE_+0x8> 400546: ff 25 14 05 20 00 jmp QWORD PTR [rip+0x200514] # 600a60 <_GLOBAL_OFFSET_TABLE_+0x10> 40054c: 0f 1f 40 00 nop DWORD PTR [rax+0x0] 0000000000400550 : 400550: ff 25 12 05 20 00 jmp QWORD PTR [rip+0x200512] # 600a68 <_GLOBAL_OFFSET_TABLE_+0x18> 400556: 68 00 00 00 00 push 0x0 40055b: e9 e0 ff ff ff jmp 400540 <_init+0x28> …… 0000000000400676 : 400676: 55 push rbp 400677: 48 89 e5 mov rbp,rsp 40067a: 48 83 ec 10 sub rsp,0x10 40067e: c7 45 fc 05 00 00 00 mov DWORD PTR [rbp-0x4],0x5 400685: 8b 45 fc mov eax,DWORD PTR [rbp-0x4] 400688: 89 c7 mov edi,eax 40068a: e8 c1 fe ff ff call 400550 40068f: c9 leave 400690: c3 ret 400691: 66 2e 0f 1f 84 00 00 nop WORD PTR cs:[rax+rax*1+0x0] 400698: 00 00 00 40069b: 0f 1f 44 00 00 nop DWORD PTR [rax+rax*1+0x0]
我們還是只關心整個可執行文件中的一小部分內容
在main函數調用show_me_the_money的函數的時候,對應的代碼是這樣的:
這里后面有一個@plt的關鍵字,代表了我們需要從PLT,也就是程序鏈接表(Procedure Link Table)里面找要調用的函數。對應的地址呢,則是400580這個地址。
那當我們把目光挪到上面的 400580 這個地址,你又會看到里面進行了一次跳轉,
這個跳轉指定的跳轉地址,你可以在后面的注釋里面可以看到:
這里的 _GLOBAL_OFFSET_TABLE_,就是我接下來要說的全局偏移表。
在動態鏈接對應的共享庫,我們在共享庫的data section里面,保存了一張全局偏移表(GOT,Global Offset Table)
雖然共享庫的代碼部分的物理內存是共享的,但是數據部分是各個動態鏈接它的應用程序里面各加載一份的。
所有需要引用當前共享庫外部的地址的指令,都會查詢GOT,來找到當前運行程序的虛擬內存里的對應位置
而GOT表里的數據,則是在我們加載一個個共享庫的時候寫進去的。
不同的進程,調用同樣的 _lib.so_,各自GOT里面指向最終加載的動態鏈接庫里面的虛擬內存地址是不同的。
這樣,雖然不同的程序調用的同樣的動態庫,各自的內存地址是獨立的,調用的又都是同一個動態庫,但是不需要去修改動態庫里面的代碼所使用的地址,
而是各個程序各自維護好自己的GOT,能夠找到對應的動態庫就好了
GOT表位于共享庫自己的數據段里
GOT表在內存里和對應的代碼段位置之間的偏移量,始終是確定的
這樣,共享庫就是地址無關的代碼,對應的各個程序只需在物理內存里加載同一份代碼
而我們又要通過各個可執行程序在加載時,生成的各不相同的GOT表,找到它需要調用到的外部變量和函數的地址
這是一個典型的、不修改代碼,而是通過修改“地址數據”來進行關聯的辦法
它有點像我們在C語言里面用函數指針來調用對應的函數,并不是通過預先已經確定好的函數名稱來調用,而是利用當時它在內存里面的動態地址來調用。4 總結
終于在靜態鏈接和程序裝載后,利用動態鏈接把我們的內存利用到了極致
同樣功能的代碼生成的共享庫,我們只要在內存里面保留一份就好了
這樣
不僅能夠做到代碼在開發階段的復用
也能做到代碼在運行階段的復用。
實際上,在進行Linux程序開發,一直會用到各種各樣的動態鏈接庫。
C語言的標準庫就在1MB以上。
撰寫任何一個程序可能都需要用到這個庫,常見的Linux服務器里,/usr/bin下面就有上千個可執行文件。
如果每一個都把標準庫靜態鏈接進來的,幾GB乃至幾十GB的磁盤空間一下子就用出去了。如果我們服務端的多進程應用要開上千個進程,幾GB的內存空間也會一下子就用出去了。這個問題在過去計算機的內存較少的時候更加顯著。
通過動態鏈接這個方式,可以說_徹底解決了這個問題_。
就像共享單車一樣,如果仔細經營,是一個很有社會價值的事情,但是如果粗暴地把它變成無限制地復制生產,給每個人造一輛,只會在系統內制造大量無用的垃圾。
已經把程序怎么從源代碼變成指令、數據,并裝載到內存里面,由CPU一條條執行下去的過程講完了。希望你能有所收獲,對于一個程序是怎么跑起來的,有了一個初步的認識。
5 推薦閱讀想要更加深入地了解動態鏈接,推薦你可以讀一讀《程序員的自我修養:鏈接、裝載和庫》的第7章
里面深入地講解了,動態鏈接里程序內的數據布局和對應數據的加載關系。
參考深入淺出計算機組成原理
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/76252.html
摘要:這個辦法,在現在計算機的內存管理里面,就叫作內存分頁和分段這樣分配一整段連續的空間給到程序相比分頁則是把整個物理內存空間切成一段段固定尺寸的大小而對應的程序所需要占用的虛擬內存空間,也會同樣切成一段段固定尺寸的大小。 showImg(https://image-static.segmentfault.com/290/765/2907653835-5d580caf245fd_articl...
摘要:鏈接器會掃描所有輸入的目標文件,然后把所有符號表里的信息收集起來,構成一個全局的符號表。這是一本難得的講解程序的鏈接裝載和運行的好書。 showImg(https://image-static.segmentfault.com/396/693/396693929-5d558865c3a7e_articlex); 既然程序最終都被變成了一條條機器碼去執行,那為什么同一個程序,在同一臺計算...
摘要:計算機組成中的大量原理和設計,都對應著性能這個詞。時間的倒數性能計算機的性能,其實和體力勞動很像,好比是我們要搬東西。對于計算機的性能,我們需要有個標準來衡量?;ǖ臅r間越少,自然性能就越好。 0 學習路線的知識點概括 showImg(https://segmentfault.com/img/remote/1460000020031616?w=3832&h=2540); 學習計算機組成原...
摘要:固有對象由標準規定,隨著運行時創建而自動創建的對象實例。普通對象由語法構造器或者關鍵字定義類創建的對象,它能夠被原型繼承。 筆記說明 重學前端是程劭非(winter)【前手機淘寶前端負責人】在極客時間開的一個專欄,每天10分鐘,重構你的前端知識體系,筆者主要整理學習過程的一些要點筆記以及感悟,完整的可以加入winter的專欄學習【原文有winter的語音】,如有侵權請聯系我,郵箱:ka...
閱讀 3483·2021-11-18 10:02
閱讀 1612·2021-10-12 10:12
閱讀 2990·2021-10-09 09:53
閱讀 4858·2021-09-09 09:34
閱讀 847·2021-09-06 15:02
閱讀 2777·2021-08-05 10:02
閱讀 3134·2019-08-30 15:44
閱讀 3121·2019-08-28 18:04