三、系統調用及實驗

保護機制:將內核程序與用戶隔離

特權級

處理器有硬件設計的保護機制,共4個特權級(0級到3級)。數值越大,權限越低。 可以用圓環表示爲:
在這裏插入圖片描述
爲了檢測特權級,有三種值:

  • 當前特權級 CPL(Current Privilege Level)。這是CPU當前執行程序或任務的特權級存放在 CS 和 SS 寄存器的最低2位。
  • 描述符特權級 DPL(或稱爲目標段特權級)(Descriptor Privilege Level) DPL 是一個段或門的特權級,存放在段或門描述符的DPL字段中(GDT表中)
  • 請求特權級 RPL(Request Privilege Level)RPL是一種賦予段選擇符的特權級,存放在選擇符的最低2位

特權級檢查

DPL >= CPL
DPL >= RPL
只有同時成立時,纔不會觸發 一般保護異常
在這裏插入圖片描述
在這裏插入圖片描述

系統調用

由操作系統實現提供的所有系統調用所構成的集合即程序接口或應用編程接口(API)。是應用程序同系統之間的接口。
系統調用,如同電源插頭一樣,方便了使用,隱藏內部細節。

進入內核的方法

這是用戶程序調用內核代碼的唯一方式:
用 int 指令中斷,將 CPL 改成 0

系統調用的核心

  • 用戶程序中包含 int 指令的代碼
  • 操作系統寫中斷處理,獲取調用程序的編號
  • 操作系統根據編號執行相應代碼

實現

在這裏插入圖片描述
write() 開始:

/*
 *  linux/lib/write.c
 *
 *  (C) 1991  Linus Torvalds
 */

#define __LIBRARY__
#include <unistd.h>

_syscall3(int,write,int,fd,const char *,buf,off_t,count)

這是一個宏,在unistd.h中有定義:

//...
#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; \
}
//...

觀察發現,其中的name參數,在宏中替換爲__NR_##name,所以當name = write時,被替換爲__NR_write

unistd.h文件中同樣可找到出處:

//...
#define __NR_setup	0	/* used only by init, to get system going */
#define __NR_exit	1
#define __NR_fork	2
#define __NR_read	3
#define __NR_write	4
#define __NR_open	5
#define __NR_close	6
//...

可以發現__NR_write是系統調用號,並保存在eax中。
同時eax也存放返回值,ebx,ecx,edx存放3個參數。

int 0x80中斷:

init / main.c 中,存在一個函數調用sched_init();
kernel / sched.c中有定義,並在最後調用set_system_gate(0x80,&system_call)

void sched_init(void)
{
	//以上略.........
	set_system_gate(0x80,&system_call);
}

又是一個宏,定義在system.h中:

#define set_system_gate(n,addr) \
	_set_gate(&idt[n],15,3,addr)

_set_gate也定義在該文件中,在處理IDT
在這裏插入圖片描述

#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))

綜上我們可以發現,set_system_gate(0x80,&system_call);這是用來設置0x80的中斷。
開始研究system_call,在文件kernel / system_call.s中,從文件名看出,這是純彙編程序。

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 as parameters
	pushl %ebx		# to the system call
	movl $0x10,%edx		# set up ds,es to kernel space
	mov %dx,%ds
	mov %dx,%es
	movl $0x17,%edx		# fs points to local data space
	mov %dx,%fs
	call sys_call_table(,%eax,4)
	pushl %eax
	movl current,%eax
	cmpl $0,state(%eax)		# state
	jne reschedule
	cmpl $0,counter(%eax)		# counter
	je reschedule

call sys_call_table(,%eax,4),這是比例變址尋址,地址值爲 sys_call_table +4*%eax
linux / sys.h中,發現函數表

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};

此時發現,原來__NR_write就是在這個數組中的下標。
call sys_call_table(,%eax,4),這個尋址是因爲地址長度4個字節,此時eax中存放的是__NR_write,所以基址sys_call_table+%eax * 4個字節 從而跳轉到 內核函數sys_write
最後,內核函數sys_writefs / read_write.c中。

實驗

實驗內容

此次實驗的基本內容是:在 Linux 0.11 上添加兩個系統調用,並編寫兩個簡單的應用程序測試它們。

(1)iam()
第一個系統調用是 iam(),其原型爲:

int iam(const char * name);

完成的功能是將字符串參數name的內容拷貝到內核中保存下來。要求name的長度不能超過 23 個字符。返回值是拷貝的字符數。如果name的字符個數超過了 23,則返回 “-1”,並置errnoEINVAL

kernal/who.c中實現此係統調用。

(2)whoami()
第二個系統調用是 whoami(),其原型爲:

int whoami(char* name, unsigned int size);

它將內核中由iam() 保存的名字拷貝到 name 指向的用戶地址空間中,同時確保不會對 name 越界訪存(name 的大小由 size 說明)。返回值是拷貝的字符數。如果 size 小於需要的空間,則返回“-1”,並置 errnoEINVAL

也是在 kernal/who.c 中實現。

實驗過程

要添加系統調用,思考前文對write()的探究,只需要在kernel /who.c中實現兩個sys_iamsys_whoami,並將兩個函數添加進函數表中即可。

值得注意的是,在實現的過程中,需要用戶態和內核態傳遞數據:
get_fs_byte() 獲得一個字節的用戶空間中的數據。
put_fs_byte()可以將一個字節的數據拷貝到用戶空間。

kernel / who.c
#include <errno.h>
#include <fcntl.h>
#include <sys/types.h>
#include <utime.h>
#include <sys/stat.h>
#include <string.h>
#include <linux/sched.h>
#include <linux/tty.h>
#include <linux/kernel.h>
#include <asm/segment.h>

char str[24]={'\0'};

int sys_iam(const char * name){
    int i=0;
    char c=NULL;
    while (i < 23){
        c = get_fs_byte(name+i);
        str[i] = c;
        if(c =='\0'){
            break;
        }
        i++;
    }
    if(i== 23 && get_fs_byte(name+i)!='\0'){
        errno = EINVAL;
        return -1;
    }
    return i;
}

int sys_whoami(char* name, unsigned int size){
    int i=0;
    while(i < size){
        put_fs_byte(str[i],name+i);
        if(str[i] == '\0'){
            break;
        }
        i++;
    }
    if(str[i] !='\0'){
        errno = EINVAL;
        return -1;
    }
    return i;
}

include / unistd.h

在這裏將系統調用號添加進去

#define __NR_whoami  72
#define __NR_iam  73
include / linux / sys.h

按照文件中的格式,添加 extern 聲明:

extern int sys_whoami();
extern int sys_iam();

向數組中添加兩個函數:

fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,sys_write, sys_open, sys_close, .........
,sys_whoami,sys_iam };
更改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)第二處

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
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

用 testlab2.c 測試內核

在Bochs中可以進系統用 vi 修改或直接掛載磁盤修改/usr / include / unitstd.h

掛載磁盤將testlab2.c拷貝進去並編譯運行。
存在個問題:errno在who.c中已經設爲EINVAL,但在測試中errno爲1,還沒找到問題原因。
在這裏插入圖片描述

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