安大大 + 原創(chuàng)作品轉(zhuǎn)載請注明出處 + 《Linux操作系統(tǒng)分析》MOOC課程
用戶態(tài)盖高、內(nèi)核態(tài)和中斷處理過程
程序員通過庫函數(shù)的方式和系統(tǒng)調(diào)用打交道蹬跃,庫函數(shù)把系統(tǒng)調(diào)用給封裝起來了煎源。
一般現(xiàn)代CPU都有幾種不同的指令執(zhí)行級別
? 在高執(zhí)行級別下,代碼可以執(zhí)行特權(quán)指令揍很,訪問任意的物理地址融柬,這種CPU執(zhí)行級別就對應(yīng)著內(nèi)核態(tài)
? 而在相應(yīng)的低級別執(zhí)行狀態(tài)下含衔,代碼的掌控范圍會受到限制煎娇。只能在對應(yīng)級別允許的范圍內(nèi)活動
? 舉例:intel x86 CPU有四種不同的執(zhí)行級別0-3,Linux只使用了其中的0級和3級分別來表示內(nèi)核態(tài)和用戶態(tài)
為什么有權(quán)限級別的劃分
防止程序員非法訪問系統(tǒng)或者是其它資源而使得系統(tǒng)崩潰
Linux中怎么區(qū)分用戶態(tài)和內(nèi)核態(tài):
? cs寄存器的最低兩位表明了當前代碼的特權(quán)級
? CPU每條指令的讀取都是通過cs:eip這兩個寄存器:
其中cs是代碼段選擇寄存器贪染,eip是偏移量寄存器缓呛。
? 上述判斷由硬件完成
? 一般來說在Linux中,地址空間是一個顯著的標志:
0xc0000000以上的地址空間只能在內(nèi)核態(tài)下訪問杭隙,0x00000000-0xbfffffff的地址空間在兩種狀態(tài)下都可以訪問
注意:這里所說的地址空間是邏輯地址而不是物理地址
在內(nèi)核態(tài)時哟绊,cs和eip可以是任意的地址
中斷處理是從用戶態(tài)進入內(nèi)核態(tài)主要的方式
用戶態(tài)進入內(nèi)核態(tài)一般來說都是用中斷來觸發(fā)的,可能是硬件中斷痰憎。也可能是用戶態(tài)程序運行當中調(diào)用了系統(tǒng)調(diào)用進入了內(nèi)核態(tài)(trap)票髓。系統(tǒng)調(diào)用是一種特殊的中斷。
? 寄存器上下文
– 從用戶態(tài)切換到內(nèi)核態(tài)時
? 必須保存用戶態(tài)的寄存器上下文铣耘,同時內(nèi)核態(tài)相應(yīng)的值放到CPU中
? 要保存哪些洽沟?
? 保存在哪里?
? 中斷/int指令會在堆棧上保存一些寄存器的值
– 如:用戶態(tài)棧頂?shù)刂肺舷浮敃r的狀態(tài)字裆操、當時的cs:eip的值
中斷發(fā)生后第一件事就是保存現(xiàn)場 SAVE_ALL
保護現(xiàn)場 就是進入中斷程序,保存需要用到的寄存器的數(shù)據(jù)
恢復(fù)現(xiàn)場 就是退出中斷程序炉媒,恢復(fù)保存寄存器的數(shù)據(jù)
中斷處理結(jié)束前最后一件事是恢復(fù)現(xiàn)場 RESTORE_ALL
中斷處理的完整過程
系統(tǒng)調(diào)用概述
以系統(tǒng)調(diào)用為例踪区,看看中斷服務(wù)具體是怎么執(zhí)行的:
系統(tǒng)調(diào)用的意義
- 操作系統(tǒng)為用戶態(tài)進程與硬件設(shè)備進行交互提供了一組接口——系統(tǒng)調(diào)用
- 把用戶從底層的硬件編程中解放出來
- 極大的提高了系統(tǒng)的安全性
- 使用戶程序具有可移植性
操作系統(tǒng)提供的API和系統(tǒng)調(diào)用的關(guān)系
應(yīng)用編程接口(application program interface, API) 和系統(tǒng)調(diào)用是不同的
- API只是一個函數(shù)定義
- 系統(tǒng)調(diào)用通過軟中斷(trap)向內(nèi)核發(fā)出一個明確的請求
Libc庫定義的一些API引用了封裝例程(wrapper routine,唯一目的就是發(fā)布系統(tǒng)調(diào)用) - 一般每個系統(tǒng)調(diào)用對應(yīng)一個封裝例程
-
庫再用這些封裝例程定義出給用戶的API
不是每個API都對應(yīng)一個特定的系統(tǒng)調(diào)用吊骤。 - API可能直接提供用戶態(tài)的服務(wù)缎岗,如一些數(shù)學(xué)函數(shù)
- 一個單獨的API可能調(diào)用幾個系統(tǒng)調(diào)用
- 不同的API可能調(diào)用了同一個系統(tǒng)調(diào)用
返回值 - 大部分封裝例程返回一個整數(shù),其值的含義依賴于相應(yīng)的系統(tǒng)調(diào)用
- -1在多數(shù)情況下表示內(nèi)核不能滿足進程的請求
- Libc中定義的errno變量(error number)包含特定的出錯碼
應(yīng)用程序白粉、封裝例程传泊、系統(tǒng)調(diào)用處理程序及系統(tǒng)調(diào)用服務(wù)例程之間的關(guān)系
左邊是用戶態(tài)User Mode,右邊是內(nèi)核態(tài)Kernel Mode鸭巴,最左邊api:xyz()封裝了一個系統(tǒng)調(diào)用眷细,這個系統(tǒng)調(diào)用會觸發(fā)一個0x80的中斷。0x80這個中斷向量就對應(yīng)著system_call這個內(nèi)核代碼的入口起點奕扣。這個內(nèi)核代碼里可能有SAVE_ALL,sys_xyz()中斷服務(wù)程序掌敬,在中斷服務(wù)程序執(zhí)行完后惯豆,可能ret_from_sys_call池磁,在return的過程中可能發(fā)生進程調(diào)度,這是一個進程調(diào)度的時機楷兽。如果沒有發(fā)生系統(tǒng)調(diào)度地熄,就會iret,再返回到用戶態(tài)接著執(zhí)行芯杀。
系統(tǒng)調(diào)用的三層皮:xyz(api)端考、system_call(中斷向量)和sys_xyz
當用戶態(tài)進程調(diào)用一個系統(tǒng)調(diào)用時,CPU切換到內(nèi)核態(tài)并開始執(zhí)行一個內(nèi)核函數(shù)揭厚。
- 在Linux中是通過執(zhí)行int $0x80來執(zhí)行系統(tǒng)調(diào)用的却特,這條匯編指令產(chǎn)生向量為128的編程異常
系統(tǒng)調(diào)用號講xyz和sys_xyz關(guān)聯(lián)起來了
傳參:
內(nèi)核實現(xiàn)了很多不同的系統(tǒng)調(diào)用,進程必須指明需要哪個系統(tǒng)調(diào)用筛圆,這需要傳遞一個名為系統(tǒng)調(diào)用號的參數(shù)
-使用eax寄存器
系統(tǒng)調(diào)用的參數(shù)傳遞方法
系統(tǒng)調(diào)用也需要輸入輸出參數(shù)裂明,例如:
- 實際的值
- 用戶態(tài)進程地址空間的變量的地址
- 甚至是包含指向用戶態(tài)函數(shù)的指針的數(shù)據(jù)結(jié)構(gòu)的地址
如果是函數(shù)調(diào)用的時候,它可以把函數(shù)壓棧的方式來傳遞太援。而用戶態(tài)到內(nèi)核態(tài)的函數(shù)傳遞的方法:
system_call是linux中所有系統(tǒng)調(diào)用的入口點闽晦,每個系統(tǒng)調(diào)用至少有一個參數(shù),即由eax傳遞的系統(tǒng)調(diào)用號
- 一個應(yīng)用程序調(diào)用fork()封裝例程提岔,那么在執(zhí)行int $0x80之前就把eax寄存器的值置為2(即__NR_fork)仙蛉。
- 這個寄存器的設(shè)置是libc庫中的封裝例程進行的,因此用戶一般不關(guān)心系統(tǒng)調(diào)用號
- 進入sys_call之后碱蒙,立即將eax的值壓入內(nèi)核堆棧
?寄存器傳遞參數(shù)具有如下限制:
?1)每個參數(shù)的長度不能超過寄存器的長度荠瘪,即32位
?2)在系統(tǒng)調(diào)用號(eax)之外,參數(shù)的個數(shù)不能超過6個(ebx振亮,ecx巧还,edx,esi坊秸,edi麸祷,ebp)
?超過6個怎么辦? 如果超過六個褒搔,某一個寄存器作為一個指針指向一塊內(nèi)存阶牍,進入到內(nèi)核態(tài)以后,它可以訪問所有的內(nèi)存空間星瘾,可以通過這塊內(nèi)存來傳遞數(shù)據(jù)走孽。
使用庫函數(shù)API來獲取系統(tǒng)當前時間
簡單的系統(tǒng)調(diào)用time,來獲取當前系統(tǒng)的時間:
#include <stdio.h>
#include <time.h>
int main()
{
time_t tt;// tt只是int型的數(shù)值
struct tm *t;// tm為了輸出的時候變成可讀的
tt = time(NULL);//time系統(tǒng)調(diào)用琳状,返回值賦給tt//使用了time這個庫函數(shù)磕瓷,api
t = localtime(&tt);//把tt改成t這種格式的,即tm格式
printf("time:%d:%d:%d:%d:%d:%d\n",t->tm_year+1900, t->tm_mon, t->tm_mday,t->tm_hour,t->tm_min,t->tm_sec);
return 0;
}
使用庫函數(shù)的方式比較簡單,下邊使用匯編的方式:
用匯編方式觸發(fā)系統(tǒng)調(diào)用獲取系統(tǒng)當前時間
#include <stdio.h>
#include <time.h>
int main()
{
time_t tt;
struct tm *t;
asm volatile(
"mov $0,%%ebx\n\t"http://把ebx清零
"mov $0xd,%%eax\n\t"http://把0xd(13)放到eax里,eax是用來傳遞系統(tǒng)調(diào)用號的
"int $0x80\n\t"
"mov %%eax,%0\n\t"http://通過eax返回值
:"=m"(tt)
);
t = localtime(&tt);
printf("time:%d:%d:%d:%d:%d:%d\n",t->tm_year+1900, t->tm_mon, t->tm_mday,t->tm_hour,t->tm_min,t->tm_sec);
return 0;
}
可以看到執(zhí)行的效果是完全一樣的困食。用戶態(tài)進程向內(nèi)核態(tài)傳遞了一個系統(tǒng)調(diào)用號边翁。在那段匯編代碼里,先是給ebx傳參數(shù)硕盹,然后給eax傳系統(tǒng)調(diào)用號符匾,int指令,系統(tǒng)調(diào)用執(zhí)行完后返回結(jié)果eax瘩例,這就完成了系統(tǒng)調(diào)用啊胶。
系統(tǒng)調(diào)用號的定義在 /usr/include/asm/unistd.h 文件中