分析OS系统调用
Posted zsben991126
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了分析OS系统调用相关的知识,希望对你有一定的参考价值。
分析OS系统调用
一些基本概念
系统调用概念
系统库中为系统调用编写了许多接口函数(API),不同的API对应了不同的真正的(OS内核中)系统调用
从实模式到保护模式
x86系统在刚开机时处于实模式,即cs:ip
的寻址方式为cs
左移四位+ip
,寻址能力只有20位,当bootsect.s
和setup.s
完成工作,将整个os
模块读入内存中,并初始化完成后,就会进入保护模式,保护模式下,cs
寄存器也称为段选择子,ip
寄存器表示cs
选择段中的偏移量,此时寻址能力有32位。因此,在内核加载完毕,切换到用户模式下时,会做一些初始化工作(如main.c
里一大堆_init()
函数),最后一步启动shell,用户在shell中可以进行系统调度
os的特权级
为了保护一些重要信息,防止被随意修改,os将内核程序和用户程序进行隔离,区分出内核态和用户态。同时用CS寄存器的最低两位来表示当前程序运行在哪一态(CPL:当前进程的权限级别)
CPL=0: 内核态
CPL=3:用户态
运行在某一态的程序要访问另一态的数据,而目标数据所在段特权级为DPL(规定了访问该段的权限级别)
,当DPL>=CPL
时,可以直接访问目标数据,反之不能访问
简单的说,cs寄存器中存储了当前进程的特权级CPL,当前进程要访问某内存段时,对应的内存段会有一个DPL特权级,CPL>=DPL,才能访问该内存段
ps:其实还有RPL
,是进程对段访问的请求权限,比如一个进程的CPL=0
,但是它设段选择子的RPL=3,
那么它就只能访问DPL=3
的段了
中断指令int
用户程序发起调用内核代码的唯一方式是使用int
指令,而系统调用为 int 0x80
,用户程序进行系统调用的流程为
1.用户程序中包含一段包含int代码指令的代码:比如printf()最终展开后为包含int指令的代码
2.os写中断处理,获取想调程序的编号
3.os根据编号执行相应代码
系统调用的实现细节
用户程序触发int 0x80
过程
我们以一个系统调用int close(int fd)
的API为例
在 include/unistd.h
中定义有一个宏_syscall1
,用来展开只有一个参数的系统调用
#define __LIBRARY__
#include <unistd.h>
#define _syscall1(type,name,atype,a)
上面的宏定义中,type
表示返回值类型,name
表示函数名,atype
表示第一个参数类型,a
表示第一个参数名,因此int clode(int fd)
可以被表示成下面这样
_syscall1(int, close, int, fd)
该宏展开后会变成下面这样
int close(int fd)
{
long __res;
__asm__ volatile ("int $0x80" //调用 int 0x80 中断
: "=a" (__res) //从EAX里取出值,存入__res
//将宏 __NR_close 存入 EAX,将参数 fd 存入 EBX
: "0" (__NR_close),"b" ((long)(fd)));
if (__res >= 0) //判断应该返回什么值
return (int) __res;
errno = -__res;
return -1;
}
上面代码的执行过程为
- 先将宏
__NR_close
存入 EAX,将参数fd
存入EBX
- 调用
int 0x80
- 调用返回后,从
EAX
取出返回值,存入__res
- 通过对
__res
的判断决定传给API
的调用者什么样的返回值
其中,__NR_close
就是系统调用close
的对应的编号,该宏在include/unistd.h
中定义(__NR_name
其实就是系统调用name的对应的编号)
通过中断进入内核
OS内核初始化时(init/main.c
函数进行初始化),调用了一个sched_init()
函数
void main(void)
{
// ……
time_init();
sched_init(); //就是这个
buffer_init(buffer_memory_end);
// ……
}
sched_init()
在kernel/sched.c
中定义。作用是将各种编号的中断和中断处理程序的入口地址进行绑定,而系统调用0x80
中断则和system_call
函数进行绑定,因此我们将system_call
称为系统调用处理程序
void sched_init(void)
{
// ……
set_system_gate(0x80,&system_call);
}
现在我们已经得到了int 0x80
的处理程序,可是仍有一个问题,用户程序无法直接访问内核态的程序!而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)
这里n
就是那个0x80
,addr
可对应为处理程序system_cal
l的地址,n
实际上是中断描述符表idt
的偏移量
这里又涉及到另一个函数_set_gate()
,其也是一个宏,展开后如下
#define _set_gate(gate_addr,type,dpl,addr) __asm__ ("movw %%dx,%%ax
" "movw %0,%%dx
" "movl %%eax,%1
" "movl %%edx,%2" : : "i" ((short) (0x8000+(dpl<<13)+(type<<8))), "o" (*((char *) (gate_addr))), "o" (*(4+(char *) (gate_addr))), "d" ((char *) (addr)),"a" (0x00080000))
上面这一段(看不懂没关系..),它做的事情就是将DPL
改为了3
,我们知道内核态的DPL
为0
,此处改为3
是为了让DPL=CPL
,所以用户程序就有了从用户态进入内核态的机会
处理中断
现在我们已经成功进入内核啦!下面要看的就是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 + 4 * %eax
,其中eax
是系统调用号,
sys_call_table
是一个指针数组,每个指针指向一个系统调用函数,其定义在 include/linux/sys.h
中
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,...
所以4*%eax
的意思其实是eax
对应的函数指针在表中的地址,因为一个指针占四位..
至此已经找到了内核中系统调用函数的那个指针,所以直接调用该指针对应的函数就可以啦!
用户态和内核态之间传递数据
至此我们已经了解了大部分过程,但是还有最后一个值得注意的地方,那就是用户态和内核态如何传递数据?我们知道用户态和内核态的寻址方式是不同的,并且指针参数传递的是应用程序所在地址空间的逻辑地址,在内核中如果直接访问这个地址,访问到的是内核空间中的数据,不会是用户空间的。所以这里还需要一点儿特殊工作,才能在内核中从用户空间得到数据
我们拿open(char *filename)来举例
int open(const char * filename, int flag, ...)
{
// ……
__asm__("int $0x80"
:"=a" (res)
:"0" (__NR_open),"b" (filename),"c" (flag),
"d" (va_arg(arg,int)));
// ……
}
同上面close
一样,eax
里存了open
的调用号,ebx
里存了filename
字符串指针,即指向了用户态的一串字符串,ecx
存了flag
...
现在的问题是,内核态要找ebx
里的地址对应的字符串时,是在内核态的地址里找的,而ebx
里的地址对应的是用户态的
所以要进行一些转换,我们再来看system_call
函数里:
system_call: //所有的系统调用都从system_call开始
! ……
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) # 即call sys_open
可见,在call sys_open
前,system
对一些寄存器做了处理,在此之前,简单说一下什么是gdt
和ldt
gdt
和ldt
保护模式下,虽然段值仍然由原来的cs
、ds
等寄存器表示,但此时它仅仅变成了一个索引,这个索引指向了一个数据结构的一个表项,表项中详细定义了段的起始地址、界限、属性等内容。这个数据结构就是全局描述符gdt
,gdt
中每个描述符都占64位/8字节(可以发现DPL存在段描述符里)
此外,内存中有一个概念称为段(os内存管理中会提到),段的信息存在在段描述符中,ldt
和gdt
都是段描述符表为了存放这些描述符,需要在内存中开辟出一段空间。在这段空间里,所有的描述符都是挨在一起,集中存放的,这就构成一个描述符表。描述符表的长度可变,最多可以包含8K个这样的描述符(为什么呢?因为段选择子是16位的,其中的13bit用来作index),其结构如下
? 查找GDT
在线性地址中的基地址,需要借助GDTR
;而查找LDT
相应基地址,需要的是GDT
中的段描述符。访问LDT
需要使用段选择符,为了减少访问LDT
时候的段转换次数,LDT
的段选择符,段基址,段限长都要放在LDTR
寄存器之中。
然后我们重点来看ds
和 fs
寄存器
? 首先ds
指向的是gdt(全局描述符)
,fs
指向的是ldt
,而ldt
对应的就是用户态的地址空间,此时我们可以通过fs
获取用户地址数据啦
总之,依靠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) {
……
}
……
}
sys_open
将最重要的功能交给open_namei()
,事实上,open_namei()
再交给dir_namei()
,get_dir()
,最后get_dir()
中调用了一个get_fs_byte()
static struct m_inode * get_dir(const char * pathname)
{
……
if ((c=get_fs_byte(pathname))==‘/‘) {
……
}
……
}
显然,get_fs_byte(pathname)
获得了一个用户空间的fs寄存器指向的字节数据,那么同理,也有put_fs_byte
函数向fs
指向的用户空间输出函数,这两个函数被定义在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()
都是用户空间和内核空间之间的桥梁
以上是关于分析OS系统调用的主要内容,如果未能解决你的问题,请参考以下文章
20135201李辰希《Linux内核分析》第三次 构造一个简单的Linux系统OS