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

Linux内核分析——扒开系统调用的三层皮(上)

2016-03-19 18:09 429 查看
[b]马悦+原创作品转载请注明出处+《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000[/b]

一、用户态、内核态和中断处理过程

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

2、在高执行级别下,代码可以执行特权指令,访问任意的物理地址,这种CPU执行级别就对应着内核态。而在相应的低级别执行状态下代码的掌控范围受到限制。只能在对应级别允许的范围内活动。

3、intel x86 CPU有四种不同的执行级别0-3。Linux只取两种,0级是内核态,3级是用户态。

4、如何区分用户态与内核态?

cs寄存器的最低两位表明了当前代码的特权级

CPU每条指令的读取都是通过cs:eip这两个寄存器:cs是代码段选择寄存器,eip是偏移量寄存器

上述判断由硬件完成

一般来说在Linux中,地址空间是一个显著的标志:0xc0000000以上的空间只能在内核态下访问,0x00000000-0xbfffffff的地址空间在两种状态下都可以访问(这里所说的地址空间是逻辑地址而不是物理地址)。

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

6、寄存器上下文

从用户态切换到内核态时,必须保存用户态的寄存器上下文到内核堆栈中,同时会把当前内核态的一些信息加载,例如cs:eip指向中断处理程序入口。

如:用户态栈顶地址、当时的状态字、当时的cs:eip的值

7、中断发生后的第一件事就是保存现场 - SAVE_ALL

中断处理结束前最后一件事是恢复现场 - RESTORE_ALL

二、系统调用概述

1. 系统调用的意义:

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

(1)把用户从底层的硬件编程中解放出来。

(2)极大地提高了系统的安全性

(3)使用户程序具有可移植性

2、API(应用编程接口)

与系统调用区别:

(1)API只是一个函数定义

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

(3)API可直接提供用户态服务;一个API调用几个系统调用;不同API可调用同一个系统调用。

3、Libc库

(1)定义的一些API引用了封装例程(唯一目的就是发布系统调用)

(2)一般每个系统调用对应一个封装例程。

(3)库再用这些封装例程定义出给用户的API;

4、返回值

(1)大多封装例程返回一个整数,其值依赖于相应的系统调用;

(2)-1表示内核不能满足进程的请求;

(3)Libc定义的errorno变量包含特定出错码;

5、系统调用的三层皮

(1)1API(xyz)

(2)中断向量(system_call)

(3)中断服务程序(sys_xyz)



(1)系统调用的服务例程中,中断向量0x80与system_call绑定起来。(Linux中可以通过执行int $128来执行系统调用。)

(2)system_call是linux中所有系统调用的入口点,每个系统调用至少有一个参数,即系统调用号。

(3)系统调用号将xyz与sys_xyz关联起来。调用号在eax寄存器中。

系统调用的参数传递:

(1)函数调用——压栈

(2)用户态到内核态——寄存器传递。

每个参数长度不能超过32位,个数不能超过6个。

三、使用库函数API和C代码中嵌入汇编代码触发同一个系统调用

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

使用time(),代码如下:

#include<stdio.h>

#include<time.h>

int main()

{

time_t tt;

struct tm *t; //构造一个结构体,方便读取

tt = time(NULL); //time系统调用

t = localtime(&tt);

printf("time:%d:%d:%d:%d:%d:%d\n", t->tm_year+1900, t->tm_mon, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec);

return 0;

}





2、使用C代码中嵌入汇编代码触发系统调用获取系统当前时间

代码如下:

#include<stdio.h>

#include<time.h>

int main()

{

time_t tt;

struct tm *t;

asm volatile(

"mov $0,%%ebx\n\t" //把ebx清零,相当于传参数

"mov $0xd,%%eax\n\t" //把0xd放入eax中,即系统调用号13,指time

"int $0x80\n\t"

"mov %%eax,%0\n\t" //返回值是在eax中,%0指tt,返回值放到tt中去。

: "=m" (tt)

);

t = localtime(&tt);

printf("time:%d:%d:%d:%d:%d:%d\n", t->tm_year+1900, t->tm_mon, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec);

return 0;

}





四、总结

   系统调用(System Call)是操作系统为在用户态运行的进程与硬件设备(如CPU、磁盘、打印机等)进行交互提供的一组接口。当用户进程需要发生系统调用时,CPU 通过软中断切换到内核态开始执行内核系统调用函数。在Linux 下三种发生系统调用的方法:

   1、通过 glibc 提供的库函数

   glibc 是 Linux 下使用的开源的标准 C 库,它是 GNU 发布的 libc 库,即运行时库。glibc 为程序员提供丰富的 API(Application Programming Interface),除了例如字符串处理、数学运算等用户态服务之外,最重要的是封装了操作系统提供的系统服务,即系统调用的封装。

   2、使用 syscall 直接调用

    如果 glibc 没有封装某个内核提供的系统调用时,就没办法通过上面的方法来调用该系统调用。此时我们可以利用 glibc 提供的
syscall
函数直接调用。

   3、通过int指令陷入

    如果我们知道系统调用的整个过程的话,应该就能知道用户态程序通过软中断指令
int 0x80
来陷入内核态(在Intel Pentium II 又引入了
sysenter
指令),参数的传递是通过寄存器,eax 传递的是系统调用号,ebx、ecx、edx、esi和edi 来依次传递最多五个参数,当系统调用返回时,返回值存放在 eax 中。

  
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: