macOS上的彙編入門(四)——操作系統基礎

當我們學習彙編的時候,除了數學基礎以及硬體基礎以外,操作系統的基礎也是一個至關重要的環節。彙編語言

本質上就是機器碼的human-readable的版本,而硬體相同,則同一個程序的機器碼一定相同。那麼我們為什麼還要研究操作系統呢?這是因為,我們通過彙編語言,最終得到的可執行文件是與操作系統有關的,是操作系統來決定我們如何裝載、執行這些可執行文件。此外,不同操作系統提供的庫、系統調用並不完全相同。因此,只有了解了操作系統以後,才能更好地編寫彙編語言。

Darwin與XNU

macOS的基本結構如下:

macOS建立在Darwin操作系統之上,以Aqua為圖形化界面。Darwin操作系統的內核是XNU. 我們可以通過在終端下鍵入

uname -a

來查看Darwin和XNU的版本號。我在macOS 10.15 public beta下的結果如下:

Darwin操作系統是開源的,Aqua圖形化界面是在Apple專利下的。

簡單來講就是,我們用的macOS里各種圖案、交互都是Apple專利下的,而系統的運行、內存的分配等等底層的操作系統都是開源的。事實上,國外也有社區在提供基於Darwin操作系統的開源的系統,如PureDarwin.

接下來,我們重點關注的是Darwin操作系統的內核——XNU.

正如上面macOS的基本結構的圖中所示,XNU位於macOS的最底層——Kernel and Device Drivers. 下圖在維基百科中用於描述XNU內核的構造(其中OSFMK 7.3指的就是Mach):

總的來說,XNU是一個混合型內核,包括FreeBSD和Mach兩層,是一個類Unix內核。

我們並不需要太過深入地了解XNU內核,只需要大致知道其分為FreeBSD和Mach兩層。

系統調用

說了這麼多,操作系統究竟能為我們做什麼呢?更具體地說,我們在彙編語言中有什麼可以利用操作系統的呢?事實上,操作系統可以為我們提供許多有用的「系統調用」(System call)。比如說,我們知道,一個進程由操作系統發起,由操作系統結束,那麼,我們怎麼在程序內部讓操作系統來結束這個進程呢?再比如說,文件系統是由操作系統管理的,那麼文件的讀取和寫入在用戶層面怎麼實現呢?這一切,都是由操作系統來提供的。從某種意義上來說,操作系統就和我們在高級編程中使用的Cocoa, React等一樣,是一種「框架」(Framework)。我們在編程的時候,可以直接使用框架提供的API. 同樣地,我們在編寫彙編程序的時候,也可以直接使用操作系統提供的系統調用。就像是我們在用毛線織衣服的時候,並不需要自己來養蠶繅絲,只需要在毛線不夠的時候向毛線的提供者說一句,然後就由毛線的提供者工作來提供毛線。關於系統調用,我們之後在彙編語言中還會詳細闡釋。

內存虛擬化

在上一篇「硬體基礎」中,我們提到,所有進程都是在內存中運行的。現在常用的操作系統都採用了一個策略「內存虛擬化」,將邏輯地址與物理地址進行區分。我們知道,內存中的存儲單元是以位元組編址的,相鄰的存儲單元的地址相鄰。這裡實際指的是「物理地址」,也就是CPU在向內存發出訪問請求時用到的地址。我們在編程中,遇到的地址都是「邏輯地址」。在一個進程啟動時,操作系統會為每個進程分配64位邏輯地址空間,並在MMU(Memory Management Unit, 內存管理單元)中維護一個邏輯地址向物理地址的映射。也就是說,在我們編程時,物理地址對於程序員是透明的,程序員接觸到的只會是物理地址。更具體地說,操作系統將地址分為4KiB, 也就是4096B大小的頁(Page), 將邏輯地址的頁與物理地址的頁進行映射。在一個頁內相鄰的邏輯地址對應的物理地址是相鄰的,但是頁之間的物理地址的關係是不確定的。

64位邏輯地址空間,有多大呢?大約是18EB. EB是一種和KB, GB一樣的單位,1EB是10的18次方位元組。而據估算,2011年整個互聯網的容量總和不超過525EB。因此,64位邏輯地址空間是非常非常大的,其總的大小遠遠大於實際的物理內存的大小。macOS為了解決這個問題,將一部分邏輯地址對應的頁儲存在硬碟上,準確地說,是/boot目錄內。也就是說,當MMU在用邏輯地址向物理地址轉化時,發現該邏輯地址在內存中沒有對應物理地址,則將/boot目錄內一部分數據調入內存中,作為那部分邏輯地址對應的存儲空間。

Mach-O文件結構

對於任何一個在macOS上的可執行文件,我們可以用file命令行工具檢查它的格式:

由此可知,在macOS上的可執行文件,都是Mach-O格式的文件。

關於Mach-O文件,詳細可參考Apple官方文檔Mach-O Programming Topics. 這裡我們只是簡單介紹一下。

如圖所示,Mach-O文件由頭(Header)、裝載指令(Load commands)和數據(Data)組成。我們可以通過MachOView軟體進行查看。其中,最重要的組成部分就是Data.

我們可以從圖中看到,Data可以分為多個段(Segment), 每個段又可以分為多個節(Section). 從邏輯角度來看,每個段內的節存儲的數據都有類似的目的。如__TEXT段內存儲的有彙編源代碼、字元串等,__DATA段內存儲非常量初始化變數等。從內存管理角度來看,每個段的大小被要求是頁大小的倍數,也就是4096B的倍數。當程序載入時,就可以正好將一個段載入到一個頁內。

當程序運行時,系統會自動給這個進程分配一個棧。這裡的棧的數據結構就是數據結構中所說的棧,也就是先進後出的線性表。在x86-64架構下,棧是向下生長的。也就是說,每向棧中PUSH一個數據,棧頂的指針就會向邏輯地址減小的方向移動。

ASLR

從Mac OS X 10.5開始,Apple引入了地址空間配置隨機載入(ASLR)機制。在每次程序執行的過程中,程序在內存中的開始地址,堆、棧、庫的地址都會隨機化,這樣可以更好地保護不受攻擊者攻擊。

我們知道,在C語言中,局部變數是在棧上分配的。那麼,我們有如下C語言程序:

//test.c
#include <stdlib.h>

int main()
{
int a = 0;
printf("The address in the stack is: 0x%x
", &a);
return 0;
}

我們在終端下用clang對其編譯

clang test.c -o test

然後運行三次:

我們可以發現,每次運行時,a的邏輯地址都不同。似乎是一個隨機值加上一個固定的偏移量。這就是ASLR的作用。

PIE

在ASLR中我們可以看到,大部分變數在每次運行時的邏輯地址都不一樣。那麼,我們在彙編層面訪問這些變數時,就不能直接訪問一個固定的邏輯地址。因此,我們在彙編語言中有許多技巧可以生成位置無關代碼(Position Independent Code, PIC). 這些代碼中沒有一處會直接訪問固定的邏輯地址。由位置無關代碼編譯生成的可執行文件稱為位置無關可執行文件(Position Independent Executable, PIE). 在我們在macOS上的彙編語言學習過程中,大多數編寫的都是PIC.

可以在哪看到這系列文章

我在我的GitHub上,知乎專欄上和CSDN上同步更新。

上一篇文章:macOS上的彙編入門(三)——硬體基礎

下一篇文章:macOS上的彙編入門(五)——第一個彙編程序


推薦閱讀:

64 位環境彙編的 "Hello World!
"

當載入二進位程序時ip不等於0時
從C語言到彙編(四)while語句

TAG:彙編語言 | macOS |