1、概述html
在C語言中,函數和初始化的全局變量(包括顯示初始化爲0)是強符號,未初始化的全局變量是弱符號。程序員
對於它們,下列三條規則使用:編程
① 同名的強符號只能有一個,不然編譯器報"重複定義"錯誤。app
② 容許一個強符號和多個弱符號,但定義會選擇強符號的。ide
③ 當有多個弱符號相同時,連接器選擇佔用內存空間最大的那個。函數
2、哪些符號是弱符號?post
咱們常常在編程中碰到一種狀況叫符號重複定義。多個目標文件中含有相同名字全局符號的定義,那麼這些目標文件連接的時候將會出現符號重複定義的錯誤。好比咱們在目標文件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"既非強符號也非弱符號,由於它是一個外部變量的引用。
In computing, a weak symbol is a symbol definition in an object file or dynamic library that may be overridden by other symbol definitions. Its value will be zero if no definition is found by the loader.測試
換句話說,就是咱們能夠定義一個符號,而該符號在連接時能夠不解析。ui
讓咱們再看一些例子:spa
- $ cat err.c
- int main(void)
- {
- f();
- return 0;
- }
很明顯,不能編譯經過,由於'undefined reference' :
- $ gcc err.c
- /tmp/ccYx7WNg.o: In function `main':
- err.c:(.text+0x12): undefined reference to `f'
- collect2: ld returned 1 exit status
那麼,若是將符號f聲明成弱符號,會怎麼呢?
- $ cat weak.c
- void __attribute__((weak)) f();
- int main(void)
- {
- if (f)
- f();
- return 0;
- }
- $ gcc weak.c
竟然編譯經過了,甚至成功執行!讓咱們看看爲何?
首先聲明瞭一個符號f(),屬性爲weak,但並不定義它,這樣,連接器會將此未定義的weak symbol賦值爲0,也就是說f()並無真正被調用,試試看,去掉if條件,確定core dump!
咱們能夠經過nm來查看符號:
- $ nm a.out
- ...
- w f
- 08048324 T main
- ...
若是咱們在另外一個文件中定義函數f,與week.c一塊兒編譯連接,那麼函數f就會正確的被調用!
- $ cat f.c
- #include <stdio.h>
- void f(void)
- {
- printf("hello from f\n");
- }
- $ gcc -c weak.c f.c
- $ gcc -o weak weak.o f.o
- $ ./weak
- hello from f
-
- $ nm weak.o
- w f
- 00000000 T main
- $ nm f.o
- 00000000 T f
- U puts
- $ nm weak
- ...
- 08048384 T f
- 08048354 T main
- U puts@@GLIBC_2.0
- ...
咱們甚至能夠定義強符號來override弱符號:
- $ cat orig.c
- #include <stdio.h>
- void __attribute__((weak)) f()
- {
- printf("original f..\n");
- }
- int main(void)
- {
- f();
- return 0;
- }
- $ gcc orig.c
- $ ./a.out
- original f..
- $ cat ovrd.c
- #include <stdio.h>
- void f(void)
- {
- printf("overridden f!\n");
- }
- $ gcc -c orig.c ovrd.c
- $ gcc -o ovrd orig.o ovrd.o
- $ ./ovrd
- overridden f!
- $ nm orig.o
- 00000000 W f
- 00000014 T main
- U puts
- $ nm ovrd.o
- 00000000 T f
- U puts
- $ nm ovrd
- ...
- 0804838c T f
- 08048368 T main
- U puts@@GLIBC_2.0
- ...
或者以下:
- $ cat orig-obj.c
- #include <stdio.h>
- int __attribute__((weak)) x = 1;
- int __attribute__((weak)) y = 1;
- int main(void)
- {
- printf("x = %d, y = %d\n", x, y);
- return 0;
- }
- $ gcc orig-obj.c
- $ ./a.out
- x = 1, y = 1
- $ cat ovrd-obj.c
- int x = 2;
- void f(void)
- {
- }
- $ gcc -c orig-obj.c ovrd-obj.c
- $ gcc -o ovrd-obj orig-obj.o ovrd-obj.o
- $ ./ovrd-obj
- x = 2, y = 1
- $ nm orig-obj.o
- 00000000 T main
- U printf
- 00000000 V x
- 00000004 V y
- $ nm ovrd-obj.o
- 00000000 T f
- 00000000 D x
- $ nm ovrd-obj
- ...
- 08048394 T f
- 08048354 T main
- U printf@@GLIBC_2.0
- 080495c8 D x
- 080495c4 V y
- ...
那麼當出現multiple symbols時,會如何呢?
- $ cat mul.c
- int main(void)
- {
- f();
- return 0;
- }
- $ cat s1.c
- #include <stdio.h>
- void f(void)
- {
- printf("1st strong f from %s\n", __FILE__);
- }
- $ cat s2.c
- #include <stdio.h>
- void f(void)
- {
- printf("2nd strong f from %s\n", __FILE__);
- }
- $ cat w1.c
- #include <stdio.h>
- void __attribute__((weak)) f(void)
- {
- printf("1st weak f from %s\n", __FILE__);
- }
- $ cat w2.c
- #include <stdio.h>
- void __attribute__((weak)) f(void)
- {
- printf("2nd weak f from %s\n", __FILE__);
- }
- $ gcc -c mul.c s1.c s2.c w1.c w2.c
- $ gcc -o test1 mul.o s1.o s2.o
- s2.o: In function `f':
- s2.c:(.text+0x0): multiple definition of `f'
- s1.o:s1.c:(.text+0x0): first defined here
- collect2: ld returned 1 exit status
-
-
- $ gcc -o test2 mul.o s1.o w1.o w2.o
- $ ./test2
- 1st strong f from s1.c
-
-
- $ gcc -o test3-1 mul.o w1.o w2.o
- $ ./test3-1
- 1st weak f from w1.c
- $ gcc -o test3-2 mul.o w2.o w1.o
- $ ./test3-2
- 2nd weak f from w2.c
關於最後一個例子,我想補充的是:若是咱們改變給出的編譯順序會怎麼樣呢?好比像下面這樣編譯:
- $ gcc -o test2 mul.o w1.o s1.o w2.o
- $ ./test2
- 1st strong f from s1.c
看,我將w1.o放在最前面,不過連接器依然選擇強符號,這是咱們所指望的。
不過,若是咱們這樣作:
- $ ar qs libw.a w1.o w2.o
- $ ar qs libs.a s1.o s2.o
-
-
-
再編譯:
- $ gcc -o test2 mul.o -L. -lw -ls
- $ ./test2
- 1st weak f from w1.c
再改爲這樣編譯:
- $ gcc -o test2 mul.o -L. -ls -lw
- $ ./test2
- 1st strong f from s1.c
看,狀況有變!這是爲何?
緣由就是GCC(準確地說是連接器)對待庫是不同的 —— 默認的,連接器使用第一個找到的符號,後面的就不搜索了。
不過咱們也能夠強制連接器搜索全部的庫,辦法以下:
- $ ar qs libw.a w1.o w2.o
- $ ar qs libs.a s1.o s2.o
- $ gcc -o test2 mul.o -L. -Wl,--whole-archive -lw -ls -Wl,--no-whole-archive
- ./libs.a(s2.o): In function `f':
- s2.c:(.text+0x0): multiple definition of `f'
- ./libs.a(s1.o):s1.c:(.text+0x0): first defined here
- collect2: error: ld returned 1 exit status
從新以下操做:
- $ rm libw.a libs.a
- $ ar qs libw.a w1.o w2.o
- $ ar qs libs.a s1.o
- $ gcc -o test2 mul.o -L. -Wl,--whole-archive -lw -ls -Wl,--no-whole-archive
- $ ./test2
- 1st strong f from s1.c
如今能夠了,儘管-lw在前!
讓咱們再來看一個具體的例子:
- #include <stdio.h>
- #include <stdlib.h>
-
- extern int fun(void);
-
- int global_var1 = 0xff00ff00;
- int global_var2 = 0x00ff00ff;
-
- int main(int argc, const char *argv[])
- {
- printf("in main.c: &global_var1 = %p", &global_var1);
- printf(" global_var1 = %x\n", global_var1);
- printf("sizeof(global_var1) = %d\n", sizeof(global_var1));
- printf("in main.c: &global_var2 = %p", &global_var2);
- printf(" global_var2 = %x\n", global_var2);
- printf("sizeof(global_var2) = %d\n", sizeof(global_var2));
- fun();
-
- printf("global_var1 = %x\n", global_var1);
- printf("global_var2 = %x\n", global_var2);
-
- return 0;
- }
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
-
- double global_var1;
-
- int fun(void)
- {
- printf("in test.c: &global_var1 = %p", &global_var1);
- printf(" global_var1 = %x\n", global_var1);
- printf("sizeof(global_var1) = %d\n", sizeof(global_var1));
-
- memset(&global_var1, 0, sizeof(global_var1));
-
- return 0;
- }
在gcc中編譯獲得以下結果:
咱們能夠看到,在main.c和test.c都有一個global_var1,在main.c中的爲強符號,在test.c中的爲弱符號。由於在test.c中global_var1沒有初始化,因此根據規則②得知:編譯器選擇main.c中的global_var1的值初始化那片內存。不要誤認爲在test.c中使用global_var1時是用的main.c中的global_var1,我以前錯誤得這樣認爲。實際上是這樣的:main.c中的global_var1和test.c中的global_var1引用的時同一塊內存區域,只是在兩個文件中表明的意義不一樣 ---- 在main.c中表明一個int型變量,在test.c中表明一個double型變量,它們的起始地址相同,但佔用內存空間是不一樣的, 在main.c中佔用4個字節,在test.c中佔用8個字節,這點從上圖的兩個sizeof輸出結果中能夠獲得驗證。
- (gdb) break main
- Breakpoint 1 at 0x804841d: file main.c, line 14.
- (gdb) run
- Starting program: /home/astrol/c/elf/dynamic/understand_weak_symbol_by_example/main
-
- Breakpoint 1, main (argc=1, argv=0xbffff6d4) at main.c:14
- 14 printf("in main.c: &global_var1 = %p", &global_var1);
- (gdb) print/x &global_var1
- $1 = 0x804a018
- (gdb) print/x &global_var2
- $2 = 0x804a01c
- (gdb) x/8xb &global_var1
- 0x804a018 <global_var1>: 0x00 0xff 0x00 0xff 0xff 0x00 0xff 0x00
- (gdb)
由於在test.c中的global_var1佔用八個字節,memset(&global_var1, 0, sizeof(global_var1))將這塊內存區域清零,這也就解釋了爲何調用fun以後,global_var1和global_var2都變成0的緣故。
- (gdb) break 27
- Breakpoint 1 at 0x80484d2: file main.c, line 27.
- (gdb) run
- Starting program: /home/astrol/c/elf/dynamic/understand_weak_symbol_by_example/main
- in main.c: &global_var1 = 0x804a018 global_var1 = ff00ff00
- sizeof(global_var1) = 4
- in main.c: &global_var2 = 0x804a01c global_var2 = ff00ff
- sizeof(global_var2) = 4
- in test.c: &global_var1 = 0x804a018 global_var1 = ff00ff00
- sizeof(global_var1) = 8
- global_var1 = 0
- global_var2 = 0
-
- Breakpoint 1, main (argc=1, argv=0xbffff6d4) at main.c:27
- 27 return 0;
- (gdb) print/x &global_var1
- $1 = 0x804a018
- (gdb) print/x &global_var2
- $2 = 0x804a01c
- (gdb) x/8xb &global_var1
- 0x804a018 <global_var1>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
- (gdb)
可見在test.c中對global_var1的改動會影響main.c中global_var1和global_var2的值。當程序很大時,這種錯誤很難發現,因此儘可能避免不一樣類型的符號在多個文件中
3、如何避免呢?
一、上策:想辦法消除全局變量。全局變量會增長程序的耦合性,對他要控制使用。若是能用其餘的方法代替最好。
二、中策:實在沒有辦法,那就把全局變量定義爲static,它是沒有強弱之分的。並且不會和其餘的全局符號產生衝突。至於其餘文件可能對他的訪問,能夠封裝成函數。把一個模塊的數據封裝起來是一個好的實踐。
三、下策:把全部的符號所有都變成強符號。全部的全局變量都初始化,記住,是全部的,哪怕初始值是0都行。若是一個沒有初始化,就可能會和其餘人產生衝突,儘管別人初始化了。(本身寫代碼測試一下)。
四、必備之策:GCC提供了一個選項,能夠檢查這類錯誤:-fno-common。
參考連接:
http://blog.csdn.net/chgaowei/article/details/7173436 (新手當心:c語言的強符號和弱符號)
http://www.embedded-bits.co.uk/2008/gcc-weak-symbols/ (GCC Weak Symbols)
http://write.blog.csdn.net/postedit/8008629 ( 什麼是weak symbol?)
http://winfred-lu.blogspot.com/2009/11/understand-weak-symbols-by-examples.html (Understand Weak Symbols by Examples)
http://discusstolearn.blogspot.sg/2012/11/symbol-resolution-weak-symbols-how.html (Symbol Resolution, Weak Symbols, How compiler resolves multiple Global Symbols)
http://wanderingcoder.net/2012/06/30/multiply-defined-symbols/ ( Dealing with multiply defined symbols)
http://www.cnblogs.com/whos/archive/2010/10/20/1856274.html(弱符號與強符號概念)
http://www.searchtb.com/2013/03/compile_problems_about_strong_weak_symbols.html (分享兩個強符號,弱符號引發的編譯問題)
http://blog.csdn.net/glp_hit/article/details/8788963 (強符號 弱符號)
補充:
最近在看《程序員的自我修養》,知道如今的編譯器和連接器支持一種叫COMMOM塊(Common Block)的機制,這種機制用來解決 一個弱符號定義在多個目標文件中,而它們的類型又不一樣(即大小不一樣) 的狀況。
在目標文件中,編譯器將未初始化的全局變量放在了COMMON段,未初始化的靜態變量(包括全局和局部靜態變量)放在BSS段。
---------------------------------------------------------------------------------------------------------------------------------------
對於全局變量來講,若是初始化了不爲0的值,那麼該全局變量存儲在.data段;
若是初始化的值爲0, 那麼將其存儲在.BSS;(依然是強符號)
若是沒有初始化,那麼編譯時將其存儲在COMMON塊,等到連接時再將其放入到.BSS段。(這點不一樣的編譯器會有所不一樣,有的編譯器會直接把沒有初始化的全局變量放在.BSS段,而沒有COMMON塊機制)
---------------------------------------------------------------------------------------------------------------------------------------
爲何這樣處理呢?
咱們能夠想到,當編譯器將一個編譯單元編譯成目標文件的時候,若是該編譯單元包含了弱符號(未初始化的全局變量就是典型的弱符號),那麼該弱符號最終所佔空間的大小此時是未知的,由於有可能其餘編譯單元中同符號名稱的弱符號所佔的空間比本編譯單元該符號所佔的空間要大。因此編譯器此時沒法爲該弱符號在BSS段分配空間,由於所須要的空間大小此時是未知的。可是連接器在連接過程當中能夠肯定弱符號的大小,由於當連接器讀取全部輸入目標文件後,任何一個弱符號的最終大小均可以肯定了,因此它能夠在最終的輸出文件的BSS段爲其分配空間。因此整體來看,未初始化的全局變量仍是被放在BSS段。 ------摘自《程序員的自我修養》
來看一個例子:
- #include <stdio.h>
- int global ;
- int main(int argc, const char *argv[])
- {
- printf("global = %d, sizeof(global) in main = %d\n", global, sizeof(global));
- bb();
- return 0;
- }
- #include <stdio.h>
- double global ;
- void bb()
- {
- printf("global = %f, sizeof(global) in bb = %d\n", global, sizeof(global));
- }
編譯成目標文件:
獲得aa.o 和 bb.o兩個目標文件
來看看他們的符號表
能夠清楚的看到,在兩個目標文件中,Ndx數值都是COM,表示此時它們被放在COMMON塊。在aa.o中global的大小是4個字節,在bb.o中global的大小是8個字節。
那麼這兩個目標文件連接生成可執行文件後,global的大小是多少呢? -- 當不一樣的目標文件須要的COMMON塊空間大小不一致時,以最大的那塊爲準。
獲得可執行文件cc
果真,global最終的大小爲8個字節。
因此整體來看,未初始化全局變量最終仍是被放在BSS段的。
若是咱們給aa.c中的global賦值把它變成強符號呢?以下:
- #include <stdio.h>
- int global = 100;
- int main(int argc, const char *argv[])
- {
- printf("global = %d, sizeof(global) in main = %d\n", global, sizeof(global));
- bb();
- return 0;
- }
- #include <stdio.h>
- double global;
- void bb()
- {
- printf("global = %f, sizeof(global) in bb = %d\n", global, sizeof(global));
- }
獲得兩個目標文件後查看符號,aa.o中global放在.data段,bb.o依然放在COMMON塊,最終的cc中global大小4字節,這很好的驗證了本文一開始的第二條規則。
但是有例外狀況,看下面程序:
- #include <stdio.h>
- int global;
- int main(int argc, const char *argv[])
- {
- printf("global = %d, sizeof(global) in main = %d\n", global, sizeof(global));
- bb();
- return 0;
- }
- #include <stdio.h>
- double __attribute__ ((weak)) global = 1.0;
- void bb()
- {
- printf("global = %f, sizeof(global) in bb = %d\n", global, sizeof(global));
- }
aa.c和bb.c中global都是弱符號,若是按照上面的規則的話,最終的可執行文件中global的大小應該是8個字節,惋惜結果並非咱們所指望的:
看到沒,最終的可執行文件cc中global所佔內存倒是4個字節!爲何? 下面是我在ELF文檔裏找到的一段:
---------------------------------------------------------------------------------------------------------------------------------------
Global and weak symbols differ in two major ways.
(全局符號和弱符號的區別主要在兩個方面。)
When the link editor combines several relocatable object files, it does not allow multiple definitions of STB_GLOBAL symbols with the same name. On the other hand, if a defined global symbol exists, the appearance of a weak symbol with the same name will not cause an error. The link editor honors the global definition and ignores the weak ones. Similarly, if a common symbol exists (i.e., a symbol whose st_shndx field holds SHN_COMMON), the appearance of a weak symbol with the same name will not cause an error. The link editor honors the common definition and ignores the weak ones.
(* 當連接器連接幾個可重定位的目標文件時,它不容許具備STB_GLOBAL屬性的符號以相同名字進行重複定義。另外一方面,若是一個已定義的全局符號存在,則即使另外一個具備相同名字的弱符號存在也不會引發錯誤。連接器將承認全局符號的定義而忽略弱符號的定義。與此類似,若是一個符號被放在COMMON塊(就是說這個符號的 st_shndx 成員的值爲SHN_COMMON),則一個同名的弱符號也不會引發錯誤。連接器一樣承認放在COMMON塊符號的定義而忽略其餘的弱符號。)
---------------------------------------------------------------------------------------------------------------------------------------
至於爲何這樣處理,目前我還不得而知,若是讀者知道的話,麻煩告訴我一下^_^!
再來看一種狀況!以下:
- #include <stdio.h>
- int __attribute__((weak)) global = 1;
- int main(int argc, const char *argv[])
- {
- printf("global = %d, sizeof(global) in main = %d\n", global, sizeof(global));
- bb();
- return 0;
- }
- #include <stdio.h>
- double __attribute__((weak)) global = 1.0;
- void bb()
- {
- printf("global = %f, sizeof(global) in bb = %d\n", global, sizeof(global));
- }
結果倒是:
看到沒,一樣都是弱符號,卻由於編譯順序的不一樣,可執行文件中的大小也不一樣,爲何會這樣,目前我也是不得而知!
簡而言之,在目標文件中沒有將未初始化的全局變量像未初始化的靜態變量那樣放在BSS段,而是放在COMMON塊,是由於如今的編譯器和連接器容許不一樣類型的弱符號存在,最本質的緣由是連接器沒法判斷各個符號的類型是否一致。
有了COMMON塊以後就能夠很好的解決這個問題了。
補充:
編程中咱們可使用GCC的「-fno-common」把全部的未初始化的全局變量不以COMMON塊的形式處理,也可使用「__attribute__ ((nocommon))」,以下:
- int global __attribute__ ((nocommon));
一旦一個未初始化的全局變量不是以COMMON塊的形式存在,那麼它就至關於一強符號,若是其餘目標文件中還有同一個變量的強符號定義,連接時就會發生符號重複定義錯誤。
參考連接:
http://blog.chinaunix.net/uid-23629988-id-2888209.html(經過未初始化全局變量,研究BSS段和COMMON段的不一樣)
http://blog.copton.net/articles/linker/index.html ( C++ and the linker)
http://www.lurklurk.org/linkers/linkers.html ( Beginner's Guide to Linkers)
https://thunked.org/programming/code-obfuscation-with-linker-symbol-abuse-t100.html