日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

《程序员的自我修养》第4章---静态链接

發布時間:2024/1/18 编程问答 31 豆豆
生活随笔 收集整理的這篇文章主要介紹了 《程序员的自我修养》第4章---静态链接 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

第4章 靜態鏈接

4.1 空間和地址分配:

a.c :

extern int shared;int main() {int a = 100;swap(&a, &shared); }

b.c :

int shared = 1; void swap(int* a, int* b) {*a ^= *b ^= *a ^= *b; }

對于鏈接器來說,整個鏈接過程,它的工作就是將幾個輸入的目標文件加工、合并成一個輸出的可執行文件。

例如將輸入的 a.o 和 b.o 文件合并成可執行文件 ab。

鏈接器的合并方式:

4.1.1 按序疊加:

pass
浪費空間。

4.1.2 相似段合并:

將所有輸入文件的 .text 合并到輸出文件的 .text段,.data段合并到 .data段,以此類推,等等。

“鏈接器為目標文件分配地址和空間” 這句話中的 “地址和空間” 其實有兩個含義:

  • 在輸出的 可執行文件 中的空間;(鏈接器通過 .o文件生成可執行文件,所以可執行文件有多大、文件內容都是由鏈接器決定的,所以可執行文件的各個段的空間由鏈接器負責生成)
  • 在裝載后的 虛擬地址 中的空間;(程序在被操作系統加載后開始運行,此時為進程,操作系統為其分配虛擬地址空間,進程的虛擬地址空間中包含 .text, .data 等各個段的空間,這個空間大小也是由鏈接器進行賦值的)
  • 當我們談到空間分配時,只關注虛擬地址空間的分配。

    “相似段合并”的空間分配方法采用 “兩步鏈接”(Two-pass Linking)的方法:

  • 第一步: 空間與地址分配;(搜集所有輸入文件.o 的符號信息、段表長度,將其統一放到一個全局符號表中,給每個符號分配虛擬地址,通過每個符號的偏移量計算各個符號的虛擬地址)
  • 第二步: 符號解析與重定位。(根據第一步中搜集到的信息進行 符號解析、重定位、調整代碼中的地址等)
  • 在Linux下,ELF可執行文件默認從地址 0x08048000 開始分配(32位操作系統)。


    4.2 符號解析與重定位:

    4.2.2 重定位表:

    鏈接器如何知道哪些指令需要被調整?
    借助于 ELF文件中的 “重定位表”(Relocation Table)。

    重定位表在ELF文件中一般是一個或多個段,例如,如果 .data段中有需要被重定位的符號,那么ELF文件中就會有一個相對應的 .rel.text段,用于保存 .data段中需要被重定位的地方。

    使用 objdump -r 命令可以查看目標文件中的所有重定位入口:

    objdump -r, --reloc Display the relocation entries in the file//查看目標文件中的所有重定位入口

    例如,查看 a.o 目標文件中的重定位入口信息:

    [linux] objdump -r a.oa.o: 文件格式 elf64-x86-64RELOCATION RECORDS FOR [.text]: OFFSET TYPE VALUE 0000000000000014 R_X86_64_32 shared 0000000000000021 R_X86_64_PC32 swap-0x0000000000000004RELOCATION RECORDS FOR [.eh_frame]: OFFSET TYPE VALUE 0000000000000020 R_X86_64_PC32 .text

    查看 b.o 目標文件中的重定位入口信息:

    [linux] objdump -r b.ob.o: 文件格式 elf64-x86-64RELOCATION RECORDS FOR [.eh_frame]: OFFSET TYPE VALUE 0000000000000020 R_X86_64_PC32 .text

    可以看到 a.o 中有兩個重定位入口,重定位入口 偏移(Offset) 表示該入口在要被重定位的段中的位置。


    4.4 C++相關問題:

    C++的一些語言特性使之必須由編譯器和鏈接器共同支持才能完成,最主要的有兩個方面:

  • 一個是C++的重復代碼消除;
  • 還有一個是 全局構造與析構。
  • 4.4.1 重復代碼消除:

    C++編譯器會產生重復代碼的場景: 模板、外部內聯函數、虛函數表, 這些都有可能在不同的編譯單元里產生相同的代碼。

    模板在本質上來講很像宏(宏也是符號的替代),

    例如一個模板 template class A {}; 在頭文件 header.h 中,
    在 a.c 中實例化 A a, 在 b.c 中實例化為 A b,這會導致在 a.o 與 b.o 的目標文件中存在重復的代碼,
    “當模板在一個編譯單元里被實例化時,它并不知道自己是否在別的單元也被實例化了。所以當一個模板在多個編譯單元同時實例化成相同的類型的時候,必然會生成重復的代碼。”
    如果不管這些,直接將重復的代碼都保留下來,會造成下面幾個問題:

  • 空間浪費;(假設幾百個編譯單元同時實例化了許多個模板)
  • 地址較易出錯;
  • 指令運行效率較低;
  • 一種解決模板產生重復代碼的方法是:

    編譯器 ----> 遇到template類模板或函數模板時,將每個模板的實例代碼都單獨存放在一個段里(目標文件中),在目標文件中以相同的規則取名 ----> 鏈接器 ----> 在最終的鏈接階段,將同類型的模板實例的段進行合并,然后生成可執行文件。

    例如有一個模板函數 add(), 某個編譯單元.c文件中以 int 和float 類型實例化了該模板函數,那么該編譯單元的目標文件中就包含了兩個該模板實例的段,假設名為:
    .temp.add , .temp.add
    這樣,當別編譯單元也以 int 或 float 類型實例化 該模板函數后,也會生成相同的名字,
    這樣鏈接器在最終鏈接的時候就可以區分這些相同的模板實例段,然后將它們合并入最后的代碼段。

    這種做法目前被主流編譯器所采用,包括 GCC 和 Visual C++。

    對于 外部內聯函數 和 虛函數表,消除重復代碼的方法也與 模板 的做法類似:

    例如對于一個有虛函數的類,有一個與之對應的虛函數表,編譯器會在用到該類的多個編譯單元生成虛函數表,造成代碼重復,默認構造函數、默認拷貝構造函數、賦值構造運算符等也有類似的問題,解決方法與類模板一樣: 編譯器在 編譯階段 在目標文件.o中為虛函數表生成單獨的段,鏈接器在 鏈接階段 合并不同目標文件中的同名的段,最終生成可執行文件。

    特殊情況是:
    不同的編譯單元使用不同的編譯器,然后將生成的多個目標文件.o 使用鏈接器進行合并,
    此時不同的編譯單元中的段的名字相同,但內容由于編譯器版本或者編譯選項的不同,導致同一個函數編譯出來的實際代碼有所不同,此時:
    鏈接器會做出一個選擇,隨機選取一個版本進行鏈接,并拋出一個警告消息。

    函數鏈接級別:

    Visual C++編譯器提供一個 編譯選項 叫 “函數級別鏈接”,針對程序和庫較大時,成千上百個函數或變量,有些函數或變量可能并沒有被用到,此時就沒必要將其鏈接進來,此編譯選項允許當鏈接器用到某個函數時,再將其合并到輸出文件中,以達到節省空間的目的。缺點是會減慢編譯和鏈接過程。

    4.4.2 全局構造與析構:

    一個C/C++程序 是從main開始執行,隨著main函數的結束而結束。
    在main函數開始調用之前,操作系統需要先初始化進程執行環境,包括:
    堆內存分配初始化、線程子系統等。 C++全局對象的構造函數也在這一時期被執行。

    C++全局對象 的構造函數在 main 之前被執行,C++全局對象 的析構函數在 main之后被執行。

    Linux系統下一般程序的入口是 _start 函數,這個函數是Linux系統庫(Glibc)的一部分。

    4.4.3 C++ 與 ABI:

    ABI = Application Binary Interface,與API(Application Programming Interface)類似,只是在不通層面的接口。

    如果要讓不同的編譯器產生的目標文件能夠兼容鏈接,要考慮它們的ABI是否兼容。


    4.5 靜態庫鏈接:

    程序之所以有用,因為它會有輸入輸出,這些輸入輸出的對象可以是數據,可以是人,也可以是另外一個程序,還可以是另一臺計算機,一個沒有輸入輸出的程序沒有任何意義。

    但是一個程序如何做到輸入輸出呢?
    最簡單的辦法是使用操作系統提供的API(應用程序編程接口,Application Programming Interface)。

    程序如何調用操作系統提供的API呢?
    一般情況下,一種語言的開發環境虎附帶有 “語言庫”(Language Library),這些庫就是對操作系統API的包裝。
    例如經典的C語言的“hello world”程序,它使用 C語言標準庫 的 printf函數來輸出一個字符串,在Linux下,它是對 write 系統調用的封裝,在Windows下,它是對 WriteConsole 系統API的封裝。

    如何組織C運行庫:
    在一個C運行庫中,包含了很多跟系統功能相關的代碼,例如輸入輸出、文件操作、時間日期、內存管理等等,glibc中由成百上千的C語言源程序文件組成,也就是編譯后會產生相同數量的目標文件。
    如果把這些零散的目標文件直接提供給庫的使用者,則會造成傳輸、管理的不便,因此,通常使用 ar 工具將這些目標進行壓縮打包,生成 libc.a 靜態庫文件。

    可以使用ar工具查看靜態庫文件中包含哪些目標文件:
    (libc.a中共包含了大概 1400個目標文件)

    ar -t libc.a...

    其實一個靜態庫可以簡單看成 一組目標文件的集合,即很多目標文件經過壓縮打包后形成的一個文件。

    ar 壓縮程序 ----> 多個 .o 文件 ----> 合成 .a 庫文件

    一個鏈接過程,就是不斷的尋找程序中的符號所在的目標文件,然后將其加入進來,如果靠人工這將是一個很復雜的過程。

    例如:
    hello.c ----> printf.o ----> stdout.o, vprintf.o ----> …
    (hello.c中包含printf()函數,在printf.o目標文件中,printf.o中又有stdout和vprintf兩個符號,以此類推,找出所有的依賴的符號所在的目標文件。。)

    “幸好ld鏈接器會處理這一切繁瑣的事務,自動尋找所有需要的符號及它們所在的目標文件,將這些目標文件從 “libc.a”中 “解壓”出來,最終將它們鏈接在一起稱為一個可執行文件。”

    然而僅僅是將.a庫中的所有依賴目標文件找到并鏈接仍是不夠的,后面再繼續介紹。

    collect2:

    collect2 可以看作是ld鏈接器的一個包裝,它會調用ld鏈接器來完成對目標文件的鏈接,然后再對鏈接結果進行一些處理,主要是收集所有與程序初始化相關的信息并且構造初始化的結構。

    Q&A:

    Q: 為什么靜態庫里面一個目標文件只包含一個函數?
    A: 鏈接器在鏈接靜態庫的時候是以目標文件為單位的。當引用了靜態庫中的printf()函數時,鏈接器就會把printf()函數所在的目標文件鏈接進來。
    如果很多函數都放在同一個目標文件中,就可能會造成其他沒用的函數都被一起鏈接進了輸出結果中。
    由于運行庫有成百上千個函數,數量非常龐大,每個函數獨立的放在一個目標文件中可以盡量減少空間的浪費,那些沒有被用到的目標文件(函數)就不要鏈接到最終的輸出文件中。


    9. C/C++ 運行庫、靜態庫、動態庫 的區別與聯系:

    https://blog.csdn.net/ithzhang/article/details/20160009

    9.1 從C和C++運行庫說起:

    為了提高C語言的開發效率,C標準定義了一系列常用的函數,稱為C庫函數。
    C標準僅僅定義了函數原型,并沒有提供實現。因此這個任務留給了各個支持C語言標準的編譯器。

    每個編譯器通常實現了標準C的超集,稱為 “C運行時庫”(C Run Time Library),簡稱 CRT。

    對于VC++編譯器來說,它提供的CRT庫支持C標準定義的標準C函數,同是也有一些專門針對Windows系統特別設計的函數(對于Linux系統、GCC編譯器也是一樣的道理)。

    可以簡單理解:
    C標準庫 + 編譯器自定義的針對具體操作提供的專門函數 = C運行時庫(CRT)

    與C語言類似,C++也定義了自己的標準,同時提供相關支持庫,我們將它稱為C++運行時庫或C++標準庫。

    由于C++對C的兼容性,C++標準庫包括了C標準庫,除此之外還包括 IO流標準模板庫STL


    9.2 VC++在何處實現C和C++運行庫:

    VC++完美支持C和C++標準,即按照C和C++的標準中定義的函數原型實現了上述運行時庫。

    為了方便有不同需求的用戶的使用,VC++分別實現了 動態鏈接庫DLL版本 和 靜態庫LIB版本。
    同時為了支持程序調試且不影響程序的性能,又分別提供了對應的調試版本。

    對于C運行時庫 CRT、VC6.0、VC2005、VC2008、VC2010等,均提供了 DLL版本 和 LIB版本(供用戶根據實際需要選擇DLL方式還是LIB方式的庫)。


    9.3 動態版(DLL)和靜態版(LIB)C和C++運行庫的優缺點:

    動態鏈接庫: DLL(Dynamic Linking Library),Windows下的 .dll 和 Linux下的 .so 文件;
    靜態鏈接庫: Static Linking Library, Windows下的 .lib 和 Linux下的 .a 文件。

    因為靜態版必須把C和C++運行庫復制到目標程序(.o)中,所以產生的可執行文件會比較大。

    使用DLL版的C和C++運行庫,程序砸運行時動態的加載對應的DLL,程序體積變小,但一個很大的問題就是一旦找不到對應的DLL,程序將無法運行

    靜態庫的鏈接方法:

    gcc main.c -static -o main -L. -lstatic

    動態庫的鏈接方法:

    gcc main.c -o main -L. -lshared

    總結

    以上是生活随笔為你收集整理的《程序员的自我修养》第4章---静态链接的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。