linux系统调用的实现-流程分析-以printf为例

为什么用户程序不能直接访问内核态的内存?

  • 因为为了安全起见,强制规定用户程序只能通过系统调用来访问内核态的内存。
  • 区分内核和用户段:一种处理器“硬件设计”
  • DPL在GDT表中会有初始化
  • CPL:当前特权级,处于CS的低2位
  • DPL:目标特权级
  • DPL >= CPL

用户程序主动进入访问内核的方法

  • 通过使用中断"int"指令可以主动进入内核

系统调用核心

  • 用户程序中包含一段int指令的代码
  • 操作系统写中断处理,获取想调程序的编号
  • 操作系统根据编号执行代码

系统调用实例实现(以printf为例)

  • 前提知识点:
  • 1、系统有内核(用户)态、内核(用户)段之分。内核态可以访问任何数据,用户态不能访问内核数据。内核段包括所有pc指针中CPL段为0的内存段,用户段包括所有pc指针中CPL段为3的内存段。
  • 2、pc指针每次跳转的前都会判断当前系统状态下的CPL与DPL的大小,满足DPL大于等于CPL则跳转。
  • 3、以printf函数为例,在执行printf函数后,pc指针会有从用户段到内核段的过程,在进入内核段的时候会照常判断CPL(此时为3)与DPL(此时为0)的大小,这个时候就有一个问题了,既然能进入内核态,那什么时候满足DPL大于等于CPL呢?
  • 4、系统调用接口核心是①用户程序中包含一段int指令的代码;②操作系统写中断处理,获取想调程序的编号;③操作系统根据编号执行相应代码。

printf详细分析

printf执行流程:
1、调用printf;2、调用库函数printf;3、调用库函数write;4、系统调用write。
系统调用write进一步是这样一句宏定义

// linux-0.11/lib/write.c
#include 
_syscall3(int,write,int,fd,const char *,buf,off_t,count)
// linux-0.11/include/unistd.h
#define __NR_write	4
#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; \
}

上述宏定义的整体解释:
1、初始化输入寄存器eax(4),ebx(fd),ecx(buf),edx(count)
2、执行指令int 0x80,执行完成后并返回值到寄存器eax中
3、输出eax的值到__res中,并退出系统调用write函数

  • 从上述代码可以看出:在系统调用write中会调用int 0x80中断。系统如何执行int 0x80的指令的呢?
  • 在思考上个问题之前我们先看看有关系统调用的初始化过程,也就是下面列出代码的解释:

1、初始化输入部分:得到立即数0xee00,得到IDT表中下标为0x80对应的那块内存A,得到IDT表中下标为0x80再加偏移地址4对应的那块内存B,edx(&system_call),eax(0x00080000)
2、movw %%dx,%%ax\n\t – 此时ax = &system_call
3、movw %0,%%dx\n\t – 此时dx = 0xee00
4、movl %%eax,%1\n\t – 此时A = 0x80000 + &system_call
5、movl %%edx,%2 – 此时B = &system_call << 16 + 0xee00
上述的A和B两块内存区共8个字节构成一个idt表,代码目的初始化这个IDT表,将DPL设置为3,段描述符置为8,调用接口设置为system_call


// linux-0.11/init/main.c
void main(void)	
{
	sched_init();  // 系统初始化时会调用这个函数
}
// linux-0.11/kernel/sched.c
void sched_init(void)
{
	// 调度函数初始化时会设置系统门,设定
    // 系统调用的中断接口
	set_system_gate(0x80,&system_call);
}
// linux-0.11/include/asm/system.h
#define set_system_gate(n,addr) \
	_set_gate(&idt[n],15,3,addr)
#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))

  • 所以pc指针在从用户段到内核段的过程,在进入内核段的时候会照常判断CPL(此时为3)与DPL(此时为0)的大小,这个时候就有一个问题了,既然能进入内核态,那什么时候满足DPL大于等于CPL呢?就在进入系统调用接口中的中断指令执行初始化IDT表时设置DPL为3这个时候满足上述满足DPL大于等于CPL的条件的。
  • 随后由于段描述符(CS)为8,CPL此时为0.进入内核态,完成从用户态到内核态的流程。
  • 这个时候我就有一个问题了,system_call怎样执行调用write的?

system_call代码分析

// linux-0.11/kernel/system_call.s
nr_system_calls = 72
system_call:
	cmpl $nr_system_calls-1,%eax  #此时eax = __NR_write = 4
	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) #相当于往后偏移4*eax个字节
	pushl %eax
	movl current,%eax
	cmpl $0,state(%eax)		# state
	jne reschedule
	cmpl $0,counter(%eax)		# counter
	je reschedule
ret_from_sys_call:
	movl current,%eax		# task[0] cannot have signals
	cmpl task,%eax
	je 3f
	cmpw $0x0f,CS(%esp)		# was old code segment supervisor ?
	jne 3f
	cmpw $0x17,OLDSS(%esp)		# was stack segment = 0x17 ?
	jne 3f
	movl signal(%eax),%ebx
	movl blocked(%eax),%ecx
	notl %ecx
	andl %ebx,%ecx
	bsfl %ecx,%ecx
	je 3f
	btrl %ecx,%ebx
	movl %ebx,signal(%eax)
	incl %ecx
	pushl %ecx
	call do_signal
	popl %eax
3:	popl %eax
	popl %ebx
	popl %ecx
	popl %edx
	pop %fs
	pop %es
	pop %ds
	iret

// linux-0.11/include/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 };

可以看出sys_write正好处于系统调用表中下标为4的位置,所以上述执行int 0x80时确实调用了sys_write接口。
那sys_write又是怎样把字符写入到显示屏中去的呢?这个问题,我们日后再说。

你可能感兴趣的:(os操作系统)