前言:
在分析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 (buildd@allspice) (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程序調試--查看二進制文件
http://www.cnblogs.com/androidme/archive/2013/04/08/3008615.html
Linux彙編教程
http://blog.csdn.net/yalizhi123/article/details/5752268
Linux進程地址空間詳解(轉載)
http://blog.sina.com.cn/s/blog_7321be1101013aua.html
書:《彙編語言程序設計》(美) Richard Blum 著