系統調用
預備知識
目標:了解系統調用的流程,在Linux 0.11上添加兩個系統調用,并編寫兩個簡單的應用程序測試它們。
對應章節:同濟大學趙炯博士的《Linux內核0.11完全注釋(修正版V3.0)》的第5.5節
下面就針對這一節做一些筆記
系統調用的概念
系統調用(通常稱為 syscalls)是 Linux 內核與上層應用程序進行交互通信的唯一接口。這些接口為了應用級的代碼能夠在不同的操作系統上都可以運行,就需要有標準去限制系統調用接口的標準。不同操作系統的系統調用接口都要按照這個標準來。
POSIX(Portable Operating System Interface for Computing Systems)
由IEEE開發,是一個標準族: 1003.1, 2003…于保證編制的應用程序可以在源代碼一級上在多種操作系統上移植和運行。
那用戶態程序想要訪問內核資源怎么辦呢?
- 從對中斷機制的說明可知,用戶程序通過直接或間接(通過庫函數)調用中斷
int 0x80
- 并在
eax
寄存器中指定系統調用功能號,即可使用內核資源,包括系統硬件資源。- 但是,這個太麻煩了還需要記住系統調用的功能號,還需設置中斷,標準接口定義的 C 函數庫中的函數間接地使用內核的系統調用,C 函數庫已經幫封裝好了
- 系統調用的實現:(C 函數庫已經給封裝好了)
- (1) 用戶程序中寫上一段包含int指令的代碼
- (2) OS寫中斷處理代碼,獲取想調程序的編號(系統調用編號)
- (3) OS根據編號轉去執行相應的代碼
上面提到的系統調用的功能號
和系統調用編號
是什么?
指的是同一個東西:在 Linux 內核中,每個系統調用都具有唯一的一個系統調用功能號。
這些號定義在文件include/unistd.h 中第 60 行開始處。例如,write 系統調用的功能號是 4,定義為符號__NR_write。
// 以下是內核實現的系統調用符號常數,用于作為系統調用函數表中的索引值。( include/linux/sys.h ) #define __NR_setup 0 /* used only by init, to get system going */ /* __NR_setup 僅用于初始化,以啟動系統 */ #define __NR_exit 1 #define __NR_fork 2 #define __NR_read 3 #define __NR_write 4
這些系統調用號對應于 include/linux/sys.h 中定義的系統調用處理程序指針數組表 sys_call_table[]中項的索引值。因此 write()系統調用的處理程序指針就位于該數組的項 4 處。
extern int sys_setup (); // 系統啟動初始化設置函數。 (kernel/blk_drv/hd.c,71) extern int sys_exit (); // 程序退出。 (kernel/exit.c, 137) extern int sys_fork (); // 創建進程。 (kernel/system_call.s, 208) extern int sys_read (); // 讀文件。 (fs/read_write.c, 55) extern int sys_write (); // 寫文件。 (fs/read_write.c, 83) // 系統調用函數指針表。用于系統調用中斷處理程序(int 0x80),作為跳轉表。 fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,sys_write, sys_open,省略了 };
這里有很多的系統調用處理函數,他們有幾個相同點:
系統調用執行的結果返回值。通常負值表示錯誤,而 0 則表示成功。
在出錯的情況下,錯誤的類型碼被存放在全局變量 errno 中。通過調用庫函數 perror(),我們可以打印出該錯誤碼對應的出錯字符串信息。
系統調用處理過程
現在以read
系統調用為例,來說明這個過程
首先,在
include/unistd.h
文件里面有個read函數的聲明// 對應各系統調用的函數原型定義。int read(int fildes, char * buf, off_t count);
但是,操作系統是負責寫系統調用的,是要提供
系統調用sys_read()
,在文件fs/read_write.c
中。接下來給出從read()->sys_read()
的具體流程。前面給出了
read()
的聲明,下面得找到定義,Linux0.11的源碼好像沒有找到,但是在lib
文件夾有一些其他的類似的函數定義例如write()
。代碼如下,這里用到了_syscall3宏
,把這個宏展開之后,得到write()
的定義。write.s文件 #include <set_seg.h> #define __LIBRARY__ // Linux 標準頭文件。定義了各種符號常數和類型,并申明了各種函數。 // 如定義了__LIBRARY__,則還包括系統調用號和內嵌匯編_syscall0()等。 #include <unistd.h> 寫文件系統調用函數。 // 該宏結構對應于函數:int write(int fd, const char * buf, off_t count) // 參數:fd - 文件描述符;buf - 寫緩沖區指針;count - 寫字節數。 // 返回:成功時返回寫入的字節數(0 表示寫入0 字節);出錯時將返回-1,并且設置了出錯號。 _syscall3(int,write,int,fd,const char *,buf,off_t,count)展開之后: int write(int fd, const char *buf, off_t count) {long __res;__asm__ volatile ("int $0x80": "=a" (__res): "0" (__NR_write), "b" ((long)(fd)), "c" ((long)(buf)), "d" ((long)(count)));if (__res >= 0)return (int) __res;errno = -__res;return -1; }
接下來介紹
宏_syscalln()
:這個宏完成了c標準庫函數的定義,也就是對相關的系統調用sys_函數名
的封裝。其中 n 代表攜帶的參數個數,可以分別 0 至 3。最多可以直接傳遞 3 個參數。其中寄存器 eax 中存放著系統調用號,而攜帶的參數可依次存放在寄存器 ebx、ecx 和 edx 中。所以用戶程序能夠向內核最多直接傳遞3個參數,當然也可以不帶參數。下面給出
_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) \: "0" (__NR_##name),"b" ((long)(a)),"c" ((long)(b)),"d" ((long)(c))); \ if (__res>=0) \return (type) __res; \ errno=-__res; \ return -1; \ }
依然以read為例來說明宏_syscall3()的作用
GCC 內聯匯編(AT&T格式)學習資源:https://zhuanlan.zhihu.com/p/606376595
#define --LIBRARY-- #include <unistd.h> -syscall3(int, read, int, fd, char *, buf, int, n) 展開之后int read(int fd, char *buf, int n) {// 存儲系統調用返回值long __res;// 內聯匯編觸發系統調用,volatile:編譯器不優化__asm__ volatile ("int $0x80" // 觸發系統調用的中斷指令: "=a" (__res) // 輸出,系統調用返回值存入 __res "=a" 表示使用寄存器 eax 來傳遞返回值,"=" 表示只寫操作: "0" (__NR_read), // 輸入,"0" 表示使用和輸出操作數列表中編號為 0 的操作數相同的寄存器(即 eax)"b" ((long)(fd)), // ebx 傳遞文件描述符"c" ((long)(buf)), // ecx 傳遞緩沖區指針"d" ((long)(n)) // edx 傳遞要讀取的字節數);// 系統調用成功,返回讀取字節數if (__res >= 0)return (int)__res;// 系統調用失敗,設置錯誤碼errno = -__res;return -1; }
接下來,將展開后的代碼分成三部分
首先,傳入給
eax
寄存器一個系統調用號__NR_read
,這個前文已經說明,__NR_read=3
是sys_read()
這個系統調用函數在sys_call_table[]
中項的索引值,有了這個才能定位到系統調用函數。輸出的結果也使用寄存器eax
來傳遞返回值
int $0x80
:進入系統調用中斷程序在kernel/system_call.s
中。下面是中斷代碼:`kernel/system_call.s`中 # ====================== 系統調用處理流程 ====================== # # 入口:用戶態執行int 0x80后CPU跳轉至此 .align 2 system_call:cmpl $nr_system_calls-1,%eax # 1. 校驗系統調用號范圍ja bad_sys_call # 超過最大值跳錯誤處理# ==== 保存用戶態上下文 ==== #push %ds # 2. 保存用戶數據段push %es # 保存用戶附加段push %fs # 保存特殊用途段pushl %edx # 系統調用參數3pushl %ecx # 參數2pushl %ebx # 參數1# ==== 設置內核環境 ==== #movl $0x10,%edx # 3. 內核數據段選擇子(GDT[2], RPL=0)mov %dx,%ds # 設置ds/es為內核數據段mov %dx,%esmovl $0x17,%edx # 用戶數據段選擇子(LDT[2], RPL=3)mov %dx,%fs # fs保持用戶數據訪問能力# ==== 執行系統調用 ==== #call *sys_call_table(,%eax,4) # 4. 查系統調用表執行對應函數# eax*4因為函數指針占4字節# ==== 調度檢查 ==== #pushl %eax # 保存系統調用返回值movl current,%eax # 獲取當前進程task_struct指針cmpl $0,state(%eax) # 5. 檢查進程狀態(0=就緒態)jne reschedule # 非就緒態跳調度cmpl $0,counter(%eax) # 檢查時間片剩余je reschedule # 時間片耗盡跳調度# ==================== 系統調用返回路徑 ==================== # ret_from_sys_call:movl current,%eax cmpl task,%eax # 6. 是否是初始任務0?je 3f # 是則跳過信號處理cmpw $0x0f,CS(%esp) # 檢查原CS是否是用戶態代碼段jne 3fcmpw $0x17,OLDSS(%esp) # 檢查原SS是否是用戶態堆棧段jne 3f# ==== 信號處理 ==== #movl signal(%eax),%ebx # 7. 獲取待處理信號位圖movl blocked(%eax),%ecx # 獲取阻塞信號掩碼notl %ecx # 反轉掩碼(允許通過的信號)andl %ebx,%ecx # 計算有效信號bsfl %ecx,%ecx # 掃描最低有效位je 3f # 無信號則跳過btrl %ecx,%ebx # 清除已處理信號位movl %ebx,signal(%eax) # 更新信號位圖incl %ecx # 信號編號轉為1-basedpushl %ecx # 參數壓棧call do_signal # 8. 調用信號處理函數popl %eax# ==== 恢復上下文 ==== # 3: popl %eax # 恢復系統調用返回值popl %ebx # 9. 逆向彈出寄存器popl %ecxpopl %edxpop %fspop %espop %dsiret # 10. 中斷返回用戶態# ====================== 錯誤處理路徑 ====================== # .align 2 bad_sys_call:movl $-1,%eax # 返回-1表示錯誤iret # 直接返回用戶態# ====================== 進程調度路徑 ====================== # .align 2 reschedule:pushl $ret_from_sys_call # 將返回地址壓棧jmp schedule # 跳轉到調度函數
這里的int 0x80是系統調用中斷是在哪設置呢?為什么int 0x80就跳到_system_call執行呢?
- 通常,異常中斷處理過程(int0 --int 31)都在 traps.c 的初始化函數中進行了重新設置(kernl/traps.c,181)
- 而系統調用中斷 int128 則在調度程序初始化函數中進行了重新設置(kernel/sched.c,385)。
// 調度程序的初始化子程序。 void sched_init (void) {// 設置時鐘中斷處理程序句柄(設置時鐘中斷門)。set_intr_gate (0x20, &timer_interrupt);// 修改中斷控制器屏蔽碼,允許時鐘中斷。outb (inb_p (0x21) & ~0x01, 0x21);// 設置系統調用中斷門。set_system_gate (0x80, &system_call); }
上面的中斷系統調用函數中
call [_sys_call_table + eax * 4]
,直接跳轉到sys_read()函數位置完成了系統調用。下面給出整個
read()->sys_read()
的簡易流程圖┌─────────────┐ ┌───────────────┐ │ 用戶態 │ │ 內核態 │ ├─────────────┤ ├───────────────┤ │ 1. read() │ │ │ └──────┬──────┘ │ │▼ │ │ ┌──────┴───────┐ │ │ │ 2. int 0x80 ├───中斷───? │3. system_call│ │ (NR=3 in EAX)│ │ - 檢查NR有效性 │ └──────────────┘ │ - 保存寄存器 │▼ │┌─────┴─────┐ ││4.查系統調用表 | │sys_call_table │ │[NR=3] → sys_read │└─────┬─────┘ ││ │▼ │┌─────┴─────┐ ││5.sys_read() │└─────┬─────┘ ││ │▼ │┌─────┴─────┐ ││ 返回用戶態 │ ││ (iret指令) │ │└───────────┘ │