1. 程式人生 > >C語言全域性未初始化資料段分析

C語言全域性未初始化資料段分析

前言:

        在分析C語言全域性未初始化變數時,發現在目標檔案中全域性未初始化變數並不是直接放在bss段中。

        再後來發現在兩個.c檔案中定義同名的全域性變數,連結時居然沒有發生符號重定義錯誤。才知道C語言弱定義的概念。這在C++中是絕對不行的。

       後來搜尋到一篇博文說:

       “全域性未初始化變數沒有被放到任何段,而是作為未定義的COMMON符號。這個和不同語言、編譯器實現有關,有的編譯器放到.bss 段,有的僅僅是預留一個COMMON符號,在連結的時候再在.bss段分配預留空間。編譯單元內部可見的靜態變數,比如在上述中加上static的 static int global_static_var則確實被放到了.bss,是因為這個僅僅是編譯單元內部可見。”

        由於最近對gcc反彙編的興趣,不禁有一種用反彙編工具分析這個問題的衝動。本文看起來很長,其實是圖片佔據了大量篇幅。所以閱讀量很小。懂這個問題的話,兩三句話就能說清楚。但是為了幾下我分析的過程,還是決定用這個篇幅來敘述一下。

零.幾個名詞解釋:

         弱定義:全域性未初始化變數的定義或宣告或者區域性靜態未初始化變數定義。(這是我理解的,實際上我沒找到關於弱定義權威的說明,難道是個人發明?)

         .data:全域性初始化資料段。

         .bss:全域性未初始化資料段。

         .rodata:只讀資料段。

一.分析的物件

  • 1.1全域性未初始化變數
  • 1.2全域性未初始化變數和另外一個檔案有同名變數定義
  • 2.1靜態全域性未初始化變數
  • 2.2靜態全域性未初始化變數+有本地同名的初始變數定義
  • 3.靜態區域性未初始化變數
  • 上面的情況遇到const

二.分析方法

1.檢視C原始碼對應的彙編程式碼;

2.用反彙編譯工具objdump和ELF檔案分析工具readelf依次分析目標檔案和可執行檔案,主要檢視符號表中變數的位置。

Ps:雖然我們用到了反彙編,但是隻是檢視變數的符號,還是比較簡單的。彙編程式碼我們也只看變數定義部分,所以還是很簡單。

三.環境和工具

環境為Ubuntu12.04 x64 (具體的環境資訊見附錄A)

工具為gcc,objdump,readelf

用gcc -S產生彙編程式碼(.s字尾名)。

用gcc -c產生目標檔案(.o字尾名)。

用gcc產生最終的可執行檔案(人為的將可執行檔案設定為.out字尾名)。

用objdump -t和readelf -s命令檢視目標檔案和可執行檔案的符號表。

四.具體分析過程

1.1全域性未初始化變數

原始碼檔案:1.1.c

彙編程式碼檔案:1.1.s

目標檔案:1.1.o

可執行檔案:1.1.out

/* 1.1.c */
#include <stdio.h>
int a;

int main()
{
    printf("%d\n",a);
    return 0;
}

執行結果截圖:


1.1.out執行結果.png

可以看出全域性未初始化變數a的值是0。

 再看彙編程式碼

;1.1.s中和變數a相關的部分
    .file    "1.1.c"
    .comm    a,4,4
從彙編程式碼看出a只是一個common符號。

對目標檔案1.1.o進行反彙編

objdump -t 1.1.o


objdump -t 1.1.o.png

從上圖中看出變數a在目標檔案中也只是一個COMMON符號,不屬於任何段。

再用readelf分析一下

readelf -s 1.1.o


readelf -s 1.1.o.png

可以看出a是全域性(GLOBAL)的COMMON符號。

對最終的可執行檔案進行反彙編

objdump -t 1.1.out


objdump -t 1.1.out.png

可以看出變數a在可執行目標檔案中屬於bss段

readelf -s 1.1.out


readelf -s 1.1.out.png

可以看出a是全域性的(GLOBAL)

1.1觀察結果小結:全域性未初始化變數a在編譯和彙編階段都不屬於任何段,只是一個COMMON符號,在連結時連結器ye沒發現其他同名的變數定義或宣告,將其放入了bss段中。

1.2全域性未初始化變數+另外一個檔案有同名變數定義

分析的物件:1.1.c 、1.2.c;1.1.s、1.2.s;1.1.o、1.2.o;1.out

1.1.c前面列出過,這裡不再贅敘。

/*1.2.c*/
int a = 100;
/*程式碼很簡單,只是定義了一個全域性變數*/

執行結果結果截圖


1.out執行結果.png

1.1.s前面貼出過,這裡不再贅敘。

;1.2.s中變數a相關的部分
.file    "1.2.c"
    .globl    a
    .data
    .align 4
    .type    a, @object
    .size    a, 4
a:
    .long    100

可以看出在1.2.s中變數a屬於全域性初始化資料段(.data),初始化值為100.

對目標檔案進行反彙編分析

1.1.o在前面分析過,這裡不再贅敘。只敘述1.2.o的分析情況

Objdump -t 1.2.o


obdump -t 1.2.o.png

readelf -s 1.2.o


 readelf -s 1.2.o.png

從上面兩張圖得出的結果和從1.2.s彙編程式碼中得出的結論一直,在1.2.o中a屬於.data段。

對可執行檔案進行反彙編分析

objdump -t 1.out


objdump -t 1.out.png

可以看出a在.data段

readelf -s 1.out


 readelf -s 1.out.png

可以看出a是全域性的。

1.2觀察結果小結:全域性未初始化變數a在1.1.o不屬於任何段,但由於在1.2.o中有一個同名的全域性初始化變數定義,所以在最終的執行檔案中a也變成了全域性初始化變數,屬於bss段了。

2.1.靜態的全域性未初始化變數

分析物件:2.1.c、2.1.s、2.1.o、2.1.out

由於靜態變數不會和其他檔案中的變數有什麼關係,所以分析單個檔案程式就夠了。

/*2.1.c*/
#include <stdio.h>

static int a; 

int main()
{
    printf("%d\n",a);
    return 0;
}

執行結果:


2.1.out執行結果.png

2.1.c對應的彙編程式碼:

;2.1.s中和a相關的部分
    .file    "2.1.c"
    .local    a
    .comm    a,4,4

可以看出a是本地COMMON符號。

對目標檔案2.1.o進行反彙編

objdump -t 2.1.o和readelf -s 2.1.o結果如下:


objdump -t readelf -s 2.1.o.png

從上圖分析結果可以看出靜態全域性未初始化變數a作用域為本地(LOCAL),並在bss段中。

2.1情況觀察結果小結:因為靜態變數連結時不會和其他檔案中的變數產生聯絡,所以靜態全域性未初始化變數a在彙編階段就能確定屬於bss段。

2.2靜態全域性未初始化變數+有本地同名的初始變數定義

    從2.1的分析結果可以看出,靜態全域性未初始化變數a在彙編階段就能確定是在bss段中了。假如在變數a的同一個檔案中還有一個a的同名初始化變數定義呢?按照弱定義的規則,這種情況a在彙編階段就能確定在.data段中。下面驗證一下:

分析物件:2.2.c、2.2.s、2.2.o、2.2.out

/* 2.2.c */
#include <stdio.h>

static int a;
static int a = 100;

int main()
{
    printf("%d\n",a);
    return 0;
}

執行結果截圖:


2.2. out執行結果.png

從執行結果看a的初始化值為100,說明static int a退化為宣告,而static int a = 100才是變數a的定義。

對應的彙編程式碼

    .file    "2.2.c"
    .data
    .align 4
    .type    a, @object
    .size    a, 4
a:
    .long    100

從彙編程式碼看出,在編譯階段就能確定a是在全域性初始化資料段(.data)了。下面對目標檔案和可執行檔案的分析只是驗證一下。

對目標檔案2.2.o進行反彙編分析

objdump -t 2.2.o

Readelf -s 2.2.o


對目標檔案2.2.o進行分析.png

可以看出a屬於.data段。但作用域為本檔案。

對最終的可執行檔案進行反彙編分析

objdump -t 2.2.out


                                                             objdump -t 2.1.out.png

readelf -s  2.2.out


 readelf -s 2.1.out .png

和對目標檔案分析的結果一致。

2.2情況分析小結:上面的分析結果驗證了在編譯階段就確定了a屬於全域性初始化資料段(.data)。

3.靜態區域性未初始化變數

靜態區域性未初始化變數是否和靜態全域性未初始化變數一樣呢?在編譯、彙編、連結階段屬於.data段、還是.bss段呢?

分析物件:3.c、3.s、3.o、3.out

3.c
#include <stdio.h>
int main()
{
    static int a;

    printf("%d\n",a);

    return 0;
}

執行結果:


3.out執行結果.png

對應的彙編程式碼:

;3.s
    .file    "3.c"
    .section    .rodata
.LC0:
    .string    "%d\n"
    .text
    .globl    main
    .type    main, @function
main:
.LFB0:
    .cfi_startproc
    pushq    %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    a.1804(%rip), %edx
    movl    $.LC0, %eax
    movl    %edx, %esi
    movq    %rax, %rdi
    movl    $0, %eax
    call    printf
    movl    $0, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size    main, .-main
    .local    a.1804                                ;a在這裡
    .comm    a.1804,4,4
    .ident    "GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3"
    .section    .note.GNU-stack,"",@progbits

可以看出在彙編程式碼中a被改名為a.1084(區域性變在編譯階段都會被改名的),而且是本地COMMON符號。

對目標檔案3.o進行反彙編分析:


3.o的反彙編分析.png

可以看出a在彙編階段就被放到bss段中了。

對最終的可執行檔案進行反彙編分析

objdump -t 3.out


objdump -t 3.out.png

Readelf -s 3.out


                                                                                       readelf -s 3.out.png

結果和目標檔案中的分析情況一致。

 3情況分析小結:靜態區域性未出世化變數屬於bss段,這在編譯階段就能確定。

如果是:靜態區域性未初始化變數+本地有靜態同名初始化變數 的情況呢?

會不會和“2.2靜態全域性未初始化變數+有本地同名的初始變數定義”情況一樣:static int a退化為宣告,而static int a = 100才是定義?

驗證的結果是在同一函式內部,同一作用域裡同時出現static int a和static int a = 100根本就通不過編譯,出現重宣告錯誤。看來只有靜態全域性變數可以這麼定義和宣告啊。

4.上面所有情況遇到const:

          分析過程和上面相似,但是據觀察:靜態全域性未初始化變數,在編譯階段就已確定屬於.rodata段;const靜態區域性未初始化變數規律和上面3種情況相同,屬於.bss段。(從彙編程式碼中看出)。

最終結論:

       弱定義變數有可能屬於bss段,也有可能屬於.data段。如果是靜態變數,在編譯階段就能確定;如果是全域性非靜態變數,在連結階段確定。至於到底屬於哪個段:如果在同一作用域內遇到其他屬於.data的同名定義,則屬於.data段;如果沒遇到,就屬於.bss段了。const變數比較特殊,const靜態全域性未初始化變數屬於.rodata段。

附錄A:具體的環境資訊

$ cat /etc/issue
Ubuntu 12.04.2 LTS \n \l

$ uname -a
Linux chao-AN408US 3.2.0-29-generic #46-Ubuntu SMP Fri Jul 27 17:03:23 UTC 2012 x86_64 x86_64 x86_64 GNU/Linux

$ cat /pro/version
Linux version 3.2.0-29-generic ([email protected]) (gcc version 4.6.3 (Ubuntu/Linaro 4.6.3-1ubuntu5) ) #46-Ubuntu SMP Fri Jul 27 17:03:23 UTC 2012

$ gcc --version
gcc (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3
Copyright © 2011 Free Software Foundation, Inc.
本程式是自由軟體;請參看原始碼的版權宣告。本軟體沒有任何擔保;
包括沒有適銷性和某一專用目的下的適用性擔保。

附錄B:參考資料

連結:Linux程式除錯--檢視二進位制檔案

Linux彙編教程

Linux程序地址空間詳解(轉載)

書:《組合語言程式設計》(美) Richard Blum 著