重學電腦組成原理(九)- 動態鏈接

来源:https://www.cnblogs.com/JavaEdge/archive/2019/08/18/11371239.html
-Advertisement-
Play Games

把對應的不同文件內的代碼段,合併到一起,成為最後的可執行文件 鏈接的方式,讓我們在寫代碼的時候做到了“復用”。 同樣的功能代碼只要寫一次,然後提供給很多不同的程式進行鏈接就行了。 “鏈接”其實有點兒像我們日常生活中的 標準化、模塊化 生產。 有一個可以生產標準螺帽的生產線,就可生產很多不同的螺帽。 ...


把對應的不同文件內的代碼段,合併到一起,成為最後的可執行文件

鏈接的方式,讓我們在寫代碼的時候做到了“復用”。

同樣的功能代碼只要寫一次,然後提供給很多不同的程式進行鏈接就行了。

“鏈接”其實有點兒像我們日常生活中的標準化、模塊化生產。

有一個可以生產標準螺帽的生產線,就可生產很多不同的螺帽。

只要需要螺帽,都可以通過鏈接的方式,去複製一個出來,放到需要的地方

但是,如果我們有很多個程式都要通過裝載器裝載到記憶體裡面,那裡面鏈接好的同樣的功能代碼,也都需要再裝載一遍,再占一遍記憶體空間。

這就好比,假設每個人都有騎自行車的需要,那我們給每個人都生產一輛自行車帶在身邊,固然大家都有自行車用了,但是馬路上肯定會特別擁擠。

1 鏈接可以分動、靜,共用運行省記憶體

我們上一節解決程式裝載到記憶體的時候,講了很多方法。說起來,最根本的問題其實就是記憶體空間不夠用

如果能夠讓同樣功能的代碼,在不同的程式裡面,不需要各占一份記憶體空間,那該有多好啊!

就好比,現在馬路上的共用單車,我們並不需要給每個人都造一輛自行車,只要馬路上有這些單車,誰需要的時候,直接通過手機掃碼,都可以解鎖騎行。

這個思路就引入一種新的鏈接方法,叫作動態鏈接(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 <show_me_the_money@plt-0x10>:
  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 <show_me_the_money@plt>:
  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 <main>:
  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 <show_me_the_money@plt>
  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章

裡面深入地講解了,動態鏈接里程式內的數據佈局和對應數據的載入關係。

參考

  • 深入淺出電腦組成原理

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 方法一: 常規的WPF操作: 後臺代碼: 方法二: 後臺直接寫: ...
  • 背景 By 魯迅 By 高爾基 說明: 1. Kernel版本:4.14 2. ARM64處理器 3. 使用工具:Source Insight 3.5, Visio 1. 介紹 ,由ARM定義的電源管理介面規範,通常由Firmware來實現,而Linux系統可以通過 指令來進入不同的 ,進而調用對應 ...
  • 一.存儲基礎知識 從工作原理區分: 機械 HDD 固態 SSD SSD的優勢: 從磁碟尺寸區分: 3.5 2.5 1.8 從插拔方式區分: 熱插拔 非熱插拔 從硬碟主要介面區分: IDE —— SATA I/II/II 個人電腦 SCSI —— SAS 伺服器 FC PCIE 從存儲連接方式區分: ...
  • 1. 查詢k8s集群部署pod的基本情況 如下圖,我們可知容器coredns和dnsutils都部署成功,但是由於功能變數名稱解析的問題,導致coredns和dnsutils的容器不斷重啟(原因heath檢查,無法請求成功,被kubelet重啟了pod) 命令如下: root >> kubectl get ...
  • vim多視窗操作 垂直方式打開多個視窗 vim filename1 filename2 -O :vsp filename 或者 :vs filename 視窗切換 ctrl + ww vim文件切換 在多個文件之間切換 ctrl + i #切換到下一個文件或同一個文件中的下一個索引 ctrl + o ...
  • 一、廢話兩句 在雲數據中心,一次幾十臺甚至幾百台伺服器上線,系統安裝將變得非常繁瑣,系統安裝好了後還會涉及很多配置,如果一臺台來安裝的話工作量非常大。(雖然有加班費,開個玩笑)為瞭解決這個問題,我們需要實現無人值守批量部署系統。 簡單看一下拓撲圖: 1. 什麼是PXE? 簡單來說:PXE主要是引導作 ...
  • 第一章Linux命令行簡介 1.1 Linux命令行概述 1.1.1 Linux 命令行的開啟和退出 開啟:登陸賬號密碼進入系統 退出:exit/logout 快捷鍵:Ctrl+d 1.1.2 Linux命令行提示符介紹 (1)提示符由PS1環境變數控制。實例代碼如下: [root@centos10 ...
  • 今天我們學習關於NTFS管理數據 以下是學習的內容NTFS分區和FAT32分區的區別,如何將FAT32分區轉化成NTFS分區,FAT 32 不支持大於4G ,NTFS許可權設置 ,EFS加密 ,文件夾的NTFS許可權 許可權累加, 查看對象的所有者,獲得對象的所有權,重置文件夾中所有對象的許可權,利用EFS ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...