系统调用(SYSTEM CALL)

OS内核中都有一组实现系统功能的过程,系统调用就是对上述过程的调用。编程人员利用系统调用,向OS提出服务请求,由OS代为完成。

一般情况下,进程是不能够存取系统内核的。它不能存取内核使用的内存段,也不能调用内核函数,CPU的硬件结构保证了这一点。只有系统调用是一个例外。

统调用是用户态进入内核态的唯一入口:一夫当关,万夫莫开。常用系统调用:

  • 控制硬件:如write/read调用。
  • 设置系统状态或读取内核数据——getpid()、getpriority()、setpriority()、sethostname()
  • 进程管理:如 fork()、clone()、execve()、exit()等

优点 编程容易,从硬件设备的低级编程中解脱出来 提高了系统的安全性,可以先检查请求的正确性

陷入指令

在 Intel CPU 中,这个指令由中断 0x80 实现。

在 ARM 中,这个指令是 SWI。

Int 0x80指令

Linux中实现系统调用利用了i386体系结构中的软件中断。即调用了int  $0x80汇编指令。

这条汇编指令将产生向量为128的编程异常,CPU便被切换到内核态执行内核函数,转到了系统调用处理程序的入口:system_call()。

int $0x80指令将用户态的执行模式转变为内核态,并将控制权交给系统调用过程的起点system_call()处理函数。

system_cal()检查系统调用号,该号码告诉内核进程请求哪种服务。

内核进程查看系统调用表(sys_call_table)找到所调用的内核函数入口地址。

接着调用相应的函数,在返回后做一些系统检查,最后返回到进程。

system_call()函数

系统调用和普通函数调用

API是用于某种特定目的的函数,供应用程序调用,而系统调用供应用程序直接进入系统内核。

Linux内核提供了一些C语言函数库,这些库对系统调用进行了一些包装和扩展,因为这些库函数与系统调用的关系非常紧密,所以习惯上把这些函数也称为系统调用。

有的API函数在用户空间就可以完成工作,如一些用于数学计算的函数,因此不需要使用系统调用。

有的API函数可能会进行多次系统调用。

不同的API 函数也可能会有相同的系统调用。比如malloc(),calloc(),free()等函数都使用相同的方法分配和释放内存。

系统命令、内核函数

系统调用与系统命令

  • 系统命令相对API来说,更高一层。每个系统命令都是一个执行程序,如ls命令等。这些命令的实现调用了系统调用。

系统调用与内核函数

  • 系统调用是用户进入内核的接口层,它本身并非内核函数,但是它由内核函数实现。
  • 进入内核后,不同的系统调用会找到各自对应的内核函数,这些内核函数被称为系统调用的“服务例程”。如系统调用getpid实际调用的服务例程为sys_getpid(),或者说系统调用getpid()是服务例程sys_getpid()的封装例程。

封装例程(wrapper routine)

由于陷入指令是一条特殊指令,依赖操作系统实现的平台,如在i386体系结构中,这条指令是int $0x80(陷入指令),不是用户在编程时应该使用的语句,因为这将使得用户程序难于移植。

在标准C库函数中,为每个系统调用设置了一个封装例程,当一个用户程序执行了一个系统调用时,就会调用到C函数库中的相对应的封装例程。

系统调用过程

system_call()片段

...pushl %eax    /*将系统调用号压栈*/
SAVE_ALL
...
cmpl$(NR_syscalls), %eax    /*检查系统调用号
Jb nobadsys
Movl $(-ENOSYS), 24(%esp)   /*堆栈中的eax设置为-ENOSYS, 作为返回值
Jmp ret_from_sys_call
nobadsys:…
call  *sys_call_table(,%eax,4) #调用系统调用表中调用号为eax的系统调用例程
movl %eax,EAX(%esp) #将返回值存入堆栈中
Jmp ret_from_sys_call

首先将系统调用号(eax)和可以用到的所有CPU寄存器保存到相应的堆栈中(由SAVE_ALL完成);

对用户态进程传递过来的系统调用号进行有效性检查(eax是系统调用号,它应该小于 NR_syscalls)

如果是合法的系统调用,再进一步检测该系统调用是否正被跟踪;

根据eax中的系统调用号调用相应的服务例程。

服务例程结束后,从eax寄存器获得它的返回值,并把这个返回值存放在堆栈中,让其位于用户态eax寄存器曾存放的位置。

然后跳转到ret_from_sys_call(),终止系统调用程序的执行。

SAVE_ALL宏定义

#define SAVE_ALL
cld;
pushl %es;
pushl %ds;
pushl %eax;
pushl %ebp;
pushl %edi;
pushl %esi;
pushl %edx;
pushl %ecx;
pushl %ebx;
movl $(__KERNEL_DS),%edx;
movl %edx,%ds;
movl %edx,%es;  
  • 将寄存器中的参数压入到核心栈中(这样内核才能使用用户传入的参数。)
  • 因为在不同特权级之间控制转换时,INT指令不同于CALL指令,它不会将外层堆栈的参数自动拷贝到内层堆栈中。所以在调用系统调用时,必须把参数指定到各个寄存器中

系统调用表与调用号

这样系统调用处理程序一旦运行,就可以从eax中得到系统调用号,然后再去系统调用表中寻找相应服务例程。

一个应用程序调用fork()封装例程,那么在执行int $0x80之前就把eax寄存器的值置为2(即__NR_fork)。

这个寄存器的设置是libc库中的封装例程进行的,因此用户一般不关心系统调用号

核心中为每个系统调用定义了一个唯一的编号,这个编号的定义在linux/include/asm/unistd.h中(最大为NR_syscall)

同时在内核中保存了一张系统调用表,该表中保存了系统调用编号和其对应的服务例程地址。第n个表项包含系统调用号为n的服务例程的地址。

系统调用陷入内核前,需要把系统调用号一起传入内核。而该标号实际上是系统调用表(  sys_call_table)的下标 在i386上,这个传递动作是通过在执行int  $0x80前把调用号装入eax寄存器实现。 这样系统调用处理程序一旦运行,就可以从eax中得到系统调用号,然后再去系统调用表中寻找相应服务例程。

系统调用号

#define __NR_exit      1
#define __NR_fork      2
#define __NR_read      3
#define __NR_write     4
#define __NR_open      5
#define __NR_close     6
#define __NR_waitpid   7
#define __NR_creat     8
#define __NR_link      9
#define __NR_unlink    10
#define __NR_execve    11
#define __NR_chdir     12
#define __NR_time      13

系统调用表 (arch/i386/kernel/entry.s)

data
ENTRY(sys_call_table)
.long SYMBOL_NAME(sys_ni_syscall)
.long SYMBOL_NAME(sys_exit)
.long SYMBOL_NAME(sys_fork)
.long SYMBOL_NAME(sys_read)
.long SYMBOL_NAME(sys_write)
.long SYMBOL_NAME(sys_open)
.long SYMBOL_NAME(sys_close)
.long SYMBOL_NAME(sys_waitpid)
.long SYMBOL_NAME(sys_creat)
.long SYMBOL_NAME(sys_link)
.long SYMBOL_NAME(sys_unlink)
.long SYMBOL_NAME(sys_execve)
.long SYMBOL_NAME(sys_chdir)
.long SYMBOL_NAME(sys_time)
.long SYMBOL_NAME(sys_mknod)
  • 系统调用表记录了各个系统调用的服务例程的入口地址。
  • 以系统调用号为偏移量能够在该表中找到对应处理函数地址。
  • 在linux/include/linux/sys.h中定义的NR_syscalls表示该表能容纳的最大系统调用数,一般NR_syscalls = 256。

系统调用的返回

当服务例程结束时,system_call( ) 从eax获得系统调用的返回值,并把这个返回值存放在曾保存用户态 eax寄存器栈单元的那个位置上,然后跳转到ret_from_sys_call( ),终止系统调用处理程序的执行。

当进程恢复它在用户态的执行前,RESTORE_ALL宏会恢复用户进入内核前被保留到堆栈中的寄存器值。其中eax返回时会带回系统调用的返回码(负数说明调用错误,0或正数说明正常完成)

ret_from_sys_call

cli              # 关中断
cmpl $0,need_resched(%ebx)
jne    reschedule          #如果进程描述符中的 need_resched位不为0,则重新调度
cmpl   $0,sigpending(%ebx)
jne      signal_return   #若有未处理完的信号,则处理
restore_all:
RESTORE_ALL     #堆栈弹栈,返回用户态

系统调用的返回值

所有的系统调用返回一个整数值。

  1. 正数或0表示系统调用成功结束
  2. 负数表示一个出错条件

这里的返回值与封装例程返回值的约定不同

  1. 内核没有设置或使用errno变量
  2. 封装例程在系统调用返回取得返回值之后设置这个变量
  3. 当系统调用出错时,返回的那个负值将要存放在errno变量中返回给应用程序

系统调用-实例分析

假设源文件名为getpid.c,内容是:

#include <syscall.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
int main(void)
{long ID;ID = getpid();printf ("getpid()=%ld\n", ID);return(0);
}
  1. 该程序调用封装例程getpid()。该封装例程将系统调用号_NR_getpid(第20个)压入EAX寄存器
  2. CPU通过int $0x80 进入内核,找到system_call(),并调用它     (以下进入内核态)
  3. 在内核中首先执行system_call(),接着执行根据系统调用号在调用表中查找到的对应的系统调用服务例程sys_getpid()。
  4. 执行sys_getpid()服务例程。
  5. 执行完毕后,转入ret_from_sys_call()例程,系统调用返回到用户态。

系统调用的参数传递

很多系统调用需要不止一个参数

普通C函数的参数传递是通过把参数值写入堆栈(用户态堆栈或内核态堆栈)来实现的。但因为系统调用是一种特殊函数,它由用户态进入了内核态,所以既不能使用用户态的堆栈也不能直接使用内核态堆栈

在int $0x80汇编指令之前,系统调用的参数被写入CPU的寄存器。然后,在进入内核态调用系统调用服务例程之前,内核再把存放在CPU寄存器中的参数拷贝到内核态堆栈中。因为毕竟服务例程是C函数,它还是要到堆栈中去寻找参数的

系统调用使用寄存器来传递参数,要传递的参数有:

  • 系统调用号
  • 系统调用所需的参数

用于传递参数的寄存器有:

  • eax:用于保存系统调用号和系统调用返回值
  • 系统调用参数保存在:ebx,ecx,edx,esi和edi中

进入内核态后,system_call通过使用SAVE_ALL宏把这些寄存器的值保存在内核态堆栈中。

用寄存器传递参数必须满足3个条件:  

  1. 每个参数的长度不能超过寄存器的长度
  2. 参数的个数不能超过6个(包括eax中传递的系统调用号);否则,需要用一个单独的寄存器指向进程地址空间中这些参数值所在的一个内存区即可
  3. 返回值必须写到eax寄存器中

参数传递举例

处理write系统调用的sys_write服务例程声明如下

该函数期望在栈顶找到fd,buf和count参数     在封装sys_write()的封装例程中,将会在ebx、ecx和edx寄存器中分别填入这些参数的值,然后在进入system_call时,SAVE_ALL会把这些寄存器保存在堆栈中,进入sys_write服务例程后,就可以在相应的位置找到这些参数

asmlinkage使得编译器不通过寄存器(x=0)而 使用堆栈传递参数

SAVE_ALL

设C库中封装的系统调用号为3的函数原形如下:

   int sys_func(int para1, int para2)
C编译器产生的汇编伪码如:
…movl  0x8(%esp),%ecx/*将用户态堆栈中的para2放入ecx
Movl 0x4(%esp),%ebx  /*#将用户态堆栈中的para1放入ebx
Movl $0x3,%eax /*系统调用号保存在eax中int$0x80 #引发系统调用
…
Movl %eax,errno /*将结果存入全局变量errno中
Movl $-1,%eax   /*eax置为-1,表示出错注

练习:添加一个系统调用mysyscall

功能要求

首先,自定义一个系统调用mysyscall ,它的功能是使用户的uid等于0 。然后,编写一段测试程序进行调用。

执行步骤如下

  1. 添加系统调用号
  2. 在系统调用表中添加相应的表项
  3. 实现系统调用服务例程
  4. 重新编译内核,启动新内核
  5. 编写一段测试程序检验实验结果

(1)添加系统调用号:它位于unistd.h,每个系统调用号都以“_NR_开头”,

  • 系统调用的编号命名为  __NR_mysyscall
  • 改写/usr/include/asm/unistd.h
240 #define __NR_llistxattr              233
241 #define __NR_flistxattr             234
242 #define __NR_removexattr            235
243 #define __NR_lremovexattr           236
244 #define __NR_fremovexattr           237
245 #define __NR_mysyscall          238

(2)在系统调用表中添加相应的表项

  • 内核中实现该系统调用的例程的名字    sys_mysyscall
  • 改写arch/i386/kernel/entry.S
398 ENTRY(sys_call_table)
399         .long SYMBOL_NAME(sys_ni_syscall)……
636         .long SYMBOL_NAME(sys_ni_syscall)
637         .long SYMBOL_NAME(sys_mysyscall)
638
639         .rept NR_syscalls-(.-sys_call_table)/4
640                 .long SYMBOL_NAME(sys_ni_syscall)
641         .endr 

3)实现系统调用服务例程   把一小段程序添加在kernel/sys.c

asmlinkage int sys_mysyscall(void)
{current->uid = current->euid = current->suid = current->fsuid = 0;return 0;
} 

(4)重新编译内核,启动新内核

(5)编写一段测试程序检验实验结果

#include <linux/unistd.h>
_syscall0(int,mysyscall)/* 注意这里没有分号 */
int main()
{mysyscall();printf(“This is my uid: %d. \n”, getuid());
}

_syscall1(int,print_info,int,testflag)

如果要在用户程序中使用系统调用函数,那么在主函数main前必须申明调用_syscall,其中1 表示该系统调用只有一个入口参数,第一个int 表示系统调用的返回值为整型,print_info为系统调用函数名,第二个int 表示入口参数的类型为整型,testflag为入口参数名。

Linux系统调用全过程详解相关推荐

  1. Linux系统调用SYSCALL_DEFINE详解

    Linux系统调用SYSCALL_DEFINE详解 Linux源码可以去这里 https://mirrors.edge.kernel.org/pub/linux/kernel/ 下载,本文是基于lin ...

  2. Linux 系统调用 Ptrace 详解

    From:https://blog.csdn.net/u012417380/article/details/60470075 Ptrace 详解:https://www.cnblogs.com/tan ...

  3. linux 系统调用函数详解,Linux系统调用之sysinfo函数解析

    [sysinfo系统调用] 功能描述: 获取系统总体统计信息. 用法: #include int sysinfo(struct sysinfo *info); 参数: info:指向sysinfo结构 ...

  4. Linux /dev目录详解和Linux系统各个目录的作用

    Linux /dev目录详解和Linux系统各个目录的作用 标签: linuxtcpfunctionclassfirefoxtimer 2012-01-11 23:08 45517人阅读 评论(2) ...

  5. Linux网络编程---详解TCP

    Linux网络编程---详解TCP的三次握手和四次挥手_shanghx_123的博客-CSDN博客_tcp的协议数据单元被称为 TCP协议详解(TCP报文.三次握手.四次挥手.TIME_WAIT状态. ...

  6. 红帽Linux故障定位技术详解与实例(1)

    红帽Linux故障定位技术详解与实例(1) 2011-09-28 14:26 圈儿 BEAREYES.COM 我要评论(0) 字号:T | T 在线故障定位就是在故障发生时, 故障所处的操作系统环境仍 ...

  7. 浏览器解析html全过程详解

    前端文摘:深入解析浏览器的幕后工作原理 关于浏览器解析html全过程详解 输入URL到浏览器接收返回的数据的整个过程 TCP报文格式详解 IP报文格式详解 Linux IO模式及 select.pol ...

  8. init.d,rc.d详解 Linux运行时详解

    Linux 启动时需要哪些步骤呢?本文将详细描述不同的运行级在启动中的作用. 对于那些在DOS/Win9x/NT 平台下的高级用户而言,Linux 似乎是一个怪物.没有config.sys ,没有 a ...

  9. 转:Linux 僵尸进程详解

    Linux 僵尸进程详解 转载:http://www.51testing.com/?uid-225738-action-viewspace-itemid-206225 1.僵尸进程概念: 僵尸进程(Z ...

最新文章

  1. 某程序员大佬北漂16年,从住地下室到身家千万,如今回老家躺平!
  2. BC:带你温习并解读《中国区块链技术和应用发展白皮书》—区块链发展生态
  3. windbg调试cpu占用率高的进程
  4. 安装Kerberos服务端和客户端
  5. php 7.1 openssl安装,介绍 php7.1 安装openssl扩展,php openssl
  6. 攻略:如何快速赚取积分,Get云栖大会资料
  7. CDH6.2 Linux离线安装
  8. 某个元素的距离页面的左边距_如何提高办公写作效率?先设置好页面上的这4类数据,准没错...
  9. 使用Python解析MNIST数据集
  10. 程序员,Linux 下如何避免从删库到跑路的悲剧?
  11. Hibernate 入门简单教程
  12. 《OpenCV 4.5计算机视觉开发实战:基于Python》图像处理技术
  13. Android之sdcard保存数据
  14. 数据链路层(帧)(二)
  15. C++小白如何入门?
  16. 【HDU 6608】Fansblog(威尔逊定理+逆元+快速乘+快速幂)
  17. PyQt学习随笔:QWidget的QFont的kerning、Antialiasing属性用途
  18. 最全Linux系统下载网站
  19. 优秀程序员的45个习惯[摘]
  20. IBM暑期实习笔试后总结

热门文章

  1. 第一视频欲打造国内最大的手机广告平台 MOBUS正式推出
  2. 电子商务常见盈利模式视频教程
  3. 提供曲库、评分、修音功能的K歌SDK-Android版本
  4. 菜鸟变大神,8个创意移动电源实例教程
  5. FREE TIME,FREE DEMURRAGE和FREE DETENTION的区别研究参考
  6. java多线程数据采集,【多线程数据采集课题】java采集网页数据方法
  7. com.microsoft.sqlserver.jdbc.SQLServerException: 传入的表格格式数据流(TDS)远程过程调用(RPC)协议流不正确。此 RPC 请求中提供了过多的参数。
  8. 微信突然公开阅读数背后
  9. 第四篇:Hyperion安装配置,这些细节你都知道吗
  10. position:absolute与relative的区别