Linux内核分析——扒开系统调用的三层皮(上)
马悦+原创作品转载请注明出处+《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
一、用户态、内核态和中断处理过程
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 中。