Linux下简单的系统调用

杨金龙 + 原创作品转载请注明出处 + 《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底层系统调用的一次封装而已,本质上是通过系统中断实现的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值