所有的程序員在寫程序的時候都離不開通過庫函數的方式和系統調用打交道
什么是用戶態和內核態?(從CPU指令級別的角度)
一般現代CPU都有幾種不同的指令執行級別,什么樣的程序可以執行什么的指令
在高執行級別下,代碼可以執行特權指令,訪問任意的物理地址,這時CPU執行級別就對應著內核態
而在相應的低級別執行狀態下,代碼的掌控范圍會受到限制。只能在對應級別允許的范圍內活動
舉例:intel x86 CPU有四種不同的執行級別0-3,Linux只使用了其中的0級和3級分別來表示內核態和用戶態
如何區分用戶態和內核態?(從進程地址空間的角度)
cs寄存器的最低兩位表明了當前代碼的特權級
CPU每條指令的讀取都是通過cs:eip這兩個寄存器:
????? 其中? cs是代碼段選擇寄存器,eip是偏移量寄存器
上述判斷由硬件完成
在32位x86的機器上,有4G的進程地址空間(邏輯地址),在內核態的時候全都可以訪問,在用戶態的時候,只能訪問0x00000000-0xbfffffff的地址空間。也就是說0xc0000000以上的地址空間只能在內核態下訪問
中斷處理是從用戶態進入內核態主要的方式
當從用戶態切換到內核態的時候,必須用戶態的寄存器上下文保存起來,同時設置內核態的寄存器內容
中斷/int指令會在堆棧上保存一些寄存器的值
????? 如:用戶態棧頂地址、當時的狀態字、當時的 cs:eip 的值
同時設置內核態的棧頂地址、內核態的狀態字,中斷處理程序的入口地址 cs:eip 的值(對于系統調用來講,它是指向system_call函數)
中斷/int指令發生后第一件事就是保護現場
保護現場就是進入中斷程序保存需要用到的寄存器的數據
當進入到中斷處理程序后,一開始就執行SAVE_ALL,把其它的一些寄存器的值push到內核堆棧里面去
中斷處理結束前最后一件事是恢復現場
恢復現場就是退出中斷程序恢復寄存器的數據
當中斷處理程序結束之后,它會RESTORE_ALL,把保存的用戶態的寄存器再pop出來到當前的CPU里面,最后iret,iret指令與中斷信號(包括int指令)發生時CPU做的動作剛好相反
中斷處理的完整過程
interrupt(ex:int 0x80)
save cs:eip/ss:esp/eflags(current) to kernel stack, then load cs:eip(entry of a specific ISR) and ss:esp(point to kernel stack)
SAVE_ALL
....? // 內核代碼,完成中斷服務,發生進程調度
RESTORE_ALL
iret
pop cs:eip/ss:esp/eflags from kernel stack
SAVE_ALL....如果發生了進程調度,那么當前的狀態都會暫時保存在系統里面,當下一次發生進程調度切換回當前進程的時候,就會接著把它執行完,RESTORE_ALL....
以系統調用為例,看中斷具體是怎么執行的
系統調用通過軟中斷向內核發出一個明確的請求,是操作系統為用戶態進程與硬件設備進行交互提供的一組接口
封裝例程 (wrapper routine),唯一目的就是發布系統調用,讓程序員在寫代碼的時候不需要用匯編指令來觸發一個系統調用,而是直接調用一個函數就可以觸發一個系統調用
應用編程接口(application program interface, API) 只是一個函數定義。一般每個系統調用對應一個封裝例程,庫再用這些封裝例程定義出給用戶的API。但并不是每個API都對應一個特定的系統調用,API可能直接提供用戶態的服務,例如一些數學函數。一個單獨的API可能調用幾個系統調用,不同的API可能調用了同一個系統調用
User Mode 用戶態????? Kernel Mode 內核態
xyz()函數,是系統調用對應的API,這個應用程序編程接口里面封裝了一個系統調用,這個系統調用會觸發一個int 0x80的中斷,0x80這個中斷向量對應著system_call這個內核代碼的起點,這個內核代碼里面會有SAVE_ALL,然后執行到sys_xyz()中斷服務程序,進入程序里面處理,在中斷服務程序執行完之后會ret_from_sys_call,在return的過程中可能會發生進程調度(這是一個進程調度的時機),如果沒有進程調度,就會iret,回到用戶態接著執行
Summary:
系統調用的三層皮:API、中斷向量對應的system_call、中斷服務程序sys_xyz
當用戶態進程調用一個系統調用時,CPU切換到內核態并開始執行一個內核函數,在Linux中是通過執行int $0x80來執行系統調用的,這條匯編指令產生向量為128的編程異常。(Intel Pentium II中引入了sysenter指令(快速系統調用),2.6已經支持)
內核實現了很多不同的系統調用,進程必須指明需要哪個系統調用,這需要傳遞一個名為系統調用號的參數,使用eax寄存器(系統調用號將xyz()和sys_xyz()關聯起來了)
系統調用的參數傳遞方法
普通函數調用的時候,可以采用把參數壓棧的方式傳遞參數。但是從用戶態到內核態,怎么傳遞參數呢?
system_call是linux中所有系統調用的入口點,每個系統調用至少有一個參數,即由eax傳遞的系統調用號
一個應用程序調用fork()封裝例程,那么在執行int $0x80之前就把eax寄存器的值置為2(即__NR_fork)
這個寄存器的設置是libc庫中的封裝例程進行的,因此用戶一般不關心系統調用號
進入sys_call之后,立即將eax的值壓入內核堆棧
寄存器傳遞參數具有如下限制:
1)每個參數的長度不能超過寄存器的長度,即32位
2)在系統調用號(eax)之外,參數的個數不能超過6個(ebx, ecx,edx,esi,edi,ebp)
超過6個怎么辦?
如果超過6個,就把某一個寄存器作為一個指針,指向一塊內存,進入到內核態之后可以訪問到所有的地址空間,通過內存來傳遞參數
通過庫函數API使用系統調用獲取系統當前時間
用匯編方式觸發系統調用獲取系統當前時間
系統調用傳遞第一個參數使用ebx,這里是NULL
使用eax傳遞系統調用號,這里time是13
系統調用的返回值使用eax存儲,和普通函數一樣
(完)