linux 0.11系统调用原理以及相关机制(1)

203 阅读2分钟

系统调用整体机制

由用户态触发0x80号中断,参数传递使用寄存器来实现,其中eax存系统调用号,ebx、ecx、edx存系统调用函数的参数,最多三个参数。大于三个参数可以通过结构体指针来实现。

内核态与用户态数据交换

在系统调用过程中,段寄存器ds、es指向内核数据空间,fs指向用户数据空间,因此在内核态中可以利用fs寄存器来执行内核数据空间与用户数据空间之间的数据复制,数据复制过程中的边界检查由CPU自动完成。

linux内核封装了get_fs_byte()和put_fs_byte()函数来进行内核态和用户态之间的数据交换,函数定义在include/asm/segment.h

system_call:
        # eax存放的是系统调用号,如果设置的系统调用号大于当前的系统调用数,抛异常
	cmpl $nr_system_calls-1,%eax
	ja bad_sys_call
        
        # 保存原有的段寄存的值
	push %ds
	push %es
	push %fs
        
        # ebx、ecx、edx分别存储参数,最多可传三个参数,如果参数很多,可以通过指针的形式来传递
	pushl %edx
	pushl %ecx		# push %ebx,%ecx,%edx as parameters
	pushl %ebx		# to the system call
        
        # ds、es寄存器指向内核空间
	movl $0x10,%edx		# set up ds,es to kernel space
	mov %dx,%ds
	mov %dx,%es

        # fs指向用户空间
	movl $0x17,%edx		# fs points to local data space
	mov %dx,%fs

        # 参数都准备好了,调用系统调用表中的函数
	call sys_call_table(,%eax,4)
	pushl %ax
	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

以write系统调用为例,说明如何通过封装的函数来调用系统调用,以及系统调用的流程是怎样的

lib/write.c中代码 // write函数的实现,使用宏实现的 _syscall3(int,write,int,fd,const char *,buf,off_t,count)

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

// write函数的声明 int write(int fildes, const char * buf, off_t count);

上面涉及到的代码的含义

宏展开,相当于

int write(int fd, const char * buf, off_t, count) {
long __res;
asm volatile ("int $0x80"
: "=a" (__res)
: "0" (__NR_write),”b” ((long)(fd)),”c” ((long)(buf)),"d" ((long)(count)));
if (__res>=0)
return (int) __res;
errno=-__res;
return -1;
}

C嵌入汇编语句的基本格式为: asm(“汇编语句” : 输出寄存器 : 输入寄存器 : 会被修改的寄存器)

asm也可以写成__asm__

上面这个write代码的流程就是:

  1. 定义一个变量__res;
  2. 嵌入汇编代码,volatile表示不希望汇编语句被gcc优化修改,汇编语句int $0x80含义为触发int 0x80中断;“=a”代表输出寄存器为eax(a代表eax,b、c、d分别代表ebx、ecx、edx),=表示这是输出寄存器;输入寄存器中,“0”代表使用与上面相同位置上的输出寄存器,因此传参中,eax是 4(write的系统调用号),ebx、ecx、edx分别代表fd、buf、count。
  3. 如果返回值__res(eax)中的值大于0,返回__res,否则把返回值变为负数,存到errno中,然后返回-1。

进入系统调用

触发int 0x80中断后,进入上面system_call.s中代码,找到在include/linux/sys.h中定义的sys_call_table,根据系统调用号4,找到数组index为4的函数sys_write,调用sys_write函数,然后返回返回值。 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 };