中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

C語言全局變量實例分析

發布時間:2022-04-13 10:23:00 來源:億速云 閱讀:167 作者:iii 欄目:開發技術

本篇內容主要講解“C語言全局變量實例分析”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“C語言全局變量實例分析”吧!

我們知道,全局變量是C語言語法和語義中一個很重要的知識點,首先它的存在意義需要從三個不同角度去理解:

  • 對于程序員來說,它是一個記錄內容的變量(variable);

  • 對于編譯/鏈接器來說,它是一個需要解析的符號(symbol);

  • 對于計算機來說,它可能是具有地址的一塊內存(memory)。

其次是語法/語義:

  • 從作用域上看,帶static關鍵字的全局變量范圍只能限定在文件里,否則會外聯到整個模塊和項目中;

  • 從生存期來看,它是靜態的,貫穿整個程序或模塊運行期間(注意,正是跨單元訪問和持續生存周期這兩個特點使得全局變量往往成為一段受攻擊代碼的突破口,了解這一點十分重要);

  • 從空間分配上看,定義且初始化的全局變量在編譯時在數據段(.data)分配空間,定義但未初始化的全局變量**暫存(tentative definition)**在.bss段,編譯時自動清零,而僅僅是聲明的全局變量只能算個符號,寄存在編譯器的符號表內,不會分配空間,直到鏈接或者運行時再重定向到相應的地址上。

我們將向您展現一下,非static限定全局變量在編譯/鏈接以及程序運行時會發生哪些有趣的事情,順便可以對C編譯器/鏈接器的解析原理管中窺豹。以下示例對ANSI C和GNU C標準都有效,筆者的編譯環境是Ubuntu下的GCC-4.4.3。

第一個例子

#ifndef _H_
#define _H_
int a;
#endif
/* foo.c */
#include <stdio.h>
#include "t.h"
struct {
   char a;
   int b;
} b = { 2, 4 };
int main();
void foo()
{
    printf("foo:\t(&a)=0x%08x\n\t(&b)=0x%08x\n
        \tsizeof(b)=%d\n\tb.a=%d\n\tb.b=%d\n\tmain:0x%08x\n",
        &a, &b, sizeof b, b.a, b.b, main);
}
/* main.c */
#include <stdio.h>
#include "t.h"
int b;
int c;
int main()
{
    foo();
    printf("main:\t(&a)=0x%08x\n\t(&b)=0x%08x\n
        \t(&c)=0x%08x\n\tsize(b)=%d\n\tb=%d\n\tc=%d\n",
        &a, &b, &c, sizeof b, b, c);
  return 0;
}

Makefile如下:

test: main.o foo.o
  gcc -o test main.o foo.o
main.o: main.c
foo.o: foo.c
clean:
  rm *.o test

運行情況:

foo:  (&a)=0x0804a024
  (&b)=0x0804a014
  sizeof(b)=8
  b.a=2
  b.b=4
  main:0x080483e4
main:  (&a)=0x0804a024
  (&b)=0x0804a014
  (&c)=0x0804a028
  size(b)=4
  b=2
  c=0

這個項目里我們定義了四個全局變量,t.h頭文件定義了一個整型a,main.c里定義了兩個整型b和c并且未初始化,foo.c里定義了一個初始化了的結構體,還定義了一個main的函數指針變量。

由于C語言每個源文件單獨編譯,所以t.h分別包含了兩次,所以int a就被定義了兩次。兩個源文件里變量b和函數指針變量main被重復定義了,實際上可以看做代碼段的地址。但編譯器并未報錯,只給出一條警告:

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

運行程序發現,main.c打印中b大小是4個字節,而foo.c是8個字節,因為sizeof關鍵字是編譯時決議,而源文件中對b類型定義不一樣。

但令人驚奇的是無論是在main.c還是foo.c中,a和b都是相同的地址,也就是說,a和b被定義了兩次,b還是不同類型,但內存映像中只有一份拷貝。

我們還看到,main.c中b的值居然就是foo.c中結構體第一個成員變量b.a的值,這證實了前面的推斷&mdash;&mdash;**即便存在多次定義,內存中只有一份初始化的拷貝。**另外在這里c是置身事外的一個獨立變量。

為何會這樣呢?這涉及到C編譯器對多重定義的全局符號的解析和鏈接。

在編譯階段,編譯器將全局符號信息隱含地編碼在可重定位目標文件的符號表里。這里有個**“強符號(strong)”和“弱符號(weak)”**的概念&mdash;&mdash;前者指的是定義并且初始化了的變量,比如foo.c里的結構體b,后者指的是未定義或者定義但未初始化的變量,比如main.c里的整型b和c,還有兩個源文件都包含頭文件里的a。當符號被多重定義時,GNU鏈接器(ld)使用以下規則決議:

  • 不允許出現多個相同強符號。

  • 如果有一個強符號和多個弱符號,則選擇強符號。

  • 如果有多個弱符號,那么先決議到size最大的那個,如果同樣大小,則按照鏈接順序選擇第一個。

像上面這個例子中,全局變量a和b存在重復定義。如果我們將main.c中的b初始化賦值,那么就存在兩個強符號而違反了規則一,編譯器報錯。

如果滿足規則二,則僅僅提出警告,實際運行時決議的是foo.c中的強符號。而變量a都是弱符號,所以只選擇一個(按照目標文件鏈接時的順序)。

事實上,這種規則是C語言里的一個大坑,編譯器對這種全局變量多重定義的“縱容”很可能會無端修改某個變量,導致程序不確定行為。如果你還沒有意識到事態嚴重性,我再舉個例子。

第二個例子

/* foo.c */
#include <stdio.h>;
struct {
    int a;
    int b;
} b = { 2, 4 };
int main();
void foo()
{
    printf("foo:\t(&b)=0x%08x\n\tsizeof(b)=%d\n
        \tb.a=%d\n\tb.b=%d\n\tmain:0x%08x\n",
        &b, sizeof b, b.a, b.b, main);
}
/* main.c */
#include <stdio.h>
int b;
int c;
int main()
{
    if (0 == fork()) {
        sleep(1);
        b = 1;
        printf("child:\tsleep(1)\n\t(&b):0x%08x\n
            \t(&c)=0x%08x\n\tsizeof(b)=%d\n\tset b=%d\n\tc=%d\n",
            &b, &c, sizeof b, b, c);
        foo();
    } else {
        foo();
        printf("parent:\t(&b)=0x%08x\n\t(&c)=0x%08x\n
            \tsizeof(b)=%d\n\tb=%d\n\tc=%d\n\twait child...\n",
            &b, &c, sizeof b, b, c);
        wait(-1);
        printf("parent:\tchild over\n\t(&b)=0x%08x\n
            \t(&c)=0x%08x\n\tsizeof(b)=%d\n\tb=%d\n\tc=%d\n",
            &b, &c, sizeof b, b, c);
    }
    return 0;
}

運行情況如下:

foo:  (&b)=0x0804a020
  sizeof(b)=8
  b.a=2
  b.b=4
  main:0x080484c8
parent:  (&b)=0x0804a020
  (&c)=0x0804a034
  sizeof(b)=4
  b=2
  c=0
  wait child...
child:  sleep(1)
  (&b):0x0804a020
  (&c)=0x0804a034
  sizeof(b)=4
  set b=1
  c=0
foo:  (&b)=0x0804a020
  sizeof(b)=8
  b.a=1
  b.b=4
  main:0x080484c8
parent:  child over
  (&b)=0x0804a020
  (&c)=0x0804a034
  sizeof(b)=4
  b=2
  c=0
 

(說明一點,運行情況是直接輸出到stdout的打印,筆者曾經將./test輸出重定向到log中,結果發現打印的執行序列不一致,所以采用默認輸出。)

這是一個多進程環境,首先我們看到無論父進程還是子進程,main.c還是foo.c,全局變量b和c的地址仍然是一致的(當然只是個邏輯地址),而且對b的大小不同模塊仍然有不同的決議。

這里值得注意的是,我們在子進程中對變量b進行賦值動作,從此子進程本身包括foo()調用中,整型b以及結構體成員b.a的值都是1,而父進程中整型b和結構體成員b.a的值仍是2,但它們顯示的邏輯地址仍是一致的。

個人認為可以這樣解釋,fork創建新進程時,子進程獲得了父進程上下文“鏡像”(自然包括全局變量),虛擬地址相同但屬于不同的進程空間,而且此時真正映射的物理地址中只有一份拷貝,所以b的值是相同的(都是2)。

隨后子進程對b改寫,觸發了操作系統的**寫時拷貝(copy on write)**機制,這時物理內存中才產生真正的兩份拷貝,分別映射到不同進程空間的虛擬地址上,但虛擬地址的值本身仍然不變,這對于應用程序來說是透明的,具有隱瞞性。

還有一點值得注意,這個示例編譯時沒有出現第一個示例的警告,即對變量b的sizeof決議,筆者也不知道為什么,或許是GCC的一個bug?

第三個例子

這個例子代碼同上一個一致,只不過我們將foo.c做成一個靜態鏈接庫libfoo.a進行鏈接,這里只給出Makefile的改動。

test: main.o foo.o
  ar rcs libfoo.a foo.o
  gcc -static -o test main.o libfoo.a
main.o: main.c
foo.o: foo.c
clean:
  rm -f *.o test

運行情況如下:

foo:  (&b)=0x080ca008
  sizeof(b)=8
  b.a=2
  b.b=4
  main:0x08048250
parent:  (&b)=0x080ca008
  (&c)=0x080cc084
  sizeof(b)=4
  b=2
  c=0
  wait child...
child:  sleep(1)
  (&b):0x080ca008
  (&c)=0x080cc084
  sizeof(b)=4
  set b=1
  c=0
foo:  (&b)=0x080ca008
  sizeof(b)=8
  b.a=1
  b.b=4
  main:0x08048250
parent:  child over
  (&b)=0x080ca008
  (&c)=0x080cc084
  sizeof(b)=4
  b=2
  c=0
 

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

到此為止,有些人可能會對上面的例子嗤之以鼻,覺得這不過是列舉了C語言的某些特性而已,算不上黑。

有些人認為既然如此,對于一切全局變量要么用static限死,要么定義同時初始化,杜絕弱符號,以便在編譯時報錯檢測出來。只要小心地使用,C語言還是很完美的嘛~

對于抱這樣想法的人,我只想說,請你在夜深人靜的時候豎起耳朵仔細聆聽,你很可能聽到Dennis Richie在九泉之下邪惡的笑聲&mdash;&mdash;不,與其說是嘲笑,不如說是詛咒&hellip;&hellip;

第四個例子

/* foo.c */
#include <stdio.h>
const struct {
    int a;
    int b;
} b = { 3, 3 };
int main();
void foo()
{
    b.a = 4;
    b.b = 4;
    printf("foo:\t(&b)=0x%08x\n\tsizeof(b)=%d\n
        \tb.a=%d\n\tb.b=%d\n\tmain:0x%08x\n",
        &b, sizeof b, b.a, b.b, main);
}
/* t1.c */
#include <stdio.h>
int b = 1;
int c = 1;
int main()
{
    int count = 5;
    while (count-- > 0) {
        t2();
        foo();
        printf("t1:\t(&b)=0x%08x\n\t(&c)=0x%08x\n
            \tsizeof(b)=%d\n\tb=%d\n\tc=%d\n",
            &b, &c, sizeof b, b, c);
        sleep(1);
    }
    return 0;
}
/* t2.c */
#include <stdio.h>
int b;
int c;
int t2()
{
    printf("t2:\t(&b)=0x%08x\n\t(&c)=0x%08x\n
        \tsizeof(b)=%d\n\tb=%d\n\tc=%d\n",
        &b, &c, sizeof b, b, c);
    return 0;
}

Makefile腳本:

export LD_LIBRARY_PATH:=.
all: test
  ./test
test: t1.o t2.o
  gcc -shared -fPIC -o libfoo.so foo.c
  gcc -o test t1.o t2.o -L. -lfoo
t1.o: t1.c
t2.o: t2.c
.PHONY:clean
clean:
  rm -f *.o *.so test*

執行結果:

./test
t2:  (&b)=0x0804a01c
  (&c)=0x0804a020
  sizeof(b)=4
  b=1
  c=1
foo:  (&b)=0x0804a01c
  sizeof(b)=8
  b.a=4
  b.b=4
  main:0x08048564
t1:  (&b)=0x0804a01c
  (&c)=0x0804a020
  sizeof(b)=4
  b=4
  c=4
t2:  (&b)=0x0804a01c
  (&c)=0x0804a020
  sizeof(b)=4
  b=4
  c=4
foo:  (&b)=0x0804a01c
  sizeof(b)=8
  b.a=4
  b.b=4
  main:0x08048564
t1:  (&b)=0x0804a01c
  (&c)=0x0804a020
  sizeof(b)=4
  b=4
  c=4
  ...
 

其實前面幾個例子只是開胃小菜而已,真正的大坑終于出現了!而且這次編譯器既沒報錯也沒警告,但我們確實眼睜睜地看到作為main()中強符號的b被改寫了,而且一旁的c也“躺槍”了。

眼尖的讀者發現,這次foo.c是作為動態鏈接庫運行時加載的,當t1第一次調用t2時,libfoo.so還未加載,一旦調用了foo函數,b立馬中彈,而且c的地址居然還相鄰著b,這使得c一同中彈了。

不過筆者有些無法解釋這種行為的原因,有種說法是強符號的全局變量在數據段中是連續分布的(相應地弱符號暫存在.bss段或者符號表里),或許可以上報GNU的編譯器開發小組。

另外筆者嘗試過將t1.c中的b和c定義前面加上const限定詞,編譯器仍然默認通過,但程序在main()中第一次調用foo()時觸發了Segment fault異常導致奔潰,在foo.c里使用指針改寫它也一樣。

推斷這是GCC對const常量所在地址啟用了類似操作系統寫保護機制,但我無法確定早期版本的GCC是否會讓這個const常量被改寫而程序不會奔潰。

至于volatile關鍵詞之于全局變量,自測似乎沒有影響。

C語言在你心目中是否還是當初那個“純潔”、“干凈”、“行為一致”的姑娘呢?也許趁著你不注意的時候她會偷偷給你戴頂綠帽,這一切都是通過全局變量,特別在動態鏈接的環境下,就算全部定義成強符號仍然無法為編譯器所察覺。

而一些IT界“恐怖分子”也經常**將惡意代碼包裝成全局變量注入到root權限下存在漏洞的操作序列中,**就像著名的棧溢出攻擊那樣。某一天當你傻傻地看著一個程序出現未定義的行為卻無法定位原因的時候,請不要忘記Richie大爺那來自九泉之下最深沉的“問候”~

或許有些人會偷換概念,把這一切歸咎于編譯器和鏈接器身上,認為這同語言無關,但這里我要提醒,正是編譯/鏈接器的行為支撐了整個語言的語法和語義。

我們可以反過來思考一下為何C的胞弟C++推出**“命名空間(namespace)”**的概念,或者你可以使用其它高級語言,對于重定義的全局變量是否能通過編譯這一關。

到此,相信大家對“C語言全局變量實例分析”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

赣榆县| 额尔古纳市| 安泽县| 都匀市| 凤庆县| 澄江县| 新密市| 杭州市| 宁陕县| 湘潭县| 嘉义市| 武穴市| 辛集市| 余江县| 泽库县| 辽中县| 德昌县| 隆化县| 丹阳市| 颍上县| 壤塘县| 永仁县| 巨鹿县| 大同县| 吉木萨尔县| 海盐县| 海安县| 北辰区| 博乐市| 铜梁县| 巴东县| 清河县| 宾川县| 铜陵市| 武清区| 定陶县| 山东| 遂溪县| 呼玛县| 和田县| 丹棱县|