【金沙js333娱乐场】Linux系统调用原理

参考文献

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

金沙js333娱乐场 1

.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

内核实现了很多不同的系统调用,进程必须指明需要哪个系统调用,这需要传递一个名为系统调用号的参数,使用eax寄存器(系统调用号将xyz()和sys_xyz()关联起来了)

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

您可能感兴趣的文章:

  • Python下调用Linux的Shell命令的方法
  • 利用Golang如何调用Linux命令详解
  • Linux makefile
    和shell文件相互调用实例详解
  • 浅谈在linux
    kernel中打印函数调用的堆栈的方法
  • 详解Linux驱动中,probe函数何时被调用
  • 三种方法实现Linux系统调用
  • 举例讲解Linux系统下Python调用系统Shell的方法
  • Python在Windows和在Linux下调用动态链接库的教程
  • Linux下编写Lua扩展so文件和调用方法实例
  • Linux下使用python调用top命令获得CPU利用率
  • Java调用linux
    shell脚本的方法

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

编程实践

下面,通过一个简单的程序,看看应用程序如何在 用户态 准备参数并通过 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;
}

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

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

注解

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

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

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

当中断处理程序结束之后,它会RESTORE_ALL,把保存的用户态的寄存器再pop出来到当前的CPU里面,最后iret,iret指令与中断信号(包括int指令)发生时CPU做的动作刚好相反

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

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

    return 0;
}

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

下一步

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

金沙js333娱乐场 2

金沙js333娱乐场 3

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

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

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

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

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

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

SAVE_ALL….如果发生了进程调度,那么当前的状态都会暂时保存在系统里面,当下一次发生进程调度切换回当前进程的时候,就会接着把它执行完,RESTORE_ALL….

执行态切换

应用程序 ( 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 指令切换回 用户态 ;

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

封装例程 (wrapper
routine),唯一目的就是发布系统调用,让程序员在写代码的时候不需要用汇编指令来触发一个系统调用,而是直接调用一个函数就可以触发一个系统调用

调用流程

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

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

金沙js333娱乐场 4

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

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

hello_world-int.S

(完)

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

注解

应用程序、封装例程、系统调用处理程序及系统调用服务例程之间的关系

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

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

 return 0;
}

通过库函数API使用系统调用获取系统当前时间

调用流程

系统调用传递第一个参数使用ebx,这里是NULL
使用eax传递系统调用号,这里time是13
系统调用的返回值使用eax存储,和普通函数一样

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

系统调用的三层皮:API、中断向量对应的system_call、中断服务程序sys_xyz

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

一个应用程序调用fork()封装例程,那么在执行int
$0x80之前就把eax寄存器的值置为2(即__NR_fork)
这个寄存器的设置是libc库中的封装例程进行的,因此用户一般不关心系统调用号
进入sys_call之后,立即将eax的值压入内核堆栈

$ 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!

Summary

发表评论

电子邮件地址不会被公开。 必填项已用*标注