破解Linux操作系統的奧祕
存儲程序計算機的概念
現代計算機的基本結構是由美藉匈牙利科學家馮· 諾依於1946年提出的。迄今為止所有進入實用的電子計算機都是按馮· 諾依曼的提出的結構體系和工作原理設計製造的,故又統稱為“馮·諾依曼型計算機”。其要點為: 1.計算機完成任務是由事先編號的程序完成的;
2.計算機的程序被事先輸入到存儲器中,程序運算的結果,也被存放在存儲器中。
3.計算機能自動連續地完成程序。
4.程序運行的所需要的信息和結果可以通輸入\輸出設備完成。
5.計算機由運算器、控制器、存儲器、輸入設備、輸出設備所組成;
其工作原理如下圖:
堆棧(函數調用堆棧)機制
CPU通過總線從存儲器中讀取指令和數據進行處理,採用的主要機制是函數調用推棧機制。在不發生中斷、異常以及系統調用的過程中,每個進程的執行都符合函數調用堆棧機制。
在內存中,棧是往低(小)地址方向擴展的,而esp指向當前棧頂處的元素。通過使用push和pop指令我們可以把數據壓入棧中或從棧中彈出。對於沒有指定初始值的數據所需要的存儲空間,我們可以通過把棧指針遞減適當的值來做到。類似地,通過增加棧指針值我們可以回收棧中已分配的空間。棧用來傳遞函數參數、存儲返回信息、臨時保存寄存器原有值以用於回覆以及存儲局部數據。棧幀結構的兩端由兩個指針來指定。寄存器ebp通常用作棧幀的指針、esp用作棧的指針。esp隨著數據的入棧和出棧。因此對於函數中大部分數據的訪問都是通過基於幀幀指針ebp來實現。函數調用時,調用者首先會將參數以及返回地址(下一條指令的EIP)壓入棧中,並將當前EIP修改為被調用函數的起始地址;被調用者開始執行後先將原棧基址EBP保存在棧中,並修改EBP和ESP的值,初始化一個新棧。返回時,被調用者恢復EBP和ESP,並且將EIP修改為棧中存儲的返回地址。
中斷機制
中斷是指在CPU正常運行期間,由於內外部事件或由程序預先安排的事件引起的CPU暫時停止正在運行的程序,轉而為該內部或外部事件或預先安排的事件服務的程序中去,服務完畢後再返回去繼續運行被暫時中斷的程序。Linux中通常分為外部中斷(又叫硬件中斷)和內部中斷(又叫異常)。
CPU執行完一條指令後,下一條指令的邏輯地址存放在cs和eip這對寄存器中。在執行新指令前,控制單元會檢查在執行前一條指令的過程中是否有中斷或異常發生。如果有,控制單元就會拋下指令,進入下面的流程:
1.確定與中斷或異常關聯的向量i (0<i<255)
2.尋找向量對應的處理程序
3.保存當前的“工作現場”,執行中斷或異常的處理程序
4.處理程序執行完畢後,把控制權交還給控制單元
5.控制單元恢復現場,返回繼續執行原程序
Linux是個什麼玩意? 讓我們從一個比較高的高度來審視一下 GNU/Linux 操作系統的體系結構。您可以從兩個層次上來考慮操作系統,如圖所示:
最上面是用戶(或應用程序)空間。這是用戶應用程序執行的地方。用戶空間之下是內核空間,Linux 內核正是位於這裡。 GNU C Library (glibc)也在這裡。它提供了連接內核的系統調用接口,還提供了在用戶空間應用程序和內核之間進行轉換的機制。這點非常重要,因為內核和用戶空間的應用程序使用的是不同的保護地址空間。每個用戶空間的進程都使用自己的虛擬地址空間,而內核則佔用單獨的地址空間。
Linux 內核可以進一步劃分成 3 層。最上面是系統調用接口,它實現了一些基本的功能,例如 read 和 write。系統調用接口之下是內核代碼,可以更精確地定義為獨立於體系結構的內核代碼。這些代碼是 Linux 所支持的所有處理器體系結構所通用的。在這些代碼之下是依賴於體系結構的代碼,構成了通常稱為BSP(Board Support Package)的部分
。這些代碼用作給定體系結構的處理器和特定於平臺的代碼。
switch_to宏解析
首先要理解的是進程切換(process switch),作為搶佔式多任務OS中重要的一個功能,其實質就是OS內核掛起正在運行的進程A,然後將先前被掛起的另一個進程B恢復運行。
每個進程都有自己的地址空間,但是所有進程在物理上共享著CPU的寄存器,因此,當恢復一個進程執行前,OS內核必須要將掛起該進程時寄存器的值裝入CPU寄存器。進程恢復執行前必須裝入寄存器的一組數據就叫做“硬件上下文”(hardware context),它是進程執行上下文的子集,後者是進程執行時需要的所有信息(如地址空間中的數據等)。
Linux中,TSS保存著部分的進程的硬件上下文(如ss、esp等寄存器的值),剩餘部分保存在內核堆棧中(如eax、ebx等通用數據寄存器的值)。
進程切換隻發生在內核態,在進程切換之前,用戶態使用的寄存器內容都已保存在內核堆棧上,如ss、esp等。
下面來看一下switch_to 宏的代碼,其中,prev是即將要被換出CPU的進程的描述符,next是即將得到CPU的進程的描述符。
#define switch_to(prev,next,last) do { \
unsigned long esi,edi; \
asm volatile("pushfl\n\t" \
"pushl %%ebp\n\t" \
"movl %%esp,%0\n\t" /* save ESP */ \
"movl %5,%%esp\n\t" /* restore ESP */ \
"movl $1f,%1\n\t" /* save EIP */ \
"pushl %6\n\t" /* restore EIP */ \
"jmp __switch_to\n" \
"1:\t" \
"popl %%ebp\n\t" \
"popfl" \
:"=m" (prev->thread.esp),"=m" (prev->thread.eip), \
"=a" (last),"=S" (esi),"=D" (edi) \
:"m" (next->thread.esp),"m" (next->thread.eip), \
"2" (prev), "d" (next)); \
} while (0)
該宏的工作步驟大致如下:
prev的值送入eax,next的值送入edx(這裡我從代碼中沒有看出來,原著上如是寫,可能是從調用switch_to宏的switch_context或schedule函數中處理的)。
保護prev進程的eflags和ebp寄存器內容,這些內容保存在prev進程的內核堆棧中。 將prev的esp寄存器中的數據保存在prev->thread.esp中,即將prev進程的內核堆棧保存起來。
將next->thread.esp中的數據存入esp寄存器中,這是加載next進程的內核堆棧。 將數值1保存到prev->thread.eip中,該數值1其實就是代碼中"1:\t"這行中的1。為了恢復prev進程執行時用。
將next->thread.eip壓入next進程的內核堆棧中。這個值往往是數值1。 跳轉到__switch_to函數處執行。
執行到這裡,prev進程重新獲得CPU,恢復prev進程的ebp和eflags內容。 將eax的內容存入last參數(這裡我也沒看出來,原著上如是寫,只是在__switch_to函數中返回prev,該值是放在eax中的)。
總結:內核的工作機制
通過上面非常重要的三個點的分析:存儲式計算機,堆棧機制,中斷機制的分析,我們對Linux內核的機制有了一個大致的瞭解,下面來總結一下。
進程是正在運行的程序的一種抽象,進程工作在從用戶態到內核態,從內核態在到用戶態之間的轉換。用戶態的進程不能直接使用硬件資源,通過中斷來切換到內核態來完成。內核態通過分析中斷請求,確定中斷向量表,然後轉向相應的中斷出倆函數,完成中斷服務。然後在轉到用戶態,繼續執行原先程序,以I/O中斷為例,如下圖: