系统调用的三个层次

 

一、用户态、内核态和中断

用户通过库函数与系统调用联系起来。

 

1、内核态 

  在高的执行级别下,代码可以执行特权指令,访问任意的物理地址,这时的CPU就对应内核态

 

2、用户态:

  在低级别的指令状态下,代码 只能在级别允许的特定范围内活动。在日常操作下,执行系统调用的方式是通过库函数,库函数封装系统调用,为用户提供接口以便直接使用。

  • intel x86 CPU有四个权限分级,0-3。Linux只取两种,0是内核态,3是用户态
  • 区分权限级别使得系统更加稳定。

 

3、中断(切换)

中断处理是从用户态进入内核态的主要方式。

  • 硬件中断
  • 系统调用

 

二、系统调用

1、系统调用概念

  系统调用(System Call)是操作系统为在用户态运行的进程与硬件设备(如CPU、磁盘、打印机等)进行交互提供的一组接口。

 

2、系统调用的意义

  • 把用户从底层的硬件编程中解放出来
  • 极大的提高了系统的安全性
  • 使用户程序具有可移植性

 

3、系统调用

1、当用户进程需要发生系统调用时,CPU 通过软中断切换到内核态开始执行内核系统调用函数。Linux 下有三种发生系统调用的方法:

  • 通过 glibc 提供的库函数
  • 使用 syscall 函数直接调用
  • 通过 int 0x80指令陷入

  2、总的来说,前两种最终都会通过int 0x80指令陷入进入中断处理程序。而系统调用也需要输入输出参数,例如实际的值,用户态进程地址空间的变量的地址,甚至是包含指向用户态函数的指针的数据结构的地址等。

  3、system_call是linux中所有系统调用的入口点,每个系统调用至少有一个参数,即由eax传递的系统调用号,其他参数依次由ebx,ecx,edx,esi,edi,ebp传入。

  4、寄存器传递参数具有如下限制:

  • 每个参数的长度不能超过寄存器的长度,即32位
  • 在系统调用号(eax)之外,参数的个数不能超过6个(ebx, ecx,edx,esi,edi,ebp)

 

4、系统调用与应用编程接口

  1、应用编程接口 (application program interface, API)

  • API只是一个函数定义
  • 系统调用通过软中断向内核发出一个明确的请求

  2、Libc库定义的一些API引用了封装例程(wrapper routine,唯一目的就是发布系统调用)

  • 一般每个系统调用对应一个封装例程
  • 库再用这些封装例程定义出给用户的API

    系统调用的三个层次依次是:xyz函数(API)、system_ call(中断向量)和 sys_ xyz(中断服务程序) 

5、C代码中嵌入汇编代码的写法

 

__asm__(
    汇编语句模板:
    输入部分:
    输出部分:
    破坏描述部分:);

 


include <stdio.h>
int main()
{
/*实现的功能:val1+val2=val3*/
unsigned int val1 = 1;
unsigned int val2 = 2;
unsigned int val3 = 0;
printf("val1:%d,val2:%d,val3:%d\n",val1,val2,val3);

asm volatile(
"movl $0,%%eax\n\t" /*两个%表示转义字符,这一句的目的是把%eax清零*/
"addl %1,%%eax\n\t" /*%1指的是参数的标记,m编号为0,c为1,d为2 这一句把val1赋给eax*/
"addl %2,%%eax\n\t" /*%eax=val1+val2*/
"movl %%eax,%0\n\t" /*把val1和val2的值存储在eax里面*/
: "=m" (val3) /* =表示把val3 的值写到内存变量里面*/
: "c" (val1),"d" (val2) /*用%ecx存储val1,用%edx存储val2*/
);

printf("val1:%d+val2:%d=val3:%d\n",val1,val2,val3);
return 0;
}

 

 

三、实验

1、使用库函数API获取当前系统时间

 

代码:

 

time.c
#include <stdio.h>
#include <time.h>
int main()
{
    time_t tt;//int型数值
    struct tm *t;//t指针指向一个结构体
    tt = time(NULL);//调用系统函数time,返回一个指针类型的变量
    t = localtime(&tt);//强制类型转换,便于输出
    printf("time:%d:%d:%d:%d:%d:%d:\n",t->tm_year+1960,t->tm_mon,t->tm_mday,t->tm_hour,t->tm_min,t->tm_sec);
    return 0;
}

 

编译:

 

gcc time.c -o time -m32

运行:

./time

2、用汇编方式触发系统调用获取系统当前时间

代码:

time_asm.c
#include <stdio.h>
#include <time.h>
int main()
{
    time_t tt;//int型数值
    struct tm *t;
    asm volatile(
        "mov $0,%%ebx\n\t"//系统调用传递第一个参数使用ebx,这里是null
        "mov $0xd,%%eax\n\t"//传递系统调用号13(13的16进制即为d)
        "int $0x80\n\t"//发生中断
        "mov %%eax,$0\n\t"//通过eax这个寄存器返回系统调用值
        :"=m"(tt)
    );
    t = localtime(&tt);
    printf("time:%d:%d:%d:%d:%d:%d:\n",t->tm_year+1960,t->tm_mon,t->tm_mda,t->tm_hour,t->tm_min,t->tm_sec);
    return 0;
}

      系统调用返回值使用eax存储,与普通函数一样。

编译:

gcc time_asm.c -o time_asm -m32

运行:

./time-asm

 

 

 3、使用库函数API和C代码中嵌入汇编代码两种方式使用同一个系统调用

  • 库函数API进行20号系统调用

代码:

 

#include<stdio.h>
#include<unistd.h>
int main(void)
{
int d = getpid();
printf("Provess id: %d\n" , d);
return 0;
}

实验结果:

 

  • C代码中嵌入汇编代码进行20号系统调用

代码:

#include<stdio.h>
#include <unistd.h>
int main( void )
{
  int d ;
  asm volatile(
    "mov $0,%%ebx\n\t"
    "mov $0x14,%%eax\n\t"//调用20号的系统调用,即是16进制的14
    "int $0x80\n\t"
    "mov %%eax,%0\n\t"
    :"=m"(d)
    );
  printf( "Process id: %d\n",d);
  return 0;
}

实验结果:

 

 

四、遇到的问题

 

   在确认代码无误后,开始运行,第一次出现了错误数据,于是再做了一次检查,对代码并未做任何修改,再运行得出了合理的数据。再次运行又出现了错误数据,不知道是个什么情况。

 

五、总结

  •   getpid函数用来取得目前进程的进程ID,许多程序利用取到的此值来建立临时文件,以避免临时文件相同带来的问题
  •   系统调用把应用程序的请求传给内核,调用相应的的内核函数完成所需的处理,将处理结果返回给应用程序  
  •   在系统调用号(eax)之外,参数的个数不能超过6个(ebx,ecx,edx,esi,edi,ebp), 若是超过了6个,那么可以传入一个地址,地址所在地存放多个参数。

 

 

 

何佳 原创作品转载请注明出处《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000 

posted on 2016-03-17 12:32  20135231  阅读(264)  评论(0编辑  收藏  举报