當一個工程中有多個文件的時候,鏈接的本質就是要把多個不同的目標文件相互粘到一起。就想玩具積木一樣整合成一個整體。為了使不同的目標文件之間能夠相互粘合,這些目標文件之間必須要有固定的規則才行。比如目標文件B用到了目標文件A中的函數”foo”,那么我們就稱目標文件A定義了函數foo,目標文件B引用了函數foo。每個函數和變量都有自己獨特的名字,避免鏈接過程中不同變量和函數之間的混淆。在鏈接過程中,我們將函數和變量統稱為符號。函數或者變量名就是符號名
每一個目標文件都會有一個相應的符號表,這個表里面記錄了目標文件中所用到的所有符號。每個定義的符號有一個對應的值,叫做符號值,對於變量和函數來說, 符號值就是它們的地址。我們可以通過nm命令來查看目標文件中的符號結果。
root@zhf-maple:/home/zhf/c_prj# nm main.o
0000000000000000 T func1
0000000000000004 C global_init_var
U _GLOBAL_OFFSET_TABLE_
0000000000000000 D global_var
0000000000000024 T main
U printf
0000000000000000 b static_var2.2257
0000000000000004 d static_var.2256
符號表條目有如下結構(from elf.h):
typedef struct {
ELF32_Word st_name;
ELF32_Addr st_value;
ELF32_Word st_size;
unsigned char st_info;
unsigned char st_other;
Elf32_Half sth_shndx;
} Elf32_Sym;
ELF符號表域說明:
域 |
描述 |
st_name |
符號串表索引. 串表用於保存符號名. |
st_value |
符號值: 符號的section索引為SHN_COMMON:符號對齊要求. 重定位文件:離section起始位置的偏移. 執行文件:符號的地址. |
st_size |
對象大小. |
st_info >> 4 |
高4位定義符號的綁定[binding ]: STB_LOCAL (0) symbol is local to the file STB_GLOBAL (1) symbol is visible to all object files STB_WEAK (2) symbol is global with lower precedence |
st_info & 15 |
低4位定義符號的類型: STT_NOTYPE (0) 無類型 STT_OBJECT (1) 數據對象(變量) STT_FUNC (2) 函數 STT_SECTION (3) section名 STT_FILE (4) 文件名 |
st_other |
未使用. |
st_shndx |
定義符號sectiond的索引.特殊的section數包括: SHN_UNDEF (0x0000) 未定義section SHN_ABS (0xfff1) 絕對, 不可重定位符號 SHN_COMMON (0xfff2) 不分配, 外部變量 |
符號所在的段
宏定義名 |
值 |
說明 |
SHN_ABS |
0xfff1 |
該符號包含了一個絕對值,比如表示文件名的符號 |
SHN_COMMON |
0xfff2 |
表示該符號是一個"COMMON塊"的符號 一般來說,未初始化的全局符號定義就是這種類型的。 |
SHN_UNDEF |
0 |
該符號在本目標文件中被引用到,但是定義在其他目標文件中 |
我們還是通過readelf命令來查看下main.o文件中的符號。下面的結果和上面的表可以進行一一對應。
root@zhf-maple:/home/zhf/c_prj# readelf -s main.o
Symbol table '.symtab' contains 17 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000004 4 OBJECT LOCAL DEFAULT 3 static_var.2256
7: 0000000000000000 4 OBJECT LOCAL DEFAULT 4 static_var2.2257
8: 0000000000000000 0 SECTION LOCAL DEFAULT 7
9: 0000000000000000 0 SECTION LOCAL DEFAULT 8
10: 0000000000000000 0 SECTION LOCAL DEFAULT 6
11: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 global_var
12: 0000000000000004 4 OBJECT GLOBAL DEFAULT COM global_init_var
13: 0000000000000000 36 FUNC GLOBAL DEFAULT 1 func1
14: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
15: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
16: 0000000000000024 40 FUNC GLOBAL DEFAULT 1 main
弱符號與強符號
我們經常在編程中碰到一種情況叫符號重復定義。多個目標文件中含有相同名字全局符號的定義,那么這些目標文件鏈接的時候將會出現符號重復定義的錯誤。比如我們在目標文件A和目標文件B都定義了一個全局整形變量global,並將它們都初始化,那么鏈接器將A和B進行鏈接時會報錯:
1 b.o:(.data+0x0): multiple definition of `global'
2 a.o:(.data+0x0): first defined here
這種符號的定義可以被稱為強符號(Strong Symbol)。有些符號的定義可以被稱為弱符號(Weak Symbol)。對於C語言來說,編譯器默認函數和初始化了的全局變量為強符號,未初始化的全局變量為弱符號(C++並沒有將未初始化的全局符號視為弱符號)。我們也可以通過GCC的"__attribute__((weak))"來定義任何一個強符號為弱符號。注意,強符號和弱符號都是針對定義來說的,不是針對符號的引用。比如我們有下面這段程序:
extern int ext;
int weak1;
int strong = 1;
int __attribute__((weak)) weak2 = 2;
int main()
{
return 0;
}
上面這段程序中,"weak"和"weak2"是弱符號,"strong"和"main"是強符號,而"ext"既非強符號也非弱符號,因為它是一個外部變量的引用。鏈接器會按照如下的規則處理被多次定義的全局符號:
規則1:不允許強符號被多次定義。
規則2:如果一個符號在某個目標文件中是強符號,在其他文件中都是弱符號,那么選擇強符號。
規則3:如果一個符號在所有的目標文件中都是弱符號,那么選擇其中占用空間最大的一個。
我們來看一個實際的例子:在下面的代碼中f()沒有被定義,因此會報錯
int main()
{
f();
return 0;
}
g++ -o bin/Debug/linux_c obj/Debug/chapter8.o obj/Debug/main.o
obj/Debug/main.o:在函數‘main’中:
/home/zhf/codeblocks_prj/linux_c/main.c:15:對‘f’未定義的引用
如果將代碼改成如下:
void __attribute__((weak)) f();
int main()
{
if (f){
f();
}
return 0;
}
居然編譯通過了,甚至成功執行!讓我們看看為什么?
首先聲明了一個符號f(),屬性為weak,但並不定義它,這樣,鏈接器會將此未定義的weak symbol賦值為0,也就是說f()並沒有真正被調用,試試看,去掉if條件,肯定core dump!
我們甚至可以定義強符號來override弱符號:
test.c中代碼如下
#include<stdlib.h>
#include<stdio.h>
void __attribute__((weak)) f(){
printf("original f()\n");
}
int main(int argc,char *argv[]){
f();
return 0;
}
test1.c中的代碼如下:
#include <stdio.h>
void f(void){
printf("override f()\n");
}
執行結果如下:
root@zhf-maple:/home/zhf/c_prj# gcc -c test.c test1.c
root@zhf-maple:/home/zhf/c_prj# gcc -o test test.o test1.o
root@zhf-maple:/home/zhf/c_prj# ./test
override f()
在Linux程序的設計中,如果一個程序被設計成可以支持單線程或多線程的模式,就可以通過弱引用的方法來判斷當前的程序是鏈接到了單線程的Glibc庫還是多線程的Glibc庫(是否在編譯時有-lpthread選項),從而執行單線程版本的程序或多線程版本的程序。我們可以在程序中定義一個pthread_create函數的弱引用,然后程序在運行時動態判斷是否鏈接到pthread庫從而決定執行多線程版本還是單線程版本:
#include <stdio.h>
#include <pthread.h>
int pthread_create( pthread_t*, const pthread_attr_t*,
void* (*)(void*), void*) __attribute__ ((weak));
int main()
{
if(pthread_create)
{
printf("This is multi-thread version!\n");
// run the multi-thread version
// main_multi_thread()
}
else
{
printf("This is single-thread version!\n");
// run the single-thread version
// main_single_thread()
}
}
執行結果如下:
$ gcc pthread.c -o pt
$ ./pt
This is single-thread version!
$ gcc pthread.c -lpthread -o pt
$ ./pt
This is multi-thread version!