操作系統接口與系統調用的實現 (李治軍操作系統課筆記2)

什麼是接口

用戶使用計算機的兩種方式:命令行、圖形。從這兩種方式理解接口。

  • 命令行
    在這裏插入圖片描述
    1、命令其實就是程序,經過編譯後生成可執行文件,在命令行鍵入可執行文件名與參數輸出結果。而實際上shell也是一個程序,開機時在main.c文件的所有xx_init()函數之後,通過/bin/sh這個可執行文件啓動shell程序
    2、關鍵是這些程序裏面調用的函數:fork()控制進程 , printf()控制屏幕,scanf()控制鍵盤……通過這些函數對計算機硬件進行使用

  • 圖形
    在這裏插入圖片描述
    鼠標、硬盤點擊之後,通過中斷放入消息隊列;然後應用程序寫一個循環,不斷從消息隊列中取消息,又對不同的消息產生不同的反應(函數)

所以不管是命令行還是圖形,用戶實質上都是通過程序來使用計算機。而這些程序的關鍵就是裏面的函數,比如read() , write()等,這些函數調用就是應用程序和操作系統的接口。又因爲這些函數調用時系統提供的,所以又叫系統調用(system_call)

POSIX標準

POSIX(Portable Operating System Interface of Unix),POSIX標準定義了操作系統應該爲應用程序提供的接口標準,目的是爲了增強程序的可移植性,在不同的操作系統上都能跑。

爲什麼需要接口?

如果用戶程序能直接通過jmp跳到內核或者mov訪問內核的數據,那麼你從網上下載一段程序就可能進入系統內核獲取你的root密碼等等。

如何隔離應用程序與系統內核?

利用硬件將內存劃分爲用戶段和內存段,在用戶態下的程序不能直接訪問內核段的數據。如何實現對這種訪問的阻止呢?

  • GDT表:
    GDT的由來詳見
    總結下來:
    1、對一個內存地址的訪問以段Base Address爲單位。
    2、在實模式下,使用16-bit段寄存器CS(Code Segment),DS(Data Segment),SS(Stack Segment)來指定段,直接左移4位與偏移量相加
    3、在保護模式下,對一個段的描述則包括【Base Address, Limit, Access】,它們加在一起被放在一個64-bit長的數據結構中,被稱爲段描述符。而段寄存器只有16bit,所以將段寄存器中的值作爲下標索引來間接引用(事實上,是將段寄存器中的高13 bit的內容作爲索引)。這個全局的數組就是GDT。
    在這裏插入圖片描述
    注意此處的DPL(description privilege level描述特權級)那一個格,表示表示訪問該段時CPU所需處於的最低特權級,與隔離原理相關,下面詳述。

  • 段選擇子
    在保護模式下,cs叫段選擇子。
    在這裏插入圖片描述
    如上圖,高13位作爲被引用的段描述符在GDT/LDT中的下標索引,bit 2用來指定被引用段描述符被放在GDT中還是到LDT中,bit 0和bit 1是RPL——請求特權等級,請求特權級(RPL)則代表選擇子的特權級,共有4個特權級(0級、1級、2級、3級)。
    在這裏插入圖片描述
    這個特權級與訪問權限相關:
    在head.s裏面建立GDT表的時候就將內核段DPL置爲0,而CPL(current privilege level)是當前指令的特權級,如果是在用戶態,那麼CPL就爲3,否則爲00的特權級是高於3,在訪問某個地址的時候,如果CPL的特權級小於等於DPL的特權級,那麼就不能訪問。由此實現用戶段和內核段的隔離。

在保護模式下如何利用段選擇子將邏輯地址準換爲線性地址?舉個例子
(1)選擇子SEL=21h=0000000000100 0 01b 它代表的意思是:選擇子的index=4即選擇GDT中的第4個描述符;TI=0代表選擇子是在GDT選擇;最後的01代表特權級RPL=1
(2)OFFSET=12345678h,若此時GDT第四個描述符中描述的段基址(Base)爲11111111h,則線性地址=11111111h+12345678h=23456789h
流程如下圖所示(圖源
在這裏插入圖片描述

有了隔離,用戶程序如何才能調用系統內核

硬件提供了用戶態訪問內核態的唯一方法——中斷,int指令將使CS中的CPL從3變爲0,這樣就可以進入內核,並且這個中斷號只能是0x80(後面解釋)。
在這裏插入圖片描述

具體理解:以printf爲例

c代碼裏面的printf是printf(“%d”,a),在printf()內部其實調用了系統函數write,而write函數的函數頭其實是這樣的:

size_t write(int fd, const void *buf, size_t count);

可以看到,printf()函數的形參和write()的形參是不一樣的,因此如果printf(“%d”,a)能調用write函數的話,肯定要對printf的形參進行處理,庫函數printf()就起到這個作用——格式化輸出write所需要的參數,再調用包含中斷int 0x80的write(),最後在內核系統調用wirte()
在這裏插入圖片描述

  • 那麼格式化參數之後是如何調用write()?
    通過_syscall3這個宏展開成_syscall3(int, write, int, fd, const char* buf, off_t, count)
    _syscall3的定義如下:
#define _syscall3(type,name,atype,a,btype,b,ctype,c)\
type name(atype a, btype b, ctype c) \
{ long __res;\
__asm__ volatile(“int 0x80”:”=a”(__res):””(__NR_##name),
”b”((long)(a)),”c”((long)(b)),“d”((long)(c)))); if(__res>=0) return
(type)__res; errno=-__res; return -1;}

要得到_syscall3(int, write, int, fd, const char* buf, off_t, count),就是進行如下替換:

type=int,name=write,atype=int,a=fd,btype=const char * ,b=buf,ctype=off_t,c=count;

因此type name(atype a, btype b, ctype c)就變成了int write(int fd,const char * buf, off_t count)調用庫函數wirte()。

現在調用的還是庫函數wirte(),由該函數代碼可以看到目的就是展開成上面的一段彙編代碼。這是一段內嵌彙編,:”=a”(__res):””(__NR_##name)意思是把存儲了返回值的eax賦給_res,把__NR_write賦值給eax,__NR_write稱爲系統調用號,後面有用;”b”((long)(a)),”c”((long)(b)),“d”((long)(c))就是把形參的a、b、c依次賦值給ebx、ecx、edx。
這段代碼的意思就是在int 0x80進入內核進行中斷處理,處理完之後,返回_res,write這個系統調用就結束了。

  • 什麼是系統調用號呢?
    int 0x80是唯一讓應用程序進入內核的途徑,所以所有的系統調用都是通過int 0x80這個中斷來調用的,那麼如何區分是write調用還是read調用or else?就是根據這個系統調用號來區分的,__NR_write表示write調用,會接着執行write對應的內核代碼,其他同理。通過宏定義 #define __NR_write 4 將真正的中斷處理函數地址在函數指針表中的索引賦給系統調用號。

  • int 0x80具體是如何操作的呢?
    1、int 0x80是進入中斷服務函數的一條指令。 凡是int 指令都要要idt(interrupt description table)錶轉去哪裏執行。
    2、void sched_init(void){ set_system_gate(0x80,&system_call); }這個初始化語句意思是int 0x80對應的中斷處理程序就是system_call。那麼系統是如何初始化IDT表,讓能夠通過int 0x80找到system_call呢?
    通過set_system_gate這個宏,set_system_gate這個宏又調用了_set_gate這個宏,如下圖:
    在這裏插入圖片描述
    關於上圖的幾處解釋:
    1、“movl %%eax,%1\n\t” “movl %%edx,%2”:意思就是把gate_addr也就是&idt[n]的前4位賦給%eax,後四位賦給%edx
    2、_set_gate(&idt[n],15,3,addr)這裏的3替換掉#define _set_gate(gate_addr, type, dpl, addr)的dpl,也就是int0x80的表項在定義時就把DPL定義成了3,所以在用戶態時也能通過int 0x80進入內核段——跳到idt表開始進一步查找。
    3、對於表項的構成,0-15和49-64位存儲的是addr也就是&system_call,16-31存儲的是段選擇符,在這裏是”a”(0x00080000))的前四位0x0008,也就是cs=0x8。在setup.s裏面有一行jmpi 0,8,這條指令表示根據gdt表跳轉到內核代碼的地址0處。所以在此處,CS=8,ip=system_call就是跳到內核代碼0處,然後根據入口點偏移量找到system_call這個函數;同時因爲CS=8=0x1000,那麼CPL=0,也就是說當前程序的特權級變了,變成內核態的了,這樣就什麼都能幹了(這就是0x80是唯一途徑的原因)。將來int 0x80返回之後,CS最後兩位又要變成3,變回用戶態。

中斷處理程序system_call又做了什麼呢

……
call _sys_call_table(,%eax,4) 
……

_sys_call_table(,%eax,4)=_sys_call_table+4*%eax:這是一種尋址方式。eax是系統調用號__NR_write,那_sys_call_table是什麼?

fn_ptr sys_call_table[]=
{sys_setup, sys_exit, sys_fork, sys_read, sys_write, ...};

sys_call_table是一個fn_ptr類型的全局函數表,fn_ptr是一個函數指針,4個字節,所以_sys_call_table+4*%eax要*4,得到的結果就是真正的中斷服務函數sys_write的入口地址了。



總結:

在這裏插入圖片描述



參考:
操作系統(二) – 操作系統的接口與實現

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章