實驗3 系統調用
提醒
這次實驗涉及的宏過于複雜,加上本人能力有限,我也沒有花大量時間去研究每一段代碼,隻是了解到每一段代碼做了什麼這一程度。
實驗目的
此次實驗的基本内容是:在 Linux 0.11 上添加兩個系統調用,并編寫兩個簡單的應用程式測試它們。
-
iam()
第一個系統調用是 iam(),其原型為:
完成的功能是将字元串參數
的内容拷貝到核心中儲存下來。要求name
的長度不能超過 23 個字元。傳回值是拷貝的字元數。如果name
的字元個數超過了 23,則傳回 “-1”,并置 errno 為 EINVAL。name
-
whoami()
第二個系統調用是 whoami(),其原型為:
它将核心中由
儲存的名字拷貝到 name 指向的使用者位址空間中,同時確定不會對iam()
越界訪存(name
的大小由name
說明)。傳回值是拷貝的字元數。如果size
小于需要的空間,則傳回“-1”,并置 errno 為 EINVAL。size
應用程式如何調用系統調用
在通常情況下,調用系統調用和調用一個普通的自定義函數在代碼上并沒有什麼差別,但調用後發生的事情有很大不同。
調用自定義函數是通過 call 指令直接跳轉到該函數的位址,繼續運作。
而調用系統調用,是調用系統庫中為該系統調用編寫的一個接口函數,叫 API(Application Programming Interface)。API 并不能完成系統調用的真正功能,它要做的是去調用真正的系統調用,過程是:
- 把系統調用的編号存入 EAX;
- 把函數參數存入其它通用寄存器;
- 觸發 0x80 号中斷(int 0x80)。
linux-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
中定義。
#define _syscall1(type,name,atype,a) \
type name(atype a) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name),"b" ((long)(a))); \
if (__res >= 0) \
return (type) __res; \
errno = -__res; \
return -1; \
}
将
_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。
*/
/*
而在應用程式中,要有:
*/
/* 有它,_syscall1 等才有效。詳見unistd.h */
#define __LIBRARY__
/* 有它,編譯器才能獲知自定義的系統調用的編号 */
#include "unistd.h"
/* iam()在使用者空間的接口函數 */
_syscall1(int, iam, const char*, name);
/* whoami()在使用者空間的接口函數 */
_syscall2(int, whoami,char*,name,unsigned int,size);
在 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
中,調用了
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
。
接下來看
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
! # push %ebx,%ecx,%edx,是傳遞給系統調用的參數
pushl %ebx
! # 讓ds, es指向GDT,核心位址空間
movl $0x10,%edx
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
修飾為其他函數可見。
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
,然後實作兩個函數就萬事大吉了。
按照上述邏輯修改相應檔案
通過上文描述,我們已經理清楚了要修改的地方在哪裡
- 添加iam和whoami系統調用編号的宏定義(_NR_xxxxxx),檔案:include/unistd.h
超詳細!作業系統實驗三 系統調用(哈工大李治軍) - 修改系統調用總數, 檔案:kernel/system_call.s
超詳細!作業系統實驗三 系統調用(哈工大李治軍) - 為新增的系統調用添加系統調用名并維護系統調用表,檔案:include/linux/sys.h
超詳細!作業系統實驗三 系統調用(哈工大李治軍) - 為新增的系統調用編寫代碼實作,在linux-0.11/kernel目錄下,建立一個檔案
who.c
#include <asm/segment.h> #include <errno.h> #include <string.h> char _myname[24]; int sys_iam(const char *name) { char str[25]; int i = 0; do { // get char from user input str[i] = get_fs_byte(name + i); } while (i <= 25 && str[i++] != '\0'); if (i > 24) { errno = EINVAL; i = -1; } else { // copy from user mode to kernel mode strcpy(_myname, str); } return i; } int sys_whoami(char *name, unsigned int size) { int length = strlen(_myname); printk("%s\n", _myname); if (size < length) { errno = EINVAL; length = -1; } else { int i = 0; for (i = 0; i < length; i++) { // copy from kernel mode to user mode put_fs_byte(_myname[i], name + i); } } return length; }
修改 Makefile
要想讓我們添加的
kernel/who.c
可以和其它 Linux 代碼編譯連結到一起,必須要修改 Makefile 檔案。
Makefile 裡記錄的是所有源程式檔案的編譯、連結規則,《注釋》3.6 節有簡略介紹。我們之是以簡單地運作 make 就可以編譯整個代碼樹,是因為 make 完全按照 Makefile 裡的訓示工作。
Makefile 在代碼樹中有很多,分别負責不同子產品的編譯工作。我們要修改的是
kernel/Makefile
。需要修改兩處。
(1)第一處
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
添加了
who.o
。
(2)第二處
### 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.h
添加了
who.s who.o: who.c ../include/linux/kernel.h ../include/unistd.h
。
Makefile 修改後,和往常一樣
make all
就能自動把
who.c
加入到核心中了。
編寫測試程式
到此為止,核心中需要修改的部分已經完成,接下來需要編寫測試程式來驗證新增的系統調用是否已經被編譯到linux-0.11核心可供調用。首先在oslab目錄下編寫iam.c,whoami.c
/* iam.c */
#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()*/
iam(argv[1]);
return 0;
}
/* whoami.c */
#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()*/
whoami(username, 24);
printf("%s\n", username);
return 0;
}
以上兩個檔案需要放到啟動後的linux-0.11作業系統上運作,驗證新增的系統調用是否有效,那如何才能将這兩個檔案從主控端轉到稍後虛拟機中啟動的linux-0.11作業系統上呢?這裡我們采用挂載方式實作主控端與虛拟機作業系統的檔案共享,在
oslab
目錄下執行以下指令挂載hdc目錄到虛拟機作業系統上。
sudo ./mount-hdc
再通過以下指令将上述兩個檔案拷貝到虛拟機linux-0.11作業系統/usr/root/目錄下,指令在oslab/目錄下執行:
cp iam.c whoami.c hdc/usr/root
如果目标目錄下存在對應的兩個檔案則可啟動虛拟機進行測試了。
- 編譯
[/usr/root]# gcc -o iam iam.c [/usr/root]# gcc -o whoami whoami.c
- 運作測試
[/usr/root]# ./iam wcf [/usr/root]# ./whoami
指令執行後,很可能會報以下錯誤:
這代表虛拟機作業系統中/usr/include/unistd.h檔案中沒有新增的系統調用調用号
為新增系統調用設定調用号
#define __NR_whoami 72
#define __NR_iam 73
再次執行:
實驗成功
- 為什麼這裡會列印2次?
- 因為在系統核心中執行了
函數,在使用者模式下又執行了一次printk()
函數。printf()
要知道到,printf() 是一個隻能在使用者模式下執行的函數,而系統調用是在核心模式中運作,是以 printf() 不可用,要用 printk()。
printk()
和
printf()
的接口和功能基本相同,隻是代碼上有一點點不同。printk() 需要特别處理一下
fs
寄存器,它是專用于使用者模式的段寄存器。
天道酬勤
實驗三總共花費7小時,看的不是特别仔細,沒有特别深入的學習宏展開和内聯彙編。但基本了解了系統調用的目的和方式,Linus永遠的神!