操作系統知識點梳理

一、編譯和鏈接

由於在之前linux學習和CSAPP學習中,已經對這部分有了很多瞭解,具體可以看我的相關博客,這裏再簡要的用流程圖來總結一下
在這裏插入圖片描述

二、ELF格式文件

ELF格式文件主要分爲以下四類

  • 可重定位文件,包括彙編得到的二進制文件以及靜態鏈接庫文件。(Linux的.o文件,.a文件,windows的.obj文件,lib文件)
  • 可執行文件(Linux的a.out這類文件以及windows的.exe文件)
  • 共享目標文件,包括動態鏈接庫文件(Linux的.so文件,以及windows中的dll文件)
  • 核心轉儲文件(Linux中的core dump文件)

其中目標文件和可執行文件的格式幾乎是一樣的,下面就着重介紹一下目標文件的格式,以CSAPP中的圖來說明:
在這裏插入圖片描述
下面挑選重點段來介紹。

1、ELF頭

其中主要存儲了ELF文件的一些版本和相關信息,其中最主要的信息是入口地址、程序頭入口及程序長度、段表(節頭部表)的位置和長度

2、.text

代碼段,存儲的就是程序二進制代碼,這裏來解釋一下用objdump得到的代碼段信息的含義:
在這裏插入圖片描述
其中指令中的-s代表輸出將所有段內容以十六進制方式打印出來(這裏只截取了代碼段的內容,其他段省略),-d表示將所包含的代碼段用再用反彙編顯示出來(所以這部分不是真正代碼段中的內容)
真正代碼段中第一列是地址偏移量,中間四列是以十六進制顯示出來的內容。最後一列是用ACSII碼形式。

3、.rodata

只讀數據段,存放的是隻讀變量(比如const修飾的變量)和字符串常量(有些編譯器會把字符串常量放到下面的數據段中)。

4、.data

數據段,存放已經初始化的全局變量和靜態變量。
注意:局部變量不在.rodata,.data,.bss中,是被保存在運行的棧中

5、.bss

未初始化段,存放未初始化的全局變量和靜態變量。
.bss段中的變量並不佔用實際存儲空間,所以減少了磁盤空間,僅僅是一個佔位符,用objdump得到的就是如下的情況,佔4字節。
在這裏插入圖片描述
但是到了加載執行的過程中會開闢相應的內存給未初始化的變量。

6、.symtab

符號表,存儲的是在程序中用到的各個符號的名字以及符號值,這裏的符號值對於變量和函數來說就是它們的地址。符號表是鏈接過程中最重要的段,尤其是符號表中的全局符號,是鏈接過程的主要處理對象。

7、.rel.text

就是針對.text段的重定位表,其中是對代碼段中運用到的外部全局函數地址重定位信息。

8、.rel.data

針對.data段的重定位表,其中是對全局外部變量的地址重定位信息。

9、.strtab

字符串表,這裏主要存儲段名以及變量名的字符串。因爲字符串的長度往往是不定的,所以先集中存儲起來,然後用偏移地址來表示字符串,如下所示:
在這裏插入圖片描述
這樣在ELF文件中只需給一個數字下標就可以得到字符串,符號表中的符號名都是這樣表示的。

10、節頭部表(段表)

這裏存儲的是各段的信息,比如各段的段名(這裏也是偏移量),段長度,在文件中的偏移,讀寫權限等等。所以想要遍歷每一段,需要通過段表才能遍歷。
注意:解析ELF表的一般過程:先解析ELF頭,可以得到段表和字符串表中段名的位置,從而可以解析整個ELF文件。

11、其他有必要說的段

.init:該段保存的是可執行的指令,構成進程初始化代碼,在一個程序開始運行,並在main函數調用之前,會執行.init中的代碼。
.fini:該段保存着進程終止代碼指令,當main函數正常退出時,會執行這個段中的代碼。
這兩個段對於全局的類對象,很有用,全局類對象在main執行之前就進行構造函數,在main結束之後,再執行析構函數,所以構造和析構函數都放在這兩個段中。

三、靜態鏈接的過程

靜態鏈接步驟是緊跟着彙編步驟之後執行的。
整個靜態鏈接的過程按照CSAPP的說法分爲兩部分,第一是符號解析,第二是重定位。

1、符號解析
a、概念

符號解析就是將所有的可重定位文件中符號表中的符號找到唯一對應的地址(偏移量)。首先講解一下符號表的結構:
在這裏插入圖片描述
符號解析主要完成的是,對於內部符號(在該可重定向目標文件中定義的符號),查看是否唯一(如果不唯一,需要鏈接器按規則解析多重定義的符號)對於外部符號(與內部符號相反,在符號表中是UND的就是外部符號),就是要在其他的可重定向文件或者靜態庫中找到對應的符號定義。

b、靜態鏈接庫的好處

在靜態鏈接過程中,如果有一個庫裏面有多個可重定位文件,但是在使用這個庫時只需要其中某幾個文件中的函數即可,這時候有兩種選擇:

  • 一種就是你將庫中所有文件都鏈接進來,這樣方便,但是可執行文件的體積就很大,因爲所有的可重定位文件都要加入進來。第
  • 另一種就是把用到的可重定位文件鏈接進來,可是這樣需要人爲篩選,比較麻煩。

這時候靜態庫的出現就完美結合了這兩者的優點。通俗點說,將庫中所有的文件打包成靜態庫,連接器就會幫你自動去找到所用到可重定位文件,然後拿出來進行鏈接。
所以靜態庫其實是可重定位文件的一個集合形式,所以認爲靜態庫也是可重定位文件
以一個例子來說明:
在這裏插入圖片描述

c、gcc靜態鏈接過程詳解

介紹gcc編譯器的靜態鏈接過程,gcc靜態鏈接是根據gcc編譯命令中從左到右的順序來解析可重定位文件與靜態庫的
下面截取了CSAPP中的原話:在這裏插入圖片描述
在這裏插入圖片描述

2、重定位

當完成符號解析以後,可執行文件中所有的內容都可以確定下來了,這時就可以輸出可執行文件了。重定位由兩步組成:重定位節和符號定義,重定位節中的符號引用。

a、重定位節和符號定義

就是將符號解析得到的E集合中的可重定位文件合併成一個可執行文件。這裏涉及兩個地址的分配。一個是多個可重定位文件中的內容分配到可執行文件中的地址上(在磁盤空間中,方法是相同的段放在一起),另一個是將可執行文件中各個段內容映射到虛擬地址上(在虛擬內存空間)。
舉例如下:
在這裏插入圖片描述
其中每一個符號地址的確定方法(外部符號除外)就是通過偏移量來確定的,之前的符號地址都是給的從段頭開始的偏移量,現在只要用虛擬地址加上偏移量即可。

2、重定位節中的符號引用

之前講了非外部地址是通過偏移量來確定在虛擬空間中的地址的,那麼重定位節中的符號引用就是確定外部符號的地址。在可重定位文件中,外部符號一般是以一個臨時的假地址來作爲外部符號的地址。
重定位的步驟是:

  • 根據重定位表,找到需要重定位的代碼以及變量。重定位表記錄了在段的那個位置需要進行重定位,比如.rel.data就是記錄數據段中那些偏移位置需要重定位。.rel.text就是定義在代碼段的哪些位置需要重定位。舉例如下:
    在這裏插入圖片描述
  • 之前通過符號解析已經得到了重定位的符號在虛擬內存的地址,根據這個符號表進行指令修正(有絕對尋址修正,相對尋址修正等方法)

四、虛擬內存

在得到可執行文件以後,下面就是加載運行可執行文件了。由於可執行文件中的各種地址已經被轉換成在虛擬內存中的地址了,所以首先要建立虛擬內存,並且將可執行文件載入到虛擬內存中去,纔可以對應使用。那麼這裏先介紹一下虛擬內存

1、前提概念
  • 無論是虛擬內存還是物理內存,最小轉換單位都是,也就是想要將虛擬內存加載到物理內存中去,最少要加載一頁。
  • 虛擬內存並不是真正存在的,虛擬內存只是將磁盤空間和內存空間有機結合的一種方式,讓人看起來好像磁盤和內存空間是在一起的。也可以說虛擬內存是磁盤空間與內存之間的橋樑。虛擬內存唯一佔用物理空間的地方就是頁表,頁表是存儲在物理內存中或者高速緩存中的,記錄了虛擬頁和物理頁之間的對應關係。
    以圖來說明:
    在這裏插入圖片描述
    頁表中每一行代表一個對應關係,有效位表明這個虛擬頁是否被加載到內存中去,後面的二進制分爲如果設置了有效位,那麼就是物理頁的起始地址,如果沒有設置有效位,就是虛擬頁在磁盤上的起始地址
2、虛擬內存運行的步驟

幾個概念

  • 頁命中:所使用的虛擬地址被緩存在內存中
  • 缺頁:頁命中的反義詞,所使用的虛擬地址沒有被緩存在內存中
    以CPU執行指令的過程爲例來講解虛擬內存機制運行的全過程
    在這裏插入圖片描述

五、可執行文件裝載過程

可執行文件裝載的過程其實就是進程創建的過程,進程創建的過程,整個過程主要做三件事:
在這裏插入圖片描述
用圖片的方式來展現:
在這裏插入圖片描述
註釋

  • 步驟一其實就是創建頁表,頁表就是虛擬內存和物理內存的映射數據結構
  • 步驟二就是建立task_struct結構體(進程控制塊PCB),其中vm_area_struct結構體按照Segment來記錄了虛擬空間中的某一片區域示意圖如下:
    在這裏插入圖片描述
  • ELF文件中Segment和Section的區別,Segment其實是一個或者多個Section合成的。之前所說的數據段,代碼段等都屬於Section。具體可以看書164頁

六、動態鏈接庫

1、動態鏈接庫的好處

動態鏈接庫是針對靜態鏈接庫的缺點來做補充的,所以先講一下靜態鏈接庫的缺點(其實也是可重定位文件的缺點):

  • 浪費磁盤空間,如果有多個可執行文件都調用了libex.a中的一個目標文件a.o,那麼就會有a.o的多份拷貝在這多個可執行文件中,浪費了磁盤空間
  • 浪費內存,如果上述的多個可執行文件被同時執行,那麼就會有有多個a.o被加載到虛擬內存中,甚至物理內存中,浪費內存空間
  • 每次更改程序,都要加入靜態庫重新編譯。

所以動態庫的好處如下:

  • 可執行文件中不會複製動態庫中的內容(依靠裝載時重定位技術)
  • 在內存中只會有動態庫的一份拷貝(依靠的是位置無關代碼)
  • 編譯時,如果動態庫編譯和源文件編譯是區分開來的,也就是如果動態庫沒有改變,就不需要再編譯動態庫。

但是動態庫也有缺點,那就是運行速度沒有靜態庫快,因爲在加載階段,動態庫需要消耗額外的時間進行符號查找,重定位工作,一般比靜態庫程序運行性能減少1%~5%。

2、動態庫鏈接的示意圖

在這裏插入圖片描述
我們可以看到在靜態鏈接時,不需要動態庫所有信息,只需要少部分信息即可。
注意:動態鏈接器也是一個動態庫。

3、動態鏈接庫中的關鍵技術
  • 地址無關代碼,就是在裝載時重定位,這時不一定能確定動態庫的具體位置,所以對於可執行文件中動態鏈接庫中的符號,只是重定位成動態庫的相對位置,而不是絕對位置(個人理解
  • 延遲綁定,延遲綁定是爲了加快動態庫的加載速度,就是對於使用到的動態庫中的函數,等到第一次使用再進行符號查找,重定位工作。
4、動態庫的調用方法
  • 在Linux中
    • 靜態庫文件是.a文件,動態庫文件是.so文件
    • 顯式調用:調用dlopen(),dlsym(), dlerror(),dlclose()這四個函數
    • 隱式調用:輸入指令時加入動態庫的庫路徑,以及在程序中加入動態庫的頭文件
  • 在Windows中
    • 靜態庫文件是.lib文件,動態庫文件是**.lib文件和.dll文件**,其中lib文件並不關鍵,是記錄了動態庫的一些相關信息,只有在隱式調用是才需要用到,而dll文件纔是真正的動態庫文件。
    • 顯式調用:只需要調用dll文件即可,使用LoadLibrary(), GetProcAddress(), FreeLibrary()三個函數
    • 隱式調用:頭文件、lib文件和dll缺一不可,lib文件可以通過#gragma comment(lib, “xxx”)語句在程序中添加,也可以通過配置VS的項目來添加,dll暫時知道的項目的庫目錄必須包含動態庫纔可以。

windows可以參考此篇博客:https://blog.csdn.net/liangyanghui/article/details/77981848

七、程序運行時的內存

1、虛擬內存佈局
a、虛擬內存佈局圖

當一個可執行文件(暫時不考慮動態庫)加載結束以後,虛擬內存的佈局如下:
需要聲明的是,這張圖其實網上出現很多,但是這個虛擬內存佈局究竟是如何得到的呢?
其實可以理解爲這是將頁表映射的內容完全羅列下來得到的。其中有的部分在內存中,比如與進程相關的數據結構,有的在磁盤上,比如代碼、未初始化,已初始化數據,這些都在ELF文件中,還有一些根本不存在,比如標藍色的區域,這些是分給堆棧,但並未被分配的區域,這些區域在實際中是不存在的。
在這裏插入圖片描述

a、虛擬內存分區

虛擬內存的分區有多種分法,這裏選用最常用的一種:
分爲內核區,棧區,堆區,全局靜態區,文字常量區,代碼區和保留區

  • 內核區,存放內核代碼數據以及進程相關數據結構
  • 棧區,一般存放函數體的局部變量、函數調用期間的所有參數壓棧、函數的返回值
  • 堆區,用戶申請的內存區域
  • 全局/靜態區,存放全局變量、靜態類型的變量
  • 文字常量區,存放常量和字符串
  • 代碼區,存放程序代碼
  • 保留區,是不可以使用的區域,因爲極小的地址就被丟棄了。
    其中全局靜態區,文字常量區以及代碼區就是由可執行文件中相對應的段在程序加載進來以後就確定了,而棧區和堆區是在程序運行過程中不斷變化調整的區域
    以一幅圖來說明全局靜態區,文字常量區以及代碼區和可執行文件中段的對應關係:
    在這裏插入圖片描述
2、棧
a、棧的特性

內存中的棧仍然具有先進後出的特性,棧總是按虛擬地址向下生長

b、函數棧幀

棧最重要的作用就是在程序運行過程中,保存正在執行函數所需要維護的信息,包括如下:

  • 函數返回地址和參數。
  • 臨時變量,包括函數的非靜態局部變量以及編譯器自動生成的臨時變量
  • 保存調用該函數的上下文,比如調用前的外部函數地址等等

函數棧幀區域是依靠ebp和esp兩個寄存器來限定的。esp始終指向棧頂,也就是當前調用函數棧幀的頂部,ebp始終指向當前調用函數棧幀的底部。所以這兩個寄存器中間的區域,就是當前執行函數的棧幀。
要想了解具體ebp和esp是怎樣工作的,可以參考我的博客https://blog.csdn.net/qq_34489443/article/details/93158460

c、函數返回值的傳遞

如果是4字節以內,用exa寄存器來傳遞
如果是5~8字節,用exa和edx兩個寄存器來傳遞
如果大於8字節,以一個例子來說明

以一個例子來解釋
在這裏插入圖片描述
在這裏插入圖片描述

3、堆
a、堆的性質

內存中的堆在存儲上沒有太多侷限,是按虛擬地址向上生長的。

b、linux中內存獲取方法

我們想要在已經成型的虛擬內存中去使用空閒的內存,可以用malloc或者new去申請,但是這屬於C++封裝過的函數,最底層的函數應該是系統調用,不同的系統不一樣,這裏着重介紹Linux系統。
linux系統中有兩種內存獲取方式:brk()和mmap()
brk():這個函數屬於動態分配內存,也是堆的分配方法,也就是在運行中分配內存,這個函數其實就是調整brk指針的位置,brk指針指向的是堆頂,增加堆頂相當於擴大堆。
mmap():將磁盤上的空間映射到虛擬內存中堆和棧的中間那部分。

c、對分配方法

內存空間的管理方法,有空閒鏈表法等,空閒鏈表法還分隱式顯式等。就不展開細講了。

八、系統調用

a、系統調用

系統調用就是操作系統提供的函數接口。
系統調用的缺點:
在這裏插入圖片描述
針對這些缺點,出現了運行庫,舉例來說Linux中read函數是系統調用,用來讀取文件,但是C語言運行庫中是fread,fread函數在所有系統下都可以使用,而在Linux系統下,fread其實就是對read系統調用的封裝,所以運行庫有如下好處:
在這裏插入圖片描述

b、系統調用的過程

系統調用不像普通的函數,直接運行就可以了,系統調用需要執行特殊的步驟。在《程序員自我修養》中說系統調用是通過中斷來執行的,但是在CSAPP中是依靠陷阱來執行的。我查閱了很多資料,最後覺得其實是說法的不一致,陷阱還有一種說法是軟中斷,與此相對的還有硬中斷。所以在《程序員自我修養》中,系統調用是軟中斷實現的。
比較系統中四種異常:
在這裏插入圖片描述

  • 中斷指的是硬中斷,硬中斷是依靠硬件產生的,所以對於CPU或者進程來說,總是被動的。
  • 陷阱指的就是軟中斷,軟中斷是依靠軟件產生,是主動發生的,系統調用就是依靠陷阱產生。
  • 故障是由錯誤情況引起的,如果嚴重會變成終止。
  • 終止就是執行abort函數。

所以以下做一個統一:以下說的中斷就是軟中斷,硬中斷會單獨指出。
系統調用的過程:
在這裏插入圖片描述

c、上下文切換

上下文:內核重新啓動一個閒置進程所需的信息,包括程序計數器,用戶棧,內核棧和各種內核數據結構等。
上下文切換就是內核將當前進程保存起來,執行其他進程,注意用戶模式切換成內核模式也需要上下文切換
上下文切換的時機:

  • 系統調用需要上下文切換,也就是軟中斷(陷阱)會產生上下文切換,用戶態切換成內核態最後再切換回用戶態
  • 進程阻塞或者sleep會先執行其他進程,產生上下文切換。
  • 硬中斷也會產生上下文切換,比如所有系統都會產生週期性定時器中斷機制,避免一個進程運行太久,會硬中斷切換成其他進程執行,產生上下文切換。

上下文切換的步驟:

  • 保存當前進程的上下文
  • 恢復先前被搶佔或者內核態的上下文
  • 將控制傳遞給新恢復的進程
    注意:Linux中用戶態和內核態使用的是不同的棧,所以在切換用戶態的時候,就需要切換棧,切換棧其實就是將原來使用的棧寄存器中的地址SS和BSP保存起來,置換成另外一個。(不同的進程上下文切換也是這樣)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章