首页 > 技术文章 > Linux学习--系统调用

yqs-0705 2019-05-10 10:43 原文

---恢复内容开始---

Linux学习—系统调用

操作系统实现系统调用的基本过程:

  1. 1.     应用程序调用库函数(API)
  2. 2.     API将系统调用号存入EAX,使用int 0x80中断进入内核态
  3. 3.     内核中的中断处理函数根据系统调用号调用对应的内核函数(系统调用)
  4. 4.     系统调用完成相应功能,将返回值存入EAX,返回到中断处理函数
  5. 5.     中断处理函数返回到API
  6. 6.     API将EAX返回给中断处理程序

 

调用一个普通的自定义函数,就是call到调用的函数的地址去执行

调用系统函数是调用系统库为调用该系统而编写的接口函数API

API的主要工作是:

       将系统调用号存入EAX

       将函数参数存入其他通用寄存器

       触发0x80号中断进入内核

 

 

一、API分析

下面以Linux0.11源码lib/close.c为例分析一下API

打开文件之后代码如下:

#define __LIBRARY__        ----使得_syscall1有效

#include <unistd.h>        ----编译器才能获知自定义的系统调用的编号

_syscall1(int,close,int,fd)     ----宏,在include/unistd.h中定义,展开后如下

 

 

其中:__asm__  表示后面代码是内嵌汇编

      volatile   表示编译器不要优化后面代码,保持原样

      括号中为汇编代码,功能是:

                            先将宏NR_close(中断号,在unistd.h中定义)存入EAX,将参数fd存入EBX,然后进行0x80中断调用。调用返回后,从EAX取出返回值,存入res

          然后c语言判断res的值做出相应的返回

    因此,自己定义API时注意以下:

          自己写中断号加入到include/unistd.h中

          写类似于close.c的文件,注意1个参数用syscall1,2个参数用syscall2

二、0x80中断处理过程

在init/main.c中用sched_init()初始化中断,其在kernel/sched.c中定义为:

 

 

其中set_system_gate为宏,include/asm/system.h中定义为

 

 

以上为填写IDT(中断描述符表),将system_call函数地址写到0x80对应的中断描述符中,也就是在中断0x80发生后,自动调用函数system_call。

接下来就是system_call,汇编打造,在kernel/system_call中

 

 

system_call用.globl修饰为其他函数可见。

call sys_call_table(,%eax,4)之前是一些压栈保护,修改段寄存器为内核段,

call sys_call_table(,%eax,4)之后是看看是否需要重新调度

对于call sys_call_table(,%eax,4)这一句,根据汇编寻址方法,他实际上是

        call sys_call_table + 4 * %eax

其中eax为系统调用号,即__NR_xxxxxx

sys_call_table一定是一个函数指针数组的起始地址,它定义在include/linux/sys.h中:

fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,…… 增加实验要求的系统调用,需要在这个函数表中增加两个函数引用——sys_iam和sys_whoami。当然该函数在sys_call_table数组中的位置必须和__NR_xxxxxx的值对应上。同时还要仿照此文件中前面各个系统调用的写法,加上:

extern int sys_whoami(); extern int sys_iam();

三、在内核中实现系统调用函数sys_###

创建一个.c文件,在里面实现sys_###函数就可以了(仿照其他系统调用函数的写法)

四、修改makefile

Makefile里记录的是所有源程序文件的编译、链接规则。我们之所以简单地运行make就可以编译整个代码树,是因为make完全按照Makefile里的指示工作。

需要修改两处:

 

 

在后面添加上你所创建的.c文件编译成的.o文件

 

 

添加依赖文件

五、用printk()调试内核

printf()函数是在用户态喜爱那向控制台打印信息,在内核态中使用printk()

可以在自己编写的系统调用函数sys_###中printk()一些信息,在用户程序调用的时候就可以知道是否调用sys_###了

六、编写.c程序,但是需要在Linux0.11下编译

使用命令gcc –o ### ###.c –Wall

用###.c来代指自己编写的.c文件

“-Wall”参数是给出所有的编译警告信息

“-o”参数指定生成的执行文件名是###

使用./###来运行程序

七、在用户态和内核态中传递数据

指针参数传递的是应用程序所在地址空间的逻辑地址,在内核中如果直接访问这个地址,访问到的是内核空间中的数据,不会是用户空间的。所以这里还需要一点儿特殊工作,才能在内核中从用户空间得到数据。

以open函数为例:

 

 

统调用是用eax、ebx、ecx、edx寄存器来传递参数:eax传递系统调用号,ebx传递第一个参数,ecx传递第二个参数……但是ebx存放的文件指针指向的是用户空间的地址,当在内核空间执行代码时,如何完成数据传递呢?

  • open接下来进行系统调用system_call

 

 

这里要注意一下AT&T汇编格式与Intel汇编格式区别

movl为传递long(32位)的数据,源操作数在前,目的操作数在后

可以看出获得用户空间数据靠的是段寄存器fs

然后代码调转到sys_open()

 

 

它将参数传给了open_namei()。再沿着open_namei()继续查找,文件名先后又被传给dir_namei()、get_dir()

 

 

通过get_fs_byte得到了fs寄存器里面的数据,完成了在内核空间中获取数据空间的数据的任务,同样的put_fs_byte将内核空间的额数据传递到用户空间

put_fs_xxx()和get_fs_xxx()都是用户空间和内核空间之间的桥梁

---恢复内容结束---

推荐阅读