本文参考MOOC哈工大操作系统课程与课件
主要基于Linux 0.11系统展开
”Author:Mayiming“
如众多接口一样,接口的存在主要是为使用者提供一种方便易懂的途径使用一件东西(不必搞懂这件东西背后的实现原理),接口屏蔽了其背后的复杂性,提供了方便和简洁的使用方法。
操作系统中的接口体现的形式就是函数,函数的存在给使用者提供了方便的调用方法,屏蔽了函数内部实现的细节。
在linux中体现在命令行、shell、代码中执行的函数等,在windows中体现在界面按钮、各种应用程序。
这些内容的本质都是通过代码实现的,这些代码又是通过操作系统提供的接口来操作计算机硬件实现的具体功能。
例如:我们执行以下命令
hostname:~$ ./output "hello"
ECHO:hello
其中output.c代码为
#include
int main(int argc, char * argv[])
{ printf(“ECHO:%s\n”, argv[1]); }
output由output.c编译而来
gcc –o output output.c
命令在shell中执行,而shell依然是一段代码 /bin/sh
int main(int argc, char * argv[])
{ char cmd[20];
while(1) { scanf(“%s”, cmd); % 无限循环 shell永不退出
if(!fork()) {exec(cmd);} % 创建进程-执行命令 (这里的cmd就是output中的代码)
else {wait();} } //while(1)
图像界面的框架主要由上图所示,主要流程为:
POSIX: Portable Operating System Interface of Unix(IEEE制定的一个标准族)
POSIX 提供了一组标准的操作系统接口,使得软件可以在支持POSIX接口的操作系统之间移植。
系统调用(system call)其中system指的就是操作系统内核(前文【操作系统启动过程】中的system模块),call就如在汇编中call指令一样(跳转到call后面标签指向的程序地址进行执行)。
那么将上述两者结合起来,system call就是指一段代码(用户态)需要跳转到system模块中执行某一段代码(内核态)的过程,至于用户态和内核态到后面会解释。
下面红色代码代表在内存低地址(system 系统内核的地址段中),蓝色代码代表在用户程序所在的地址段中。
为什么main()不能直接调用whoami()?
答案是为了系统安全考虑,如果用户程序可以任意访问任意内存,那么你的计算机、操作系统、各种信息将全部暴露在你下载的应用程序面前,如果遇到恶意程序这是非常恐怖的。另一方面,是为了系统稳定性考虑,系统内核中有许多重要的代码和数据(IDT,GDT),如果遇到应用程序纂改也会导致系统崩溃。
如何实现内核态和用户态的隔离的?
答案是通过硬件设计实现的,也只能通过硬件设计实现。
具体是通过对寄存器的检查实现的,CPL代表CS寄存器的最低两位,RPL为DS寄存器的最低两位。
硬件提供了"主动进入内核的方法",如果无法进入内核执行代码则意味着应用程序无法调动计算机硬件,所以需要一种方法让应用程序安全的进入内核执行代码。
对于Intel x86,那就是中断指令int,int指令将使CS中的CPL从 3 改成 0 ,“进入内核”,还记得CPL = 0意味着内核态,此时代表内核已经打开了大门,DPL >= CPL检查通过即可JMP到系统内核段中执行代码。
系统调用的大致过程分为以下三步:
接下来,又出现几个疑问?
此时我们用C语言中的printf()函数作为例子,解析以下系统调用的过程。
1. 代码是最上层是C语言代码printf()
2. 接下来printf()函数内部由C语言函数库解释,其中调用了__syscall3()函数
3. __syscall3() 函数中给出INT的名字为write
4. __syscall3() 函数内部执行了一段内嵌汇编,执行了中断 INT 0x80
5. 执行了INT 0x80引发中断后,就来到了OS内核中,中断返回后会回到用户态
上述过程中,INT 0x80之外的代码都由C库函数解释(在用户态执行),只有INT 0x80中断过程中会在OS内核中执行。
下面对上述中断过程展开介绍:
在linux/include/unistd.h中
#define _syscall3(type,name,atype,a,btype,b,ctype,c)\ % syscall3代表3个参数
type name(atype a, btype b, ctype c) \
{ long __res;\ % 定义返回变量
__asm__ volatile(“int 0x80”:”=a”(__res):””(__NR_##name), % __NR_write 为输入,因为name参数为write ,__res为输出
”b”((long)(a)),”c”((long)(b)),“d”((long)(c)))); if(__res>=0) return % fd传给ebx, char *buf传给ecx, count 传给edx
(type)__res; errno=-__res; return -1;}
””(__NR_##name) 等价于代码 a”(__NR_##name) % 意思是将__NR_##name值赋给eax寄存器
eax寄存器放返回值(即__NR_write系统调用号),ebx/ecx/edx存放参数。
在linux/include/unistd.h中
#define __NR_write 4 //一堆连续正整数(数组下标,函数表索引)
回忆一下在前文【操作系统启动过程】中,head.s代码执行后main函数代码执行了一系列的初始化过程
void sched_init(void) % 初始化系统调用入口
{ set_system_gate(0x80,&system_call); } % 表示INT 0x80就要调用system_call来处理
下面这段代码的主要功能就是初始化上面这个IDT表,该表第一行为edx,第二行为eax,将中断处理函数的入口写入到IDT中。
在linux/include/asm/system.h中
#define set_system_gate(n, addr) \
_set_gate(&idt[n],15,3,addr); //idt是中断向量表基址,从该表中找到0x80偏移地址
#define _set_gate(gate_addr, type, dpl, addr)\ //这里dpl就是内核态那里的DPL,gate_addr就是上面那个表的地址,addr是&system_call
__asm__(“movw %%dx,%%ax\n\t” “movw %0,%%dx\n\t”\ // dx=ax dx=%0
“movl %%eax,%1\n\t” “movl %%edx,%2”:\ // %1 = eax, %2 = edx
:”i”((short)(0x8000+(dpl<<13)+type<<8))),“o”(*(( \ // "i"代表立即数,"o"代表内存变量
char*)(gate_addr))),”o”(*(4+(char*)(gate_addr))),\
“d”((char*)(addr),”a”(0x00080000))
// %1 为0x8000 + dpl左移13位 + type左移8位 = 0xEF00
// %1为gate_addr的低4位赋
// %2 为gate_addr的高四位
// 0x00080000赋给eax
// 将addr赋给edx
// movw位字传送16位,movl为字传送32位
// movl指令以寄存器作为目的时,它会把该寄存器的高4位字节设置为0
此时段选择符为0x0008,即CS=0x0008(CPL=0),回忆前文【操作系统启动过程】中 jmpi 0,8即为跳转到内核代码段,执行其中system_call
在linux/kernel/system_call.s中
nr_system_calls=72
.globl _system_call
_system_call: cmpl $nr_system_calls-1,%eax //eax中存放系统调用号
ja bad_sys_call
push %ds push %es push %fs
pushl %edx pushl %ecx pushl %ebx //调用的参数
movl $0x10,%edx mov %dx,%ds mov %dx,%es //内核数据 edx=0x10,ds=dx=0x10,es=dx=0x10 这个段对应内核数据段
movl $0x17,%edx mov %dx,%fs //fs可以找到用户数据
call _sys_call_table(,%eax,4) //a(,%eax,4)=a+4*eax
// 4代表每个_sys_call的函数地址占4字节
// _sys_call_table为系统调用表的基地址
pushl %eax //返回值压栈,留着ret_from_sys_call时用
... //其他代码
ret_from_sys_call: popl %eax, 其他pop, iret
// _sys_call_table+4*%eax就是相应系统调用处理函数入口
在include/linux/sys.h中
fn_ptr sys_call_table[]= // 函数指针表
{sys_setup, sys_exit, sys_fork, sys_read, sys_write,
...};
// sys_write对应的数组下标为4,__NR_write=4
call _sys_call_table(,%eax=4,4)就是call sys_write
call _sys_call_table(,%eax,4)
调用sys_write