xml地图|网站地图|网站标签 [设为首页] [加入收藏]
来自 新闻动态 2019-09-30 03:21 的文章
当前位置: 新濠国际登录平台 > 新闻动态 > 正文

(从CPU指令级别的角度),这个系统调用

操作系统通过系统调用为运行于其上的进程提供服务。

操作系统通过系统调用为运行于其上的进程提供服务。

所有的程序员在写程序的时候都离不开通过库函数的方式和系统调用打交道

安大大 + 原创作品转载请注明出处 + 《Linux操作系统分析》MOOC课程

当用户态进程发起一个系统调用, CPU 将切换到 内核态 并开始执行一个 内核函数 。 内核函数负责响应应用程序的要求,例如操作文件、进行网络通讯或者申请内存资源等。

当用户态进程发起一个系统调用, CPU 将切换到 内核态 并开始执行一个 内核函数 。 内核函数负责响应应用程序的要求,例如操作文件、进行网络通讯或者申请内存资源等。

什么是用户态和内核态?(从CPU指令级别的角度)

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

程序员通过库函数的方式和系统调用打交道,库函数把系统调用给封装起来了。
一般现代CPU都有几种不同的指令执行级别
♦ 在高执行级别下,代码可以执行特权指令,访问任意的物理地址,这种CPU执行级别就对应着内核态
♦ 而在相应的低级别执行状态下,代码的掌控范围会受到限制。只能在对应级别允许的范围内活动
♦ 举例:intel x86 CPU有四种不同的执行级别0-3,Linux只使用了其中的0级和3级分别来表示内核态和用户态


原文地址:https://learn-linux.readthedocs.io
玩转Linux旧群已满,请加新群:278378501。
欢迎关注我们的公众号:小菜学编程 (coding-fan)

举一个最简单的例子,应用进程需要输出一行文字,需要调用 write 这个系统调用:

一般现代CPU都有几种不同的指令执行级别,什么样的程序可以执行什么的指令
在高执行级别下,代码可以执行特权指令,访问任意的物理地址,这时CPU执行级别就对应着内核态
而在相应的低级别执行状态下,代码的掌控范围会受到限制。只能在对应级别允许的范围内活动
举例:intel x86 CPU有四种不同的执行级别0-3,Linux只使用了其中的0级3级分别来表示内核态用户态

为什么有权限级别的划分

防止程序员非法访问系统或者是其它资源而使得系统崩溃

举一个最简单的例子,应用进程需要输出一行文字,需要调用 write 这个系统调用:

hello_world.c

如何区分用户态和内核态?(从进程地址空间的角度)

Linux中怎么区分用户态和内核态:

♦ cs寄存器的最低两位表明了当前代码的特权级
♦ CPU每条指令的读取都是通过cs:eip这两个寄存器:
其中cs是代码段选择寄存器,eip是偏移量寄存器。
♦ 上述判断由硬件完成
♦ 一般来说在Linux中,地址空间是一个显著的标志:
0xc0000000以上的地址空间只能在内核态下访问,0x00000000-0xbfffffff的地址空间在两种状态下都可以访问
注意:这里所说的地址空间是逻辑地址而不是物理地址

在内核态时,cs和eip可以是任意的地址


#include <string.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    char *msg = "Hello, world!n";
    write(1, msg, strlen(msg));

    return 0;
}
#include <string.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
 char *msg = "Hello, world!n";
 write(1, msg, strlen(msg));

 return 0;
}

cs寄存器的最低两位表明了当前代码的特权级
CPU每条指令的读取都是通过cs:eip这两个寄存器:
      其中  cs是代码段选择寄存器,eip是偏移量寄存器
上述判断由硬件完成

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

用户态进入内核态一般来说都是用中断来触发的,可能是硬件中断。也可能是用户态程序运行当中调用了系统调用进入了内核态(trap)。系统调用是一种特殊的中断。

♦ 寄存器上下文
– 从用户态切换到内核态时
• 必须保存用户态的寄存器上下文,同时内核态相应的值放到CPU中
• 要保存哪些?
• 保存在哪里?
♦ 中断/int指令会在堆栈上保存一些寄存器的值
– 如:用户态栈顶地址、当时的状态字、当时的cs:eip的值

注解

读者可能会有些疑问——输出文本不是用 printf 等函数吗?

确实是。 printf 是更高层次的库函数,建立在系统调用之上,实现数据格式化等功能。 因此,本质上还是系统调用起决定性作用。

注解

在32位x86的机器上,有4G的进程地址空间(逻辑地址),在内核态的时候全都可以访问,在用户态的时候,只能访问0x00000000-0xbfffffff的地址空间。也就是说0xc0000000以上的地址空间只能在内核态下访问

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

保护现场 就是进入中断程序,保存需要用到的寄存器的数据
恢复现场 就是退出中断程序,恢复保存寄存器的数据

调用流程

那么,在应用程序内,调用一个系统调用的流程是怎样的呢?

我们以一个假设的系统调用 xyz 为例,介绍一次系统调用的所有环节。

新濠国际登录平台 1

如上图,系统调用执行的流程如下:

  1. 应用程序 代码调用系统调用( xyz ),该函数是一个包装系统调用的 库函数 ;
  2. 库函数 ( xyz )负责准备向内核传递的参数,并触发 软中断 以切换到内核;
  3. CPU 被 软中断 打断后,执行 中断处理函数 ,即 系统调用处理函数 ( system_call);
  4. 系统调用处理函数 调用 系统调用服务例程 ( sys_xyz ),真正开始处理该系统调用;

读者可能会有些疑问——输出文本不是用 printf 等函数吗?

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

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

新濠国际登录平台 2

执行态切换

应用程序 ( application program )与 库函数 ( libc )之间, 系统调用处理函数 ( system call handler )与 系统调用服务例程 ( system call service routine )之间, 均是普通函数调用,应该不难理解。 而 库函数 与 系统调用处理函数 之间,由于涉及用户态与内核态的切换,要复杂一些。

Linux 通过 软中断 实现从 用户态 到 内核态 的切换。 用户态 与 内核态 是独立的执行流,因此在切换时,需要准备 执行栈 并保存 寄存器 。

内核实现了很多不同的系统调用(提供不同功能),而 系统调用处理函数 只有一个。 因此,用户进程必须传递一个参数用于区分,这便是 系统调用号 ( system call number )。 在 Linux 中, 系统调用号 一般通过 eax 寄存器 来传递。

总结起来, 执行态切换 过程如下:

  1. 应用程序 在 用户态 准备好调用参数,执行 int 指令触发 软中断 ,中断号为 0x80 ;
  2. CPU 被软中断打断后,执行对应的 中断处理函数 ,这时便已进入 内核态 ;
  3. 系统调用处理函数 准备 内核执行栈 ,并保存所有 寄存器 (一般用汇编语言实现);
  4. 系统调用处理函数 根据 系统调用号 调用对应的 C 函数—— 系统调用服务例程 ;
  5. 系统调用处理函数 准备 返回值 并从 内核栈 中恢复 寄存器 ;
  6. 系统调用处理函数 执行 ret 指令切换回 用户态 ;

确实是。 printf 是更高层次的库函数,建立在系统调用之上,实现数据格式化等功能。 因此,本质上还是系统调用起决定性作用。

当从用户态切换到内核态的时候,必须用户态的寄存器上下文保存起来,同时设置内核态的寄存器内容
中断/int指令会在堆栈上保存一些寄存器的值
      如:用户态栈顶地址、当时的状态字、当时的 cs:eip 的值
同时设置内核态的栈顶地址、内核态的状态字,中断处理程序的入口地址 cs:eip 的值(对于系统调用来讲,它是指向system_call函数)

中断处理的完整过程

新濠国际登录平台 3


编程实践

下面,通过一个简单的程序,看看应用程序如何在 用户态 准备参数并通过 int 指令触发 软中断 以陷入 内核态 执行 系统调用 :

.section .rodata

msg:
    .ascii "Hello, world!n"

.section .text

.global _start

_start:
    # call SYS_WRITE
    movl $4, %eax
    # push arguments
    movl $1, %ebx
    movl $msg, %ecx
    movl $14, %edx
    int $0x80

    # Call SYS_EXIT
    movl $1, %eax
    # push arguments
    movl $0, %ebx
    # initiate
    int $0x80

这是一个汇编语言程序,程序入口在 *_start* 标签之后。

第 12 行,准备 系统调用号 :将常数 4 放进 寄存器 eax 。 系统调用号 4 代表 系统调用 SYS_write , 我们将通过该系统调用向标准输出写入一个字符串。

第 14-16 行, 准备系统调用参数:第一个参数放进 寄存器 ebx ,第二个参数放进 ecx , 以此类推。

write 系统调用需要 3 个参数:

  • 文件描述符 ,标准输出文件描述符为 1 ;
  • 写入内容(缓冲区)地址;
  • 写入内容长度(字节数);

第 17 行,执行 int 指令触发软中断 0x80 ,程序将陷入内核态并由内核执行系统调用。 系统调用执行完毕后,内核将负责切换回用户态,应用程序继续执行之后的指令( 从 20 行开始 )。

第 20-24 行,调用 exit 系统调用,以便退出程序。

注解

注意到,这里必须显式调用 exit 系统调用退出程序。 否则,程序将继续往下执行,最终遇到 段错误segmentation fault )!

读者可能很好奇——在写 C 语言或者其他程序时,这个调用并不是必须的!

这是因为 C 库( libc )已经帮你把脏活累活都干了。

接下来,我们编译并执行这个汇编语言程序:

$ ls
hello_world-int.S
$ as -o hello_world-int.o hello_world-int.S
$ ls
hello_world-int.o  hello_world-int.S
$ ld -o hello_world-int hello_world-int.o
$ ls
hello_world-int  hello_world-int.o  hello_world-int.S
$ ./hello_world-int
Hello, world!

其实,将 系统调用号 和 调用参数 放进正确的 寄存器 并触发正确的 软中断 是个重复的麻烦事。 C 库已经把这脏累活给干了——试试 syscall 函数吧!

#include <string.h>
#include <sys/syscall.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    char *msg = "Hello, world!n";
    syscall(SYS_write, 1, msg, strlen(msg));

    return 0;
}

调用流程

中断/int指令发生后第一件事就是保护现场

系统调用概述

以系统调用为例,看看中断服务具体是怎么执行的:

系统调用的意义

  • 操作系统为用户态进程与硬件设备进行交互提供了一组接口——系统调用
  • 把用户从底层的硬件编程中解放出来
  • 极大的提高了系统的安全性
  • 使用户程序具有可移植性

下一步

订阅更新,获取更多学习资料,请关注我们的 微信公众号 :

新濠国际登录平台 4

那么,在应用程序内,调用一个系统调用的流程是怎样的呢?

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

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

应用编程接口(application program interface, API) 和系统调用是不同的

  • API只是一个函数定义
  • 系统调用通过软中断(trap)向内核发出一个明确的请求
    Libc库定义的一些API引用了封装例程(wrapper routine,唯一目的就是发布系统调用)
  • 一般每个系统调用对应一个封装例程
  • 新濠国际登录平台,库再用这些封装例程定义出给用户的API
    不是每个API都对应一个特定的系统调用。
  • API可能直接提供用户态的服务,如一些数学函数
  • 一个单独的API可能调用几个系统调用
  • 不同的API可能调用了同一个系统调用
    返回值
  • 大部分封装例程返回一个整数,其值的含义依赖于相应的系统调用
  • -1在多数情况下表示内核不能满足进程的请求
  • Libc中定义的errno变量(error number)包含特定的出错码
    应用程序、封装例程、系统调用处理程序及系统调用服务例程之间的关系

新濠国际登录平台 5

左边是用户态User Mode,右边是内核态Kernel Mode,最左边api:xyz()封装了一个系统调用,这个系统调用会触发一个0x80的中断。0x80这个中断向量就对应着system_call这个内核代码的入口起点。这个内核代码里可能有SAVE_ALL,sys_xyz()中断服务程序,在中断服务程序执行完后,可能ret_from_sys_call,在return的过程中可能发生进程调度,这是一个进程调度的时机。如果没有发生系统调度,就会iret,再返回到用户态接着执行。
系统调用的三层皮:xyz(api)、system_call(中断向量)和sys_xyz


参考文献

  1. Serg Iakovlev
  2. write(2) - Linux manual page
  3. syscall(2) - Linux manual page
  4. _exit(2) - Linux manual page

新濠国际登录平台 6

我们以一个假设的系统调用 xyz 为例,介绍一次系统调用的所有环节。

当进入到中断处理程序后,一开始就执行SAVE_ALL,把其它的一些寄存器的值push到内核堆栈里面去

当用户态进程调用一个系统调用时,CPU切换到内核态并开始执行一个内核函数。
  • 在Linux中是通过执行int $0x80来执行系统调用的,这条汇编指令产生向量为128的编程异常
    系统调用号讲xyz和sys_xyz关联起来了

新濠国际登录平台 7

新濠国际登录平台 8

传参:

内核实现了很多不同的系统调用,进程必须指明需要哪个系统调用,这需要传递一个名为系统调用号的参数
-使用eax寄存器

本文由新濠国际登录平台发布于新闻动态,转载请注明出处:(从CPU指令级别的角度),这个系统调用

关键词: