閱讀747 返回首頁    go 阿裏雲 go 技術社區[雲棲]


你的變量究竟存儲在什麼地方 && 全局內存

我相信大家都有過這樣的經曆,在麵試過程中,考官通常會給你一道題目,然後問你某個變量存儲在什麼地方,在內存中是如何存儲的等等一係列問題。不僅僅是在麵試中,學校裏麵的考試也會碰到同樣的問題。
 如果你還不知道答案,請接著往下看。接下來,我們將在Linux操作係統上,以GCC編譯器為例來講解變量的存儲。
 在計算機係統中,目標文件通常有三種形式:
1. 可重定位的目標文件:包含二進製代碼和數據,與其他可重定位目標文件合並起來,創建一個可執行目標文件。
2. 可執行的目標文件:包含二進製代碼和數據,其形式可以被直接拷貝到存儲器中並執行
3. 共享目標文件:一種特殊的可重定位目標文件,即我們通常所說的動(靜)態鏈接庫
一個典型的可重定位目標文件如下圖所示:
 高地址
節頭部表
.strtab
.line
.debug
.rel.data
.rel.text
.symtab
.bss
>        .dataa (3)
.rodata
>        .textt (1)
ELF頭
>                                                                        0
1典型的ELF可重定位目標文件(數字代表索引)
 夾在ELF頭和節頭部表之間的都是節(section),各個節的意思如下:
含義
.text
已編譯程序的機器代碼
.rodata
隻讀數據,如pintf和switch語句中的字符串和常量值
.data
已初始化的全局變量
.bss
未初始化的全局變量
.symtab
符號表,存放在程序中被定義和引用的函數和全局變量的信息
.rel.text
當鏈接器吧這個目標文件和其他文件結合時,.text節中的信息需修改
.rel.data
被模塊定義和引用的任何全局變量的信息
.debug
一個調試符號表。
.line
原始C程序的行號和.text節中機器指令之間的映射
.strtab
一個字符串表,其內容包含.systab和.debug節中的符號表
 對於static類型的變量,gcc編譯器在.data和.bss中為每個定義分配空間,並在.symtab節中創建一個有唯一名字的本地鏈接器符號。對於malloc而來的變量存儲在堆(heap)中,局部變量都存儲在棧(stack)中。
 下麵我們以實際的例子來分析變量的存儲:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
 
int z = 9;
int a;
static int b =10;
static int c;
void swap(int* x,int* y)
{
int temp;
temp=*x;
*x=*y;
*y=temp;
} 
 
int main()
{
int x=4,y=5;
swap(&x,&y);
printf(“x=%d,y=%d,z=%d,w=%dn”,x,y,z,b);
return 0;
} 
 根據以上題目和理論知識,我們可以推斷出:
變量
存儲區域
a
.bss
b
.data
c
.bss
x
stack
y
stack
temp
stack
z
.data
swap
.text
main
.text
x=……
.rodata
 我們將從匯編代碼和符號表中來分析以上答案是否正確。我們首先來看該程序的匯編代碼:
>   >    .filee "var.c"
.globl z
>       .dataa     #數據段
>       .align 4
>       .typee       z, @object
>       .size z, 4
z:
>       .longg       9
>       .align 4
>       .typee       b, @object
>       .size b, 4
b:
>       .longg       10
>       .textt     #代碼段
.globl swap
>       .typee       swap, @function
swap:
>       pushll       %ebp
>       movll       %esp, %ebp
>       subl $4, %esp
>       movll       8(%ebp), %eax
>       movll       (%eax), %eax
>       movll       %eax, -4(%ebp)
>       movll       8(%ebp), %edx
>       movll       12(%ebp), %eax
>       movll       (%eax), %eax
>       movll       %eax, (%edx)
>       movll       12(%ebp), %edx
>       movll       -4(%ebp), %eax
>       movll       %eax, (%edx)
>       leave
>       ret
>       .size swap, .-swap
>       .sectionn   .rodataa     #隻讀段
.LC0:
>       .stringg     "x=%d,y=%d,z=%d,w=%dn"
>       .textt           #代碼段
.globl main
>       .typee       main, @function
main:
>       pushll       %ebp
>       movll       %esp, %ebp
>       subl $40, %esp
>       andl $-16, %esp
>       movll       $0, %eax
>       subl %eax, %esp
>       movll       $4, -4(%ebp)
>       movll       $5, -8(%ebp)
>       leall   -8(%ebp), %eax
>       movll       %eax, 4(%esp)
>       leall   -4(%ebp), %eax
>       movll       %eax, (%esp)
>       calll swap
>       movll       b, %eax
>       movll       %eax, 16(%esp)
>       movll       z, %eax
>       movll       %eax, 12(%esp)
>       movll       -8(%ebp), %eax
>       movll       %eax, 8(%esp)
>       movll       -4(%ebp), %eax
>       movll       %eax, 4(%esp)
>       movll       $.LC0, (%esp)
>       calll printf
>       movll       $0, %eax
>       leave
>       ret
>       .size main, .-main
>       .commm    a,4,4
>       .locall       c
>       .commm    c,4,4
>       .sectionn   .note.GNU-stack,"",@progbits
>       .identt      "GCC: (GNU) 3.3.5 (Debian 1:3.3.5-13)"
 通過以上匯編代碼可以發現,z和b在.data段,main和swap在.text段,a和c在.bss段,x,y,temp在stack中,printf函數所打印的字符串在.rodata中。
 下麵我們在通過符號表來解釋變量的存儲。
 每個可重定位目標文件都有一個符號表,它包含該文件所定義和引用的符號的信息。在鏈接器的上下文中,有三種不同的符號:
1. 由該文件定義並能被其他模塊引用的全局符號。即非靜態的C函數和非靜態的全局變量,如程序中的a,z,swap。
2. 由其他模塊定義並被該文件引用的全局符號。用extern關鍵字所定義的變量和函數。
3. 隻被該文件定義和引用的本地符號。用static關鍵字定義的函數和變量。如程序中的b和c。
該程序所對應的符號表如圖所示:
2符號表
首先,我們解釋上圖中各字段的含義:
字段名
含義
Num
序號
Value
符號地址。
可重定位目標文件:距定義目標文件的節的起始位置的偏移
可執行目標文件:一個絕對運行的地址
Size
目標的大小
Type
要麼是數據,要麼是函數,或各個節的表目
Bind
符號是全局的還是本地的
Vis
目前還沒有查到資料,待以後改正
Ndx
通過索引來表示每個節
ABS:不該被重定位的符號
UND:代表未定義的符號(在其他地方定義)
COM:未初始化的數據目標
Name
指向符號的名字
 對 於變量b和z,Ndx索引為3,我們觀察圖1,不難發現索引3對應的是.data段。變量c對應的索引為4(.bss段),變量a對應的索引是COM,最 終當該程序被鏈接時,它將做為一個.bss目標分配。我們從反匯編代碼中,對於變量a和c都是.comm(反匯編代碼中以“.”開頭的行,是指導匯編器和 鏈接器運行的命令):
>        ……
 .commm    a,4,4
>       .locall       c
>       .commm    c,4,4
>       ……
注 意:a所對應的Bind為GLOBAL,即為全局變量,雖然變量c也在.bss段中,但Bind卻是LOCAL,則為本地變量。.data段中的變量b和 c也是類似的情況。swap和main都在索引1所對應的.text段中。由於printf是在庫中所定義的,所以索引為UND。
 符號表中不包含對應於本地非靜態程序變量中的任何符號。這些符號是在棧中被管理的,所以符號表中沒有出現x,y,temp符號。
 相信大家讀完這篇文章以後,再也用不著對類似的題目膽戰心驚了。

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

大內高手全局內存

 

轉載時請注明出處和作者聯係方式:https://blog.csdn.net/absurd

作者聯係方式:李先靜 <xianjimli at hotmail dot com>

更新時間:2007-7-9

有 人可能會說,全局內存就是全局變量嘛,有必要專門一章來介紹嗎?這麼簡單的東西,還能玩出花來?我從來沒有深究它,不一樣寫程序嗎?關於全局內存這個主題 雖然玩不出花來,但確實有些重要,了解這些知識,對於優化程序的時間和空間很有幫助。因為有好幾次這樣經曆,我才決定花一章篇幅來介紹它。

 

正如大家所知道的,全局變量是放在全局內存中的,但反過來卻未必成立。用static修飾的局部變量就是放在放全局內存的,它的作用域是局部的,但生命期是全局的。在有的嵌入式平台中,堆實際上就是一個全局變量,它占用相當大的一塊內存,在運行時,把這塊內存進行二次分配。

 

這裏我們並不強調全局變量和全局內存的差別。在本文中,全局強調的是它的生命期,而不是它的作用域,所以有時可能把兩者的概念互換。

 

一般來說,在一起定義的兩個全局變量,在內存的中位置是相鄰的。這是一個簡單的常識,但有時挺有用,如果一個全局變量被破壞了,不防先查查其前後相關變量的訪問代碼,看看是否存在越界訪問的可能。

 

ELF格式的可執行文件中,全局內存包括三種:bssdatarodata。其它可執行文件格式與之類似。了解了這三種數據的特點,我們才能充分發揮它們的長處,達到速度與空間的最優化。

 

1.         bss

已經記不清bss代表Block Storage Start還是Block Started by Symbol。像這我這種沒有用過那些史前計算機的人,終究無法明白這樣怪異的名字,也就記不住了。不過沒有關係,重要的是,我們要清楚bss全局變量有什麼樣特點,以及如何利用它。

 

通俗的說,bss是指那些沒有初始化的和初始化為0的全局變量。它有什麼特點呢,讓我們來看看一個小程序的表現。

int bss_array[1024 * 1024] = {0};

 

int main(int argc, char* argv[])

{

    return 0;

}

[root@localhost bss]# gcc -g bss.c -o bss.exe

[root@localhost bss]# ll

total 12

-rw-r--r-- 1 root root   84 Jun 22 14:32 bss.c

-rwxr-xr-x 1 root root 5683 Jun 22 14:32 bss.exe

 

變量bss_array的大小為4M,而可執行文件的大小隻有5K 由此可見,bss類型的全局變量隻占運行時的內存空間,而不占文件空間。

 

另外,大多數操作係統,在加載程序時,會把所有的bss全局變量全部清零,無需要你手工去清零。但為保證程序的可移植性,手工把這些變量初始化為0也是一個好習慣。

 

2.         data

bss相比,data就容易明白多了,它的名字就暗示著裏麵存放著數據。當然,如果數據全是零,為了優化考慮,編譯器把它當作bss處理。通俗的說,data指那些初始化過(非零)的非const的全局變量。它有什麼特點呢,我們還是來看看一個小程序的表現。

int data_array[1024 * 1024] = {1};

 

int main(int argc, char* argv[])

{

    return 0;

}

 

[root@localhost data]# gcc -g data.c -o data.exe

[root@localhost data]# ll

total 4112

-rw-r--r-- 1 root root      85 Jun 22 14:35 data.c

-rwxr-xr-x 1 root root 4200025 Jun 22 14:35 data.exe

 

僅僅是把初始化的值改為非零了,文件就變為4M多。由此可見,data類型的全局變量是即占文件空間,又占用運行時內存空間的。

 

3.         rodata

rodata的意義同樣明顯,ro代表read only,即隻讀數據(const)。關於rodata類型的數據,要注意以下幾點:

l         常量不一定就放在rodata裏,有的立即數直接編碼在指令裏,存放在代碼段(.text)中。

l         對於字符串常量,編譯器會自動去掉重複的字符串,保證一個字符串在一個可執行文件(EXE/SO)中隻存在一份拷貝。

l         rodata是在多個進程間是共享的,這可以提高空間利用率。

l         在有的嵌入式係統中,rodata放在ROM(norflash)裏,運行時直接讀取ROM內存,無需要加載到RAM內存中。

l         在嵌入式linux係統中,通過一種叫作XIP(就地執行)的技術,也可以直接讀取,而無需要加載到RAM內存中。

 

由此可見,把在運行過程中不會改變的數據設為rodata類型的,是有很多好處的:在多個進程間共享,可以大大提高空間利用率,甚至不占用RAM空間。同時由於rodata在隻讀的內存頁麵(page)中,是受保護的,任何試圖對它的修改都會被及時發現,這可以幫助提高程序的穩定性。

 

4.         變量與關鍵字

static關鍵字用途太多,以致於讓新手模煳。不過,總結起來就有兩種作用,改變生命期限製作用域。如:

l         修飾inline函數:限製作用域

l         修飾普通函數:限製作用域

l         修飾局部變量:改變生命期

l         修飾全局變量:限製作用域

 

const 關鍵字倒是比較明了,用const修飾的變量放在rodata裏,字符串默認就是常量。對const,注意以下幾點就行了。

l         指針常量:指向的數據是常量。如 const char* p = “abc”; p指向的內容是常量 ,但p本身不是常量,你可以讓p再指向”123”

l         常量指針:指針本身是常量。如:char* const p = “abc”; p本身就是常量,你不能讓p再指向”123”

l         指針常量 + 常量指針:指針和指針指向的數據都是常量。const char* const p =”abc”; 兩者都是常量,不能再修改。

 

violatile關鍵字通常用來修飾多線程共享的全局變量和IO內存。告訴編譯器,不要把此類變量優化到寄存器中,每次都要老老實實的從內存中讀取,因為它們隨時都可能變化。這個關鍵字可能比較生僻,但千萬不要忘了它,否則一個錯誤讓你調試好幾天也得不到一點線索。

最後更新:2017-04-03 07:57:10

  上一篇:go android的&lt;uses-feature&gt;詳解
  下一篇:go 數字電視標準綜述(1)