[Linux] ls和size命令以及程序內存映像、磁盤映像的理解

轉自:http://blog.chinaunix.net/uid-9012903-id-2011435.html

下午試驗一個小程序來着,用到size 命令,後來發現只是一個空程序體的話,size 命令仍然顯示編譯出的a.out 的bss 段大小有四個字節,於是去google 了下size 命令的相關用法之類,沒想到找出來一篇關於程序內存映像和磁盤映像等的不錯的說明。又給自己掃盲了。一共有兩個帖子覺得內容很犀利,組織一下貼過來備份下。

第一個帖子詳細說了可運行程序的磁盤映像、內存映像、進程地址空間的內容和對應關係。以及ls 命令和 size 命令的輸出:(對文章不做更改,只對個別錯別字有旁註,表示對原作者版權的尊重,

———————————————————— 轉載一,start —————————————————————————

GNU/Linux平臺的C開發及運行環境

文章來源: 唯一    發表日期: 2007-8-11    訪問次數: 1036
鏈接:
http://onlyone.hpsbhq.com/showitemend.asp?endid=78&leiclass=Linux

本文介紹在GNU/Linux環境下一個C程序由源代碼到程序,到加載運行,最後終止的過程。同時以此過程爲載體,介紹GNU/Linux平臺下軟件開發工具的使用。


本文以我們最常見的hello, world!爲例:

 

#include <stdio.h>

main ()

{

      printf(“hello, world!\n”);

}

 

   

C程序生成


 

下圖是一個由C源代碼轉化爲可執行文件的過程:

 

代碼編輯: 比較流行的編輯器是GNU Emacs和vim。Emacs具有非常強大的功能,並且可擴展。

編譯:包括編譯預處理,編譯,彙編,連接過程。它們都可以通過GCC來實現。關於GCC,可以參考我關於GCC的筆記

 
C編譯器將源文件轉換爲目標文件,如果有多個目標文件,編譯器還將它們與所需的庫相連接,生成可執行模塊。當程序執行時,操作系統將可執行模塊拷貝到內存中的程序映象。


程序又是如何執行的呢?執行中的程序稱之爲進程。程序轉化爲進程的步驟如下:


1,  內核將程序讀入內存,爲程序鏡像分配內存空間。

2,  內核爲該進程分配進程標誌符(PID)。

3,  內核爲該進程保存PID及相應的進程狀態信息。

經過上述步驟,程序轉變爲進程,即可以被調度執行。

 

上述的hello, world程序實際是不規範的,POSIX規定main函數的原型爲:

 

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

 

argc是命令行參數的個數,argv是一個指針數組,每個指針元素指向一個命令行參數。

 

e.g:  $ ./a.out arg1 arg2

argc = 4

argv[0] = ./a.out   argv[1] = arg1  argv[2] = arg2


 

 C程序的開始及終止


 

   

程序的運行:

唯一入口:exec函數族(包括execl, execv, execle, execve, execlp, execvp)


程序開始執行時,在調用main函數之前會運行C啓動例程,該例程將命令行參數和環境變量從內核傳遞到main函數。

 

程序的終止:有8種途徑:

正常終止

1,    從main返回。

2,    調用exit。

3,    調用_exit或_Exit。

4,    從最後一個線程的開始例程返回。

異常終止

5,    調用abort。

6,    接收到一個終止信號。

7,    對最後一個線程發出的取消請求做出響應。

 

_exit與_Exit的區別        :前者由POSIX定義,後者由ISO C定義。

exit與_exit, _Exit的區別:前者在退出時會調用由用戶定義的退出處理函數,而後兩者直接退出. (關於退出處理函數atexit(), 參考APUE2, P182.)


另外, 調用exit()或_Exit()需要包含<stdlib.h>, 調用_exit()需要包含<unistd.h>.


   

要退出程序,除了return只能在main中調用外,exit, _exit, _Exit可以在任意函數中調用。


main函數最後調用return (0); 與調用exit (0)是等價的。


程序中調用exit時,exit首先調用註冊的退出處理函數(通過atexit註冊),然後關閉所有的文件流。

 

在程序運行結束時,main函數會向調用它的父進程(shell)返回一個整數值,稱之爲返回狀態。該數值由exit或return定義。如果沒有顯示地調用它們,程序還是會正常終止,但返回數值不確定(以前面的hello, world程序爲例,返回值爲13,實際上是printf函數的字符個數)。


$ gcc -Wall -o hello hello.c
$ ./hello
$ echo $?             (echo $? 用於在bash中查看子程序的返回值)
13

                                                                      

程序映象


我們已經瞭解了一個可執行模塊(executable module)是怎樣由源代碼生成的. 那麼, 執行這個程序時, 又是怎樣的情況呢? 下面介紹一個位於磁盤中的可執行程序是如何被執行的.


(1) 程序被執行時, 操作系統將可執行模塊拷貝到內存的程序映像(program image)中去.

(2) 正在執行的程序實例被稱爲進程: 當操作系統向內核數據結構中添加了適當的信息, 併爲運行程序代碼分配了必要的資源之後, 程序就變成了進程. 這裏所說的資源就包括分配給進程的地址空間和至少一個被稱爲線程(thread)的控制流.


上面只是大而化之地介紹了程序是如何轉化爲進程的, 這裏關注的是內存程序映像. 在第(1)步中, 操作系統將可執行模塊由硬盤拷貝到內存的程序映像中, 程序映像的一般佈局如下圖:

 

從低地址到高地址依次爲下列段:

1, 代碼段:即機器碼,只讀,可共享(多個進程共享代碼段)。

2, 數據段:儲存已被初始化了的靜態數據。

3, 未初始化的數據段(也被稱爲BSS段):儲存未始化的靜態數據。

4, 堆:儲存動態分配的內存.

5, 棧:儲存函數調用的上下文, 動態數據.


另外, 在高地址還儲存了命令行參數及環境變量.


程序代碼(text)段一般是在進程之間共享的. 比如一個進程fork出一個子進程時, 父子進程共享text段, 子進程獲得父進程數據段, 堆, 棧的拷貝.
 

磁盤映像, 內存映像, 地址空間之比較

前面提到, 可執行程序首先被操作系統從磁盤中拷貝到內存中, 還要爲進程分配地址空間. 加上已經介紹的內存程序映像, 這就有三種關於可執行程序的存儲組織了:

磁盤: 可執行文件段    內存: 內存程序映像  進程: 進程地址空間

下標列出了它們之間的對應關係:
 內存程序映像 進程地址空間
可執行文件段
 code(text)
code(text)
 code(text)
 data  data data
bss    data  bss
 heap data
-
stack
stack
-

內存程序映像和進程地址空間之比較

(1) 它們的代碼段和棧相互對應.
(2) 內存程序映像的data, bss, heap對應到進程地址空間的data段. 也就是說, data, bss, heap會位於一個連續的地址空間中, code和stack可能位於另外的地址空間. 這就可以針對不同的段實現不同的內存管理策略: code段所在的地址空間可以是"只能被執行的", data, bss, heap所在的地址空間是不可執行的...

正因爲內存程序映像中的各段可能位於不同的地址空間中, 它們不一定位於連續的內存塊中. 操作系統將程序映像映射到地址空間時, 通常將內存程序映像劃分爲大小相同的塊(也就是page, 頁). 只有該頁被引用時, 它才被加載到內存中. 不過對於程序員來說, 可以視內存程序映像在邏輯上是連續的.

內存程序映像和可執行文件段之比較

(1) 明顯, 前者位於內存中, 後者位於磁盤中.
(2) 內存程序映像中的code, data, bss段分別對應於可執行文件段中的code, data, bss段.
(3) 堆棧在可執行文件段中是沒有的, 因爲只有程序被加載到內存中運行時纔會被分配堆棧.
(4) 雖然可執行文件段中包含了bss, 但bss並不被儲存在位於磁盤中的可執行文件中.

使用file, ls, size, strip命令來查看相關信息

我們利用下面3個簡單的例子來理清上述概念:

 (1) array1.c

int a[50000] = {1, 2, 3, 4};      /*  被顯式初始化爲非0的靜態數據  */

int main(void) { 
   a[0] = 3;
   return 0;
}

 

(2) array2.c

int b[50000];            /* 未被顯式初始化的靜態數據 */
int main(void) {
   b[0] = 3;
   return 0;
}

 

(3) array3.c

int c[50000] = {0,0,0,0};  /* 被顯式初始化爲0的靜態數據 */
int main(void) {
   c[0] = 3;
   return 0;
}


array1.c中, 數組a被顯式初始化爲非0.

array2.c中, 數組b未被顯式初始化, 但由於它是靜態變量, 所以被編譯器初始化爲默認的值: b中所有元素被初始化爲0.

array3.c中, 數組c的所有元素被顯式地初始化爲全0.


$gcc -Wall -o init array1.c

$gcc -Wall -o noinit array2.c

$gcc -Wall -o init-0 array3.c


使用ls命令, 查看磁盤文件大小:

$ls -l init noinit init-0

-rwxr-xr-x 1 zp zp 209840 2006-08-21 15:56 init
-rwxr-xr-x 1 zp zp   9808 2006-08-21 15:57 init-0
-rwxr-xr-x 1 zp zp   9808 2006-08-21 15:57 noinit

我們發現array1.c 生成的init可執行文件比array2.c, array3.c生成的要大大約200000字節. 而array2.c 和array3.c生成的可執行文件在大小上是一樣的!


嚴格地說, 上述內存程序映像中的"未初始化的靜態數據"應該改稱爲"被初始化爲全0的靜態數據": 被程序員顯式地初始化爲0或被編譯起隱式地初始化爲默認的0. 而且, 只有程序被加載到內存中時, 被初始化爲全0的靜態數據所對應的內存空間才被分配, 同時被賦予0值.


使用size命令, 查看內存程序映像信息:

$ size init noinit init-0

 text    data          bss         dec         hex    filename
 822  200272        4        201098   3118a   init
 822     252       200032  201106   31192   noinit
 822     252       200032  201106   31192   init-0


size命令顯示內存程序映像中的text, data, bss三個段大小, 以及這3個段大小之和的十進制和十六進制表示. (由於堆棧是在程序執行時動態分配的, size無法顯示它們的大小.  可以使用ps命令查看進程地址空間信息. )


通過size命令, 我們可以得知如下事實:

1, 不管靜態數據是否被初始化, 加載到內存中的程序映像大小是不變的. 它們之間的區別只是data和bss段大小的不同( 影響磁盤文件的大小).

2, 由於size不計算堆棧大小, 所以ls命令和size命令類出的磁盤程序映像大小和內存程序映像大小應該是一樣的, 但通過上面的ls和size命令輸出我們發現:

(1) 若靜態變量被初始化爲非0, 磁盤映像要大於內存映像.

(2) 若靜態變量被初始化爲全0, 磁盤影響(映像)要小於內存影響(映像).


這是因爲:

(1) 位於磁盤中的可執行程序中不關(光)包含上面類出的磁盤映像的內容(code, data, bss), 它還包括: 符號表, 調試信息, 針對動態庫的鏈接表等內容. 但這些內容在程序被執行時是不會被加載到內存中的.

使用file命令可以查看可執行文件的信息. 使用strip命令可以刪除可執行程序中的符號表:
$ strip init; ls -l init
-rwxr-xr-x 1 zp zp 205920 2006-08-21 16:41 init
雖然符號表被刪除了, 但init中還有其他信息, 所以仍比內存鏡像大.)

(2) 靜態變量被初始化爲全0時(不管是程序員顯式地初始化還是被編譯器初始化爲默認的0), 這一過程是在程序被加載到內存中時進行的, 數據無非位於data和bss段中, 所以它們是否被初始化爲全0對於size來說, 內存映像總的大小是不變的, 但由於磁盤映像中不包含bss的值, 所以此時磁盤映像可能小於內存映像(如果bss段大於符號表, 調試信息, 鏈接表等的大小).

size命令不光可以查看最終生成的可執行文件的內存映像信息, 還可以查看可.o目標文件.

進程地址空間的數據段還包括了堆, 即內存程序映像中的堆. 堆一般用作動態分配內存. ( malloc(), calloc(), realloc(), free()).



———————————————————— 轉載一,end ————————————————————————


而在轉載二里面的答帖中,更是有高手給出了爲什麼ls 命令和size 命令有區別的間接解說:



———————————————————— 轉載二,start ———————————————————————
鏈接:
http://bbs.chinaunix.net/archiver/?tid-1719587.html

glq2000 發表於 2010-06-10 17:21

【求教】關於bss段(未初始化數據)是否佔用空間的問題?

[i=s] 本帖最後由 glq2000 於 2010-06-10 17:25 編輯 [/i]

關於bss段,請教個問題,它是不是並不佔用可執行程序的硬盤空間?
hello.c文件內容如下[code]#include <stdio.h>
int main()
{
        return 0;
}[/code]編譯後
[root@localhost ctest]# ll hello
-rwxr-xr-x 1 root root [b][color=Red]4613[/color][/b] 06-10 16:06 hello
[root@localhost ctest]# size hello
   text    data     bss     dec     hex filename
    803     248      [color=Red] 8  [/color]  1059     423 hello

[color=Red]可見這時hello的大小爲 4613字節,用size去看hello,其bss段爲8字節[/color]


然後修改代碼,填上一行 int bss[1000]; (該數組在運行時應占4000字節 )[code]#include <stdio.h>

int bss[1000];//這行是增加的,它應位於bss段

int main()
{
        return 0;
}[/code][root@localhost ctest]# ll hello
-rwxr-xr-x 1 root root 4633 06-10 16:07 hello
[root@localhost ctest]# size hello
   text    data     bss     dec     hex filename
    803     248    4032    5083    13db hello

[color=Red]發現hello的大小變爲4633,比4613增加了20字節,而不是增加了4000字節,這應該就說明了bss段的數據並不佔用可執行文件的空間吧?
但奇怪的是 第二次對hello執行size命令時, 其bss段大小變爲4032,而第一次時是8 [/color]

第一次
[root@localhost ctest]# size hello
   text    data     bss     dec     hex filename
    803     248     [color=Red]  8 [/color]   1059     423 hello

第二次

[root@localhost ctest]# size hello
   text    data     bss     dec     hex filename
    803     248    [color=Red]4032 [/color]   5083    13db hello

[color=Red]實際上hello僅增加了20字節 ,是不是size命令得到的bss段的大小是指程序運行後,映射到虛擬內存空間後,bss段實際所佔空間的大小?而不是說在硬盤上的大小? 這樣理解對麼?[/color]


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

沒本 發表於 2010-06-10 20:19

用 readelf -t  看.bss section大小變化。

另外如果對bss[]賦值初始化數據
int bss[1000]={1,2,3,4,5};
這個變量會被放在.data section裏面。

不是不映射,是先映射再挖空.bss,看.so加載比看elf執行文件加載代碼緊湊些。
文件: /usr/src/linux/fs/binfmt_elf.c  (kernel v2.6.33)[code]
static int load_elf_library(struct file *file)
{
......
         /* Now use mmap to map the library into memory. */
         down_write(&current->mm->mmap_sem);
         error = do_mmap(file,
                         ELF_PAGESTART(eppnt->p_vaddr),
                         (eppnt->p_filesz +
                          ELF_PAGEOFFSET(eppnt->p_vaddr)),
                         PROT_READ | PROT_WRITE | PROT_EXEC,
                         MAP_FIXED | MAP_PRIVATE | MAP_DENYWRITE,
                         (eppnt->p_offset -
                          ELF_PAGEOFFSET(eppnt->p_vaddr)));
         up_write(&current->mm->mmap_sem);
         if (error != ELF_PAGESTART(eppnt->p_vaddr))
                 goto out_free_ph;

         elf_bss = eppnt->p_vaddr + eppnt->p_filesz;
         if (padzero(elf_bss)) {
                 error = -EFAULT;
                 goto out_free_ph;
         }

         len = ELF_PAGESTART(eppnt->p_filesz + eppnt->p_vaddr +
                             ELF_MIN_ALIGN - 1);
         bss = eppnt->p_memsz + eppnt->p_vaddr;
         if (bss > len) {
                 down_write(&current->mm->mmap_sem);
                 do_brk(len, bss - len);
                 up_write(&current->mm->mmap_sem);
         }
         error = 0;

[/code]

———————————————————— 轉載二,end ———————————————————————

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章