哈工大 作業系統 lab2解答
實驗目的
- 建立對系統呼叫介面的深入認識
- 掌握系統呼叫的基本過程
- 能完成系統呼叫的全面控制
- 為後續實驗做準備
實驗內容
此次實驗的基本內容是:在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
實驗原理
作業系統實現系統呼叫的基本過程是:
- 應用程式呼叫庫函式(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中定義為:
#define set_system_gate(n,addr) \
_set_gate(&idt[n],15,3,addr)
_set_gate的定義是:
#define _set_gate(gate_addr,type,dpl,addr) \
__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_call
push %ds
push %es
push %fs
pushl %edx
pushl %ecx
pushl %ebx # push %ebx,%ecx,%edx,是傳遞給系統呼叫的引數
movl $0x10,%edx # 讓ds,es指向GDT,核心地址空間
mov %dx,%ds
mov %dx,%es
movl $0x17,%edx # 讓fs指向LDT,使用者地址空間
mov %dx,%fs
call sys_call_table(,%eax,4)
pushl %eax
movl current,%eax
cmpl $0,state(%eax)
jne reschedule
cmpl $0,counter(%eax)
je reschedule
system_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();
不然,編譯會出錯的。
實驗過程
新增系統呼叫的流程
新增一個系統呼叫的流程如下:
-
修改
include/unistd.h
, 新增#define __NR_foo num
,num為接下來使用的系統呼叫號 -
修改
include/linux/sys.h
, 新增extern rettype sys_foo();
, 在sys_call_table
陣列對應位置加入sys_foo
-
修改
kernel/system_call.s
,修改nr_system_calls = num
(num
為系統呼叫總數目) -
在
kernel
中新增foo.c
(若需要支援核心態與使用者態資料互動,則包含include/asm/segment.h
,其中有put_fs_XXX
和get_fs_XXX
函式) -
在
foo.c
實現系統呼叫sys_foo() -
修改
kernel
的Makefile,將foo.c
與核心其它程式碼編譯連結到一起 -
系統呼叫使用者需要使用
#define __LIBRARY__
#include <unistd.h>
_syscallN巨集展開系統呼叫,提供使用者態的系統呼叫介面(引數數目確定具體巨集)
新增whoami和iam兩個系統呼叫:
- 修改
include/unistd.h
, 新增#define __NR_foo num
,num為接下來使用的系統呼叫號
#define __NR_iam 72
#define __NR_whoami 73
- 修改
include/linux/sys.h
, 新增extern rettype sys_foo();
, 在sys_call_table
陣列對應位置加入sys_foo
extern int sys_iam();
extern int sys_whoami();
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,
sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,
sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,
sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
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};
- 修改
kernel/system_call.s
,修改nr_system_calls = num
(num
為系統呼叫總數目)
nr_system_calls = 74
- 在
kernel
中新增foo.c
(若需要支援核心態與使用者態資料互動,則包含include/asm/segment.h
,其中有put_fs_XXX
和get_fs_XXX
函式) - 在
foo.c
實現系統呼叫sys_foo()
who.c內容如下:
#include <string.h>
#include <errno.h>
#include <asm/segment.h>
char username[24];
int sys_iam(const char * name) {
char tmp[26];
short break_flag = 0, i = 0;
for (i = 0; i < 26; ++i) {
tmp[i] = get_fs_byte(name + i);
if (tmp[i] == '\0') {
break_flag = 1;
break;
}
}
if (!break_flag || i > 23) {
return -(EINVAL);
}
char* dest = username;
strcpy(dest, tmp);
return i;
}
int sys_whoami(char* name, unsigned int size) {
short length = strlen(username);
if (length > size) {
return -(EINVAL);
}
short i = 0;
for (i; i < size; ++i) {
put_fs_byte(username[i], name + i);
if (username[i] == '\0') {
break;
}
}
return i;
}
- 修改
kernel
的Makefile,將foo.c
與核心其它程式碼編譯連結到一起
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
- 使用者呼叫系統呼叫:(在執行的linux0.11上編寫編譯執行)
whoami.c:
#include <errno.h>
#define __LIBRARY__
#include <unistd.h>
_syscall2(int, whoami,char*,name,unsigned int,size);
int main()
{
char s[30];
whoami(s,30);
printf("%s",s);
return 0;
}
iam.c:
#include <errno.h>
#define __LIBRARY__
#include <unistd.h>
#include <stdio.h>
_syscall1(int, iam, const char*, name);
int main(int argc,char ** argv)
{
iam(argv[1]);
return 0;
}
如編譯錯誤,說__NR_whoami和__NR_iam未定義,則是下面的問題:
在0.11環境下編譯C程式,包含的標頭檔案都在/usr/include目錄下。該目錄下的unistd.h是標準標頭檔案(它和0.11原始碼樹中的unistd.h並不是同一個檔案,雖然內容可能相同),沒有__NR_whoami和__NR_iam兩個巨集,需要手工加上它們,也可以直接從修改過的0.11原始碼樹中拷貝新的unistd.h過來。
實驗結果
可以發現可以很好地執行!證明此次實驗是成功的!
reference
[1] 實驗指導書
[2] 現成程式碼