<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ThinkChat2.0新版上線,更智能更精彩,支持會話、畫圖、視頻、閱讀、搜索等,送10W Token,即刻開啟你的AI之旅 廣告
                # 第?20?章?鏈接詳解 **目錄** + [1\. 多目標文件的鏈接](ch20s01.html) + [2\. 定義和聲明](ch20s02.html) + [2.1\. `extern`和`static`關鍵字](ch20s02.html#id2787367) + [2.2\. 頭文件](ch20s02.html#id2788051) + [2.3\. 定義和聲明的詳細規則](ch20s02.html#id2788815) + [3\. 靜態庫](ch20s03.html) + [4\. 共享庫](ch20s04.html) + [4.1\. 編譯、鏈接、運行](ch20s04.html#id2789691) + [4.2\. 動態鏈接的過程](ch20s04.html#id2790795) + [4.3\. 共享庫的命名慣例](ch20s04.html#id2791032) + [5\. 虛擬內存管理](ch20s05.html) ## 1.?多目標文件的鏈接 現在我們把[例?12.1 “用堆棧實現倒序打印”](ch12s02.html#stackqueue.stackreverse)拆成兩個程序文件,`stack.c`實現堆棧,而`main.c`使用堆棧: ``` /* 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; } ``` 這段程序和原來有點不同,在[例?12.1 “用堆棧實現倒序打印”](ch12s02.html#stackqueue.stackreverse)中`top`總是指向棧頂元素的下一個元素,而在這段程序中`top`總是指向棧頂元素,所以要初始化成-1才表示空堆棧,這兩種堆棧使用習慣都很常見。 ``` /* main.c */ #include <stdio.h> int a, b = 1; int main(void) { push('a'); push('b'); push('c'); while(!is_empty()) putchar(pop()); putchar('\n'); return 0; } ``` `a`和`b`這兩個變量沒有用,只是為了順便說明鏈接過程才加上的。編譯的步驟和以前一樣,可以一步編譯: ``` $ gcc main.c stack.c -o main ``` 也分可以多步編譯: ``` $ gcc -c main.c $ gcc -c stack.c $ gcc main.o stack.o -o main ``` 如果按照[第?2?節 “`main`函數和啟動例程”](ch19s02.html#asmc.main)的做法,用`nm`命令查看目標文件的符號表,會發現`main.o`中有未定義的符號`push`、`pop`、`is_empty`、`putchar`,前三個符號在`stack.o`中實現了,鏈接生成可執行文件`main`時可以做符號解析,而`putchar`是`libc`的庫函數,在可執行文件`main`中仍然是未定義的,要在程序運行時做動態鏈接。 我們通過`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`段,包含了各函數的定義。如下圖所示。 **圖?20.1.?多目標文件的鏈接** ![多目標文件的鏈接](https://box.kancloud.cn/2016-04-02_56ff80d4eea5c.png) 為什么在可執行文件`main`的每個段中來自`main.o`的變量或函數都在前面,而來自`stack.o`的變量或函數都在后面呢?我們可以試試把`gcc`命令中的兩個目標文件反過來寫: ``` $ gcc stack.o main.o -o main ``` 結果正如我們所預料的,可執行文件`main`的每個段中來自`main.o`的變量或函數都排到后面了。實際上鏈接的過程是由一個鏈接腳本(Linker Script)控制的,鏈接腳本決定了給每個段分配什么地址,如何對齊,哪個段在前,哪個段在后,哪些段合并到同一個Segment,另外鏈接腳本還要插入一些符號到最終生成的文件中,例如`__bss_start`、`_edata`、`_end`等。如果用`ld`做鏈接時沒有用`-T`選項指定鏈接腳本,則使用`ld`的默認鏈接腳本,默認鏈接腳本可以用`ld --verbose`命令查看(由于比較長,只列出一些片斷): ``` $ ld --verbose ... using internal linker script: ================================================== /* Script for -z combreloc: combine and sort reloc sections */ OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386") OUTPUT_ARCH(i386) ENTRY(_start) ... SECTIONS { /* Read-only sections, merged into text segment: */ PROVIDE (__executable_start = 0x08048000); . = 0x08048000 + SIZEOF_HEADERS; .interp : { *(.interp) } .note.gnu.build-id : { *(.note.gnu.build-id) } .hash : { *(.hash) } .gnu.hash : { *(.gnu.hash) } .dynsym : { *(.dynsym) } .dynstr : { *(.dynstr) } .gnu.version : { *(.gnu.version) } .gnu.version_d : { *(.gnu.version_d) } .gnu.version_r : { *(.gnu.version_r) } .rel.dyn : ... .rel.plt : { *(.rel.plt) } ... .init : ... .plt : { *(.plt) } .text : ... .fini : ... .rodata : { *(.rodata .rodata.* .gnu.linkonce.r.*) } ... .eh_frame : ONLY_IF_RO { KEEP (*(.eh_frame)) } ... /* Adjust the address for the data segment. We want to adjust up to the same address within the page on the next page up. */ . = ALIGN (CONSTANT (MAXPAGESIZE)) - ((CONSTANT (MAXPAGESIZE) - .) & (CONSTANT (MAXPAGESIZE) - 1)); . = DATA_SEGMENT_ALIGN (CONSTANT (MAXPAGESIZE), CONSTANT (COMMONPAGESIZE)); ... .ctors : ... .dtors : ... .jcr : { KEEP (*(.jcr)) } ... .dynamic : { *(.dynamic) } .got : { *(.got) } ... .got.plt : { *(.got.plt) } .data : ... _edata = .; PROVIDE (edata = .); __bss_start = .; .bss : ... _end = .; PROVIDE (end = .); . = DATA_SEGMENT_END (.); /* Stabs debugging sections. */ ... /* DWARF debug sections. Symbols in the DWARF debugging sections are relative to the beginning of the section so we begin them at 0\. */ ... } ================================================== ``` `ENTRY(_start)`說明`_start`是整個程序的入口點,因此`_start`是入口點并不是規定死的,是可以改用其它函數做入口點的。 `PROVIDE (__executable_start = 0x08048000); . = 0x08048000 + SIZEOF_HEADERS;`是Text Segment的起始地址,這個Segment包含后面列出的那些段,`.plt`、`.text`、`.rodata`等等。每個段的描述格式都是“段名 : { 組成 }”,例如`.plt : { *(.plt) }`,左邊表示最終生成的文件的`.plt`段,右邊表示所有目標文件的`.plt`段,意思是最終生成的文件的`.plt`段由各目標文件的`.plt`段組成。 `. = ALIGN (CONSTANT (MAXPAGESIZE)) - ((CONSTANT (MAXPAGESIZE) - .) & (CONSTANT (MAXPAGESIZE) - 1)); . = DATA_SEGMENT_ALIGN (CONSTANT (MAXPAGESIZE), CONSTANT (COMMONPAGESIZE));`是Data Segment的起始地址,要做一系列的對齊操作,這個Segment包含后面列出的那些段,`.got`、`.data`、`.bss`等等。 Data Segment的后面還有其它一些Segment,主要是調試信息。關于鏈接腳本就介紹這么多,本書不做深入討論。 ## 2.?定義和聲明 ### 2.1.?`extern`和`static`關鍵字 在上一節我們把兩個程序文件放在一起編譯鏈接,`main.c`用到的函數`push`、`pop`和`is_empty`由`stack.c`提供,其實有一點小問題,我們用`-Wall`選項編譯`main.c`可以看到: ``` $ gcc -c main.c -Wall main.c: In function ‘main’: main.c:8: warning: implicit declaration of function ‘push’ main.c:12: warning: implicit declaration of function ‘is_empty’ main.c:13: warning: implicit declaration of function ‘pop’ ``` 這個問題我們在[第?2?節 “自定義函數”](ch03s02.html#func.ourfirstfunc)討論過,由于編譯器在處理函數調用代碼時沒有找到函數原型,只好根據函數調用代碼做隱式聲明,把這三個函數聲明為: ``` int push(char); int pop(void); int is_empty(void); ``` 現在你應該比學[第?2?節 “自定義函數”](ch03s02.html#func.ourfirstfunc)的時候更容易理解這條規則了。為什么編譯器在處理函數調用代碼時需要有函數原型?因為必須知道參數的類型和個數以及返回值的類型才知道生成什么樣的指令。為什么隱式聲明靠不住呢?因為隱式聲明是從函數調用代碼推導而來的,而事實上函數定義的形參類型可能跟函數調用代碼傳的實參類型并不一致,如果函數定義帶有可變參數(例如`printf`),那么從函數調用代碼也看不出來這個函數帶有可變參數,另外,從函數調用代碼也看不出來返回值應該是什么類型,所以隱式聲明只能規定返回值都是`int`型的。既然隱式聲明靠不住,那編譯器為什么不自己去找函數定義,而非要讓我們在調用之前寫函數原型呢?因為編譯器往往不知道去哪里找函數定義,像上面的例子,我讓編譯器編譯`main.c`,而這幾個函數的定義卻在`stack.c`里,編譯器又怎么會知道呢?所以編譯器只能通過隱式聲明來猜測函數原型,這種猜測往往會出錯,但在比較簡單的情況下還算可用,比如上一節的例子這么編譯過去了也能得到正確結果。 現在我們在`main.c`中聲明這幾個函數的原型: ``` /* main.c */ #include <stdio.h> extern void push(char); extern char pop(void); extern int is_empty(void); int main(void) { push('a'); push('b'); push('c'); while(!is_empty()) putchar(pop()); putchar('\n'); return 0; } ``` 這樣編譯器就不會報警告了。在這里`extern`關鍵字表示這個標識符具有External Linkage。External Linkage的定義在上一章講過,但現在應該更容易理解了,`push`這個標識符具有External Linkage指的是:如果把`main.c`和`stack.c`鏈接在一起,如果`push`在`main.c`和`stack.c`中都有聲明(在`stack.c`中的聲明同時也是定義),那么這些聲明指的是同一個函數,鏈接之后是同一個`GLOBAL`符號,代表同一個地址。函數聲明中的`extern`也可以省略不寫,不寫`extern`的函數聲明也表示這個函數具有External Linkage。 如果用`static`關鍵字修飾一個函數聲明,則表示該標識符具有Internal Linkage,例如有以下兩個程序文件: ``` /* foo.c */ static void foo(void) {} ``` ``` /* main.c */ void foo(void); int main(void) { foo(); return 0; } ``` 編譯鏈接在一起會出錯: ``` $ gcc foo.c main.c /tmp/ccRC2Yjn.o: In function `main': main.c:(.text+0x12): undefined reference to `foo' collect2: ld returned 1 exit status ``` 雖然在`foo.c`中定義了函數`foo`,但這個函數只具有Internal Linkage,只有在`foo.c`中多次聲明才表示同一個函數,而在`main.c`中聲明就不表示它了。如果把`foo.c`編譯成目標文件,函數名`foo`在其中是一個`LOCAL`的符號,不參與鏈接過程,所以在鏈接時,`main.c`中用到一個External Linkage的`foo`函數,鏈接器卻找不到它的定義在哪兒,無法確定它的地址,也就無法做符號解析,只好報錯。_凡是被多次聲明的變量或函數,必須有且只有一個聲明是定義,如果有多個定義,或者一個定義都沒有,鏈接器就無法完成鏈接。_ 以上講了用`static`和`extern`修飾函數聲明的情況。現在來看用它們修飾變量聲明的情況。仍然用`stack.c`和`main.c`的例子,如果我想在`main.c`中直接訪問`stack.c`中定義的變量`top`,則可以用`extern`聲明它: ``` /* main.c */ #include <stdio.h> void push(char); char pop(void); int is_empty(void); extern int top; int main(void) { push('a'); push('b'); push('c'); printf("%d\n", top); while(!is_empty()) putchar(pop()); putchar('\n'); printf("%d\n", top); return 0; } ``` 變量`top`具有External Linkage,它的存儲空間是在`stack.c`中分配的,所以`main.c`中的變量聲明`extern int top;`不是變量定義,因為它不分配存儲空間。以上函數和變量聲明也可以寫在`main`函數體里面,使所聲明的標識符具有塊作用域: ``` int main(void) { void push(char); char pop(void); int is_empty(void); extern int top; push('a'); push('b'); push('c'); printf("%d\n", top); while(!is_empty()) putchar(pop()); putchar('\n'); printf("%d\n", top); return 0; } ``` 注意,變量聲明和函數聲明有一點不同,函數聲明的`extern`可寫可不寫,而變量聲明如果不寫`extern`意思就完全變了,如果上面的例子不寫`extern`就表示在`main`函數中定義一個局部變量`top`。另外要注意,`stack.c`中的定義是`int top = -1;`,而`main.c`中的聲明不能加Initializer,如果上面的例子寫成`extern int top = -1;`則編譯器會報錯。 在`main.c`中可以通過變量聲明來訪問`stack.c`中的變量`top`,但是從實現`stack.c`這個模塊的角度來看,`top`這個變量是不希望被外界訪問到的,變量`top`和`stack`都屬于這個模塊的內部狀態,外界應該只允許通過`push`和`pop`函數來改變模塊的內部狀態,這樣才能保證堆棧的LIFO特性,如果外界可以隨機訪問`stack`或者隨便修改`top`,那么堆棧的狀態就亂了。那怎么才能阻止外界訪問`top`和`stack`呢?答案就是用`static`關鍵字把它們聲明為Internal Linkage的: ``` /* stack.c */ static char stack[512]; static 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`中用`extern`聲明也訪問不到`stack.c`的變量`top`和`stack`。從而保護了`stack.c`模塊的內部狀態,這也是一種封裝(Encapsulation)的思想。 用`static`關鍵字聲明具有Internal Linkage的函數也是出于這個目的。在一個模塊中,有些函數是提供給外界使用的,也稱為導出(Export)給外界使用,這些函數聲明為External Linkage的。有些函數只在模塊內部使用而不希望被外界訪問到,則聲明為Internal Linkage的。 ### 2.2.?頭文件 我們繼續前面關于`stack.c`和`main.c`的討論。`stack.c`這個模塊封裝了`top`和`stack`兩個變量,導出了`push`、`pop`、`is_empty`三個函數接口,已經設計得比較完善了。但是使用這個模塊的每個程序文件都要寫三個函數聲明也是很麻煩的,假設又有一個`foo.c`也使用這個模塊,`main.c`和`foo.c`中各自要寫三個函數聲明。重復的代碼總是應該盡量避免的,以前我們通過各種辦法把重復的代碼提取出來,比如在[第?2?節 “數組應用實例:統計隨機數”](ch08s02.html#array.statistic)講過用宏定義避免硬編碼的問題,這次有什么辦法呢?答案就是可以自己寫一個頭文件`stack.h`: ``` /* stack.h */ #ifndef STACK_H #define STACK_H extern void push(char); extern char pop(void); extern int is_empty(void); #endif ``` 這樣在`main.c`中只需包含這個頭文件就可以了,而不需要寫三個函數聲明: ``` /* main.c */ #include <stdio.h> #include "stack.h" int main(void) { push('a'); push('b'); push('c'); while(!is_empty()) putchar(pop()); putchar('\n'); return 0; } ``` 首先說為什么`#include &lt;stdio.h&gt;`用角括號,而`#include "stack.h"`用引號。對于用角括號包含的頭文件,`gcc`首先查找`-I`選項指定的目錄,然后查找系統的頭文件目錄(通常是`/usr/include`,在我的系統上還包括`/usr/lib/gcc/i486-linux-gnu/4.3.2/include`);而對于用引號包含的頭文件,`gcc`首先查找包含頭文件的`.c`文件所在的目錄,然后查找`-I`選項指定的目錄,然后查找系統的頭文件目錄。 假如三個代碼文件都放在當前目錄下: ``` $ tree . |-- main.c |-- stack.c `-- stack.h 0 directories, 3 files ``` 則可以用`gcc -c main.c`編譯,`gcc`會自動在`main.c`所在的目錄中找到`stack.h`。假如把`stack.h`移到一個子目錄下: ``` $ tree . |-- main.c `-- stack |-- stack.c `-- stack.h 1 directory, 3 files ``` 則需要用`gcc -c main.c -Istack`編譯。用`-I`選項告訴`gcc`頭文件要到子目錄`stack`里找。 在`#include`預處理指示中可以使用相對路徑,例如把上面的代碼改成`#include "stack/stack.h"`,那么編譯時就不需要加`-Istack`選項了,因為`gcc`會自動在`main.c`所在的目錄中查找,而頭文件相對于`main.c`所在目錄的相對路徑正是`stack/stack.h`。 在`stack.h`中我們又看到兩個新的預處理指示`#ifndef STACK_H`和`#endif`,意思是說,如果`STACK_H`這個宏沒有定義過,那么從`#ifndef`到`#endif`之間的代碼就包含在預處理的輸出結果中,否則這一段代碼就不出現在預處理的輸出結果中。`stack.h`這個頭文件的內容整個被`#ifndef`和`#endif`括起來了,如果在包含這個頭文件時`STACK_H`這個宏已經定義過了,則相當于這個頭文件里什么都沒有,包含了一個空文件。這有什么用呢?假如`main.c`包含了兩次`stack.h`: ``` ... #include "stack.h" #include "stack.h" int main(void) { ... ``` 則第一次包含`stack.h`時并沒有定義`STACK_H`這個宏,因此頭文件的內容包含在預處理的輸出結果中: ``` ... #define STACK_H extern void push(char); extern char pop(void); extern int is_empty(void); #include "stack.h" int main(void) { ... ``` 其中已經定義了`STACK_H`這個宏,因此第二次再包含`stack.h`就相當于包含了一個空文件,這就避免了頭文件的內容被重復包含。這種保護頭文件的寫法稱為Header Guard,以后我們每寫一個頭文件都要加上Header Guard,宏定義名就用頭文件名的大寫形式,這是規范的做法。 那為什么需要防止重復包含呢?誰會把一個頭文件包含兩次呢?像上面那么明顯的錯誤沒人會犯,但有時候重復包含的錯誤并不是那么明顯的。比如: ``` #include "stack.h" #include "foo.h" ``` 然而`foo.h`里又包含了`bar.h`,`bar.h`里又包含了`stack.h`。在規模較大的項目中頭文件包含頭文件的情況很常見,經常會包含四五層,這時候重復包含的問題就很難發現了。比如在我的系統頭文件目錄`/usr/include`中,`errno.h`包含了`bits/errno.h`,后者又包含了`linux/errno.h`,后者又包含了`asm/errno.h`,后者又包含了`asm-generic/errno.h`。 另外一個問題是,就算我是重復包含了頭文件,那有什么危害么?像上面的三個函數聲明,在程序中聲明兩次也沒有問題,對于具有External Linkage的函數,聲明任意多次也都代表同一個函數。重復包含頭文件有以下問題: 1. 一是使預處理的速度變慢了,要處理很多本來不需要處理的頭文件。 2. 二是如果有`foo.h`包含`bar.h`,`bar.h`又包含`foo.h`的情況,預處理器就陷入死循環了(其實編譯器都會規定一個包含層數的上限)。 3. 三是頭文件里有些代碼不允許重復出現,雖然變量和函數允許多次聲明(只要不是多次定義就行),但頭文件里有些代碼是不允許多次出現的,比如`typedef`類型定義和結構體Tag定義等,在一個程序文件中只允許出現一次。 還有一個問題,既然要`#include`頭文件,那我不如直接在`main.c`中`#include "stack.c"`得了。這樣把`stack.c`和`main.c`合并為同一個程序文件,相當于又回到最初的[例?12.1 “用堆棧實現倒序打印”](ch12s02.html#stackqueue.stackreverse)了。當然這樣也能編譯通過,但是在一個規模較大的項目中不能這么做,假如又有一個`foo.c`也要使用`stack.c`這個模塊怎么辦呢?如果在`foo.c`里面也`#include "stack.c"`,就相當于`push`、`pop`、`is_empty`這三個函數在`main.c`和`foo.c`中都有定義,那么`main.c`和`foo.c`就不能鏈接在一起了。如果采用包含頭文件的辦法,那么這三個函數只在`stack.c`中定義了一次,最后可以把`main.c`、`stack.c`、`foo.c`鏈接在一起。如下圖所示: **圖?20.2.?為什么要包含頭文件而不是`.c`文件** ![為什么要包含頭文件而不是.c文件](https://box.kancloud.cn/2016-04-02_56ff80d50b92c.png) 同樣道理,頭文件中的變量和函數聲明一定不能是定義。如果頭文件中出現變量或函數定義,這個頭文件又被多個`.c`文件包含,那么這些`.c`文件就不能鏈接在一起了。 ### 2.3.?定義和聲明的詳細規則 以上兩節關于定義和聲明只介紹了最基本的規則,在寫代碼時掌握這些基本規則就夠用了,但其實C語言關于定義和聲明還有很多復雜的規則,在分析錯誤原因或者維護規模較大的項目時需要了解這些規則。本節的兩個表格出自[[Standard C]](bi01.html#bibli.standardc "Standard C: A Reference")。 首先看關于函數聲明的規則。 **表?20.1.?Storage Class關鍵字對函數聲明的作用** | Storage Class | File Scope Declaration | Block Scope Declaration | | --- | --- | --- | | none | previous?linkage, can?define | previous?linkage, cannot?define | | extern | previous?linkage, can?define | previous?linkage, cannot?define | | static | internal?linkage, can?define | N/A | 以前我們說“`extern`關鍵字表示這個標識符具有External Linkage”其實是不準確的,準確地說應該是Previous Linkage。Previous Linkage的定義是:這次聲明的標識符具有什么樣的Linkage取決于前一次聲明,這前一次聲明具有相同的標識符名,而且必須是文件作用域的聲明,如果在程序文件中找不到前一次聲明(這次聲明是第一次聲明),那么這個標識符具有External Linkage。例如在一個程序文件中在文件作用域兩次聲明同一個函數: ``` static int f(void); /* internal linkage */ extern int f(void); /* previous linkage */ ``` 則這里的`extern`修飾的標識符具有Interanl Linkage而不是External Linkage。從上表的前兩行可以總結出我們先前所說的規則“函數聲明加不加`extern`關鍵字都一樣”。上表也說明了在文件作用域允許定義函數,在塊作用域不允許定義函數,或者說函數定義不能嵌套。另外,在塊作用域中不允許用`static`關鍵字聲明函數。 關于變量聲明的規則要復雜一些: **表?20.2.?Storage Class關鍵字對變量聲明的作用** | Storage Class | File Scope Declaration | Block Scope Declaration | | --- | --- | --- | | none | external?linkage, static?duration, static?initializer, tentative?definition | no?linkage, automatic?duration, dynamic?initializer, definition | | extern | previous?linkage, static?duration, no?initializer[*], not?a?definition | previous?linkage, static?duration, no?initializer, not?a?definition | | static | internal?linkage, static?duration, static?initializer, tentative?definition | no?linkage, static?duration, static?initializer, definition | 上表的每個單元格里分成四行,分別描述變量的鏈接屬性、生存期,以及這種變量如何初始化,是否算變量定義。鏈接屬性有External Linkage、Internal Linkage、No Linkage和Previous Linkage四種情況,生存期有Static Duration和Automatic Duration兩種情況,請參考本章和上一章的定義。初始化有Static Initializer和Dynamic Initializer兩種情況,前者表示Initializer中只能使用常量表達式,表達式的值必須在編譯時就能確定,后者表示Initializer中可以使用任意的右值表達式,表達式的值可以在運行時計算。是否算變量定義有三種情況,Definition(算變量定義)、Not a Definition(不算變量定義)和Tentative Definition(暫定的變量定義)。什么叫“暫定的變量定義”呢?一個變量聲明具有文件作用域,沒有Storage Class關鍵字修飾,或者用`static`關鍵字修飾,那么如果它有Initializer則編譯器認為它就是一個變量定義,如果它沒有Initializer則編譯器暫定它是變量定義,如果程序文件中有這個變量的明確定義就用明確定義,如果程序文件沒有這個變量的明確定義,就用這個暫定的變量定義<sup>[[32](#ftn.id2789106)]</sup>,這種情況下變量以0初始化。在[[C99]](bi01.html#bibli.c99 "ISO/IEC 9899: Programming Languages - C")中有一個例子: ``` int i1 = 1; // definition, external linkage static int i2 = 2; // definition, internal linkage extern int i3 = 3; // definition, external linkage int i4; // tentative definition, external linkage static int i5; // tentative definition, internal linkage int i1; // valid tentative definition, refers to previous int i2; // 6.2.2 renders undefined, linkage disagreement int i3; // valid tentative definition, refers to previous int i4; // valid tentative definition, refers to previous int i5; // 6.2.2 renders undefined, linkage disagreement extern int i1; // refers to previous, whose linkage is external extern int i2; // refers to previous, whose linkage is internal extern int i3; // refers to previous, whose linkage is external extern int i4; // refers to previous, whose linkage is external extern int i5; // refers to previous, whose linkage is internal ``` 變量`i2`和`i5`第一次聲明為Internal Linkage,第二次又聲明為External Linkage,這是不允許的,編譯器會報錯。注意上表中標有`[*]`的單元格,對于文件作用域的`extern`變量聲明,C99是允許帶Initializer的,并且認為它是一個定義,但是`gcc`對于這種寫法會報警告,為了兼容性應避免這種寫法。 * * * <sup>[[32](#id2789106)]</sup> 由于本書沒有提及將不完全類型進行組合的問題,所以這條規則被我簡化了,真正的規則還要復雜一些。讀者可以參考C99中有關Incomplete Type和Composite Type的條款。Tentative Definition的完整定義在C99的6.9.2節條款2。 ## 3.?靜態庫 有時候需要把一組代碼編譯成一個庫,這個庫在很多項目中都要用到,例如`libc`就是這樣一個庫,我們在不同的程序中都會用到`libc`中的庫函數(例如`printf`),也會用到`libc`中的變量(例如以后要講到的`environ`變量)。本節介紹怎么創建這樣一個庫。 我們繼續用`stack.c`的例子。為了便于理解,我們把`stack.c`拆成四個程序文件(雖然實際上沒太大必要),把`main.c`改得簡單一些,頭文件`stack.h`不變,本節用到的代碼如下所示: ``` /* stack.c */ char stack[512]; int top = -1; ``` ``` /* push.c */ extern char stack[512]; extern int top; void push(char c) { stack[++top] = c; } ``` ``` /* pop.c */ extern char stack[512]; extern int top; char pop(void) { return stack[top--]; } ``` ``` /* is_empty.c */ extern int top; int is_empty(void) { return top == -1; } ``` ``` /* stack.h */ #ifndef STACK_H #define STACK_H extern void push(char); extern char pop(void); extern int is_empty(void); #endif ``` ``` /* main.c */ #include <stdio.h> #include "stack.h" int main(void) { push('a'); return 0; } ``` 這些文件的目錄結構是: ``` $ tree . |-- main.c `-- stack |-- is_empty.c |-- pop.c |-- push.c |-- stack.c `-- stack.h 1 directory, 6 files ``` 我們把`stack.c`、`push.c`、`pop.c`、`is_empty.c`編譯成目標文件: ``` $ gcc -c stack/stack.c stack/push.c stack/pop.c stack/is_empty.c ``` 然后打包成一個靜態庫`libstack.a`: ``` $ ar rs libstack.a stack.o push.o pop.o is_empty.o ar: creating libstack.a ``` 庫文件名都是以`lib`開頭的,靜態庫以`.a`作為后綴,表示Archive。`ar`命令類似于`tar`命令,起一個打包的作用,但是把目標文件打包成靜態庫只能用`ar`命令而不能用`tar`命令。選項`r`表示將后面的文件列表添加到文件包,如果文件包不存在就創建它,如果文件包中已有同名文件就替換成新的。`s`是專用于生成靜態庫的,表示為靜態庫創建索引,這個索引被鏈接器使用。`ranlib`命令也可以為靜態庫創建索引,以上命令等價于: ``` $ ar r libstack.a stack.o push.o pop.o is_empty.o $ ranlib libstack.a ``` 然后我們把`libstack.a`和`main.c`編譯鏈接在一起: ``` $ gcc main.c -L. -lstack -Istack -o main ``` `-L`選項告訴編譯器去哪里找需要的庫文件,`-L.`表示在當前目錄找。`-lstack`告訴編譯器要鏈接`libstack`庫,`-I`選項告訴編譯器去哪里找頭文件。注意,即使庫文件就在當前目錄,編譯器默認也不會去找的,所以`-L.`選項不能少。編譯器默認會找的目錄可以用`-print-search-dirs`選項查看: ``` $ gcc -print-search-dirs install: /usr/lib/gcc/i486-linux-gnu/4.3.2/ programs: =/usr/lib/gcc/i486-linux-gnu/4.3.2/:/usr/lib/gcc/i486-linux-gnu/4.3.2/:/usr/lib/gcc/i486-linux-gnu/:/usr/lib/gcc/i486-linux-gnu/4.3.2/:/usr/lib/gcc/i486-linux-gnu/:/usr/libexec/gcc/i486-linux-gnu/4.3.2/:/usr/libexec/gcc/i486-linux-gnu/:/usr/lib/gcc/i486-linux-gnu/4.3.2/:/usr/lib/gcc/i486-linux-gnu/:/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../i486-linux-gnu/bin/i486-linux-gnu/4.3.2/:/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../i486-linux-gnu/bin/ libraries: =/usr/lib/gcc/i486-linux-gnu/4.3.2/:/usr/lib/gcc/i486-linux-gnu/4.3.2/:/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../i486-linux-gnu/lib/i486-linux-gnu/4.3.2/:/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../i486-linux-gnu/lib/../lib/:/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../i486-linux-gnu/4.3.2/:/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../lib/:/lib/i486-linux-gnu/4.3.2/:/lib/../lib/:/usr/lib/i486-linux-gnu/4.3.2/:/usr/lib/../lib/:/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../i486-linux-gnu/lib/:/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../:/lib/:/usr/lib/ ``` 其中的`libraries`就是庫文件的搜索路徑列表,各路徑之間用`:`號隔開。編譯器會在這些搜索路徑以及`-L`選項指定的路徑中查找用`-l`選項指定的庫,比如`-lstack`,編譯器會首先找有沒有共享庫`libstack.so`,如果有就鏈接它,如果沒有就找有沒有靜態庫`libstack.a`,如果有就鏈接它。所以編譯器是優先考慮共享庫的,如果希望編譯器只鏈接靜態庫,可以指定`-static`選項。 那么鏈接共享庫和鏈接靜態庫有什么區別呢?在[第?2?節 “`main`函數和啟動例程”](ch19s02.html#asmc.main)講過,在鏈接`libc`共享庫時只是指定了動態鏈接器和該程序所需要的庫文件,并沒有真的做鏈接,可執行文件`main`中調用的`libc`庫函數仍然是未定義符號,要在運行時做動態鏈接。而在鏈接靜態庫時,鏈接器會把靜態庫中的目標文件取出來和可執行文件真正鏈接在一起。我們通過反匯編看上一步生成的可執行文件`main`: ``` $ objdump -d main ... 08048394 <main>: 8048394: 8d 4c 24 04 lea 0x4(%esp),%ecx 8048398: 83 e4 f0 and $0xfffffff0,%esp 804839b: ff 71 fc pushl -0x4(%ecx) ... 080483c0 <push>: 80483c0: 55 push %ebp 80483c1: 89 e5 mov %esp,%ebp 80483c3: 83 ec 04 sub $0x4,%esp ``` 有意思的是,`main.c`只調用了`push`這一個函數,所以鏈接生成的可執行文件中也只有`push`而沒有`pop`和`is_empty`。這是使用靜態庫的一個好處,鏈接器可以從靜態庫中只取出需要的部分來做鏈接。如果是直接把那些目標文件和`main.c`編譯鏈接在一起: ``` $ gcc main.c stack.o push.o pop.o is_empty.o -Istack -o main ``` 則沒有用到的函數也會鏈接進來。當然另一個好處就是使用靜態庫只需寫一個庫文件名,而不需要寫一長串目標文件名。 ## 4.?共享庫 ### 4.1.?編譯、鏈接、運行 組成共享庫的目標文件和一般的目標文件有所不同,在編譯時要加`-fPIC`選項,例如: ``` $ gcc -c -fPIC stack/stack.c stack/push.c stack/pop.c stack/is_empty.c ``` `-f`后面跟一些編譯選項,`PIC`是其中一種,表示生成位置無關代碼(Position Independent Code)。那么用`-fPIC`生成的目標文件和一般的目標文件有什么不同呢?下面分析這個問題。 我們知道一般的目標文件稱為Relocatable,在鏈接時可以把目標文件中各段的地址做重定位,重定位時需要修改指令。我們先不加`-fPIC`選項編譯生成目標文件: ``` $ gcc -c -g stack/stack.c stack/push.c stack/pop.c stack/is_empty.c ``` 由于接下來要用`objdump -dS`把反匯編指令和源代碼穿插起來分析,所以用`-g`選項加調試信息。注意,加調試信息必須在編譯每個目標文件時用`-g`選項,而不能只在最后編譯生成可執行文件時用`-g`選項。反匯編查看`push.o`: ``` $ objdump -dS push.o push.o: file format elf32-i386 Disassembly of section .text: 00000000 <push>: /* push.c */ extern char stack[512]; extern int top; void push(char c) { 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 04 sub $0x4,%esp 6: 8b 45 08 mov 0x8(%ebp),%eax 9: 88 45 fc mov %al,-0x4(%ebp) stack[++top] = c; c: a1 00 00 00 00 mov 0x0,%eax 11: 83 c0 01 add $0x1,%eax 14: a3 00 00 00 00 mov %eax,0x0 19: 8b 15 00 00 00 00 mov 0x0,%edx 1f: 0f b6 45 fc movzbl -0x4(%ebp),%eax 23: 88 82 00 00 00 00 mov %al,0x0(%edx) } 29: c9 leave 2a: c3 ret ``` 指令中凡是用到`stack`和`top`的地址都用0x0表示,準備在重定位時修改。再看`readelf`輸出的`.rel.text`段的信息: ``` Relocation section '.rel.text' at offset 0x848 contains 4 entries: Offset Info Type Sym.Value Sym. Name 0000000d 00001001 R_386_32 00000000 top 00000015 00001001 R_386_32 00000000 top 0000001b 00001001 R_386_32 00000000 top 00000025 00001101 R_386_32 00000000 stack ``` 標出了指令中有四處需要在重定位時修改。下面編譯鏈接成可執行文件之后再做反匯編分析: ``` $ gcc -g main.c stack.o push.o pop.o is_empty.o -Istack -o main $ objdump -dS main ... 080483c0 <push>: /* push.c */ extern char stack[512]; extern int top; void push(char c) { 80483c0: 55 push %ebp 80483c1: 89 e5 mov %esp,%ebp 80483c3: 83 ec 04 sub $0x4,%esp 80483c6: 8b 45 08 mov 0x8(%ebp),%eax 80483c9: 88 45 fc mov %al,-0x4(%ebp) stack[++top] = c; 80483cc: a1 10 a0 04 08 mov 0x804a010,%eax 80483d1: 83 c0 01 add $0x1,%eax 80483d4: a3 10 a0 04 08 mov %eax,0x804a010 80483d9: 8b 15 10 a0 04 08 mov 0x804a010,%edx 80483df: 0f b6 45 fc movzbl -0x4(%ebp),%eax 80483e3: 88 82 40 a0 04 08 mov %al,0x804a040(%edx) } 80483e9: c9 leave 80483ea: c3 ret 80483eb: 90 nop ... ``` 原來指令中的0x0被修改成了0x804a010和0x804a040,這樣做了重定位之后,各段的加載地址就定死了,因為在指令中使用了絕對地址。 現在看用`-fPIC`編譯生成的目標文件有什么不同: ``` $ gcc -c -g -fPIC stack/stack.c stack/push.c stack/pop.c stack/is_empty.c $ objdump -dS push.o push.o: file format elf32-i386 Disassembly of section .text: 00000000 <push>: /* push.c */ extern char stack[512]; extern int top; void push(char c) { 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 53 push %ebx 4: 83 ec 04 sub $0x4,%esp 7: e8 fc ff ff ff call 8 <push+0x8> c: 81 c3 02 00 00 00 add $0x2,%ebx 12: 8b 45 08 mov 0x8(%ebp),%eax 15: 88 45 f8 mov %al,-0x8(%ebp) stack[++top] = c; 18: 8b 83 00 00 00 00 mov 0x0(%ebx),%eax 1e: 8b 00 mov (%eax),%eax 20: 8d 50 01 lea 0x1(%eax),%edx 23: 8b 83 00 00 00 00 mov 0x0(%ebx),%eax 29: 89 10 mov %edx,(%eax) 2b: 8b 83 00 00 00 00 mov 0x0(%ebx),%eax 31: 8b 08 mov (%eax),%ecx 33: 8b 93 00 00 00 00 mov 0x0(%ebx),%edx 39: 0f b6 45 f8 movzbl -0x8(%ebp),%eax 3d: 88 04 0a mov %al,(%edx,%ecx,1) } 40: 83 c4 04 add $0x4,%esp 43: 5b pop %ebx 44: 5d pop %ebp 45: c3 ret Disassembly of section .text.__i686.get_pc_thunk.bx: 00000000 <__i686.get_pc_thunk.bx>: 0: 8b 1c 24 mov (%esp),%ebx 3: c3 ret ``` 指令中用到的`stack`和`top`的地址不再以0x0表示,而是以`0x0(%ebx)`表示,但其中還是留有0x0準備做進一步修改。再看`readelf`輸出的`.rel.text`段: ``` Relocation section '.rel.text' at offset 0x94c contains 6 entries: Offset Info Type Sym.Value Sym. Name 00000008 00001202 R_386_PC32 00000000 __i686.get_pc_thunk.bx 0000000e 0000130a R_386_GOTPC 00000000 _GLOBAL_OFFSET_TABLE_ 0000001a 00001403 R_386_GOT32 00000000 top 00000025 00001403 R_386_GOT32 00000000 top 0000002d 00001403 R_386_GOT32 00000000 top 00000035 00001503 R_386_GOT32 00000000 stack ``` `top`和`stack`對應的記錄類型不再是`R_386_32`了,而是`R_386_GOT32`,有什么區別呢?我們先編譯生成共享庫再做反匯編分析: ``` $ gcc -shared -o libstack.so stack.o push.o pop.o is_empty.o $ objdump -dS libstack.so ... 0000047c <push>: /* push.c */ extern char stack[512]; extern int top; void push(char c) { 47c: 55 push %ebp 47d: 89 e5 mov %esp,%ebp 47f: 53 push %ebx 480: 83 ec 04 sub $0x4,%esp 483: e8 ef ff ff ff call 477 <__i686.get_pc_thunk.bx> 488: 81 c3 6c 1b 00 00 add $0x1b6c,%ebx 48e: 8b 45 08 mov 0x8(%ebp),%eax 491: 88 45 f8 mov %al,-0x8(%ebp) stack[++top] = c; 494: 8b 83 f4 ff ff ff mov -0xc(%ebx),%eax 49a: 8b 00 mov (%eax),%eax 49c: 8d 50 01 lea 0x1(%eax),%edx 49f: 8b 83 f4 ff ff ff mov -0xc(%ebx),%eax 4a5: 89 10 mov %edx,(%eax) 4a7: 8b 83 f4 ff ff ff mov -0xc(%ebx),%eax 4ad: 8b 08 mov (%eax),%ecx 4af: 8b 93 f8 ff ff ff mov -0x8(%ebx),%edx 4b5: 0f b6 45 f8 movzbl -0x8(%ebp),%eax 4b9: 88 04 0a mov %al,(%edx,%ecx,1) } 4bc: 83 c4 04 add $0x4,%esp 4bf: 5b pop %ebx 4c0: 5d pop %ebp 4c1: c3 ret 4c2: 90 nop 4c3: 90 nop ... ``` 和先前的結果不同,指令中的`0x0(%ebx)`被修改成`-0xc(%ebx)`和`-0x8(%ebx)`,而不是修改成絕對地址。所以共享庫各段的加載地址并沒有定死,可以加載到任意位置,因為指令中沒有使用絕對地址,因此稱為位置無關代碼。另外,注意這幾條指令: ``` 494: 8b 83 f4 ff ff ff mov -0xc(%ebx),%eax 49a: 8b 00 mov (%eax),%eax 49c: 8d 50 01 lea 0x1(%eax),%edx ``` 和先前的指令對比一下: ``` 80483cc: a1 10 a0 04 08 mov 0x804a010,%eax 80483d1: 83 c0 01 add $0x1,%eax ``` 可以發現,`-0xc(%ebx)`這個地址并不是變量`top`的地址,這個地址的內存單元中又保存了另外一個地址,這另外一個地址才是變量`top`的地址,所以`mov -0xc(%ebx),%eax`是把變量`top`的地址傳給`eax`,而`mov (%eax),%eax`才是從`top`的地址中取出`top`的值傳給`eax`。`lea 0x1(%eax),%edx`是把`top`的值加1存到`edx`中,如下圖所示: **圖?20.3.?間接尋址** ![間接尋址](https://box.kancloud.cn/2016-04-02_56ff80d520858.png) `top`和`stack`的絕對地址保存在一個地址表中,而指令通過地址表做間接尋址,因此避免了將絕對地址寫死在指令中,這也是一種避免硬編碼的策略。 現在把`main.c`和共享庫編譯鏈接在一起,然后運行: ``` $ gcc main.c -g -L. -lstack -Istack -o main $ ./main ./main: error while loading shared libraries: libstack.so: cannot open shared object file: No such file or directory ``` 結果出乎意料,編譯的時候沒問題,由于指定了`-L.`選項,編譯器可以在當前目錄下找到`libstack.so`,而運行時卻說找不到`libstack.so`。那么運行時在哪些路徑下找共享庫呢?我們先用`ldd`命令查看可執行文件依賴于哪些共享庫: ``` $ ldd main linux-gate.so.1 => (0xb7f5c000) libstack.so => not found libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7dcf000) /lib/ld-linux.so.2 (0xb7f42000) ``` `ldd`模擬運行一遍`main`,在運行過程中做動態鏈接,從而得知這個可執行文件依賴于哪些共享庫,每個共享庫都在什么路徑下,加載到進程地址空間的什么地址。`/lib/ld-linux.so.2`是動態鏈接器,它的路徑是在編譯鏈接時指定的,我們在[第?2?節 “`main`函數和啟動例程”](ch19s02.html#asmc.main)講過`gcc`在做鏈接時用`-dynamic-linker`指定動態鏈接器的路徑,它也像其它共享庫一樣加載到進程的地址空間中。`libc.so.6`的路徑`/lib/tls/i686/cmov/libc.so.6`是由動態鏈接器`ld-linux.so.2`在做動態鏈接時搜索到的,而`libstack.so`的路徑沒有找到。`linux-gate.so.1`這個共享庫其實并不存在于文件系統中,它是由內核虛擬出來的共享庫,所以它沒有對應的路徑,它負責處理系統調用。總之,共享庫的搜索路徑由動態鏈接器決定,從`ld.so(8)`的Man Page可以查到共享庫路徑的搜索順序: 1. 首先在環境變量`LD_LIBRARY_PATH`所記錄的路徑中查找。 2. 然后從緩存文件`/etc/ld.so.cache`中查找。這個緩存文件由`ldconfig`命令讀取配置文件`/etc/ld.so.conf`之后生成,稍后詳細解釋。 3. 如果上述步驟都找不到,則到默認的系統路徑中查找,先是/usr/lib然后是/lib。 先試試第一種方法,在運行`main`時通過環境變量`LD_LIBRARY_PATH`把當前目錄添加到共享庫的搜索路徑: ``` $ LD_LIBRARY_PATH=. ./main ``` 這種方法只適合在開發中臨時用一下,通常`LD_LIBRARY_PATH`是不推薦使用的,盡量不要設置這個環境變量,理由可以參考Why LD_LIBRARY_PATH is bad([http://www.visi.com/~barr/ldpath.html](http://www.visi.com/~barr/ldpath.html))。 再試試第二種方法,這是最常用的方法。把`libstack.so`所在目錄的絕對路徑(比如/home/akaedu/somedir)添加到`/etc/ld.so.conf`中(該文件中每個路徑占一行),然后運行`ldconfig`: ``` $ sudo ldconfig -v ... /home/akaedu/somedir: libstack.so -> libstack.so /lib: libe2p.so.2 -> libe2p.so.2.3 libncursesw.so.5 -> libncursesw.so.5.6 ... /usr/lib: libkdeinit_klauncher.so -> libkdeinit_klauncher.so libv4l2.so.0 -> libv4l2.so.0 ... /usr/lib64: /lib/tls: (hwcap: 0x8000000000000000) /usr/lib/sse2: (hwcap: 0x0000000004000000) ... /usr/lib/tls: (hwcap: 0x8000000000000000) ... /usr/lib/i686: (hwcap: 0x0008000000000000) /usr/lib/i586: (hwcap: 0x0004000000000000) ... /usr/lib/i486: (hwcap: 0x0002000000000000) ... /lib/tls/i686: (hwcap: 0x8008000000000000) /usr/lib/i686/cmov: (hwcap: 0x0008000000008000) ... /lib/tls/i686/cmov: (hwcap: 0x8008000000008000) ``` `ldconfig`命令除了處理`/etc/ld.so.conf`中配置的目錄之外,還處理一些默認目錄,如`/lib`、`/usr/lib`等,處理之后生成`/etc/ld.so.cache`緩存文件,動態鏈接器就從這個緩存中搜索共享庫。hwcap是x86平臺的Linux特有的一種機制,系統檢測到當前平臺是i686而不是`i586`或`i486`,所以在運行程序時使用i686的庫,這樣可以更好地發揮平臺的性能,也可以利用一些新的指令,所以上面`ldd`命令的輸出結果顯示動態鏈接器搜索到的`libc`是`/lib/tls/i686/cmov/libc.so.6`,而不是`/lib/libc.so.6`。現在再用`ldd`命令查看,`libstack.so`就能找到了: ``` $ ldd main linux-gate.so.1 => (0xb809c000) libstack.so => /home/akaedu/somedir/libstack.so (0xb806a000) libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7f0c000) /lib/ld-linux.so.2 (0xb8082000) ``` 第三種方法就是把`libstack.so`拷到`/usr/lib`或`/lib`目錄,這樣可以確保動態鏈接器能找到這個共享庫。 其實還有第四種方法,在編譯可執行文件`main`的時候就把`libstack.so`的路徑寫死在可執行文件中: ``` $ gcc main.c -g -L. -lstack -Istack -o main -Wl,-rpath,/home/akaedu/somedir ``` `-Wl,-rpath,/home/akaedu/somedir`表示`-rpath /home/akaedu/somedir`是由`gcc`傳遞給鏈接器的選項。可以看到`readelf`的結果多了一條`rpath`記錄: ``` $ readelf -a main ... Dynamic section at offset 0xf10 contains 23 entries: Tag Type Name/Value 0x00000001 (NEEDED) Shared library: [libstack.so] 0x00000001 (NEEDED) Shared library: [libc.so.6] 0x0000000f (RPATH) Library rpath: [/home/akaedu/somedir] ... ``` 還可以看出,可執行文件運行時需要哪些共享庫也都記錄在`.dynamic`段中。當然`rpath`這種辦法也是不推薦的,把共享庫的路徑定死了,失去了靈活性。 ### 4.2.?動態鏈接的過程 本節研究一下在`main.c`中調用共享庫的函數`push`是如何實現的。首先反匯編看一下`main`的指令: ``` $ objdump -dS main ... Disassembly of section .plt: 080483a8 <__gmon_start__@plt-0x10>: 80483a8: ff 35 f8 9f 04 08 pushl 0x8049ff8 80483ae: ff 25 fc 9f 04 08 jmp *0x8049ffc 80483b4: 00 00 add %al,(%eax) ... 080483d8 <push@plt>: 80483d8: ff 25 08 a0 04 08 jmp *0x804a008 80483de: 68 10 00 00 00 push $0x10 80483e3: e9 c0 ff ff ff jmp 80483a8 <_init+0x30> Disassembly of section .text: ... 080484a4 <main>: /* main.c */ #include <stdio.h> #include "stack.h" int main(void) { 80484a4: 8d 4c 24 04 lea 0x4(%esp),%ecx 80484a8: 83 e4 f0 and $0xfffffff0,%esp 80484ab: ff 71 fc pushl -0x4(%ecx) 80484ae: 55 push %ebp 80484af: 89 e5 mov %esp,%ebp 80484b1: 51 push %ecx 80484b2: 83 ec 04 sub $0x4,%esp push('a'); 80484b5: c7 04 24 61 00 00 00 movl $0x61,(%esp) 80484bc: e8 17 ff ff ff call 80483d8 <push@plt> ... ``` 和[第?3?節 “靜態庫”](ch20s03.html#link.staticlib)鏈接靜態庫不同,`push`函數沒有鏈接到可執行文件中。而且`call 80483d8 &lt;push@plt&gt;`這條指令調用的也不是`push`函數的地址。共享庫是位置無關代碼,在運行時可以加載到任意地址,其加載地址只有在動態鏈接時才能確定,所以在`main`函數中不可能直接通過絕對地址調用`push`函數,也是通過間接尋址來找`push`函數的。對照著上面的指令,我們用`gdb`跟蹤一下: ``` $ gdb main ... (gdb) start Breakpoint 1 at 0x80484b5: file main.c, line 7. Starting program: /home/akaedu/somedir/main main () at main.c:7 7 push('a'); (gdb) si 0x080484bc 7 push('a'); (gdb) si 0x080483d8 in push@plt () Current language: auto; currently asm ``` 跳轉到`.plt`段中,現在將要執行一條`jmp *0x804a008`指令,我們看看0x804a008這個地址里存的是什么: ``` (gdb) x 0x804a008 0x804a008 <_GLOBAL_OFFSET_TABLE_+20>: 0x080483de ``` 原來就是下一條指令`push $0x10`的地址。繼續跟蹤下去: ``` (gdb) si 0x080483de in push@plt () (gdb) si 0x080483e3 in push@plt () (gdb) si 0x080483a8 in ?? () (gdb) si 0x080483ae in ?? () (gdb) si 0xb806a080 in ?? () from /lib/ld-linux.so.2 ``` 最終進入了動態鏈接器`/lib/ld-linux.so.2`,在其中完成動態鏈接的過程并調用`push`函數,我們不深入這些細節了,直接用`finish`命令返回到`main`函數: ``` (gdb) finish Run till exit from #0 0xb806a080 in ?? () from /lib/ld-linux.so.2 main () at main.c:8 8 return 0; Current language: auto; currently c ``` 這時再看看0x804a008這個地址里存的是什么: ``` (gdb) x 0x804a008 0x804a008 <_GLOBAL_OFFSET_TABLE_+20>: 0xb803f47c (gdb) x 0xb803f47c 0xb803f47c <push>: 0x53e58955 ``` 動態鏈接器已經把`push`函數的地址存在這里了,所以下次再調用`push`函數就可以直接從`jmp *0x804a008`指令跳到它的地址,而不必再進入`/lib/ld-linux.so.2`做動態鏈接了。 ### 4.3.?共享庫的命名慣例 你可能已經注意到了,系統的共享庫通常帶有符號鏈接,例如: ``` $ ls -l /lib ... -rwxr-xr-x 1 root root 1315024 2009-01-09 22:10 libc-2.8.90.so lrwxrwxrwx 1 root root 14 2008-07-04 05:58 libcap.so.1 -> libcap.so.1.10 -rw-r--r-- 1 root root 10316 2007-08-01 03:20 libcap.so.1.10 lrwxrwxrwx 1 root root 14 2008-11-01 08:55 libcap.so.2 -> libcap.so.2.10 -rw-r--r-- 1 root root 13792 2008-06-12 21:39 libcap.so.2.10 ... lrwxrwxrwx 1 root root 14 2009-01-13 09:28 libc.so.6 -> libc-2.8.90.so ... $ ls -l /usr/lib/libc.so -rw-r--r-- 1 root root 238 2009-01-09 21:59 /usr/lib/libc.so ``` 按照共享庫的命名慣例,每個共享庫有三個文件名:real name、soname和linker name。真正的庫文件(而不是符號鏈接)的名字是real name,包含完整的共享庫版本號。例如上面的`libcap.so.1.10`、`libc-2.8.90.so`等。 soname是一個符號鏈接的名字,只包含共享庫的主版本號,主版本號一致即可保證庫函數的接口一致,因此應用程序的`.dynamic`段只記錄共享庫的soname,只要soname一致,這個共享庫就可以用。例如上面的`libcap.so.1`和`libcap.so.2`是兩個主版本號不同的`libcap`,有些應用程序依賴于`libcap.so.1`,有些應用程序依賴于`libcap.so.2`,但對于依賴`libcap.so.1`的應用程序來說,真正的庫文件不管是`libcap.so.1.10`還是`libcap.so.1.11`都可以用,所以使用共享庫可以很方便地升級庫文件而不需要重新編譯應用程序,這是靜態庫所沒有的優點。注意`libc`的版本編號有一點特殊,`libc-2.8.90.so`的主版本號是6而不是2或2.8。 linker name僅在編譯鏈接時使用,`gcc`的`-L`選項應該指定linker name所在的目錄。有的linker name是庫文件的一個符號鏈接,有的linker name是一段鏈接腳本。例如上面的`libc.so`就是一個linker name,它是一段鏈接腳本: ``` $ cat /usr/lib/libc.so /* GNU ld script Use the shared library, but some functions are only in the static library, so try that secondarily. */ OUTPUT_FORMAT(elf32-i386) GROUP ( /lib/libc.so.6 /usr/lib/libc_nonshared.a AS_NEEDED ( /lib/ld-linux.so.2 ) ) ``` 下面重新編譯我們的`libstack`,指定它的soname: ``` $ gcc -shared -Wl,-soname,libstack.so.1 -o libstack.so.1.0 stack.o push.o pop.o is_empty.o ``` 這樣編譯生成的庫文件是`libstack.so.1.0`,是real name,但這個庫文件中記錄了它的soname是`libstack.so.1`: ``` $ readelf -a libstack.so.1.0 ... Dynamic section at offset 0xf10 contains 22 entries: Tag Type Name/Value 0x00000001 (NEEDED) Shared library: [libc.so.6] 0x0000000e (SONAME) Library soname: [libstack.so.1] ... ``` 如果把`libstack.so.1.0`所在的目錄加入`/etc/ld.so.conf`中,然后運行`ldconfig`命令,`ldconfig`會自動創建一個soname的符號鏈接: ``` $ sudo ldconfig $ ls -l libstack* lrwxrwxrwx 1 root root 15 2009-01-21 17:52 libstack.so.1 -> libstack.so.1.0 -rwxr-xr-x 1 akaedu akaedu 10142 2009-01-21 17:49 libstack.so.1.0 ``` 但這樣編譯鏈接`main.c`卻會報錯: ``` $ gcc main.c -L. -lstack -Istack -o main /usr/bin/ld: cannot find -lstack collect2: ld returned 1 exit status ``` 注意,要做這個實驗,你得把先前編譯的`libstack`共享庫、靜態庫都刪掉,如果先前拷到`/lib`或者`/usr/lib`下了也刪掉,只留下`libstack.so.1.0`和`libstack.so.1`,這樣你會發現編譯器不認這兩個名字,因為編譯器只認linker name。可以先創建一個linker name的符號鏈接,然后再編譯就沒問題了: ``` $ ln -s libstack.so.1.0 libstack.so $ gcc main.c -L. -lstack -Istack -o main ``` ## 5.?虛擬內存管理 我們知道操作系統利用體系結構提供的VA到PA的轉換機制實現虛擬內存管理。有了共享庫的基礎知識之后,現在我們可以進一步理解虛擬內存管理了。首先分析一個例子: ``` $ ps PID TTY TIME CMD 29977 pts/0 00:00:00 bash 30032 pts/0 00:00:00 ps $ cat /proc/29977/maps 08048000-080f4000 r-xp 00000000 08:15 688142 /bin/bash 080f4000-080f9000 rw-p 000ac000 08:15 688142 /bin/bash 080f9000-080fe000 rw-p 080f9000 00:00 0 09283000-09497000 rw-p 09283000 00:00 0 [heap] b7ca8000-b7cb2000 r-xp 00000000 08:15 581665 /lib/tls/i686/cmov/libnss_files-2.8.90.so b7cb2000-b7cb3000 r--p 00009000 08:15 581665 /lib/tls/i686/cmov/libnss_files-2.8.90.so b7cb3000-b7cb4000 rw-p 0000a000 08:15 581665 /lib/tls/i686/cmov/libnss_files-2.8.90.so ... b7e15000-b7f6d000 r-xp 00000000 08:15 581656 /lib/tls/i686/cmov/libc-2.8.90.so b7f6d000-b7f6f000 r--p 00158000 08:15 581656 /lib/tls/i686/cmov/libc-2.8.90.so b7f6f000-b7f70000 rw-p 0015a000 08:15 581656 /lib/tls/i686/cmov/libc-2.8.90.so ... b7fbd000-b7fd7000 r-xp 00000000 08:15 565466 /lib/ld-2.8.90.so b7fd7000-b7fd8000 r-xp b7fd7000 00:00 0 [vdso] b7fd8000-b7fd9000 r--p 0001a000 08:15 565466 /lib/ld-2.8.90.so b7fd9000-b7fda000 rw-p 0001b000 08:15 565466 /lib/ld-2.8.90.so bfac5000-bfada000 rw-p bffeb000 00:00 0 [stack] ``` 用`ps`命令查看當前終端下的進程,得知`bash`進程的id是29977,然后用`cat /proc/29977/maps`命令查看它的虛擬地址空間。`/proc`目錄中的文件并不是真正的磁盤文件,而是由內核虛擬出來的文件系統,當前系統中運行的每個進程在`/proc`下都有一個子目錄,目錄名就是進程的id,查看目錄下的文件可以得到該進程的相關信息。此外,用`pmap 29977`命令也可以得到類似的輸出結果。 **圖?20.4.?進程地址空間** ![進程地址空間](https://box.kancloud.cn/2016-04-02_56ff80d52fc91.png) 在[第?4?節 “MMU”](ch17s04.html#arch.mmu)講過,x86平臺的虛擬地址空間是0x0000 0000~0xffff ffff,大致上前3GB(0x0000 0000~0xbfff ffff)是用戶空間,后1GB(0xc000 0000~0xffff ffff)是內核空間,在這里得到了印證。0x0804 8000-0x080f 4000是從`/bin/bash`加載到內存的,訪問權限為`r-x`,表示Text Segment,包含`.text`段、`.rodata`段、`.plt`段等。0x080f 4000-0x080f 9000也是從`/bin/bash`加載到內存的,訪問權限為`rw-`,表示Data Segment,包含`.data`段、`.bss`段等。 0x0928 3000-0x0949 7000不是從磁盤文件加載到內存的,這段空間稱為堆(Heap),以后會講到用`malloc`函數動態分配內存是在這里分配的。從0xb7ca 8000開始是共享庫的映射空間,每個共享庫也分為幾個Segment,每個Segment有不同的訪問權限。可以看到,從堆空間的結束地址(0x0949 7000)到共享庫映射空間的起始地址(0xb7ca 8000)之間有很大的地址空洞,在動態分配內存時堆空間是可以向高地址增長的。堆空間的地址上限(0x09497000)稱為Break,堆空間要向高地址增長就要抬高Break,映射新的虛擬內存頁面到物理內存,這是通過系統調用`brk`實現的,`malloc`函數也是調用`brk`向內核請求分配內存的。 `/lib/ld-2.8.90.so`就是動態鏈接器`/lib/ld-linux.so.2`,后者是前者的符號鏈接。標有`[vdso]`的地址范圍是`linux-gate.so.1`的映射空間,我們講過這個共享庫是由內核虛擬出來的。0xbfac 5000-0xbfad a000是棧空間,其中高地址的部分保存著進程的環境變量和命令行參數,低地址的部分保存函數棧幀,棧空間是向低地址增長的,但顯然沒有堆空間那么大的可供增長的余地,因為實際的應用程序動態分配大量內存的并不少見,但是有幾十層深的函數調用并且每層調用都有很多局部變量的非常少見。總之,棧空間是可能用盡的,并且比堆空間更容易用盡,在[第?3?節 “遞歸”](ch05s03.html#func2.recursion)講過,無窮遞歸會用盡棧空間最終導致段錯誤。 虛擬內存管理起到了什么作用呢?可以從以下幾個方面來理解。 第一,虛擬內存管理可以控制物理內存的訪問權限。物理內存本身是不限制訪問的,任何地址都可以讀寫,而操作系統要求不同的頁面具有不同的訪問權限,這是利用CPU模式和MMU的內存保護機制實現的。例如,Text Segment被只讀保護起來,防止被錯誤的指令意外改寫,內核地址空間也被保護起來,防止在用戶模式下執行錯誤的指令意外改寫內核數據。這樣,執行錯誤指令或惡意代碼的破壞能力受到了限制,頂多使當前進程因段錯誤終止,而不會影響整個系統的穩定性。 第二,虛擬內存管理最主要的作用是讓每個進程有獨立的地址空間。所謂獨立的地址空間是指,不同進程中的同一個VA被MMU映射到不同的PA,并且在某一個進程中訪問任何地址都不可能訪問到另外一個進程的數據,這樣使得任何一個進程由于執行錯誤指令或惡意代碼導致的非法內存訪問都不會意外改寫其它進程的數據,不會影響其它進程的運行,從而保證整個系統的穩定性。另一方面,每個進程都認為自己獨占整個虛擬地址空間,這樣鏈接器和加載器的實現會比較容易,不必考慮各進程的地址范圍是否沖突。 繼續前面的實驗,再打開一個終端窗口,看一下這個新的`bash`進程的地址空間,可以發現和先前的`bash`進程地址空間的布局差不多: ``` $ ps PID TTY TIME CMD 30697 pts/1 00:00:00 bash 30749 pts/1 00:00:00 ps $ cat /proc/30697/maps 08048000-080f4000 r-xp 00000000 08:15 688142 /bin/bash 080f4000-080f9000 rw-p 000ac000 08:15 688142 /bin/bash 080f9000-080fe000 rw-p 080f9000 00:00 0 082d7000-084f9000 rw-p 082d7000 00:00 0 [heap] b7cf1000-b7cfb000 r-xp 00000000 08:15 581665 /lib/tls/i686/cmov/libnss_files-2.8.90.so b7cfb000-b7cfc000 r--p 00009000 08:15 581665 /lib/tls/i686/cmov/libnss_files-2.8.90.so b7cfc000-b7cfd000 rw-p 0000a000 08:15 581665 /lib/tls/i686/cmov/libnss_files-2.8.90.so ... b7e5e000-b7fb6000 r-xp 00000000 08:15 581656 /lib/tls/i686/cmov/libc-2.8.90.so b7fb6000-b7fb8000 r--p 00158000 08:15 581656 /lib/tls/i686/cmov/libc-2.8.90.so b7fb8000-b7fb9000 rw-p 0015a000 08:15 581656 /lib/tls/i686/cmov/libc-2.8.90.so ... b8006000-b8020000 r-xp 00000000 08:15 565466 /lib/ld-2.8.90.so b8020000-b8021000 r-xp b8020000 00:00 0 [vdso] b8021000-b8022000 r--p 0001a000 08:15 565466 /lib/ld-2.8.90.so b8022000-b8023000 rw-p 0001b000 08:15 565466 /lib/ld-2.8.90.so bff0e000-bff23000 rw-p bffeb000 00:00 0 [stack] ``` 該進程也占用了0x0000 0000-0xbfff ffff的地址空間,Text Segment也是0x0804 8000-0x080f 4000,Data Segment也是0x080f 4000-0x080f 9000,和先前的進程一模一樣,因為這些地址是在編譯鏈接時寫進`/bin/bash`這個可執行文件的,兩個進程都加載它。這兩個進程在同一個系統中同時運行著,它們的Data Segment占用相同的VA,但是兩個進程各自干各自的事情,顯然Data Segment中的數據應該是不同的,相同的VA怎么會有不同的數據呢?因為它們被映射到不同的PA。如下圖所示。 **圖?20.5.?進程地址空間是獨立的** ![進程地址空間是獨立的](https://box.kancloud.cn/2016-04-02_56ff80d5405ca.png) 從圖中還可以看到,兩個進程都是`bash`進程,Text Segment是一樣的,并且Text Segment是只讀的,不會被改寫,因此操作系統會安排兩個進程的Text Segment共享相同的物理頁面。由于每個進程都有自己的一套VA到PA的映射表,整個地址空間中的任何VA都在每個進程自己的映射表中查找相應的PA,因此不可能訪問到其它進程的地址,也就沒有可能意外改寫其它進程的數據。 另外,注意到兩個進程的共享庫加載地址并不相同,共享庫的加載地址是在運行時決定的,而不是寫在`/bin/bash`這個可執行文件中。但即使如此,也不影響兩個進程共享相同物理頁面中的共享庫,當然,只有只讀的部分是共享的,可讀可寫的部分不共享。 使用共享庫可以大大節省內存。比如`libc`,系統中幾乎所有的進程都映射`libc`到自己的進程地址空間,而`libc`的只讀部分在物理內存中只需要存在一份,就可以被所有進程共享,這就是“共享庫”這個名稱的由來了。 現在我們也可以理解為什么共享庫必須是位置無關代碼了。比如`libc`,不同的進程雖然共享`libc`所在的物理頁面,但這些物理頁面被映射到各進程的虛擬地址空間時卻位于不同的地址,所以要求`libc`的代碼不管加載到什么地址都能正確執行。 第三,VA到PA的映射會給分配和釋放內存帶來方便,物理地址不連續的幾塊內存可以映射成虛擬地址連續的一塊內存。比如要用`malloc`分配一塊很大的內存空間,雖然有足夠多的空閑物理內存,卻沒有足夠大的_連續_空閑內存,這時就可以分配多個不連續的物理頁面而映射到連續的虛擬地址范圍。如下圖所示。 **圖?20.6.?不連續的PA可以映射為連續的VA** ![不連續的PA可以映射為連續的VA](https://box.kancloud.cn/2016-04-02_56ff80d552187.png) 第四,一個系統如果同時運行著很多進程,為各進程分配的內存之和可能會大于實際可用的物理內存,虛擬內存管理使得這種情況下各進程仍然能夠正常運行。因為各進程分配的只不過是虛擬內存的頁面,這些頁面的數據可以映射到物理頁面,也可以臨時保存到磁盤上而不占用物理頁面,在磁盤上臨時保存虛擬內存頁面的可能是一個磁盤分區,也可能是一個磁盤文件,稱為交換設備(Swap Device)。當物理內存不夠用時,將一些不常用的物理頁面中的數據臨時保存到交換設備,然后這個物理頁面就認為是空閑的了,可以重新分配給進程使用,這個過程稱為換出(Page out)。如果進程要用到被換出的頁面,就從交換設備再加載回物理內存,這稱為換入(Page in)。換出和換入操作統稱為換頁(Paging),因此: 系統中可分配的內存總量?=?物理內存的大小?+?交換設備的大小 如下圖所示。第一張圖是換出,將物理頁面中的數據保存到磁盤,并解除地址映射,釋放物理頁面。第二張圖是換入,從空閑的物理頁面中分配一個,將磁盤暫存的頁面加載回內存,并建立地址映射。 **圖?20.7.?換頁** ![換頁](https://box.kancloud.cn/2016-04-02_56ff80d562ecd.png)
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看