日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問(wèn)題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營(yíng)銷解決方案
C語(yǔ)言全局變量那些事兒

作為一名程序員,如果說(shuō)沉迷一門編程語(yǔ)言算作一種樂(lè)趣的話,那么與此同時(shí)反過(guò)來(lái)去黑一門編程語(yǔ)言就是這種樂(lè)趣的升華。今天我們就來(lái)黑一把C語(yǔ)言,好好展示一下這門經(jīng)典語(yǔ)言令人抓狂的一面。

專注于為中小企業(yè)提供成都網(wǎng)站設(shè)計(jì)、網(wǎng)站制作服務(wù),電腦端+手機(jī)端+微信端的三站合一,更高效的管理,為中小企業(yè)張家界免費(fèi)做網(wǎng)站提供優(yōu)質(zhì)的服務(wù)。我們立足成都,凝聚了一批互聯(lián)網(wǎng)行業(yè)人才,有力地推動(dòng)了近千家企業(yè)的穩(wěn)健成長(zhǎng),幫助中小企業(yè)通過(guò)網(wǎng)站建設(shè)實(shí)現(xiàn)規(guī)模擴(kuò)充和轉(zhuǎn)變。

我們知道,全局變量是C語(yǔ)言語(yǔ)法和語(yǔ)義中一個(gè)很重要的知識(shí)點(diǎn),首先它的存在意義需要從三個(gè)不同角度去理解:對(duì)于程序員來(lái)說(shuō),它是一個(gè)記錄內(nèi)容的變量(variable);對(duì)于編譯/鏈接器來(lái)說(shuō),它是一個(gè)需要解析的符號(hào)(symbol);對(duì)于計(jì)算機(jī)來(lái)說(shuō),它可能是具有地址的一塊內(nèi)存(memory)。其次是語(yǔ)法/語(yǔ)義:從作用域上看,帶static關(guān)鍵字的全局變量范圍只能限定在文件里,否則會(huì)外聯(lián)到整個(gè)模塊和項(xiàng)目中;從生存期來(lái)看,它是靜態(tài)的,貫穿整個(gè)程序或模塊運(yùn)行期間(注意,正是跨單元訪問(wèn)和持續(xù)生存周期這兩個(gè)特點(diǎn)使得全局變量往往成為一段受攻擊代碼的突破口,了解這一點(diǎn)十分重要);從空間分配上看,定義且初始化的全局變量在編譯時(shí)在數(shù)據(jù)段(.data)分配空間,定義但未初始化的全局變量暫存(tentative definition)在.bss段,編譯時(shí)自動(dòng)清零,而僅僅是聲明的全局變量只能算個(gè)符號(hào),寄存在編譯器的符號(hào)表內(nèi),不會(huì)分配空間,直到鏈接或者運(yùn)行時(shí)再重定向到相應(yīng)的地址上。

我們將向您展現(xiàn)一下,非static限定全局變量在編譯/鏈接以及程序運(yùn)行時(shí)會(huì)發(fā)生哪些有趣的事情,順便可以對(duì)C編譯器/鏈接器的解析原理管中窺豹。以下示例對(duì)ANSI C和GNU C標(biāo)準(zhǔn)都有效,筆者的編譯環(huán)境是Ubuntu下的GCC-4.4.3。

 
 
 
  1. /* t.h */
  2. #ifndef _H_
  3. #define _H_
  4. int a;
  5. #endif
  6.  
  7. /* foo.c */
  8. #include 
  9. #include "t.h"
  10.  
  11. struct {
  12.    char a;
  13.    int b;
  14. } b = { 2, 4 };
  15.  
  16. int main();
  17.  
  18. void foo()
  19. {
  20.     printf("foo:\t(&a)=0x%08x\n\t(&b)=0x%08x\n
  21.         \tsizeof(b)=%d\n\tb.a=%d\n\tb.b=%d\n\tmain:0x%08x\n",
  22.         &a, &b, sizeof b, b.a, b.b, main);
  23. }
  24.  
  25. /* main.c */
  26. #include 
  27. #include "t.h"
  28.  
  29. int b;
  30. int c;
  31.  
  32. int main()
  33. {
  34.     foo();
  35.     printf("main:\t(&a)=0x%08x\n\t(&b)=0x%08x\n
  36.         \t(&c)=0x%08x\n\tsize(b)=%d\n\tb=%d\n\tc=%d\n",
  37.         &a, &b, &c, sizeof b, b, c);
  38.     return 0;
  39. }

Makefile如下:

 
 
 
  1. test: main.o foo.o
  2.     gcc -o test main.o foo.o
  3.  
  4. main.o: main.c
  5. foo.o: foo.c
  6.  
  7. clean:
  8.     rm *.o test

運(yùn)行情況:

 
 
 
  1. foo:    (&a)=0x0804a024
  2.     (&b)=0x0804a014
  3.     sizeof(b)=8
  4.     b.a=2
  5.     b.b=4
  6.     main:0x080483e4
  7. main:   (&a)=0x0804a024
  8.     (&b)=0x0804a014
  9.     (&c)=0x0804a028
  10.     size(b)=4
  11.     b=2
  12.     c=0

這個(gè)項(xiàng)目里我們定義了四個(gè)全局變量,t.h頭文件定義了一個(gè)整型a,main.c里定義了兩個(gè)整型b和c并且未初始化,foo.c里定義了一個(gè)初始化了的 結(jié)構(gòu)體,還定義了一個(gè)main的函數(shù)指針變量。由于C語(yǔ)言每個(gè)源文件單獨(dú)編譯,所以t.h分別包含了兩次,所以int a就被定義了兩次。兩個(gè)源文件里變量b和函數(shù)指針變量main被重復(fù)定義了,實(shí)際上可以看做代碼段的地址。但編譯器并未報(bào)錯(cuò),只給出一條警告:

 
 
 
  1. /usr/bin/ld: Warning: size of symbol 'b' changed from 4 in main.o to 8 in foo.o

運(yùn)行程序發(fā)現(xiàn),main.c打印中b大小是4個(gè)字節(jié),而foo.c是8個(gè)字節(jié),因?yàn)閟izeof關(guān)鍵字是編譯時(shí)決議,而源文件中對(duì)b類型定義不一 樣。但令人驚奇的是無(wú)論是在main.c還是foo.c中,a和b都是相同的地址,也就是說(shuō),a和b被定義了兩次,b還是不同類型,但內(nèi)存映像中只有一份 拷貝。我們還看到,main.c中b的值居然就是foo.c中結(jié)構(gòu)體第一個(gè)成員變量b.a的值,這證實(shí)了前面的推斷——即便存在多次定義,內(nèi)存中只有一份初始化的拷貝。另外在這里c是置身事外的一個(gè)獨(dú)立變量。

為何會(huì)這樣呢?這涉及到C編譯器對(duì)多重定義的全局符號(hào)的解析和鏈接。在編譯階段,編譯器將全局符號(hào)信息隱含地編碼在可重定位目標(biāo)文件的符號(hào)表里。這里有個(gè)“強(qiáng)符號(hào)(strong)”和“弱符號(hào)(weak)”的概念——前者指的是定義并且初始化了的變量,比如foo.c里的結(jié)構(gòu)體b,后者指的是未定義或者定義但未初始化的變量,比如main.c里的整型b和c,還有兩個(gè)源文件都包含頭文件里的a。當(dāng)符號(hào)被多重定義時(shí),GNU鏈接器(ld)使用以下規(guī)則決議:

  • 不允許出現(xiàn)多個(gè)相同強(qiáng)符號(hào)。
  • 如果有一個(gè)強(qiáng)符號(hào)和多個(gè)弱符號(hào),則選擇強(qiáng)符號(hào)。
  • 如果有多個(gè)弱符號(hào),那么先決議到size最大的那個(gè),如果同樣大小,則按照鏈接順序選擇第一個(gè)。

像上面這個(gè)例子中,全局變量a和b存在重復(fù)定義。如果我們將main.c中的b初始化賦值,那么就存在兩個(gè)強(qiáng)符號(hào)而違反了規(guī)則一,編譯器報(bào)錯(cuò)。如果 滿足規(guī)則二,則僅僅提出警告,實(shí)際運(yùn)行時(shí)決議的是foo.c中的強(qiáng)符號(hào)。而變量a都是弱符號(hào),所以只選擇一個(gè)(按照目標(biāo)文件鏈接時(shí)的順序)。

事實(shí)上,這種規(guī)則是C語(yǔ)言里的一個(gè)大坑,編譯器對(duì)這種全局變量多重定義的“縱容”很可能會(huì)無(wú)端修改某個(gè)變量,導(dǎo)致程序不確定行為。如果你還沒(méi)有意識(shí)到事態(tài)嚴(yán)重性,我再舉個(gè)例子。

#p#

第二個(gè)例子

 
 
 
  1. /* foo.c */
  2. #include ;
  3.  
  4. struct {
  5.     int a;
  6.     int b;
  7. } b = { 2, 4 };
  8.  
  9. int main();
  10.  
  11. void foo()
  12. {
  13.     printf("foo:\t(&b)=0x%08x\n\tsizeof(b)=%d\n
  14.         \tb.a=%d\n\tb.b=%d\n\tmain:0x%08x\n",
  15.         &b, sizeof b, b.a, b.b, main);
  16. }
  17.  
  18. /* main.c */
  19. #include 
  20.  
  21. int b;
  22. int c;
  23.  
  24. int main()
  25. {
  26.     if (0 == fork()) {
  27.         sleep(1);
  28.         b = 1;
  29.         printf("child:\tsleep(1)\n\t(&b):0x%08x\n
  30.             \t(&c)=0x%08x\n\tsizeof(b)=%d\n\tset b=%d\n\tc=%d\n",
  31.             &b, &c, sizeof b, b, c);
  32.         foo();
  33.     } else {
  34.         foo();
  35.         printf("parent:\t(&b)=0x%08x\n\t(&c)=0x%08x\n
  36.             \tsizeof(b)=%d\n\tb=%d\n\tc=%d\n\twait child...\n",
  37.             &b, &c, sizeof b, b, c);
  38.         wait(-1);
  39.         printf("parent:\tchild over\n\t(&b)=0x%08x\n
  40.             \t(&c)=0x%08x\n\tsizeof(b)=%d\n\tb=%d\n\tc=%d\n",
  41.             &b, &c, sizeof b, b, c);
  42.     }
  43.     return 0;
  44. }

運(yùn)行情況如下:

 
 
 
  1. foo:    (&b)=0x0804a020
  2.     sizeof(b)=8
  3.     b.a=2
  4.     b.b=4
  5.     main:0x080484c8
  6. parent: (&b)=0x0804a020
  7.     (&c)=0x0804a034
  8.     sizeof(b)=4
  9.     b=2
  10.     c=0
  11.     wait child...
  12. child:  sleep(1)
  13.     (&b):0x0804a020
  14.     (&c)=0x0804a034
  15.     sizeof(b)=4
  16.     set b=1
  17.     c=0
  18. foo:    (&b)=0x0804a020
  19.     sizeof(b)=8
  20.     b.a=1
  21.     b.b=4
  22.     main:0x080484c8
  23. parent: child over
  24.     (&b)=0x0804a020
  25.     (&c)=0x0804a034
  26.     sizeof(b)=4
  27.     b=2
  28.     c=0

(說(shuō)明一點(diǎn),運(yùn)行情況是直接輸出到stdout的打印,筆者曾經(jīng)將./test輸出重定向到log中,結(jié)果發(fā)現(xiàn)打印的執(zhí)行序列不一致,所以采用默認(rèn)輸出。)

這是一個(gè)多進(jìn)程環(huán)境,首先我們看到無(wú)論父進(jìn)程還是子進(jìn)程,main.c還是foo.c,全局變量b和c的地址仍然是一致的(當(dāng)然只是個(gè)邏輯地址),而且對(duì)b的大小不同模塊仍然有不同的決議。這里值得注意的是,我們?cè)谧舆M(jìn)程中對(duì)變量b進(jìn)行賦值動(dòng)作,從此子進(jìn)程本身包括foo()調(diào)用中,整型b以及結(jié)構(gòu)體成員b.a的值都是1,而父進(jìn)程中整型b和結(jié)構(gòu)體成員b.a的值仍是2,但它們顯示的邏輯地址仍是一致的。

個(gè)人認(rèn)為可以這樣解釋,fork創(chuàng)建新進(jìn)程時(shí),子進(jìn)程獲得了父進(jìn)程上下文“鏡像”(自然包括全局變量),虛擬地址相同但屬于不同的進(jìn)程空間,而且此時(shí)真正映射的物理地址中只有一份拷貝,所以b的值是相同的(都是2)。隨后子進(jìn)程對(duì)b改寫(xiě),觸發(fā)了操作系統(tǒng)的寫(xiě)時(shí)拷貝(copy on write)機(jī)制,這時(shí)物理內(nèi)存中才產(chǎn)生真正的兩份拷貝,分別映射到不同進(jìn)程空間的虛擬地址上,但虛擬地址的值本身仍然不變,這對(duì)于應(yīng)用程序來(lái)說(shuō)是透明的,具有隱瞞性。

還有一點(diǎn)值得注意,這個(gè)示例編譯時(shí)沒(méi)有出現(xiàn)第一個(gè)示例的警告,即對(duì)變量b的sizeof決議,筆者也不知道為什么,或許是GCC的一個(gè)bug?

#p#

第三個(gè)例子

這個(gè)例子代碼同上一個(gè)一致,只不過(guò)我們將foo.c做成一個(gè)靜態(tài)鏈接庫(kù)libfoo.a進(jìn)行鏈接,這里只給出Makefile的改動(dòng)。

 
 
 
  1. test: main.o foo.o
  2.     ar rcs libfoo.a foo.o
  3.     gcc -static -o test main.o libfoo.a
  4.  
  5. main.o: main.c
  6. foo.o: foo.c
  7.  
  8. clean:
  9.     rm -f *.o test

運(yùn)行情況如下:

 
 
 
  1. foo:    (&b)=0x080ca008
  2.     sizeof(b)=8
  3.     b.a=2
  4.     b.b=4
  5.     main:0x08048250
  6. parent: (&b)=0x080ca008
  7.     (&c)=0x080cc084
  8.     sizeof(b)=4
  9.     b=2
  10.     c=0
  11.     wait child...
  12. child:  sleep(1)
  13.     (&b):0x080ca008
  14.     (&c)=0x080cc084
  15.     sizeof(b)=4
  16.     set b=1
  17.     c=0
  18. foo:    (&b)=0x080ca008
  19.     sizeof(b)=8
  20.     b.a=1
  21.     b.b=4
  22.     main:0x08048250
  23. parent: child over
  24.     (&b)=0x080ca008
  25.     (&c)=0x080cc084
  26.     sizeof(b)=4
  27.     b=2
  28.     c=0

從這個(gè)例子看不出有啥差別,只不過(guò)使用靜態(tài)鏈接后,全局變量加載的地址有所改變,b和c的地址之間似乎相隔更遠(yuǎn)了些。不過(guò)這次編譯器倒是給出了變量b的sizeof決議警告。

到此為止,有些人可能會(huì)對(duì)上面的例子嗤之以鼻,覺(jué)得這不過(guò)是列舉了C語(yǔ)言的某些特性而已,算不上黑。有些人認(rèn)為既然如此,對(duì)于一切全局變量要么用 static限死,要么定義同時(shí)初始化,杜絕弱符號(hào),以便在編譯時(shí)報(bào)錯(cuò)檢測(cè)出來(lái)。只要小心地使用,C語(yǔ)言還是很完美的嘛~對(duì)于抱這樣想法的人,我只想說(shuō), 請(qǐng)你在夜深人靜的時(shí)候豎起耳朵仔細(xì)聆聽(tīng),你很可能聽(tīng)到Dennis Richie在九泉之下邪惡的笑聲——不,與其說(shuō)是嘲笑,不如說(shuō)是詛咒……

#p#

第四個(gè)例子

 
 
 
  1. /* foo.c */
  2. #include 
  3.  
  4. const struct {
  5.     int a;
  6.     int b;
  7. } b = { 3, 3 };
  8.  
  9. int main();
  10.  
  11. void foo()
  12. {
  13.     b.a = 4;
  14.     b.b = 4;
  15.     printf("foo:\t(&b)=0x%08x\n\tsizeof(b)=%d\n
  16.         \tb.a=%d\n\tb.b=%d\n\tmain:0x%08x\n",
  17.         &b, sizeof b, b.a, b.b, main);
  18. }
  19.  
  20. /* t1.c */
  21. #include 
  22.  
  23. int b = 1;
  24. int c = 1;
  25.  
  26. int main()
  27. {
  28.     int count = 5;
  29.     while (count-- > 0) {
  30.         t2();
  31.         foo();
  32.         printf("t1:\t(&b)=0x%08x\n\t(&c)=0x%08x\n
  33.             \tsizeof(b)=%d\n\tb=%d\n\tc=%d\n",
  34.             &b, &c, sizeof b, b, c);
  35.         sleep(1);
  36.     }
  37.     return 0;
  38. }
  39.  
  40. /* t2.c */
  41. #include 
  42.  
  43. int b;
  44. int c;
  45.  
  46. int t2()
  47. {
  48.     printf("t2:\t(&b)=0x%08x\n\t(&c)=0x%08x\n
  49.         \tsizeof(b)=%d\n\tb=%d\n\tc=%d\n",
  50.         &b, &c, sizeof b, b, c);
  51.     return 0;
  52. }

Makefile腳本:

 
 
 
  1. export LD_LIBRARY_PATH:=.
  2.  
  3. all: test
  4.     ./test
  5.  
  6. test: t1.o t2.o
  7.     gcc -shared -fPIC -o libfoo.so foo.c
  8.     gcc -o test t1.o t2.o -L. -lfoo
  9.  
  10. t1.o: t1.c
  11. t2.o: t2.c
  12.  
  13. .PHONY:clean
  14. clean:
  15.     rm -f *.o *.so test*

執(zhí)行結(jié)果:

 
 
 
  1. ./test
  2. t2: (&b)=0x0804a01c
  3.     (&c)=0x0804a020
  4.     sizeof(b)=4
  5.     b=1
  6.     c=1
  7. foo:    (&b)=0x0804a01c
  8.     sizeof(b)=8
  9.     b.a=4
  10.     b.b=4
  11.     main:0x08048564
  12. t1: (&b)=0x0804a01c
  13.     (&c)=0x0804a020
  14.     sizeof(b)=4
  15.     b=4
  16.     c=4
  17. t2: (&b)=0x0804a01c
  18.     (&c)=0x0804a020
  19.     sizeof(b)=4
  20.     b=4
  21.     c=4
  22. foo:    (&b)=0x0804a01c
  23.     sizeof(b)=8
  24.     b.a=4
  25.     b.b=4
  26.     main:0x08048564
  27. t1: (&b)=0x0804a01c
  28.     (&c)=0x0804a020
  29.     sizeof(b)=4
  30.     b=4
  31.     c=4
  32.     ...

其實(shí)前面幾個(gè)例子只是開(kāi)胃小菜而已,真正的大坑終于出現(xiàn)了!而且這次編譯器既沒(méi)報(bào)錯(cuò)也沒(méi)警告,但我們確實(shí)眼睜睜地看到作為main()中強(qiáng)符號(hào)的b 被改寫(xiě)了,而且一旁的c也“躺槍”了。眼尖的讀者發(fā)現(xiàn),這次foo.c是作為動(dòng)態(tài)鏈接庫(kù)運(yùn)行時(shí)加載的,當(dāng)t1第一次調(diào)用t2時(shí),libfoo.so還未加 載,一旦調(diào)用了foo函數(shù),b立馬中彈,而且c的地址居然還相鄰著b,這使得c一同中彈了。不過(guò)筆者有些無(wú)法解釋這種行為的原因,有種說(shuō)法是強(qiáng)符號(hào)的全局變量在數(shù)據(jù)段中是連續(xù)分布的(相應(yīng)地弱符號(hào)暫存在.bss段或者符號(hào)表里),或許可以上報(bào)GNU的編譯器開(kāi)發(fā)小組。

另外筆者嘗試過(guò)將t1.c中的b和c定義前面加上const限定詞,編譯器仍然默認(rèn)通過(guò),但程序在main()中第一次調(diào)用foo()時(shí)觸發(fā)了Segment fault異常導(dǎo)致奔潰,在foo.c里使用指針改寫(xiě)它也一樣。推斷這是GCC對(duì)const常量所在地址啟用了類似操作系統(tǒng)寫(xiě)保護(hù)機(jī)制,但我無(wú)法確定早期版本的GCC是否會(huì)讓這個(gè)const常量被改寫(xiě)而程序不會(huì)奔潰。

至于volatile關(guān)鍵詞之于全局變量,自測(cè)似乎沒(méi)有影響。

怎么樣?看了最后一個(gè)例子是否有點(diǎn)“不明覺(jué)厲”呢?C語(yǔ)言在你心目中是否還是當(dāng)初那個(gè)“純潔”、“干凈”、“行為一致”的姑娘呢?也許趁著你不注意 的時(shí)候她會(huì)偷偷給你戴頂綠帽,這一切都是通過(guò)全局變量,特別在動(dòng)態(tài)鏈接的環(huán)境下,就算全部定義成強(qiáng)符號(hào)仍然無(wú)法為編譯器所察覺(jué)。而一些IT界“恐怖分子” 也經(jīng)常將惡意代碼包裝成全局變量注入到root權(quán)限下存在漏洞的操作序列中,就像著名的棧溢出攻擊那樣。某一天當(dāng)你傻傻地看著一個(gè)程序出現(xiàn)未定義的行為卻無(wú)法定位原因的時(shí)候,請(qǐng)不要忘記Richie大爺那來(lái)自九泉之下最深沉的“問(wèn)候”~

或許有些人會(huì)偷換概念,把這一切歸咎于編譯器和鏈接器身上,認(rèn)為這同語(yǔ)言無(wú)關(guān),但我要提醒你,正是編譯/鏈接器的行為支撐了整個(gè)語(yǔ)言的語(yǔ)法和語(yǔ)義。你可以反過(guò)來(lái)思考一下為何C的胞弟C++推出“命名空間(namespace)”的概念,或者你可以使用其它高級(jí)語(yǔ)言,對(duì)于重定義的全局變量是否能通過(guò)編譯這一關(guān)。

所以請(qǐng)時(shí)刻謹(jǐn)記,C是一門很恐怖的語(yǔ)言!

P.S.題外話寫(xiě)在最后。我無(wú)意挑起語(yǔ)言之爭(zhēng),只是就事論事地去“黑(hack)”一門語(yǔ)言而已,而且要黑就要黑得有理有力有層次,還要帶點(diǎn)娛樂(lè)精神。其實(shí)黑一門語(yǔ)言并非什么尖端復(fù)雜的技術(shù),個(gè)人覺(jué)得起碼要做到兩點(diǎn):

  • 親自動(dòng)手寫(xiě)測(cè)試程序。動(dòng)手寫(xiě)測(cè)試程序是開(kāi)發(fā)人員必備的基礎(chǔ)技能,只有現(xiàn)成的代碼才能讓人心服口服,那些只會(huì)停留在口頭上的爭(zhēng)論只能算作cheap hack。
  • 測(cè)試程序不能依賴于不成熟的代碼。軟件開(kāi)發(fā)99%以上的bug都是基于不合格(substandard)開(kāi)發(fā) 人員導(dǎo)致,這并不能怪罪于語(yǔ)言以及編譯器本身。使用諸如#define TRUE FALSE或者#define NULL 1之類的trick來(lái)黑C語(yǔ)言只能證明此人很有娛樂(lè)精神而不是真正的”hack value”,拿老北京梨園行當(dāng)里的一句話——“那是下三濫的玩意兒”。

網(wǎng)站名稱:C語(yǔ)言全局變量那些事兒
網(wǎng)站鏈接:http://www.dlmjj.cn/article/dpieepe.html