1. 程式人生 > >讀書筆記--《程式設計師的自我修養》第3章:目標檔案裡有什麼(3)

讀書筆記--《程式設計師的自我修養》第3章:目標檔案裡有什麼(3)

3.5 連結的介面–符號

在連結中,我們將函式名和變數統稱為符號,函式名和變數名就是符號名。
每一個目標檔案都有一個符號表,裡面記錄了目標檔案中所有用到的符號。
每一個符號都有一個對應的值,叫做符號值。對於變數和函式來說,符號值就是地址。
符號分為5類。
**(1)本目標檔案中定義的全域性符號。可以被其他目標檔案引用。如fun1、main、global_init_var
(2)不在本目標檔案中定義的全域性符號。稱為外部符號。如printf**
(3)段名。由編譯器產生,它的值就是該段的起始地址。如.text、.data等。
(4)區域性符號。只在編譯單元內可見。如static_var、static_var2
(5)行號資訊。即目標檔案指令與原始碼中程式碼行的對應關係,可選。
其中,(1)(2)最為重要。

使用:$nm SimpleSection.o檢視符號
這裡寫圖片描述
nm命令用來列出一個目標檔案中的各種符號。
T:Text段的符號。
D:已初始化的變數符號
C:該符號為common。common symbol是未初始化的的資料段。只在連結時被分配。
U:在當前檔案中未定義,定義在別的檔案中。

3.5.1 ELF符號表結構
符號表是一個Elf32_Sym結構的陣列,一個Elf32_Sym對應一個符號。
typedef struct{
Elf32_Word st_name; //符號名
Elf32_Addr st_value; //符號名對應的值。
Elf32_Word st_size; //符號大小。
unsigned char st_info; //符號型別和繫結資訊。
unsigned char st_other; //為0,沒用。
Elf32_Half st_shndx; //符號所在的段。
}Elf32_Sym;
利用readelf檢視ELF檔案的符號:
這裡寫圖片描述


解釋:

  1. 第6列vis段未使用,忽略。
  2. printf在本檔案中未定義,因此它的Ndx是SHN_UNDEF。
  3. static_var.1488和staic_var2.1489是兩個靜態變數,繫結屬性是STB_LOCAL在編譯單元內部可見。用到了”符號修飾“

(1)符號型別和繫結資訊
高28位表示符號繫結資訊;第4位表示符號型別。
這裡寫圖片描述
這裡寫圖片描述

(2)符號所在的段
如果符號定義在本目標檔案中,則表示符號所在段在段表中的下標;如果不在或有些特殊符號,則如表所示:
這裡寫圖片描述
如:這裡寫圖片描述

(3)符號值
分下列3種情況:

  1. 如果時符號的定義且符號不是”COMMON塊“型別的,則表示該符號在段中的偏移。???
  2. 如果是”COMMON塊“型別的,則表示該符號的對齊屬性。如global_uninit_var.
  3. 在可執行檔案中,表示符號的虛擬地址。該地址對動態連結器十分有用。

3.5.2 特殊符號
有些符號沒有在程式中定義,但可以直接宣告並引用,我們稱為特殊符號。連結器會在連結時將其解釋為正確的值。
幾個代表性的特殊符號如下:

  1. __executabel_start:程式起始地址。注意不是入口地址,是程式最開始地址
  2. __etext、_etext、etext:程式碼段結束地址
  3. _edata、edata:資料段結束地址
  4. _end、end:程式結束地址
    以上都是虛擬地址。

使用這些符號:

#include<stdio.h>

extern char __executable_start[];
extern char __etext[],etext[],_etext[];
extern char _edata[],edata[];
extern char _end[],end[];

int main()
{
    printf("Executable Start %X\n",__executable_start);
    printf("Text End %X %X %X\n",etext,_etext,__etext);
    printf("Data End %X %X\n",edata,_edata);
    printf("Executable End %X\n",end,_end);

    return 0;
}

編譯並執行:
這裡寫圖片描述

3.5.3 符號修飾與函式簽名
為防止庫函式與自定義函式的命名衝突,採取了符號修飾來避免。例如:C語言程式碼中所有全域性變數和函式經過編譯後,會在符號名前加上”_”;C++增加了名稱空間來解決符號衝突問題。
例如:

int func(int);
float func(float);
class C{
    int func(int);
    class C2{
        int func(int);
    };
};
namespace N{
    int func(int);
    class C{
        int func(int);
    };
};

這6個func都是不同的,因為它們的函式簽名不同。函式簽名包含了一個函式的資訊,包括函式名、引數型別、所在的類和名稱空間。由於它們的引數型別、所處的類、名稱空間的不同,因此函式簽名是不同的。

3.5.4 extern “C”
C++為了與C相容,在符號的管理上,C++有一個用來宣告或定義一個C的符號的“extern ”C”“關鍵字用法。如

extern "C"{
    int func(int);
    int var;
    }

C++編譯器會將extern “C”的大括號內部的程式碼當作C語言程式碼處理。
考慮以下情況:
C語言程式碼或C++包含了用C語言編寫的函式或全域性變數,如C語言庫函式中的string.h中聲明瞭memset這個函式,它的原型如下:
void memset(void ,int,size_t);
當C語言用到這個函式時,會正確處理;但是在C++語言中,編譯器會認為memset是一個C++函式,於是將memset的符號修飾成_Z6memsetPvii,這樣連結器就無法與C語言庫中的memset符號進行連結。為了解決這個問題,我們使用C++的巨集”__cplusplus“,C++編譯器在編譯C++的程式時預設定義這個巨集,我們可以使用這個巨集判斷當前編譯單元是不是C++程式碼。具體程式碼如下:

#ifdef __cplusplus
extern "C"{
#endif

void *memset(void *,int,size_t);

#ifdef __cplusplus
}
#endif

如果當前編譯單元是C++程式碼,那麼memset會在extern “C”裡面被宣告;
如果是C程式碼,就直接宣告。
這個技巧很重要!幾乎在所有的系統標頭檔案中都被用到。

3.5.5 弱符號與強符號
1、對於C/C++語言來說,編譯器預設函式和初始化了的全域性變數為強符號,未初始化的全域性變數為弱符號。
可以通過GCC的”attribute((weak))“來將強符號定義為弱符號。
注意:強符號和弱符號都是針對定義來說,不是針對引用
連結器對全域性符號的處理規則:

  1. 不允許強符號被多次定義
  2. 如果一個符號在某個目標檔案中是強符號,在其他檔案是弱符號,則選擇強符號。
  3. 如果一個符號在所有目標檔案中是弱符號,那麼選擇其中佔用空間最大的那個。

2、強引用與弱引用
強引用:對外部檔案的符號引用在目標檔案被最終連結成可執行檔案時,需要被正確決議,如果沒有找到該符號的定義,連結器就會報未定義錯誤。
弱引用:如果該符號有定義,連結器將正確處理;如果未定義,連結器不報錯。
對於未定義的弱引用,連結器將其預設為0,或一個特殊的值,以便於程式程式碼能夠識別。
如下面這段程式碼:

__attribute__((weakref)) void foo();
int main()
{
    foo();
}

當將其編譯成一個可執行檔案,GCC並不會報連結錯誤。但是當我們執行這個可執行檔案時,會發送錯誤。因為foo函式的地址是0,找不到。對其進行改進:

__attribute__((weakref)) void foo();
int main
{
    if(foo) foo();
}

弱符號和弱引用對庫來說十分有用,比如庫中定義的弱符號可以被使用者定義的強符號覆蓋,從而使得程式可以使用自定義版本的庫函式。或者程式將某些功能模組的引用定義為弱引用,當我們將擴充套件模組與程式連結在一起時,功能模組就可以正常使用。

3.6 除錯資訊
如果我們在GCC編譯時加上-g引數,編譯器就會在產生的目標檔案裡面加上除錯的資訊。
這裡寫圖片描述
這些段中儲存的就是除錯資訊。現在的ELF檔案採用一個叫DWARF的標準的除錯資訊格式。
可以使用strip命令去掉ELF檔案中的除錯資訊。
這裡寫圖片描述