基于Linux0.11内核分析:系统调用

前沿

1、与内核通信
在现代操作系统中,内核提供了用户进程与内核进行交互的一组接口.系统调用在用户空间和硬件设备之间添加了一个中间层。
该层主要作用:
一为用户空间提供了一种硬件的抽象接口
一系统调用保证了系统的稳定和安全
一出于进程都运行在虚拟系统中,所以在用户空间和系统的其余部分提供这样一层接口
在linux中,系统调用是用户空间访问内核的唯一手段,除异常和陷入外,它们是内核惟一的合法入口。
2、 API、POSIX、C库
一般情况下,应用程序通过在用户空间实现的应用编程接口(API)而不是直接调用系统调用来编程。一个API可以实现成一个系统调用,也可以通过调用多个系统调用来实现,也可以不使用任何系统调用。API可以在各种不同的OS上实现,虽然各个系统系统调用可能迥异,但是可以通过不同的实现来统一应用程序接口。
POSIX表示可移植操作系统接口,POSIX标准定义了操作系统应该为应用程序提供的接口标准,并期望获得源码级的软件可移植性,例如:各个系统的系统调用接口实现都有差异,在linux和windows下创建进程的系统调用分别为fork(),creatprocess()函数,如果把linux下的fork程序移植在windows上,就得把fork替换为creatprocess,现在linux和windows都遵守POSIX标准对系统调用进行封装,封装成统一的函数接口,以实现源码级的移植。
C库实现了Unix系统的主要API,包括标准C库函数和系统调用接口,所有的C程序都可以使用C库,而由于C语言本身的特点,其它语言也可以很方便的把它们封装使用,从程序员的角度来看只需要跟API打交道就可以,内核只跟系统调用打交道。
对接口而言,“提供机制而不是策略”:
“需要提供什么功能”(机制),“怎样实现这些功能”(策略)
linux0.11

系统调用接口

由操作系统实现提供的所有系统调用所构成的集合即程序接口或应用编程接口(Application Programming Interface,API),是应用程序同系统之间的接口

1、什么是系统调用
指运行在用户空间的程序向系统内核请求更高运行权限的服务。操作系统中的状态分为管态(核心态)和目态(用户态)。
举例说明:

	----当应用层的开发者需要在显示器上显示信息,就需要操作显示器,显示器模块的硬件电路设计已由其生产厂家制作好,并且留有通信操作接口
	----显示器与电脑主板连接,显示器的芯片模块和电脑主板间就可以进行通信
	----比如写几个代码,一个用来向显示器发送调节亮度的控制信息,一个用来向显示器发送显示信息的控制信息,再一个向显示器发送显示位置信息的控制信息
	----接着这三个代码就能通过电脑控制显示器的部分使用功能
	----然后把这三个代码整合一下,注册到内核,就成为内核中的驱动程序
	----最后把这驱动程序封装为系统调用接口,为安装此操作系统的开发人员直接使用,隐藏了硬件的信息,只留下函数调用的参数接口

最后这些各种各样的硬件资源操作,都被封装起来,组成庞大的内核函数库,用户层的调用就称为系统调用。

API和系统调用的关系:
API(应用程序编程接口)和系统调用:应用编程接口和系统调用是不同的:

  1. API只是一个函数定义
  2. 系统调用通过软中断向内核发出了一个明确的请求
    Libc库定义的一些API引用了封装例成,唯一目的就是发布系统调用:
    1. 一般每个系统调用对应一个封装例程
    2. 库函数再用这些封装例程定义出给用户的API
    API可能直接提供用户态的服务 如:一些数学函数
    1. 一个单独的API可能调用几个系统调用
    2. 不同的API可能调用了同一个系统调用返回:大部分封装例程返回一个整数,其值的含义依赖于相应的系统调用
    3. -1在多数情况下表示内核不能满足进程的请求,Libc中定义的errno变量包含特定的出错码;

系统调用(通常称为syscalls)是linux内核与上层应用程序进行交互通信的唯一接口,用户可以直接或间接(通过库函数)调用中断 int 0x80 ,并在寄存器中指定系统调用功能号。

在这里插入图片描述

#include <stdio.h>
void main(void)
{
	printf("Hello World!\n");
}

这是平时写程序的 printf 库函数调用,通过一系列转换进入内核调用内核函数来使用资源:
在这里插入图片描述
2、系统调用处理过程
当应用程序经过库函数向内核发出一个中断调用 int 0x80 时,就开始执行一个系统调用,其中寄存器eax 中存放系统调用号,参数一次存放在 ebx、ecx、edx中。linux 0.11最多传递3个参数。
如果我们在用户程序中直接使用系统调用,那么该系统调用的宏形式为:

如图:
系统调用的简单流程,发生中断后,判断中断类型,如系统调用号linux为0x80,
接着通过寄存器传入中断号等一些参数给内核,cpu的执行也从用户态跳到内核态,
内核中使用的内核栈,用来返回用户态位置和保存用户态的中断现场,
接着通过中断号,去system_call数组中寻找相应的中断入口地址

在这里插入图片描述

unistd.h代码:

//部分索引值
// 以下是内核实现的系统调用符号常数,用于作为系统调用函数表中的索引值。( include/linux/sys.h )
#define __NR_setup 0		/* used only by init, to get system going */
/* __NR_setup 仅用于初始化,以启动系统 */
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
#define __NR_open 5
#define __NR_close 6
#define __NR_waitpid 7
#define __NR_creat 8
#define __NR_link 9
#define __NR_unlink 10
#define __NR_execve 11
#define __NR_chdir 12
#define __NR_time 13
#define __NR_mknod 14
#define __NR_chmod 15
#define __NR_chown 16
#define __NR_break 17
#define __NR_stat 18

write.c代码:

#include <set_seg.h>

#define __LIBRARY__
// Linux 标准头文件。定义了各种符号常数和类型,并申明了各种函数。
// 如定义了__LIBRARY__,则还包括系统调用号和内嵌汇编_syscall0()等。
#include <unistd.h>

//// 写文件系统调用函数。
// 该宏结构对应于函数:int write(int fd, const char * buf, off_t count)
// 参数:fd - 文件描述符;buf - 写缓冲区指针;count - 写字节数。
// 返回:成功时返回写入的字节数(0 表示写入0 字节);出错时将返回-1,并且设置了出错号。
_syscall3(int,write,int,fd,const char *,buf,off_t,count)

unistd.h代码:

   //这是系统调用内嵌汇编宏函数
    //0个参数的宏函数
    #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(宏定义的值)
    //接着把该值赋给“0”,也就是“=a”,也就是eax,再把参数a、b、c依此放入ebx、ecx、edx寄存器
    //接着把“=a”(eax)的值赋给__res变量
    //如果该变量正确,返回该值
    //否则置错误变量errno为该值的相反数,返回-1
    //int $0x80,系统调用中断

在这里插入图片描述
执行0x80中断操作部分代码:

;//// int 0x80 --linux 系统调用入口点(调用中断int 0x80,eax 中是调用号)。
align 4
_system_call:
	cmp eax,nr_system_calls-1 ;// 调用号如果超出范围的话就在eax 中置-1 并退出。
	ja bad_sys_call
	push ds ;// 保存原段寄存器值。
	push es
	push fs
	push edx ;// ebx,ecx,edx 中放着系统调用相应的C 语言函数的调用参数。
	push ecx ;// push %ebx,%ecx,%edx as parameters
	push ebx ;// to the system call
	mov edx,10h ;// set up ds,es to kernel space
	mov ds,dx ;// ds,es 指向内核数据段(全局描述符表中数据段描述符)。
	mov es,dx
	mov edx,17h ;// fs points to local data space
	mov fs,dx ;// fs 指向局部数据段(局部描述符表中数据段描述符)。
;// 下面这句操作数的含义是:调用地址 = _sys_call_table + %eax * 4。参见列表后的说明。
;// 对应的C 程序中的sys_call_table 在include/linux/sys.h 中,其中定义了一个包括72 个
;// 系统调用C 处理函数的地址数组表。
	call [_sys_call_table+eax*4]
	push eax ;// 把系统调用号入栈。
	mov eax,_current ;// 取当前任务(进程)数据结构地址??eax。

头文件部分代码

//在另一文件内,这是部分外部函数声明
extern int sys_setup ();	// 系统启动初始化设置函数。
extern int sys_exit ();		// 程序退出。
extern int sys_fork ();		// 创建进程。
extern int sys_read ();		// 读文件。
extern int sys_write ();	// 写文件。
extern int sys_open ();		// 打开文件。
extern int sys_close ();	// 关闭文件。
extern int sys_waitpid ();	// 等待进程终止。
extern int sys_creat ();	// 创建文件。
extern int sys_link ();		// 创建一个文件的硬连接。
extern int sys_unlink ();	// 删除一个文件名(或删除文件)。
extern int sys_execve ();	// 执行程序。
extern int sys_chdir ();	// 更改当前目录。
extern int sys_time ();		// 取当前时间。 
// 系统调用函数指针表。用于系统调用中断处理程序(int 0x80),作为跳转表。
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
};

在这里插入图片描述

printf函数简单追踪:

1、用户使用printf 函数在libc 中调用内核 write 函数
2、write 函数使用_syscall3(int,write,int,fd,const char *,buf,off_t,count)
3、展开后,把调用号等参数加载进对应寄存器,执行0x80 中断号,进入内核
4、在 sys_call_table[]数组中找到对应的sys_write 函数入口地址
5、进入sys_wite 函数执行

第一步:

#include <stdio.h>
printf(“Hello World!\n”);

stdio.h中:

extern int printf (const char *__restrict __format, …);外部定义了printf函数

printf.c中:

int
__printf (const char *format, …)
{
va_list arg;
int done;
va_start (arg, format);
done = __vfprintf_internal (stdout, format, arg, 0);
va_end (arg);
return done;
}

继续追踪,发现调用 putc 函数,在继续追踪找到 write 函数。
最终发现printf 调用 write 系统调用函数

第二步:
write.c中

#include <set_seg.h>
#define __LIBRARY__
#include <unistd.h>
_syscall3(int,write,int,fd,const char *,buf,off_t,count)

第三步:

#define __NR_write 4

展开syscall3 宏:

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

进入内核切换内核堆栈:

;//// int 0x80 --linux 系统调用入口点(调用中断int 0x80,eax 中是调用号)。
align 4
_system_call:
	cmp eax,nr_system_calls-1 ;// 调用号如果超出范围的话就在eax 中置-1 并退出。
	ja bad_sys_call
	push ds ;// 保存原段寄存器值。
	push es
	push fs
	push edx ;// ebx,ecx,edx 中放着系统调用相应的C 语言函数的调用参数。
	push ecx ;// push %ebx,%ecx,%edx as parameters
	push ebx ;// to the system call
	mov edx,10h ;// set up ds,es to kernel space
	mov ds,dx ;// ds,es 指向内核数据段(全局描述符表中数据段描述符)。
	mov es,dx
	mov edx,17h ;// fs points to local data space
	mov fs,dx ;// fs 指向局部数据段(局部描述符表中数据段描述符)。
;// 下面这句操作数的含义是:调用地址 = _sys_call_table + %eax * 4。参见列表后的说明。
;// 对应的C 程序中的sys_call_table 在include/linux/sys.h 中,其中定义了一个包括72 个
;// 系统调用C 处理函数的地址数组表。
	call [_sys_call_table+eax*4]
	push eax ;// 把系统调用号入栈。
	mov eax,_current ;// 取当前任务(进程)数据结构地址??eax。

第四步:

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_call_table[4] = sys_write;

extern int sys_write ();	// 写文件。

第五步:

kernel模块代码
kernel /fs /read_write.c 部分代码:

int sys_read (unsigned int fd, char *buf, int count)
{
	struct file *file;
	struct m_inode *inode;

// 如果文件句柄值大于程序最多打开文件数NR_OPEN,或者需要读取的字节计数值小于0,或者该句柄
// 的文件结构指针为空,则返回出错码并退出。
	if (fd >= NR_OPEN || count < 0 || !(file = current->filp[fd]))
		return -EINVAL;
// 若需读取的字节数count 等于0,则返回0,退出
	if (!count)
		return 0;
// 验证存放数据的缓冲区内存限制。
	verify_area (buf, count);
// 取文件对应的i 节点。若是管道文件,并且是读管道文件模式,则进行读管道操作,若成功则返回
// 读取的字节数,否则返回出错码,退出。
	inode = file->f_inode;
	if (inode->i_pipe)
		return (file->f_mode & 1) ? read_pipe (inode, buf, count) : -EIO;
// 如果是字符型文件,则进行读字符设备操作,返回读取的字符数。
	if (S_ISCHR (inode->i_mode))
		return rw_char (READ, inode->i_zone[0], buf, count, &file->f_pos);
// 如果是块设备文件,则执行块设备读操作,并返回读取的字节数。
	if (S_ISBLK (inode->i_mode))
		return block_read (inode->i_zone[0], &file->f_pos, buf, count);
// 如果是目录文件或者是常规文件,则首先验证读取数count 的有效性并进行调整(若读取字节数加上
// 文件当前读写指针值大于文件大小,则重新设置读取字节数为文件长度-当前读写指针值,若读取数
// 等于0,则返回0 退出),然后执行文件读操作,返回读取的字节数并退出。
	if (S_ISDIR (inode->i_mode) || S_ISREG (inode->i_mode))
	{
		if (count + file->f_pos > inode->i_size)
			count = inode->i_size - file->f_pos;
		if (count <= 0)
			return 0;
		return file_read (inode, file, buf, count);
	}
// 否则打印节点文件属性,并返回出错码退出。
	printk ("(Read)inode->i_mode=%06o\n\r", inode->i_mode);
	return -EINVAL;
}

int sys_write (unsigned int fd, char *buf, int count)
{
	struct file *file;
	struct m_inode *inode;

// 如果文件句柄值大于程序最多打开文件数NR_OPEN,或者需要写入的字节计数小于0,或者该句柄
// 的文件结构指针为空,则返回出错码并退出。
	if (fd >= NR_OPEN || count < 0 || !(file = current->filp[fd]))
		return -EINVAL;
// 若需读取的字节数count 等于0,则返回0,退出
	if (!count)
		return 0;
// 取文件对应的i 节点。若是管道文件,并且是写管道文件模式,则进行写管道操作,若成功则返回
// 写入的字节数,否则返回出错码,退出。
	inode = file->f_inode;
	if (inode->i_pipe)
		return (file->f_mode & 2) ? write_pipe (inode, buf, count) : -EIO;
// 如果是字符型文件,则进行写字符设备操作,返回写入的字符数,退出。
	if (S_ISCHR (inode->i_mode))
		return rw_char (WRITE, inode->i_zone[0], buf, count, &file->f_pos);
// 如果是块设备文件,则进行块设备写操作,并返回写入的字节数,退出。
	if (S_ISBLK (inode->i_mode))
		return block_write (inode->i_zone[0], &file->f_pos, buf, count);
// 若是常规文件,则执行文件写操作,并返回写入的字节数,退出。
	if (S_ISREG (inode->i_mode))
		return file_write (inode, file, buf, count);
// 否则,显示对应节点的文件模式,返回出错码,退出。
	printk ("(Write)inode->i_mode=%06o\n\r", inode->i_mode);
	return -EINVAL;
}

再往下就是关于文件的几个函数的驱动程序实现,和具体硬件联系,暂不介绍。

猜你喜欢

转载自blog.csdn.net/qq_42856154/article/details/89765350