1. 程式人生 > >C語言中的強符號與弱符號

C語言中的強符號與弱符號

_weak 多個 obj china 很難 字節 whole sta dump

註意,強符號和弱符號都是針對定義來說的,不是針對符號的引用。

一、概述

在C語言中,函數和初始化的全局變量(包括顯示初始化為0)是強符號,未初始化的全局變量是弱符號。

對於它們,下列三條規則使用:

① 同名的強符號只能有一個,否則編譯器報"重復定義"錯誤。

② 允許一個強符號和多個弱符號,但定義會選擇強符號的。

③ 當有多個弱符號相同時,鏈接器選擇占用內存空間最大的那個。

二、哪些符號是弱符號?

我們經常在編程中碰到一種情況叫符號重復定義。多個目標文件中含有相同名字全局符號的定義,那麽這些目標文件鏈接的時候將會出現符號重復定義的錯誤。比如我們在目標文件A和目標文件B都定義了一個全局整形變量global,並將它們都初始化,那麽鏈接器將A和B進行鏈接時會報錯:
  1. 1 b.o:(.data+0x0): multiple definition of `global‘
  2. 2 a.o:(.data+0x0): first defined here

這種符號的定義可以被稱為強符號(Strong Symbol)。有些符號的定義可以被稱為弱符號(Weak Symbol)對於C語言來說,編譯器默認函數和初始化了的全局變量為強符號,未初始化的全局變量為弱符號(C++並沒有將未初始化的全局符號視為弱符號)。我們也可以通過GCC的"__attribute__((weak))"來定義任何一個強符號為弱符號。註意,強符號和弱符號都是針對定義來說的,不是針對符號的引用。比如我們有下面這段程序:
  1. extern int ext;
  2. int weak1;
  3. int strong = 1;
  4. int __attribute__((weak)) weak2 = 2;
  5. int main()
  6. {
  7. return 0;
  8. }
上面這段程序中,"weak"和"weak2"是弱符號,"strong"和"main"是強符號,而"ext"既非強符號也非弱符號,因為它是一個外部變量的引用。 下面一段話摘自wikipedia:

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.

換句話說,就是我們可以定義一個符號,而該符號在鏈接時可以不解析。

讓我們再看一些例子:

  1. $ cat err.c
  2. int main(void)
  3. {
  4. f();
  5. return 0;
  6. }

很明顯,不能編譯通過,因為‘undefined reference‘ :

  1. $ gcc err.c
  2. /tmp/ccYx7WNg.o: In function `main‘:
  3. err.c:(.text+0x12): undefined reference to `f‘
  4. collect2: ld returned 1 exit status

那麽,如果將符號f聲明成弱符號,會怎麽呢?

  1. $ cat weak.c
  2. void __attribute__((weak)) f();
  3. int main(void)
  4. {
  5. if (f)
  6. f();
  7. return 0;
  8. }
  9. $ gcc weak.c

居然編譯通過了,甚至成功執行!讓我們看看為什麽?

首先聲明了一個符號f(),屬性為weak,但並不定義它,這樣,鏈接器會將此未定義的weak symbol賦值為0,也就是說f()並沒有真正被調用,試試看,去掉if條件,肯定core dump!

我們可以通過nm來查看符號:

  1. $ nm a.out
  2. ...
  3. w f
  4. 08048324 T main
  5. ...

如果我們在另一個文件中定義函數f,與week.c一起編譯鏈接,那麽函數f就會正確的被調用!

  1. $ cat f.c
  2. #include <stdio.h>
  3. void f(void)
  4. {
  5. printf("hello from f\n");
  6. }
  1. $ gcc -c weak.c f.c
  2. $ gcc -o weak weak.o f.o
  3. $ ./weak
  4. hello from f
  5. $ nm weak.o
  6. w f
  7. 00000000 T main
  8. $ nm f.o
  9. 00000000 T f
  10. U puts
  11. $ nm weak
  12. ...
  13. 08048384 T f
  14. 08048354 T main
  15. U puts@@GLIBC_2.0
  16. ...


我們甚至可以定義強符號來override弱符號:

  1. $ cat orig.c
  2. #include <stdio.h>
  3. void __attribute__((weak)) f()
  4. {
  5. printf("original f..\n");
  6. }
  7. int main(void)
  8. {
  9. f();
  10. return 0;
  11. }
  12. $ gcc orig.c
  13. $ ./a.out
  14. original f..
  1. $ cat ovrd.c
  2. #include <stdio.h>
  3. void f(void)
  4. {
  5. printf("overridden f!\n");
  6. }
  7. $ gcc -c orig.c ovrd.c
  8. $ gcc -o ovrd orig.o ovrd.o
  9. $ ./ovrd
  10. overridden f!
  1. $ nm orig.o
  2. 00000000 W f
  3. 00000014 T main
  4. U puts
  5. $ nm ovrd.o
  6. 00000000 T f
  7. U puts
  8. $ nm ovrd
  9. ...
  10. 0804838c T f
  11. 08048368 T main
  12. U puts@@GLIBC_2.0
  13. ...


或者如下:

  1. $ cat orig-obj.c
  2. #include <stdio.h>
  3. int __attribute__((weak)) x = 1;
  4. int __attribute__((weak)) y = 1;
  5. int main(void)
  6. {
  7. printf("x = %d, y = %d\n", x, y);
  8. return 0;
  9. }
  10. $ gcc orig-obj.c
  11. $ ./a.out
  12. x = 1, y = 1
  1. $ cat ovrd-obj.c
  2. int x = 2;
  3. void f(void)
  4. {
  5. }
  6. $ gcc -c orig-obj.c ovrd-obj.c
  7. $ gcc -o ovrd-obj orig-obj.o ovrd-obj.o
  8. $ ./ovrd-obj
  9. x = 2, y = 1
  1. $ nm orig-obj.o
  2. 00000000 T main
  3. U printf
  4. 00000000 V x
  5. 00000004 V y
  6. $ nm ovrd-obj.o
  7. 00000000 T f
  8. 00000000 D x
  9. $ nm ovrd-obj
  10. ...
  11. 08048394 T f
  12. 08048354 T main
  13. U printf@@GLIBC_2.0
  14. 080495c8 D x
  15. 080495c4 V y
  16. ...

那麽當出現multiple symbols時,會如何呢?

  1. $ cat mul.c
  2. int main(void)
  3. {
  4. f();
  5. return 0;
  6. }
  7. $ cat s1.c
  8. #include <stdio.h>
  9. void f(void)
  10. {
  11. printf("1st strong f from %s\n", __FILE__);
  12. }
  13. $ cat s2.c
  14. #include <stdio.h>
  15. void f(void)
  16. {
  17. printf("2nd strong f from %s\n", __FILE__);
  18. }
  19. $ cat w1.c
  20. #include <stdio.h>
  21. void __attribute__((weak)) f(void)
  22. {
  23. printf("1st weak f from %s\n", __FILE__);
  24. }
  25. $ cat w2.c
  26. #include <stdio.h>
  27. void __attribute__((weak)) f(void)
  28. {
  29. printf("2nd weak f from %s\n", __FILE__);
  30. }
  31. $ gcc -c mul.c s1.c s2.c w1.c w2.c
  1. $ gcc -o test1 mul.o s1.o s2.o
  2. s2.o: In function `f‘:
  3. s2.c:(.text+0x0): multiple definition of `f‘
  4. s1.o:s1.c:(.text+0x0): first defined here
  5. collect2: ld returned 1 exit status
  6. $ gcc -o test2 mul.o s1.o w1.o w2.o
  7. $ ./test2
  8. 1st strong f from s1.c
  9. $ gcc -o test3-1 mul.o w1.o w2.o
  10. $ ./test3-1
  11. 1st weak f from w1.c
  12. $ gcc -o test3-2 mul.o w2.o w1.o
  13. $ ./test3-2
  14. 2nd weak f from w2.c


關於最後一個例子,我想補充的是:如果我們改變給出的編譯順序會怎麽樣呢?比如像下面這樣編譯:

  1. $ gcc -o test2 mul.o w1.o s1.o w2.o
  2. $ ./test2
  3. 1st strong f from s1.c

看,我將w1.o放在最前面,不過鏈接器依然選擇強符號,這是我們所期望的。

不過,如果我們這樣做:

  1. $ ar qs libw.a w1.o w2.o
  2. $ ar qs libs.a s1.o s2.o

再編譯:

  1. $ gcc -o test2 mul.o -L. -lw -ls
  2. $ ./test2
  3. 1st weak f from w1.c

再改成這樣編譯:

  1. $ gcc -o test2 mul.o -L. -ls -lw
  2. $ ./test2
  3. 1st strong f from s1.c

看,情況有變!這是為什麽?

原因就是GCC(準確地說是鏈接器)對待庫是不一樣的 —— 默認的,鏈接器使用第一個找到的符號,後面的就不搜索了。

不過我們也可以強制鏈接器搜索所有的庫,辦法如下:

  1. $ ar qs libw.a w1.o w2.o
  2. $ ar qs libs.a s1.o s2.o
  3. $ gcc -o test2 mul.o -L. -Wl,--whole-archive -lw -ls -Wl,--no-whole-archive
  4. ./libs.a(s2.o): In function `f‘:
  5. s2.c:(.text+0x0): multiple definition of `f‘
  6. ./libs.a(s1.o):s1.c:(.text+0x0): first defined here
  7. collect2: error: ld returned 1 exit status

重新如下操作:

  1. $ rm libw.a libs.a
  2. $ ar qs libw.a w1.o w2.o
  3. $ ar qs libs.a s1.o
  4. $ gcc -o test2 mul.o -L. -Wl,--whole-archive -lw -ls -Wl,--no-whole-archive
  5. $ ./test2
  6. 1st strong f from s1.c

現在可以了,盡管-lw在前!

讓我們再來看一個具體的例子:

  1. // main.c
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. extern int fun(void);
  5. int global_var1 = 0xff00ff00; // 強符號
  6. int global_var2 = 0x00ff00ff; // 強符號
  7. int main(int argc, const char *argv[])
  8. {
  9. /////////////////////////////////////////////////////////////////////
  10. printf("in main.c: &global_var1 = %p", &global_var1);
  11. printf(" global_var1 = %x\n", global_var1);
  12. printf("sizeof(global_var1) = %d\n", sizeof(global_var1));
  13. /////////////////////////////////////////////////////////////////////
  14. printf("in main.c: &global_var2 = %p", &global_var2);
  15. printf(" global_var2 = %x\n", global_var2);
  16. printf("sizeof(global_var2) = %d\n", sizeof(global_var2));
  17. /////////////////////////////////////////////////////////////////////
  18. fun();
  19. printf("global_var1 = %x\n", global_var1);
  20. printf("global_var2 = %x\n", global_var2);
  21. return 0;
  22. }
  1. // test.c
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <string.h>
  5. double global_var1;
  6. int fun(void)
  7. {
  8. printf("in test.c: &global_var1 = %p", &global_var1);
  9. printf(" global_var1 = %x\n", global_var1);
  10. printf("sizeof(global_var1) = %d\n", sizeof(global_var1));
  11. memset(&global_var1, 0, sizeof(global_var1));
  12. return 0;
  13. }


在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輸出結果中可以得到驗證。

  1. (gdb) break main
  2. Breakpoint 1 at 0x804841d: file main.c, line 14.
  3. (gdb) run
  4. Starting program: /home/astrol/c/elf/dynamic/understand_weak_symbol_by_example/main
  5. Breakpoint 1, main (argc=1, argv=0xbffff6d4) at main.c:14
  6. 14 printf("in main.c: &global_var1 = %p", &global_var1);
  7. (gdb) print/x &global_var1
  8. $1 = 0x804a018
  9. (gdb) print/x &global_var2
  10. $2 = 0x804a01c
  11. (gdb) x/8xb &global_var1
  12. 0x804a018 <global_var1>: 0x00 0xff 0x00 0xff 0xff 0x00 0xff 0x00
  13. (gdb)


因為在test.c中的global_var1占用八個字節,memset(&global_var1, 0, sizeof(global_var1))將這塊內存區域清零,這也就解釋了為什麽調用fun之後,global_var1和global_var2都變成0的緣故。

  1. (gdb) break 27
  2. Breakpoint 1 at 0x80484d2: file main.c, line 27.
  3. (gdb) run
  4. Starting program: /home/astrol/c/elf/dynamic/understand_weak_symbol_by_example/main
  5. in main.c: &global_var1 = 0x804a018 global_var1 = ff00ff00
  6. sizeof(global_var1) = 4
  7. in main.c: &global_var2 = 0x804a01c global_var2 = ff00ff
  8. sizeof(global_var2) = 4
  9. in test.c: &global_var1 = 0x804a018 global_var1 = ff00ff00
  10. sizeof(global_var1) = 8
  11. global_var1 = 0
  12. global_var2 = 0
  13. Breakpoint 1, main (argc=1, argv=0xbffff6d4) at main.c:27
  14. 27 return 0;
  15. (gdb) print/x &global_var1
  16. $1 = 0x804a018
  17. (gdb) print/x &global_var2
  18. $2 = 0x804a01c
  19. (gdb) x/8xb &global_var1
  20. 0x804a018 <global_var1>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
  21. (gdb)

可見在test.c中對global_var1的改動會影響main.c中global_var1和global_var2的值。當程序很大時,這種錯誤很難發現,所以盡量避免不同類型的符號在多個文件中

三、如何避免呢?

1、上策:想辦法消除全局變量。全局變量會增加程序的耦合性,對他要控制使用。如果能用其他的方法代替最好。

2、中策:實在沒有辦法,那就把全局變量定義為static,它是沒有強弱之分的。而且不會和其他的全局符號產生沖突。至於其他文件可能對他的訪問,可以封裝成函數。把一個模塊的數據封裝起來是一個好的實踐。

3、下策:把所有的符號全部都變成強符號。所有的全局變量都初始化,記住,是所有的,哪怕初始值是0都行。如果一個沒有初始化,就可能會和其他人產生沖突,盡管別人初始化了。(自己寫代碼測試一下)。

4、必備之策: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段。 ------摘自《程序員的自我修養》

來看一個例子:

  1. /* aa.c */
  2. #include <stdio.h>
  3. int global ; /* weak symbol */
  4. int main(int argc, const char *argv[])
  5. {
  6. printf("global = %d, sizeof(global) in main = %d\n", global, sizeof(global));
  7. bb();
  8. return 0;
  9. }


  1. /* bb.c */
  2. #include <stdio.h>
  3. double global ; /* weak symbol */
  4. void bb()
  5. {
  6. printf("global = %f, sizeof(global) in bb = %d\n", global, sizeof(global));
  7. }


編譯成目標文件:

  1. gcc -c aa.c bb.c


得到aa.o 和 bb.o兩個目標文件

來看看他們的符號表

技術分享圖片

技術分享圖片

可以清楚的看到,在兩個目標文件中,Ndx數值都是COM,表示此時它們被放在COMMON塊。在aa.o中global的大小是4個字節,在bb.o中global的大小是8個字節。

那麽這兩個目標文件鏈接生成可執行文件後,global的大小是多少呢? -- 當不同的目標文件需要的COMMON塊空間大小不一致時,以最大的那塊為準。

  1. gcc aa.o bb.o -o cc


得到可執行文件cc

技術分享圖片

果然,global最終的大小為8個字節。

技術分享圖片

所以總體來看,未初始化全局變量最終還是被放在BSS段的。

如果我們給aa.c中的global賦值把它變成強符號呢?如下:

  1. /* aa.c */
  2. #include <stdio.h>
  3. int global = 100; /* strong symbol */
  4. int main(int argc, const char *argv[])
  5. {
  6. printf("global = %d, sizeof(global) in main = %d\n", global, sizeof(global));
  7. bb();
  8. return 0;
  9. }
  1. /* bb.c */
  2. #include <stdio.h>
  3. double global; /* weak symbol */
  4. void bb()
  5. {
  6. printf("global = %f, sizeof(global) in bb = %d\n", global, sizeof(global));
  7. }

得到兩個目標文件後查看符號,aa.o中global放在.data段,bb.o依然放在COMMON塊,最終的cc中global大小4字節,這很好的驗證了本文一開始的第二條規則。

可是有例外情況,看下面程序:

  1. /* aa.c */
  2. #include <stdio.h>
  3. int global; /* weak symbol */
  4. int main(int argc, const char *argv[])
  5. {
  6. printf("global = %d, sizeof(global) in main = %d\n", global, sizeof(global));
  7. bb();
  8. return 0;
  9. }
  1. /* bb.c */
  2. #include <stdio.h>
  3. double __attribute__ ((weak)) global = 1.0; /* weak symbol */
  4. void bb()
  5. {
  6. printf("global = %f, sizeof(global) in bb = %d\n", global, sizeof(global));
  7. }

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塊符號的定義而忽略其他的弱符號。)

---------------------------------------------------------------------------------------------------------------------------------------

至於為什麽這樣處理,目前我還不得而知,如果讀者知道的話,麻煩告訴我一下^_^!

再來看一種情況!如下:

  1. /* aa.c */
  2. #include <stdio.h>
  3. int __attribute__((weak)) global = 1; /* weak symbol */
  4. int main(int argc, const char *argv[])
  5. {
  6. printf("global = %d, sizeof(global) in main = %d\n", global, sizeof(global));
  7. bb();
  8. return 0;
  9. }
  1. /* bb.c */
  2. #include <stdio.h>
  3. double __attribute__((weak)) global = 1.0; /* weak symbol */
  4. void bb()
  5. {
  6. printf("global = %f, sizeof(global) in bb = %d\n", global, sizeof(global));
  7. }

結果卻是:

技術分享圖片

看到沒,同樣都是弱符號,卻因為編譯順序的不同,可執行文件中的大小也不同,為什麽會這樣,目前我也是不得而知!

簡而言之,在目標文件中沒有將未初始化的全局變量未初始化的靜態變量那樣放在BSS段,而是放在COMMON塊,是因為現在的編譯器和鏈接器允許不同類型的弱符號存在,最本質的原因是鏈接器無法判斷各個符號的類型是否一致。

有了COMMON塊之後就可以很好的解決這個問題了。

補充:

編程中我們可以使用GCC的“-fno-common”把所有的未初始化的全局變量不以COMMON塊的形式處理,也可以使用“__attribute__ ((nocommon))”,如下:

  1. int global __attribute__ ((nocommon)); /* strong symbol */

一旦一個未初始化的全局變量不是以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

http://blog.csdn.net/astrotycoon/article/details/8008629

C語言中的強符號與弱符號