【软件开发底层知识修炼】二十 深入理解可执行程序的结构
上一篇文章記錄了GDB調試從入門到熟練掌握的學習全過程。點擊鏈接查看:【軟件開發底層知識修煉】十九 GDB調試從入門到熟練掌握超級詳細實戰教程學習目錄
- 還記得在以前的學習Binutils工具的時候,學習了很多工具來查看可執行程序的結構,那個時候并沒有詳細說明程序的結構,今天就來學。下面是之前學習的Binutils工具集的幾篇文章,可以參考學習:
- 【軟件開發底層知識修煉】六 Binutils輔助工具之- addr2line與strip工具
- 【軟件開發底層知識修煉】七 Binutils輔助工具之- ar工具與nm工具
- 【軟件開發底層知識修煉】八 Binutils輔助工具之- objdump工具 與 size,strings工具
本篇文章開始學習可執行程序的結構。也就是我們平時說的可執行文件的結構。本文不會像《程序員的自我修養》那樣詳細解釋可執行文件的每一個細節,我希望通過這次學習能夠對程序的結構有一個永久性的認知。當然,還是建議要把《程序員的自我修養》仔細閱讀完。
文章目錄
- 1 程序是由不同的段構成的
- 1.1 代碼示例
- 2 程序中的棧結構
- 3 堆(Heap)的簡要概述
- 4 內存映射段(mmap)
- 4 總結
1 程序是由不同的段構成的
- 至于什么是段,如果看了《X86匯編語言-從實模式到保護模式》應該會非常清楚。段不過就是一段內存結構。把類似的指令放到連續的內存區域就是代碼段(.text段),把初始化了的數據放到連續的內存區域就是數據段(.data段),把未初始化的數據放在一起就是(.bss段)
- 程序的靜態特征是指令和數據:實際上就是一堆二進制放在那里(磁盤)不動。
- 程序的動態特征就是執行指令來處理數據:實際上就是把磁盤上的二進制文件加載到內存,讓指令來處理數據。
那么你用C語言寫一個代碼,它與可執行文件的內部結構是如何對應的?
- 代碼段(.text):
-
數據段(.data , .bss, .rodata)
-
數據段中用于存放源代碼中具有全局生命周期的變量。不具有全局生命期的局部非靜態變量不在數據段中,而是在棧中。棧后面會講。
-
.bss
- .bss是存儲未初始化的變量。或者說初始化為0的變量
-
.data
- .data存儲的是具有非0初始值的變量
-
.rodata
- .rodata存儲的是const修飾的變量
為什么同是全局變量和靜態局部變量,為什么初始化的和未初始化的變量放在不同的段中?
可以這樣想,有初始化值的變量在可執行文件中就直接將它的值保存到文件中,加載到內存的時候,直接將變量對應的值也加載到內存中。而未初始化的變量(或者本來就賦值為0的變量),在可執行文件中不用保存初始值,這減小了可執行文件的體積,將其加載到內存中時啥也不管直接全部賦值為0,這也也可以提高加載的效率。總結來說就是以下兩點:
-
.bss段在可執行文件中不賦初值。在加載到內存中時直接全部初始化為0。這樣減少了可執行文件的體積也提高額加載的效率
-
.data段中,在可執行文件中直接將變量對應的初始值保存,加載到內存中時直接將文件中對應的值加載到內存中即可。這也提高了程序的加載效率。
- 文件頭(File header)
文件頭并不是今天的重點。簡單來說文件頭中保存了程序的各個段的信息,操作系統加載程序的時候,首先要讀取這個文件頭,先計算出各個段的大小,才能從磁盤中準確的讀取相應的執行與數據。
并且在文件頭中也記錄了類似于符號,符號變之類的信息。這些不再多講。
1.1 代碼示例
下面我們寫一個代碼來使用一些具體的工具,查看各個段。
test.c
char g_no_val; // .bss 1byte int g_value=1; //.data 4byte char g_str[]="D.T.SoftWare_lyy"; // .data 17byte const int g_const=3; //.rodata 4byteint dt_main(){static char c_no_value; // .bss 1bytestatic int c_value=2; // .data 4bytereturn 0; }- 可以看到上述程序沒有main函數,但是我們可以指定dt_main()函數為入口函數。使用以下方式進行編譯:
- gcc -e dt_main -nostartfiles test.c -o test.out
- 得到可執行代碼文件test.out,使用下面的命令查看它的各個段的信息:
- objdump -h test.out
- 由上圖可以看到各個段的大小,起始段的地址等
- .data段大小0x1c=28字節:我們由上述代碼可以看到,.data段的變量一共是25字節,由于對其,最終是28字節,也就是16進制的1c。.data段的起始地址是:08049ff4
- .bss段大小0x4=4字節:這個很明顯。.bss段的起始地址是:0804a010
- .rodata段大小:4字節:這個也很明顯。起始地址為:0804819c
- 使用nm test.out 查看各個符號的屬性:
- 因為符號g_const所在的.rodata段只有它一個,所以它的地址自然就是.rodata段的起始地址。如上圖
其他的信息也很容易看懂。這里不再贅述。
- 我們還可以使用命令:objdump -s -j .rodata test.out 查看某一個段的信息:
2 程序中的棧結構
在最開始的那張圖:
我們始終沒有說a,b這兩個變量在哪里。它們不在上述的那些段中。當然,它們肯定也是在某一塊內存中的。這塊內存,我們叫做:棧。
對于棧,我們這也不說特別詳細值說明棧的基本用處。
- 棧的本質是一塊連續的內存結構。它與數據結構中的棧不是一個概念,但是操作很像。
- 其中SP寄存器(當然這是最基本的16位的,32位的叫ESP)存的是棧頂的指針。它用于棧的入棧操作與出棧操作。
- 棧的增長方向一般是向下。這與下面即將要說的堆內存正好相反。
棧一般有什么用處呢?
除了保存類似于上述的a,b這種局部變量以外。還有以下幾種用途。
- 中斷發生時,棧用于保存一些寄存器的值。
- 函數調用時,棧用于保存函數的上下文信息(活動記錄,依然是一些寄存器的值等)
- 并發編程時,每一個線程都有一個自己獨立的棧。(說是獨立有點不恰當,其實它們都是在一個大的獨立的進程空間中。)
在本文,就不打算再詳細說棧這種結構與作用。可以參考《程序員的自我修養》
知道了棧結構,理應還要知道堆結構。它們總是放到一起做對比。
3 堆(Heap)的簡要概述
在這里我們知道堆是用于以下用途即可,具體的后面還會學習。
- 堆,是一片閑置的內存空間,用于程序在運行的時候動態分配的
- 堆空間的分配需要函數的支持,比如malloc。C++的new關鍵字底層也是調用相應的函數
- 堆空間的使用后需要顯示的釋放(棧就不需要),一般是用free。C++中為delete。不過更加高級的語言具有垃圾回收機制比如java
4 內存映射段(mmap)
上述學習了各個段以及堆結構與棧結構。
還有一種內存段,叫做內存映射段。它是用來做什么的呢?
如果了解動態鏈接的過程,應該知道如果程序加載時需要動態鏈接相關動態庫的話,操作系統內核會將相應的動態庫的文件直接映射到內存中。映射的位置可以稱為內存映射段。
還有一種情況是如果想要讀取一個文件的內容,一點一點的讀,開銷總是相當大。如果操作系統內核直接將文件映射到某一塊內存,再來讀取文件的內容,將會塊的多。
還有一種是程序執行時可以創建匿名映射區來存放程序數據。什么是匿名映射區?比如一個程序生產了很多數據,要將它最終存到一個文件中。那么不可能說生產一個數據就存放到文件中,更加高效的做法是,在內存中創建一個賦值全為0的區域,將生產的數據暫時先存放到這里,生產完或者生產了足夠多的數據后再將數據寫到磁盤上的文件。這就是匿名映射區。畢竟磁盤的讀寫都是以扇區為單位,一個扇區大小為512字節,一次寫多一點數據總是比你一次寫1字節的數據更加高效。
-
有一點要明白,將文件的內容映射到內存,實際上是映射到進程的虛擬地址空間,而且,映射的過程,是沒有數據的遷移的,也就是沒有數據的拷貝。
-
其實上面我們并沒有說明白為什么將文件映射到內存后再讀寫會快一些。
-
如果使用正常的read函數進行讀文件,是需要兩次的數據拷貝:一次是內核從磁盤將文件的內容拷貝到內核地址空間,然后再從內核地址空間拷貝到用戶地址空。這里進行了兩次數據的拷貝。開銷比較大
但是如果是使用內存映段的話,就不一樣了
如下圖:
- 首先將硬盤上的文件數據從邏輯上映射到內存中,這沒有數據拷貝,零耗時。
- 當用戶程序讀數據的時候,從虛擬地址空間讀,通過缺頁中斷進行文件數據的實際載入(這里就是真正的數據的拷貝,從文件中拷貝到真實的物理內存)
- 這里要注意一點:映射后的內存(虛擬內存)的讀寫,就是對文件數據的讀寫。
4 總結
其實這些內容以前都見過學過。下面給一個大的進程的虛擬地址空間的內存分配圖:
- 當然在Linux系統中內核與用戶空間的比例是1:3,但是在windows系統中就是2:2了.
-
本文章參考狄泰軟件學院相關課程 想學習的可以加狄泰軟件學院群, 群聊號碼:199546072
-
學習探討加個人(可以免費幫忙下載CSDN資源):
-
qq:1126137994
-
微信:liu1126137994
-
學習交流資源分享qq群:962535112
總結
以上是生活随笔為你收集整理的【软件开发底层知识修炼】二十 深入理解可执行程序的结构的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 华为mate30怎么申请鸿蒙内测,华为新
- 下一篇: 【离散数学中的数据结构与算法】五 排列