实验3 系统调用
系統調用
實驗目的
- 建立對系統調用接口的深入認識
- 掌握系統調用的基本過程
- 能完成系統調用的全面控制
- 為后續實驗做準備
實驗內容
此次實驗的基本內容是:在Linux 0.11上添加兩個系統調用,并編寫兩個簡單的應用程序測試它們。
iam()
第一個系統調用是iam(),其原型為:
int iam(const char * name);
完成的功能是將字符串參數name的內容拷貝到內核中保存下來。要求name的長度不能超過23個字符。返回值是拷貝的字符數。如果name的字符個數超過了23,則返回“-1”,并置errno為EINVAL。
在kernal/who.c中實現此系統調用。
whoami()
第二個系統調用是whoami(),其原型為:
int whoami(char* name, unsigned int size);
它將內核中由iam()保存的名字拷貝到name指向的用戶地址空間中,同時確保不會對name越界訪存(name的大小由size說明)。返回值是拷貝的字符數。如果size小于需要的空間,則返回“-1”,并置errno為EINVAL。
也是在kernal/who.c中實現。
測試程序
運行添加過新系統調用的Linux 0.11,在其環境下編寫兩個測試程序iam.c和whoami.c。最終的運行結果是:
\$ ./iam lizhijun
\$ ./whoami
lizhijun
實驗報告
在實驗報告中回答如下問題:
- 從Linux 0.11現在的機制看,它的系統調用最多能傳遞幾個參數?你能想出辦法來擴大這個限制嗎?
- 用文字簡要描述向Linux 0.11添加一個系統調用foo()的步驟。
評分標準
- 將 testlab2.c(在/home/teacher目錄下) 在修改過的Linux 0.11上編譯運行,顯示的結果即內核程序的得分。滿分50%
- 只要至少一個新增的系統調用被成功調用,并且能和用戶空間交換參數,可得滿分
- 將腳本 testlab2.sh(在/home/teacher目錄下) 在修改過的Linux 0.11上運行,顯示的結果即應用程序的得分。滿分30%
- 實驗報告,20%
實驗提示
首先,請將Linux 0.11的源代碼恢復到原始狀態。
操作系統實現系統調用的基本過程(在MOOC課程中已經給出了詳細的講解)是:
- 應用程序調用庫函數(API);
- API將系統調用號存入EAX,然后通過中斷調用使系統進入內核態;
- 內核中的中斷處理函數根據系統調用號,調用對應的內核函數(系統調用);
- 系統調用完成相應功能,將返回值存入EAX,返回到中斷處理函數;
- 中斷處理函數返回到API中;
-
API將EAX返回給應用程序。
-
應用程序如何調用系統調用
在通常情況下,調用系統調用和調用一個普通的自定義函數在代碼上并沒有什么區別,但調用后發生的事情有很大不同。調用自定義函數是通過call指令直接跳轉到該函數的地址,繼續運行。而調用系統調用,是調用系統庫中為該系統調用編寫的一個接口函數,叫API(Application Programming Interface)。API并不能完成系統調用的真正功能,它要做的是去調用真正的系統調用,過程是:
- 把系統調用的編號存入EAX
- 把函數參數存入其它通用寄存器
- 觸發0x80號中斷(int 0x80)
0.11的lib目錄下有一些已經實現的API。Linus編寫它們的原因是在內核加載完畢后,會切換到用戶模式下,做一些初始化工作,然后啟動shell。而用戶模式下的很多工作需要依賴一些系統調用才能完成,因此在內核中實現了這些系統調用的API。我們不妨看看lib/close.c,研究一下close()的API:
#define __LIBRARY__
#include "unistd.h"
_syscall1(int,close,int,fd)
其中_syscall1是一個宏,在include/unistd.h中定義。將_syscall1(int,close,int,fd)進行宏展開,可以得到:
int close(int fd) { long __res; __asm__ volatile ("int $0x80" : "=a" (__res) : "0" (__NR_close),"b" ((long)(fd))); if (__res >= 0)return (int) __res; errno = -__res; return -1; }這就是API的定義。它先將宏NR_close存入EAX,將參數fd存入EBX,然后進行0x80中斷調用。調用返回后,從EAX取出返回值,存入res,再通過對res的判斷決定傳給API的調用者什么樣的返回值。其中NR_close就是系統調用的編號,在include/unistd.h中定義:
#define NR_close 6
所以添加系統調用時需要修改include/unistd.h文件,使其包含NR_whoami和__NR_iam。而在應用程序中,要有:
#define __LIBRARY__ / 有它,_syscall1等才有效。詳見unistd.h /
#include "unistd.h" / 有它,編譯器才能獲知自定義的系統調用的編號 /
_syscall1(int, iam, const char, name); / iam()在用戶空間的接口函數 /
_syscall2(int, whoami,char,name,unsigned int,size); / whoami()在用戶空間的接口函數 /
在0.11環境下編譯C程序,包含的頭文件都在/usr/include目錄下。該目錄下的unistd.h是標準頭文件(它和0.11源碼樹中的unistd.h并不是同一個文件,雖然內容可能相同),沒有NR_whoami和NR_iam兩個宏,需要手工加上它們,也可以直接從修改過的0.11源碼樹中拷貝新的unistd.h過來。
- 從“int 0x80”進入內核函數
int 0x80觸發后,接下來就是內核的中斷處理了。先了解一下0.11處理0x80號中斷的過程。
在內核初始化時,主函數(在init/main.c中,Linux實驗環境下是main(),Windows下因編譯器兼容性問題被換名為start())調用了sched_init()初始化函數:
void main(void) { ……time_init();sched_init();buffer_init(buffer_memory_end);…… }sched_init()在kernel/sched.c中定義為:
void sched_init(void) {……set_system_gate(0x80,&system_call); }set_system_gate是個宏,在include/asm/system.h中定義為:
\ _set_gate的定義是: \ __asm__ ("movw %%dx,%%ax\n\t" \"movw %0,%%dx\n\t" \"movl %%eax,%1\n\t" \"movl %%edx,%2" \: \: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \"o" (*((char *) (gate_addr))), \"o" (*(4+(char *) (gate_addr))), \"d" ((char *) (addr)),"a" (0x00080000))雖然看起來挺麻煩,但實際上很簡單,就是填寫IDT(中斷描述符表),將system_call函數地址寫到0x80對應的中斷描述符中,也就是在中斷0x80發生后,自動調用函數system_call。具體細節請參考《注釋》的第4章。
接下來看system_call。該函數純匯編打造,定義在kernel/system_call.s中:
…… nr_system_calls = 72 #這是系統調用總數。如果增刪了系統調用,必須做相應修改 …… .globl system_call .align 2 system_call:cmpl \$nr_system_calls-1,%eax #檢查系統調用編號是否在合法范圍內ja bad_sys_callpush %dspush %espush %fspushl %edxpushl %ecx pushl %ebx # push %ebx,%ecx,%edx,是傳遞給系統調用的參數movl $0x10,%edx # 讓ds,es指向GDT,內核地址空間mov %dx,%dsmov %dx,%esmovl $0x17,%edx # 讓fs指向LDT,用戶地址空間mov %dx,%fscall sys_call_table(,%eax,4)pushl %eaxmovl current,%eaxcmpl $0,state(%eax)jne reschedulecmpl $0,counter(%eax)je reschedulesystem_call用.globl修飾為其他函數可見。Windows實驗環境下會看到它有一個下劃線前綴,這是不同版本編譯器的特質決定的,沒有實質區別。call sys_call_table(,%eax,4)之前是一些壓棧保護,修改段選擇子為內核段,call sys_call_table(,%eax,4)之后是看看是否需要重新調度,這些都與本實驗沒有直接關系,此處只關心call sys_call_table(,%eax,4)這一句。根據匯編尋址方法它實際上是:
call sys_call_table + 4 * %eax # 其中eax中放的是系統調用號,即__NR_xxxxxx
顯然,sys_call_table一定是一個函數指針數組的起始地址,它定義在include/linux/sys.h中:
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,……
增加實驗要求的系統調用,需要在這個函數表中增加兩個函數引用——sys_iam和sys_whoami。當然該函數在sys_call_table數組中的位置必須和__NR_xxxxxx的值對應上。同時還要仿照此文件中前面各個系統調用的寫法,加上:
extern int sys_whoami();
extern int sys_iam();
不然,編譯會出錯的。
- 實現sys_iam()和sys_whoami()
添加系統調用的最后一步,是在內核中實現函數sys_iam()和sys_whoami()。
每個系統調用都有一個sys_xxxxxx()與之對應,它們都是我們學習和模仿的好對象。比如在fs/open.c中的sys_close(int fd):
int sys_close(unsigned int fd) {……return (0); }它沒有什么特別的,都是實實在在地做close()該做的事情。所以只要自己創建一個文件:kernel/who.c,然后實現兩個函數就萬事大吉了。
- 修改Makefile
要想讓我們添加的kernel/who.c可以和其它Linux代碼編譯鏈接到一起,必須要修改Makefile文件。Makefile里記錄的是所有源程序文件的編譯、鏈接規則,《注釋》3.6節有簡略介紹。我們之所以簡單地運行make就可以編譯整個代碼樹,是因為make完全按照Makefile里的指示工作。
Makefile在代碼樹中有很多,分別負責不同模塊的編譯工作。我們要修改的是kernel/Makefile。需要修改兩處。一處是:
OBJS = sched.o system_call.o traps.o asm.o fork.o \panic.o printk.o vsprintf.o sys.o exit.o \signal.o mktime.o 改為: OBJS = sched.o system_call.o traps.o asm.o fork.o \panic.o printk.o vsprintf.o sys.o exit.o \signal.o mktime.o who.o另一處:
\### Dependencies: exit.s exit.o: exit.c ../include/errno.h ../include/signal.h \../include/sys/types.h ../include/sys/wait.h ../include/linux/sched.h \../include/linux/head.h ../include/linux/fs.h ../include/linux/mm.h \../include/linux/kernel.h ../include/linux/tty.h ../include/termios.h \../include/asm/segment.h改為:
\### Dependencies: who.s who.o: who.c ../include/linux/kernel.h ../include/unistd.h exit.s exit.o: exit.c ../include/errno.h ../include/signal.h \../include/sys/types.h ../include/sys/wait.h ../include/linux/sched.h \../include/linux/head.h ../include/linux/fs.h ../include/linux/mm.h \../include/linux/kernel.h ../include/linux/tty.h ../include/termios.h \../include/asm/segment.hMakefile修改后,和往常一樣“make all”就能自動把who.c加入到內核中了。如果編譯時提示who.c有錯誤,就說明修改生效了。所以,有意或無意地制造一兩個錯誤也不完全是壞事,至少能證明Makefile是對的。
- 用printk()調試內核
oslab實驗環境提供了基于C語言和匯編語言的兩種調試手段。除此之外,適當地向屏幕輸出一些程序運行狀態的信息,也是一種很高效、便捷的調試方法,有時甚至是唯一的方法,被稱為“printf法”。
要知道到,printf()是一個只能在用戶模式下執行的函數,而系統調用是在內核模式中運行,所以printf()不可用,要用printk()。它和printf的接口和功能基本相同,只是代碼上有一點點不同。printk()需要特別處理一下fs寄存器,它是專用于用戶模式的段寄存器。看一看printk的代碼(在kernel/printk.c中)就知道了:
int printk(const char *fmt, ...) {……__asm__("push %%fs\n\t""push %%ds\n\t""pop %%fs\n\t""pushl %0\n\t""pushl $buf\n\t""pushl $0\n\t""call tty_write\n\t""addl $8,%%esp\n\t""popl %0\n\t""pop %%fs"::"r" (i):"ax","cx","dx");…… }顯然,printk()首先push %fs保存這個指向用戶段的寄存器,在最后pop %fs將其恢復,printk的核心仍然是調用tty_write()。查看printf()可以看到,它最終也要落實到這個函數上。
- 編寫測試程序
激動地運行一下由你親手修改過的“Linux 0.11 pro++”!然后編寫一個簡單的應用程序進行測試。比如在sys_iam()中向終端printk()一些信息,讓應用程序調用iam(),從結果可以看出系統調用是否被真的調用到了。
可以直接在Linux 0.11環境下用vi編寫(別忘了經常執行“sync”以確保內存緩沖區的數據寫入磁盤),也可以在Ubuntu或Windows下編完后再傳到Linux 0.11下。無論如何,最終都必須在Linux 0.11下編譯。編譯命令是:
# gcc -o iam iam.c -Wall
gcc的“-Wall”參數是給出所有的編譯警告信息,“-o”參數指定生成的執行文件名是iam,用下面命令運行它:
# ./iam
如果如愿輸出了你的信息,就說明你添加的系統調用生效了。否則,就還要繼續調試,祝你好運!
- 在用戶態和核心態之間傳遞數據
指針參數傳遞的是應用程序所在地址空間的邏輯地址,在內核中如果直接訪問這個地址,訪問到的是內核空間中的數據,不會是用戶空間的。所以這里還需要一點兒特殊工作,才能在內核中從用戶空間得到數據。
要實現的兩個系統調用參數中都有字符串指針,非常象open(char *filename, ……),所以我們看一下open()系統調用是如何處理的。
int open(const char * filename, int flag, ...) { ……__asm__("int $0x80":"=a" (res):"0" (__NR_open),"b" (filename),"c" (flag),"d" (va_arg(arg,int)));…… }可以看出,系統調用是用eax、ebx、ecx、edx寄存器來傳遞參數的。其中eax傳遞了系統調用號,而ebx、ecx、edx是用來傳遞函數的參數的,其中ebx對應第一個參數,ecx對應第二個參數,依此類推。如open所傳遞的文件名指針是由ebx傳遞的,也即進入內核后,通過ebx取出文件名字符串。open的ebx指向的數據在用戶空間,而當前執行的是內核空間的代碼,如何在用戶態和核心態之間傳遞數據?接下來我們繼續看看open的處理:
system_call: //所有的系統調用都從system_call開始……pushl %edxpushl %ecx pushl %ebx # push %ebx,%ecx,%edx,這是傳遞給系統調用的參數movl $0x10,%edx # 讓ds,es指向GDT,指向核心地址空間mov %dx,%dsmov %dx,%esmovl $0x17,%edx # 讓fs指向的是LDT,指向用戶地址空間mov %dx,%fscall sys_call_table(,%eax,4) # 即call sys_open由上面的代碼可以看出,獲取用戶地址空間(用戶數據段)中的數據依靠的就是段寄存器fs,下面該轉到sys_open執行了,在fs/open.c文件中:
int sys_open(const char * filename,int flag,int mode) //filename這些參數從哪里來? /*是否記得上面的pushl %edx, pushl %ecx, pushl %ebx?實際上一個C語言函數調用另一個C語言函數時,編譯時就是將要傳遞的參數壓入棧中(第一個參數最后壓,…),然后call …,所以匯編程序調用C函數時,需要自己編寫這些參數壓棧的代碼…*/ {……if ((i=open_namei(filename,flag,mode,&inode))<0) {……}…… }它將參數傳給了open_namei()。再沿著open_namei()繼續查找,文件名先后又被傳給dir_namei()、get_dir()。在get_dir()中可以看到:
static struct m_inode * get_dir(const char * pathname) {……if ((c=get_fs_byte(pathname))=='/') {……}…… }處理方法就很顯然了:用get_fs_byte()獲得一個字節的用戶空間中的數據。所以,在實現iam()時,調用get_fs_byte()即可。但如何實現whoami()呢?即如何實現從核心態拷貝數據到用心態內存空間中呢?猜一猜,是否有put_fs_byte()?有!看一看include/asm/segment.h:
extern inline unsigned char get_fs_byte(const char * addr) {unsigned register char _v;__asm__ ("movb %%fs:%1,%0":"=r" (_v):"m" (*addr));return _v; } extern inline void put_fs_byte(char val,char *addr) {__asm__ ("movb %0,%%fs:%1"::"r" (val),"m" (*addr)); }他倆以及所有put_fs_xxx()和get_fs_xxx()都是用戶空間和內核空間之間的橋梁,在后面的實驗中還要經常用到。
- 運行腳本程序
Linux的一大特色是可以編寫功能強大的shell腳本,提高工作效率。本實驗的部分評分工作由腳本 testlab2.sh 完成。它的功能是測試iam.c和whoami.c。
首先將iam.c和whoami.c分別編譯成iam和whoami,然后將 testlab2.sh 拷貝到同一目錄下。用下面命令為此腳本增加執行權限:
chmod +x testlab2.sh
然后運行之:
./testlab2.sh
根據輸出,可知iam.c和whoami.c的得分。
- errno
errno是一種傳統的錯誤代碼返回機制。當一個函數調用出錯時,通常會返回-1給調用者。但-1只能說明出錯,不能說明錯是什么。為解決此問題,全局變量errno登場了。錯誤值被存放到errno中,于是調用者就可以通過判斷errno來決定如何應對錯誤了。各種系統對errno的值的含義都有標準定義。Linux下用“man errno”可以看到這些定義。
====================實驗報告=======================
大致的實驗思路在李老師的實驗提示中已經十分詳細了,基本上一步步的做就可以了,個別要點要注意一下,具體實驗步驟如下:
1,模仿close的API,在~/oslab/linux-0.11/lib目錄下建立iam,whoami的API,代碼如下:
/* * linux/lib/iam.c * * (C) 2017 Fibonacci */ #define __LIBRARY__ #include <unistd.h> _syscall1(int, iam, const char*, name);/* * linux/lib/whoami.c* * (C) 2017 Fibonacci */#define __LIBRARY__ #include <unistd.h>_syscall2(int, whoami,char*,name,unsigned int,size);
說明:要注意的是實驗提示中的_syscall1,_syscall2的參數寫的有點問題,char *寫成了char;
2,修改~/oslab/linux-0.11/include下的unistd.h文件,修改的地方有兩處,具體位置如下:
#define __NR_ssetmask 69 #define __NR_setreuid 70 #define __NR_setregid 71/* added by Fibonacci as flowing two lines*/ #define __NR_iam 72 #define __NR_whoami 73 #define _syscall0(type,name) \ type name(void) \pid_t getpgrp(void); pid_t setsid(void);/* added by Fibonacci as flowing two lines*/ int iam(const char * name); int whoami(char* name, unsigned int size);#endif
3,修改~/oslab/linux-0.11/kernel/system_call.s,下面第61行: 59 sa_restorer = 12 60 61 nr_system_calls = 7462 63 /* 64 * Ok, I get parallel printer interrupts while using the floppy for some65 * stra
4,修改文件include/linux/sys.h,修改有兩個地方: extern int sys_setregid();/* added by Fibonacci as flowing two lines*/ extern int sys_iam(); extern int sys_whoami();fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read, sys_write, sys_ope
sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid, sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask, sys_setreuid,sys_setregid, sys_iam, sys_whoami};
5,在~/oslab/linux-0.11/kernel實現,即是在內核中實現函數sys_iam()和sys_whoami(),所以只要自己創建一個文件:kernel/who.c即可。 /*************************************************************************> File Name: who.c> Author: Fibonacci> Created Time: Sun 10 Dec 2017 08:47:01 PM CST************************************************************************/#define __LIBRARY__ #include <unistd.h> #include <errno.h> #include <asm/segment.h> #include <linux/kernel.h>char string_buf[64] = {0};int sys_iam(const char * name) {/* just for debug */printk("debug: iam is called successfully\n");int i = 0;char char_buf;while ((char_buf=get_fs_byte(name+i)) != '\0' && i < 63){string_buf[i] = char_buf;i++;}string_buf[i] = '\0';if (i > 23){errno = EINVAL;return -1;}else{return i;} }int sys_whoami(char* name, unsigned int size) {int i = 0;char char_buf;while (i < 23){char_buf = string_buf[i];put_fs_byte(char_buf, name+i);i++;}put_fs_byte('\0', name+i);if (string_buf[i] != '\0'){errno = EINVAL;return -1;}else{return i;} }
6,Makefile文件 OBJS = sched.o system_call.o traps.o asm.o fork.o \panic.o printk.o vsprintf.o sys.o exit.o \signal.o mktime.o who.o
### Dependencies: who.s who.o: who.c ../include/linux/kernel.h ../include/unistd.h exit.s exit.o: exit.c ../include/errno.h ../include/signal.h \../include/sys/types.h ../include/sys/wait.h ../include/linux/sched.h \../include/linux/head.h ../include/linux/fs.h ../include/linux/mm.h \../include/linux/kernel.h ../include/linux/tty.h ../include/termios.h
7,編寫測試程序,我把它放在了~/oslab/hdc/usr/root,具體代碼如下: /************************************************************************* > File Name: iam.c> Author: Fibonacci> Created Time: Sun 10 Dec 2017 03:23:06 PM CST************************************************************************/ #define __LIBRARY__ #include <unistd.h> #include <errno.h> #include <asm/segment.h> #include <linux/kernel.h> _syscall1(int, iam, const char*, name);int main(int argc, char *argv[]) {iam(argv[1]);return 0; }
/************************************************************************* > File Name: whoami.c> Author: Fibonacci> Created Time: Sun 10 Dec 2017 03:23:06 PM CST************************************************************************/ #define __LIBRARY__ #include <unistd.h> #include <errno.h> #include <asm/segment.h> #include <linux/kernel.h> #include <stdio.h>_syscall2(int, whoami,char *,name,unsigned int,size);int main(int argc, char *argv[]) {char username[64] = {0};whoami(username, 24);printf("%s\n", username);return 0; }
testlab2.sh#!/bin/bashgcc iam.c -o iam gcc whoami.c -o whoami ./iam Lizhijun ./whoami
8,程序運行結果:
9,實驗中要點思考
10,實驗體會
實驗做出來之前,感覺好難,雖然李老師已經提示的很詳細明白了,感覺還是無從下手,經過參考別人的思路,自己不斷的摸索,終于做出來了,再回頭看提示,感覺每一步都已經很明顯了,只是自己做的不對或者理解的不對。學習這個課程沒有它法,只有仔細聽課,閱讀實驗提示,結合網上的資料,尋找蛛絲馬跡,不斷嘗試。
總結
- 上一篇: ArcPy常用基础功能
- 下一篇: python如何自定义函数_python