多目標文件鏈接
//stack.c
char stack[512];
int top =-1;
void push(char c){stack[++top] = c;
}char pop(void){return stack[top--];
}int is_empty(void){return top == 1;
}// main.c
#include <stdio.h>
int a,b = 1;
int main(){
push('a');
push('b');
push('c');
while(!is_empty())putchar(pop());putchar('\n');return 0;
}
通過 readelf -a main命令可以看到
- main的.bss段合并了 main.o和stack.o的.bss段,包含了變量a和stack;
- main的.data段合并了main.o和 stack.o的.data段,其中包含了變量b和top;
- main的.text段合并了main.o和 stack.o的.text段

PS: GDB如何調試多個文件的code
//main.c 和 stack.c
(gdb)list stack.c:1
(gdb)b stack.c:10
定義和聲明
extern和static
- 用extern聲明的函數名具有external linkage
- 用static聲明的函數名具有internal linkage
- 函數默認是extern的
凡是被多次聲明的變量或函數,必須有且只有一個聲明是定義,如果有多個定義,或者一個定義都沒有,鏈接器就無法完成鏈接。
變量聲明和函數聲明有一點不同,函數聲明的 extern關鍵字可以省略,而變量聲明如果不寫 extern意思就完全變了
用 static關鍵字聲明具有 Internal Linkage,保護了函數的內部狀態,是一種封裝
頭文件
通過宏定義避免硬編碼
//stack.h
#ifdef STACK_H
#define STACK_H void push(char); char pop(void); int is_empty(void);
#endif//main.c
#include "stack.h"
對于用角括號包含的頭文件**,gcc首先查找**-I**選項指定的目錄,然后查找系統的頭文件目錄);
而對于用引號包含的頭文件**,gcc首先查找包含這個頭文件的當前文件所在的目錄,然后查找**-I選項指定的目錄,然后查找系統的頭文件目錄
則可以用gcc- c maln.c編譯,gcc會自動在main.C所在的目錄中找到stack. h。假如把 stack.h移到一個子目錄下

則需要用gcc- c main.c -Istack編譯,用-I選項告訴gcc頭文件要到子目錄 stack里找
在#include預處理指示中可以使用相對路徑,例如把上面的代碼改成#include “stack/stack,h”,那么編譯時就不需要加-Istack選項了,因為是main.c要包含頭文件,gcc會自動在main.c所在的目錄中查找,而頭文件相對于main.c所在目錄的相對路徑正是 stack/ stack.h
PS:gcc -E可以產生預編譯文件
避免頭文件被重復包含的方法為header guard
寫.C文件和頭文件時一般來說應遵循以下原則:
- C文件中可以有變量或函數定義,而.h文件中應該只有變量或函數聲明而沒有定義。
- 不要把一個C文件包含到另一個C文件中。
靜態庫
把一組代碼編譯成一個庫,很多項目中復用
例如將stack.c文件拆分為四個文件,main.c保持不變

gcc -c stakc/stack.c stack/push.c stack/pop.c stack/is_empty.c
ar rs libstack.a stack.o push.o pop.o is_empty.o
# r表示將文件打包進libstack.a中,s表示為靜態鏈接庫
# 等價于
ar r libstack.a stack.o push.o pop.o is_empty.o
ranlib libstack.a
# 鏈接libstack.a main.c
gcc mian.c -L. -lstack -Istack -o main
-L選項告訴編譯器去哪里找需要的庫文件,L.表示在當前目錄找。-lstack選項告訴編譯器要鏈接 libstack庫,-I選項告訴編譯器去哪里找頭文件
編譯器默認會找哪些目錄,用-print-search-dirs選項查看一下
gcc -print-search-dirs
在處理-lstack選項時,gcc首先到-L選項指定的目錄下查找,看有沒有共享庫Iibstack.so,如果有就鏈接它,否則再找有沒有靜態庫 Iibstack,a,如果有就鏈接它,如果還是沒有,就到默認搜索路徑下按同樣的步驟查找。
gcc在鏈接時優先考慮共享庫,其次才是靜態庫,如果希望gcc只考慮靜態庫,可以指定-static選項。
main.c只調用了push這一個函數,所以鏈接生成的可執行文件中也只有push而沒有pop和 is_empty。鏈接器從靜態庫中只取出需要的目標文件來做鏈接,不需要的目標文件可以不鏈接
共享庫
組成共享庫的目標文件和一般的目標文件有所不同,在編譯時要加-fPIC選項,即位置無關編碼
gcc -c -fPIC stakc/stack.c stack/push.c stack/pop.c stack/is_empty.c
指令中凡是用到stack和top變量的地址都用0x0表示,以備在重定位時修改。

原來指令中的0x0被改成了0x804a010和0x804a040,這樣做了重定位之后,各段的加載地址就定死了,因為在指令中使用了絕對地址

和先前的結果不同,指令中的0x0(%ebx)被修改成-0xc(ebx)和-0x8(%ebx),而不是修改成絕對地址。所以共享庫各段的加載地址并沒有定死,可以加載到任意位置。因為指令中的地址都是相對于ebx的,沒有使用絕對地址,只要根據實際的加載情況修改ebx就可以了,這就是位置無關代碼的特點.

對比前后的指令差異

-0xc(%ebx)這個地址并不是變量top的地址,這個地址的內存單元中又保存了另外一個地址,而它才是變量top的地址。指令mov -0xc(%ebx),%eax
是從地址ebx-12
取出變量top的地址傳給eax,而指令mov (%eax),%eax
才是從top的地址取出top的值傳給eax。指令lea 0x1(%eax),%edx
是把top的值加1存到edx中。lea指令算出第一個操作數所代表的地址,但并不訪問內存,而是直接把這個地址傳給第二個操作數。我們知道x86的內存尋址方式涉及加法和乘法運算,lea指令只是利用尋址電路做加法和乘法運算,而不是真的尋址,
將main.c文件和共享庫鏈接
用Ldd命令査看可執行文件依賴于哪些共享庫:
動態鏈接器在那些目錄搜索共享庫?
- 首先在環境變量LD_ LIBRARY_PATH保存的路徑中查找
- 然后從緩存文件/etc/ld.so. cache中查找這個緩存文件是由 ldconfig命令讀取配置文件/etc/ld.so.conf生成的
- 如果上述步驟都找不到,則到默認的系統庫文件目錄中查找,先是/usr/ib然后是/Lib。
最常用的方法。把lsibtack.so所在目錄的絕對路徑(比如/home/ akaedu/somedir)添加到配置文件/etc/ld.so.conf(該文件中每個路徑占一行),然后運行ldconfig命令:

再查看動態庫

函數的動態鏈接
和鏈接靜態庫的情況不同,push函數的指令沒有鏈接到可執行文件中,而且call 86483d8-push@pLt>這條指令調用的也不是push函數的地址,而是plt段里的地址。PLT是 Procedure Linkage Table的縮寫,.plt段里保存的也是指令,和.text一起合并到 Text Segment

共享庫命名
按照共享庫的命名慣例,每個共享庫有三個文件名:real name、 soname和 linker name。
真正的庫文件(而不是符號鏈接)的名字是 real name,包含完整的共享庫版本號,例如上面的 libcap.so.1.10、libc-2.8.90.so等
soname是符號鏈接的名字,只包含共享庫的主版本號
但對于依賴libcap.So.1的程序來說,真正的庫文件不管是 Libcap.S0.1.16還是Libcap.so.1.11都可以用,所以使用共享庫可以很方便地升級庫文件而不需要重新編譯程序,這是靜態庫所沒有的優點。注意libc的版本編號有一點特殊,libc-2.8.90.s0的主版本號是6而不是2或28
linker name僅在編譯鏈接時使用,gcc的-L選項應該指定 linker name所在的目錄。有的 linker name是庫文件的一個符號鏈接,有的 linker name是一段鏈接腳本。
虛擬內存管理
ps //查看進程
cat /porc/29977/maps //查看進程地址空間

堆空間的地址上限(0x09497000)稱為 Break,堆空間要向高地址增長就要抬高 Break,映射新的虛擬內存頁面到物理內存,這是通過系統調用brk實現的, malloc函數也是調用brk向內核請求分配內存的。
操作系統虛擬內存控制機制的作用
(1)可以控制物理內存的訪問權限。
物理內存本身是不限制訪問的,任何地址都可以讀寫,而操作系統要求不同的頁面具有不同的訪問權限,這是利用CPU模式和MMU的內存保護機制實現的。錯誤的指令或惡意代碼的破壞能力受到了限制,最多使當前進程因段錯誤而終止,不會影響到整個系統的穩定性。
(2)使每個進程有獨立的地址空間。
不同進程中相同的VA被MMU映射到不同的PA,因此在某一個進程中訪問任何虛擬地址都不可能訪問到屬于另外一個進程的物理內存頁面,并且每個進程都認為自己獨占0x0000000 xbffffffff 整個用戶地址空間。獨立地址空間的好處是:任何一個進程由于執行了錯誤指令或惡意代碼而導致的非法內存訪問都不會意外改寫其他進程的數據,也不會影響其他進程的運行;鏈接器和加載器的實現也比較容易,不必考慮各進程的地址范圍是否沖突。

兩個進程都是bash進程, Text Segment是一樣的,并且 Text Segment是只讀的,不會被改寫,因此操作系統安排兩個進程的TextSegment共享相同的物理頁面。由于每個進程都有自己的一套VA到PA的映射表,在一個進程中通過VA只能訪問到屬于自己的物理頁面,而不會訪問到其他進程的物理頁面。
(3)VA到PA的映射會給分配和釋放內存帶來方便
物理地址不連續的幾塊內存可以映射成虛擬地址連續的一塊內存。比如要用 malloc分配一塊很大的內存空間,雖然有足夠多的空閑物理內存,卻沒有足夠大的連續空閑內存,這時就可以分配多個不連續的物理頁面而映射到連續的虛擬地址范圍。

(4)一個系統如果同時運行著很多進程,為各進程分配的內存之和可能會大于實際可用的物理內存,虛擬內存管理機制使這種情況下各進程仍然能夠正常運行。
進程訪問的是虛擬內存頁面,這些頁面的數據可以保存在物理頁面中,也可以臨時保存在磁盤上而不占用物理頁面,可以在磁盤上開一個分區或者建一個文件專門用于臨時保存虛擬內存頁面的數據,這稱為交換設備( Swap Device)。啟用了交換設備之后,系統中可分配的內存總量等于物理內存的大小與交換設備的大小之和