您的位置:首页 > 运维架构 > Linux

Linux下简单的系统调用

2017-03-17 21:31 183 查看
杨金龙 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

  本周是Linux系统分析课程的第四周课程,本周主要讲Linux系统调用的过程,具体知识点和实验结果总结如下。

系统调用的相关知识

  系统调用:系统调用只是一个特殊的中断。我们通过库函数和系统调用打交道,库函数把系统调用封装起来。

1、储备知识——内核态和用户态

  内核态:在高执行级别下,代码可以执行特权指令,访问任意的物理内存,这种CPU执行级别就对应着内核态。

  用户态:在用户态级别下,代码的掌控范围会受到限制,只能在对应级别允许的范围内活动。

注:Intel x86CPU有四种不同的执行级别0-3,Linux只使用了其中的0级和3级分别来表示内核态和用户态。

2、为什么有权限级别的划分?

  若没有用户态和内核态的划分,用户写的不健壮的程序就可以执行特权指令时,就很容易是系统崩溃。操作系统发展过程中划分了用户态和内核态,是系统更稳定的机制。

3、寄存器在系统调用中的作用

  Cs寄存器的最低两位表明了当前代码的特权级。CPU每条指令的读取都是通过CS:EIP这两个寄存器:其中CS是代码段选择寄存器,EIP是偏移量寄存器。

4、内存地址空间

  一般在Linux中,地址空间是一个显著的标志:0xc0000000以上的地址空间只能在内核态下访问,0x0000000–0xbfffffff的地址空间在两种状态下都可以访问。

注:产生中断是从用户态进入内核态的主要方式。

5、寄存器上下文:

 5.1从用户态切换到内核态时:

  必须保存用户态的寄存器上下文;

  同时把内核态的寄存器的值放到寄存器中。

 5.2中断/int指令会在堆栈上保存一些寄存器的值:

  如用户态栈顶地址;

  当前的状态字;

  当时的CS:EIP的值。

 5.3中断发生后的第一件事就是保存现场:

  保存现场:就是进入中断程序,保存需要用到的寄存器的数据。

 5.4中断处理结束前最后一件事就是恢复现场:

  恢复现场:就是退出中断程序恢复保存寄存器的数据。

  注:Iret指令和中断信号(包括int指令)发生时的CPU做的动作整好相反。

中断处理的完整过程

如下图所示



  中断指令interrupt(ex:int 0x80)开始进行系统调用;

  保存当前CS:EIP,SS:ESP,eflags的值到内核堆栈,同时加载了中断服务程序的地址到CS:EIP以及内核堆栈栈顶指针到SS:ESP中。Int指令完成上述操作过程。

  内核代码,完成中断服务:

  发生进程调度,则保存调度时的现场,进行调度,完成调度后再恢复现场;

  不发生进程调度,则恢复之前的保存现场:iret - pop cs:EIP/SS:ESP/eflags from kernel stack.

系统调用的意义

1、操作系统为用户态进程与硬件设备进行交互提供了一组接口——系统调用。

  把用户从底层的硬件编程中解放出来;

  极大的提高了系统的安全性;

  使用户程序具有可移植性。

2、操作系统提供的API和系统调用的关系。

  应用编程接口(Application program interface,API)和系统调用时不同的;

  API只是一个函数定义;

  系统调用通过软中断向内核发出一个明确的请求。

  Libc库定义的一些API引用了封装例程(wrapper routine,唯一的目的就是发布系统调用):一般每个系统调用对应一个封装例程。库再用这些封装例程定义给出用户的API。

3、不是每个API都对应一个特定的系统调用:

  API可能直接提供用户态的服务;

  一个单独的API可能调用几个系统调用;

  不同的API可能调用了同一个系统调用。

4、返回值

  大部分封装例程返回一个整数,其值的含义依赖于相应的系统调用;

  -1在多数情况下表示内核不能满足进程的请求;

  Libc中定义的errno变量包含特定的出错码。

  

注:系统调用的三层皮:API,中断向量,中断服务程序

5、系统调用的服务例程:

  5.1 当用户态进程调用一个系统调用时,CPU切换内核态并开始执行一个内核函数。

  在Linux中是通过执行
int $0x80
来执行系统调用的,这条汇编指令产生向量为128的编程异常;

  Inter Pentium II中引入了sysenter指令(快速系统调用),2,6已经支持。

5.2 传参:

  内核实现了很多不同的系统调用;

  进程必须指明需要哪个系统调用,这需要使用EAX寄存器传递一个名为系统调用号的参数

5.3 系统调用也需要输入输出参数,例如:

  实际的值;

  用户态进程地址空间的变量的地址;

  甚至包含指向用户态函数的指针的数据结构的地址。

  System call是Linux中所有系统调用的入口点,每个系统调用至少有一个参数,即由eax

5.3 传递的系统调用号;

  系统调用号将xyz和sys_xyz关联起来;用eax寄存器来传递参数;

  一个应用程序调用fork()封装例程,那么在执行int $0x80之前就把EAX寄存器的值置为2(即 NR fork);

  这个寄存器的设置是libc库中的封装例程进行的,因此用户一般不关心系统调用号;

  进入sys,call之后,立即将EAX的值压入内核堆栈;

5.4 寄存器参数具有如下限制:

  每个参数的长度不能超过寄存器的长度,即32位;

  在系统调用号(EAX)之外,参数的个数不能超过6个(ebx,ecx,edx,esi,edi,ebp)。

实验代码

  实验过程中调用mkdir系统函数,mkdir调用号为39,函数原型如下:

  

int mkdir(const char *path, mode_t mode);

参数:

 path是目录名

 mode是目录权限

返回值:

 返回0 表示成功, 返回 -1表示错误,并且会设置errno值。

通过C代码调用

mkdir.c

#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>

int main()
{
int ret = 0;

ret = mkdir("./test", 0777);

if(ret == 0)
{
printf("Mkdir success!\n") ;
}
else
printf("Mkdir failed!\n");

return 0;
}




通过嵌入式汇编调用

mkdir_asm.c

#include <stdio.h>

int main()
{
int ret = 0;
char *dir = "./test_asm";
int mode = 0777;
asm volatile(
"movl $39, %%eax\n\t"
"int $0x80\n\t"
"movl %%eax, %0\n\t"
: "=m"(ret)
: "b"(dir), "c"(mode)                                                       );
if(ret == 0)
printf("Mkdir through asm success!\n");
else
printf("Mkdir through asm failed!\n");
return 0;
}




实验总结

  通过对系统调用的两种代码实现方法的分析,我们可以知道C语言的API只不过是对Linux底层系统调用的一次封装而已,本质上是通过系统中断实现的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息