作為程序員你肯定寫過無數的函數,假設有這樣兩個函數:
void funcB() {
}
void funcA() {
funcB();
}
函數之間是可以相互調用的,這很簡單很happy有沒有。
要知道是代碼、是函數就可以相互調用,不管你用什麼語言寫的。
假設funcB是内核中的函數,funcA是你自己寫的函數,就像這樣:
// Linux内核中的函數
void funcB() {
}
// 你的函數
void funcA() {
funcB();
}
那麼funcA應該也能調用funcB(如果funcB可以供外界調用的話)。
有的同學可能會驚呼,我們可以自己編寫代碼調用操作系統的函數,那豈不是可以直接控制操作系統了?
too yong too simple!
如果我們編寫的代碼可以直接調用所有的操作系統函數那麼從某種程度上講的确可以說是能控制操作系統,但如果操作系統隻允許你調用内核中的有限的幾個函數呢?
怎麼樣,你(應用程序)是不是就被限制住了。
你又會問,操作系統是怎樣限制應用程序能調用哪些内核中的函數呢?
實際上單靠操作系統這種軟件是沒有辦法限制應用程序能調用哪些以及多少個内核函數的,因此為施加這種限制必須依靠——硬件。
這裡的硬件指的就是CPU。
那麼CPU又是怎麼施加這種限制的呢?
我們先來看看普通的函數調用,函數調用對應的機器指令是call指令,就像這樣:
call 0x400410
call指令後的這個地址0x400410就是被調函數的第一條機器指令所在的内存地址。
當CPU執行到這條機器指令時直接跳轉到對應的地址繼續執行指令,從程序員的角度看就是函數調用。
而如果是我們程序的函數調用操作系統的函數就不允許使用call指令了,而是syscall機器指令(x86_64)。
使用syscall指令調用操作系統函數時也是把相應函數的第一條指令的地址放到syscall之後嗎?
顯然不是的,因為操作系統系統代碼和你的代碼都是單獨編譯以及運行的,你根本就不知道操作系統的某個函數存放在内存的什麼位置上,也不應該讓你知道,因此使用syscall調用操作系統的函數時我們隻能附加一個序号,比如序号0對應操作系統中的A函數、序号1對應操作系統中的B函數等等,這樣使用syscall指令時隻需要将該序号寫入rax寄存器即可,CPU在執行syscall指令時通過讀取rax寄存器的值就能知道到底該調用操作系統中的哪個函數了。
可以看到,利用這種機制操作系統限制了應用程序可以調用哪些内核中的函數。
有的同學可能會有疑問,如果一個call指令因為種種原因後面跟上的地址”無意“中指向了一個内核函數的地址,那麼CPU執行call指令時會怎樣呢?就像這樣:
call 0x400410
這裡假設0x400410這個地址指向了一個内核函數地址。
很簡單,CPU在執行這條指令時會判斷出當前進程沒有權限訪問0x400410這個地址,因此CPU在執行這條指令時會産生異常,該進程會被直接kill掉。
這裡列舉了Linux在各種處理器上怎樣進行系統調用。
看到了吧,syscall和call在使用方法上還是有很大不同的,可以看到call是直接調用的,也就是說應用程序這一層中的函數調用是直接調用的,而syscall其實是間接調用的,即我們調用操作系統中的函數時其實是間接調用的。
除此之外,CPU在執行call指令以及syscall指令時另外一個不同點在于模式的切換。
當CPU執行普通函數時其實是運行在用戶态,user mode,在這種模式下CPU不能執行某些特權指令,這也就意味着我們的程序其實是受限的;而當CPU執行syscall開始執行操作系統的代碼時會切換到内核态,kernel mode,在這種模式下CPU可以執行任何特權指令,不受任何限制,操作系統才是真正的管理計算機的大boss。
可以看到,當在普通程序中進行函數調用時就是函數調用,而普通函數調用操作系統中的函數時才叫系統調用。
最後再說一點,普通的函數調用所使用的棧全部位于進程的棧區,假設main函數調用funcA函數,funcA調用funcB函數,那麼此時的進程内存布局就像這樣:
而進行系統調用時當CPU開始執行操作系統的代碼時不再基于進程棧區而是會跳轉到操作系統某個特定内存區域,該區域作為進程在内核中的棧區,因此也叫做内核棧,每個進程在内核中都有自己的内核棧,因此我們可以看到一個進程其實有兩個棧區,一個在用戶态一個在内核态。
假設main函數調用funcA,funcA進行系統調用,調用内核中的funcB函數,funcB函數調用内核中的funcC函數,那麼此時的内存布局就像這樣:
好啦,這個話題就到這裡,希望對大家理解操作系統有所幫助。
,更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!