深入理解Linux内核--系统调用

在应用程序和硬件间设置一个额外层优点:
1.使得编程更加容易
把用户从学习硬件设备的低级编程特性中解放出来
2.极大提高了系统的安全性
内核在试图满足某个请求前在接口级就可检查这种请求的正确性
3.接口使得程序更具有可移植性Unix系统通过向内核发出系统调用实现了用户态进程和硬件设备间的大部分接口

POSIX API和系统调用

API:
一个函数定义,说明了如何获得一个给定的服务
系统调用:
通过软中断向内核态发出一个明确的请求Unix系统给程序员提供了很多API的库函数。
libc的标准C库所定义的一些API引用了封装例程
通常下,每个系统调用应对应一个封装例程
而封装例程定义了应用程序使用的API一个API没必要对应一个特定的系统调用
首先,
API可能直接提供用户态的服务
其次,
一个单独的API函数可能调几个系统调用Posix标准针对API而不针对系统调用
判断一个系统是否与POSIX兼容要看它是否提供了一组合适的应用程序接口,
而不管对应的函数是如何实现的
事实上,
一些非Unix系统被认为是与POSIX兼容的
因为它们在用户态的库函数中提供了传统Unix能提供的所有服务从编程者观点看,
API和系统调用间的差别没关系:
唯一相关的事情就是函数名,参数类型,返回代码的含义从内核设计者观点看,
这种差别确实有关系,
因为系统调用属于内核,用户态的库函数不属于内核大部分封装例程返回一个整数,
其值的含义依赖于相应的系统调用
返回值-1通常表示内核不能满足进程的要求。
在libc库中定义的errno变量包含特定的出错码。

系统调用处理程序及服务例程

当用户态的进程调一个系统调用时,
CPU切换到内核态并开始执行一个内核函数。
在80x86体系结构中,
可用两种不同的方式调Linux的系统调用
两种方式的最终结果都是跳转到所谓系统调用处理程序的汇编语言函数因为内核实现了很多不同的系统调用
故进程必须传递一个名为系统调用号的参数来识别所需的系统调用
eax寄存器就用作此目的
当调用一个系统调用时通常还要传递另外的参数所有的系统调用都返回一个整数值
这些返回值与封装例程返回值的约定不同
在内核中
正数或0表示系统调用成功结束
负数表示一个出错条件
后一种情况下,这个值就是存放在errno中必须返回给应用的负出错码
内核没设置或使用errno变量,
而封装例程从系统调用返回后设置这个变量
系统调用处理程序与其他异常处理程序结构类似
执行:
1.在内核态保存大多数寄存器的内容
2.调名为系统调用服务例程的相应C函数来处理系统调用
3.退出系统调用处理程序:
用保存在内核栈中的值加载寄存器
CPU从内核态切换回用户态
xyz系统调用对应的服务例程名通常是sys_xyz
1.在应用程序中系统调用的调用
xyz()
2.在Libc标准库中的封装例程
xyz()
{...SYSCALL...
}
3.系统调用处理程序--内核态
system_call:...sys_xyz()...SYSEXIT
4.系统调用服务例程--内核态
sys_xyz()
{...
}
为了把系统调用号与相应的服务例程关联起来
内核利用了一个系统调用分派表
这个表存放在sys_call_table数组
有NR_syscalls个表项
第n个表项包含系统调用号为n的服务例程的地址NR_syscalls只是对可实现的系统调用最大个数的静态限制,
并不表示实际已实现的系统调用个数
实际上,
分派表中的任意一个表项也可包含sys_ni_syscall函数的地址
这个函数是"未实现"系统调用的服务例程
它仅仅返回出错码-ENOSYS

进入和退出系统调用

本地应用可通过两种不同方式调系统调用:
1.执行int $0x80
在Linux内核老版本,
这是从用户态切换到内核态的唯一方式
2.执行sysenter
在Intel Pentium 2中引入了这条指令
Linux 2,6内核支持此指令
同样,内核可通过两种方式从系统调用退出,从而使CPU切换回用户态
1.执行iret
2.执行sysexit
但支持进入内核的两种不同方式不像看起来那么简单
1.内核必须既支持只使用int $0x80
也支持sysenter
2.使用sysenter的标准库必须能处理仅支持int $0x80的旧内核
3.内核和标准库必须既能运行在不包含sysenter指令的旧处理器上
也能运行在包含它的新处理器上

通过int $0x80发出系统调用

向量128对应于内核入口点
在内核初始化期间调的函数trap_init用下面的方式建立对应于向量128的中断描述符表项:
set_system_gate(0x80, &system_call);
该调用把下列值存入这个门描述符的相应字段
Segment Selector内核代码段__KERNEL_CS的段选择符
Offset指向system_call系统调用处理程序的指针
Type15,表示这个异常是一个陷阱。相应的处理程序不禁止可屏蔽中断
DPL3。允许用户态进程调这个异常处理程序
当用户态进程发出int $0x80时,
CPU切换到内核态并开始从地址system_call处开始执行指令

system_call

首先把系统调用号和这个异常处理程序可用到的所有CPU寄存器保存到相应栈,
不包含由控制单元已自动保存的eflags,cs,eip,ss和esp在第4章已经讨论的SAVE_ALL,也在ds和es中装入内核数据段的段选择符
system_call:pushl %eaxSAVE_ALLmovl $0xffffe000, %ebxandl %esp, %ebx
随后,
这个函数在ebx中存放当前进程的thread_info的地址
这是通过获得内核栈指针的值并把它取整到4KB或8KB的倍数而完成的接下来,
system_call检查thread_info结构flag字段的TIF_SYSCALL_TRACE和TIF_SYSCALL_AUDIT之一是否被设置为1,
即检查是否有某一调试程序正在跟踪执行程序对系统调用的调用
如是,
则system_call两次调do_syscall_trace:
一次正好在这个系统调用服务例程执行前,
一次在其之后
这个函数停止current
并因此允许调试进程收集关于current的信息然后, 
对用户进程传递来的系统调用号进行有效性检查
如这个号大于或等于系统调用分派表中的表项数
系统调用处理程序就终止
	cmpl $NR_syscalls, %eaxjb nobadsysmovl $(-ENOSYS), 24(%esp)jmp resume_userspace
nobadsys:
如系统调用号无效
该函数就把-ENOSYS值存放在栈中曾保存eax寄存器的单元中
(从当前栈顶开始偏移量为24的单元)
然后跳到resume_userspace
这样,
当进程恢复它在用户态的执行时,
会在eax中发现一个负的返回码最后,调与eax中所包含的系统调用号对应的特定服务例程
call *sys_call_table(0, %eax, 4)
因为分派表中的每个表项占4个字节
故首先把系统调用号乘以4
再加上sys_call_table分派表的起始地址
然后从这个地址单元获取指向服务例程的指针
内核就找到了要调用的服务例程

从系统调用退出

当系统调用服务例程结束时,
system_call从eax获得它的返回值,
把这个返回值存放在曾保存用户态eax寄存器值的那个栈单元的位置上
movl %eax, 24(%esp)
故,用户态进程将在eax中找到系统调用的返回码
然后,
system_call关闭本地中断并检查当前进程的thread_info结构中的标志
cli
movl 8(%ebp), %ecx
testw $0xffff, %cx
je restore_all
flags字段在thread_info结构中的偏移量为8
所有标志都没设置,函数就跳到restore_all
restore_all恢复保存在内核栈中的寄存器的值,
并执行iret以重新开始执行用户态进程只要任何一种标志被设置
则就要在返回用户态之前完成一些工作
如TIF_SYSCALL_TRACE被设置,
system_call就第二次调do_syscall_trace
然后跳到resume_userspace
否则,如TIF_SYSCALL_TRACE没被设置,
就跳到work_pending在resume_userspace和work_pending处的代码检查重新调度请求,虚拟8086模式,挂起信号,单步执行
最终跳到restore_all处以恢复用户态进程的运行

通过sysenter发出系统调用

int由于要执行几个一致性检查和安全性检查
故速度慢
在Intel文档中被称为"快速系统调用"的sysenter指令,
提供了一种从用户态到内核态的快速切换方法

sysenter

使用三种特殊的寄存器,它们需装入以下信息
SYSENTER_CS_MSR内核代码段的段选择符
SYSENTER_EIP_MSR内核入口点的线性地址
SYSENTER_ESP_MSR内核堆栈指针
执行sysenter指令时,CPU控制单元:
1.把SYSENTER_CS_MSR内容拷贝到cs
2.把SYSENTER_EIP_MSR内容拷贝到eip
3.把SYSENTER_ESP_MSR内容拷贝到esp
4.把SYSENTER_CS_MSR加8值装入ss
故,CPU切换到内核态并开始执行内核入口点的第一条指令在内核初始化期间,
一旦系统中的每个CPU执行enable_sep_cpu
三个特定于模型的寄存器就由该函数初始化
enable_sep_cpu执行:
1.把内核代码__KERNEL_CS的段选择符写入SYSENTER_CS_MSR
2.把下面要说明的函数sysenter_entry的线性地址写入SYSENTER_CS_EIP
3.计算本地TSS末端的线性地址,把这个值写入SYSENTER_CS_ESP
对SYSENTER_CS_ESP的设置有必要进行一些说明。
系统调用开始的时候,
内核堆栈是空的。
故,esp寄存器应指向4KB或8KB内存区域的末端
该内存区域包括内核堆栈和当前进程的描述符
因为用户态的封装例程不知这个内存区域的地址
所以它不能正确设置此寄存器另一方面,
必须在切换到内核态前设置该寄存器的值
故,内核初始化这个寄存器以便为本地CPU的任务状态段编址每次进程切换时,
内核把当前进程的内核栈指针保存到本地TSS的esp0
这样,系统调用处理程序读esp,
计算本地TSS的esp0
把正确的内核堆栈指针装入esp

vsyscall页

只要CPU和Linux都支持sysenter
标准库libc中的封装函数就可使用它这个兼容性问题需要复杂的解决方案
本质上,
初始化阶段sysenter_setup建立一个称为vsyscall页的页框
其中包括一个小的EFL共享对象(一个小的EFL动态链接库)
当进程发出execve开始执行一个ELF程序时,
vsyscall页中的代码会自动被链接到进程的地址空间
vsyscall页中的代码使用最有用的指令发出系统调用sysenter_setup为vsyscall页分配一个新页框
把它的物理地址与FIX_VSYSCALL固定映射的线性地址相关联
函数sysenter_setup把预先定义好的一个或两个EFL共享对象拷贝到该页
1.如CPU不支持sysenter
sysenter_setup建立一个包含下列代码的vsyscall页
__kernel_vsyscall:int $0x80ret
2.否则,如CPU的确支持sysenter,sysenter_setup建立一个包括下列代码的vsyscall页
__kernel_vsyscall:pushl %ecxpushl %edxpushl %ebpmovl %esp, %ebpsysenter
当标准库中的封装例程必须调系统调用时,
调__kernel_vsyscall,不管它的实现代码是什么最后一个兼容问题是由于老版本Linux内核不支持sysenter
此情况下
内核当然不建立vsyscall页
且函数__kernel_vsyscall不会被链接到用户态进程的地址空间
当新近的标准库识别这种状况后,
简单执行int $0x80调系统调用

进入系统调用

当用sysenter发出系统调用时,依次执行下述:
1.标准库的封装例程把系统调用号装入eax寄存器
调__kernel_vsyscall
2.__kernel_vsyscall把ebp,edx,ecx的内容保存到用户态堆栈
(系统调用处理程序将使用这些寄存器)
把用户栈指针拷贝到ebp
执行sysenter
3.CPU从用户态切换到内核态
内核开始执行sysenter_entry
(由SYSENTER_EIP_MSR指向)
4.sysenter_enter执行下述:
4.1.建立内核堆栈指针
movl -508(%esp), %esp
开始时,
esp寄存器指向本地TSS的第一个位置
本地TSS的大小为512字节。
故,sysenter把本地TSS中偏移量为4处的字段的内容(esp0)装入esp
esp0总数存放当前进程的内核堆栈指针
4.2.打开本地中断
sti
4.3.把用户数据段的段选择符,当前用户栈指针,eflags,用户代码段的段选择符及从系统调用退出时要执行的指令的地址保存到内核堆栈
pushl $(__USER_DS)
pushl %ebp
pushfl
pushl $(__USER_CS)
pushl $SYSENTER_RETURN
这些指令仿效int所执行的一些操作
4.4.把原来由封装例程传递的寄存器的值恢复到ebp
movl (%ebp), %ebp
上面这指令完成恢复工作,
因为__kernel_vsyscall把ebp原始值存入用户态堆栈
在随后把用户堆栈指针的当前值装入ebp
4.5.通过执行一系列指令调用系统调用处理程序,
这些指令与前面通过int $0x80指令发出系统调用
一节描述的在system_call处开始的指令是一样的

退出系统调用

系统调用服务例程结束时,
sysenter_entry本质上执行与system_call相同的操作
首先,
它从eax获得系统调用服务例程的返回码
将返回码存入内核栈中保存用户态eax寄存器值的位置
然后,
函数禁止本地中断,
检查current的thread_info结构中的标志如有任何标志被设置
则在返回到用户态前需完成一些工作
为避免代码复制
函数跳到resume_userspace或work_pending处
最后,
汇编语言指令iret从内核堆栈中取五个参数
这样CPU切换安东用户态并开始执行SYSENTER_RETURN标记处代码如sysenter_entry确定标志被清0
它就快速返回到用户态
movl 40(%esp), %edx
movl 52(%esp), %ecx
xorl %ebp, %ebp
sti
sysexit
把在上一节由sysenter_entry函数在第4c步保存的一对堆栈值加载到edx和ecx
edx获得SYSENTER_RETURN标记处地址
而ecx获得当前用户数据栈的指针

sysexit

sysexit是与sysenter配对的指令
它允许从内核态快速切换到用户态
执行此指令时,
CPU控制单元执行下述:
1.把SYSENTER_CS_MSR的值加16结果加载到cs
2.把edx寄存器的内容拷贝到eip
3.把SYSENTER_CS_MSR中的值加24得到的结果加载到ss
4.把ecx的内容拷贝到esp
因为SYSENTER_CS_MSR加载的是内核代码的选择符
cs加载的是用户代码的选择符
ss加载的用户数据段的选择符
结果,CPU从内核态切换到用户态
开始执行其地址存放在edx中的那条指令

SYSENTER_RETURN

存放在vsyscall页
当通过sysenter进入的系统调用被iret或sysexit终止时,
该页框中的代码被执行该代码恢复保存在用户态堆栈中的ebp,edx,ecx寄存器的原始内容
并把控制权返回给标准库中的封装例程
SYSENTER_RETURN:popl %ebppopl %edxpopl %ecxret

参数传递

系统调用通常也许输入/输出参数
这些参数可能是实际的值
也可能是用户态进程地址空间的变量
甚至是指向用户态函数的指针的数据结构地址因为system_call和sysenter_entry是Linux中所有系统调用的公共入口点
故每个系统调用至少有一个参数,
即通过eax寄存器传递来的系统调用号
如,如一个应用程序调fork,
则在执行int $0x80或sysenter之前就把eax置为2因为这个寄存器的值由libc中的封装例程进行
故程序员通常不关系系统调用号fork系统调用并不需其他的参数
不过,很多系统调用确实需由应用程序明确传递另外的参数
如mmap可能需多达6个额外参数普通c函数参数传递通过把参数值写入活动的程序栈
系统调用是横跨用户和内核的特殊函数。
故,既不能用用户态栈也不能用内核态栈发出系统调用前,
系统调用的参数被写入CPU寄存器
在调用系统调用服务例程前,
内核再把存放在CPU中的参数拷贝到内核态堆栈
因为,系统调用服务例程是普通的c函数为何内核不直接把用户态的栈拷贝到内核态的栈?
1.同时操作两个栈比较复杂
2.寄存器的使用使系统调用处理程序的结构与其他异常处理程序的结构类似
然而,
为了用寄存器传递参数,需满足:
1.每个参数的长度不能超过寄存器的长度, 即32位
2.参数的个数不能超过6个(除eax中传递的系统调用号)
因为80x86处理器的寄存器数量有限确实存在多于6个参数的系统调用
在此情况下,
用一个单独的寄存器指向进程地址空间中这些参数值所在的一个内存区
编程者不关系此工作区
调封装例程时,
参数被自动保存在栈。
封装例程将找到合适的方式把参数传递给内核用于存放系统调用号和系统调用参数的寄存器是:
eax:系统调用号
ebx
ecx
edx
esi
edi
ebo
system_call和sysenter_entry使用

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/24819.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何保证Redis缓存和数据库的一致性问题

熟练掌握Redis缓存技术? 那么请问Redis缓存中有几种读写策略,又是如何保证与数据库的一致性问题 今天来聊一聊常用的三种缓存读写策略 首先我们来思考一个问题 服务端到底是先更新db还是先更新cache 如果先更新缓存 写 先更新缓存再更新数据库 首先…

篇二:工厂方法模式:灵活创建对象

篇二:“工厂方法模式:灵活创建对象” 开始本篇文章之前先推荐一个好用的学习工具,AIRIght,借助于AI助手工具,学习事半功倍。欢迎访问:http://airight.fun/。 另外有2本不错的关于设计模式的资料&#xff…

Lombok 的安装与使用

文章目录 一、什么是 Lombok1.1 Lombok 的概念1.2 为什么使用 Lombok1.3 Lombok 的相关注解 二、Lombok 的安装2.1 引入依赖2.2 安装插件 三、Lombok 的使用案例四、Lombok 的原理 一、什么是 Lombok 1.1 Lombok 的概念 Lombok(“Project Lombok”)是一…

MySQL 窗口函数

聚合函数作为窗口函数 设聚合函数为op语法结构: op(字段名A) over(partition by 字段名B order by 字段名C rows between D1 and D2) 其中: partition by:按照某一字段将数据进行分组 order by:按照某一字段将数据进行排序&…

Java实现八皇后问题

八皇后问题说明 八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯贝瑟尔于 1848 年提出:在 88 格的国际象棋上摆放八个皇后,使其不能互相攻击,即:任意两个皇后都不…

HTTP常用状态码及其含义

HTTP常用状态码及其含义 1XX:信息,服务器收到请求,需要请求者继续执行操 状态码状态码英文名称中文描述100Continue继续。客户端应继续其请求101Switching Protocols切换协议。服务器根据客户端的请求切换协议。只能切换到更高级的协议&…

筛选给定范围内的日志

目录 1.时间戳 2.实例 1.首先创建ubuntu.log日志 2.写dem.awk创建规则 3.筛选 1.时间戳 一个能表示一份数据在某个特定时间之前已经存在的、 完整的、 可验证的数据,通常是一个字符序列,唯一地标识某一刻的时间。 awk提供了mktime()函数,它可以将时间…

31 对集合中的字符串,按照长度降序排列

思路&#xff1a;使用集合的sort方法&#xff0c;新建一个Comparator接口&#xff0c;泛型是<String>&#xff0c;重写里面的compare方法。 package jiang.com; import java.util.Arrays; import java.util.Comparator; import java.util.List;public class Practice4 {…

Spring Security 和 Apache Shiro 登录安全架构选型

Spring Security和Apache Shiro都是广泛使用的Java安全框架&#xff0c;它们都提供了许多功能来保护应用程序的安全性&#xff0c;包括身份验证、授权、加密、会话管理等。 Spring Security和Apache Shiro都是非常常用的登录安全框架,两者在登录安全架构的选型上各有特点: Sp…

WSL 2 installation is incomplete的解决方案

问题描述 解决方案 在Windows功能中开启Hyper-v 如果没有Hyper-v选项&#xff0c;新建文本粘贴以下内容后以.cmd为后缀保存后执行即可 pushd "%~dp0" dir /b %SystemRoot%\servicing\Packages\*Hyper-V*.mum >hyper-v.txt for /f %%i in (findstr /i . hyper-v.t…

C语言进阶-3

1、程序为什么需要内存 1.1、计算机程序运行的目的 计算机为什么需要编程&#xff1f;编程已经编了很多年&#xff0c;已经写了很多程序&#xff0c;为什么还需要另外写程序&#xff1f;计算机有这个新的程序到底为了什么&#xff1f; 程序的目的是为了去运行&#xff0c;程序…

C++ 派生类的拷贝构造函数

当存在类的继承关系时&#xff0c;对于一个类&#xff0c;如果程序员没有编写拷贝构造函数&#xff0c;编译系统会在必要时自动生成一个隐含的拷贝构造函数&#xff0c;这个隐含的拷贝构造函数会自动调用基类的拷贝构造函数&#xff0c;然后对派生类新增的成员对象一一执行拷贝…

git merge 和rebase区别

Merge the incoming changes into the current branch 找到两个分支的祖先 commit&#xff0c;然后将公共分支最新版合并到自己的分支&#xff0c;形成一个新的 commit 提交&#xff0c;用图表示如下。 Rebase the current branch on top of the incoming Rebase 则是重新基于…

ubuntu samba 配置常见问题

samba配置&#xff1a; sudo vi /etc/samba/smb.conf [xxx 共享文件名] comment share folder browseable yes writable yes guest ok yes path /workdir/code/favarite create mask 0777 directory mask 0777 sudo /etc/init.d/smbd restart 重启smb服务 以上操作…

在服务器上搭建gitlab

最终效果展示&#xff1a; 官方文档&#xff1a; 安装部署GitLab服务 1.在服务器上下载gitlab wget https://mirrors.tuna.tsinghua.edu.cn/gitlab-ce/yum/el7/gitlab-ce-12.9.0-ce.0.el7.x86_64.rpm rpm -ivh gitlab-ce-12.9.0-ce.0.el7.x86_64.rpm 2.编辑站点位置 vim …

Kotlin 1.9.0 发布:带来多项新特性,改进 Multiplatform/Native 支持

新特性 Kotlin 的最新版本引入了许多新的语言特性&#xff0c;包括用于开放范围的…<操作符、扩展正则表达式等。此外&#xff0c;它还改进了 Kotlin Multiplatform 和 Kotlin/Native 支持。 Kotlin 1.9 稳定了与枚举类关联的 entries 属性&#xff0c;它会返回所定义的枚…

Spring Boot Logback日志格式改为JSON

在阿里云、或者日志分析时使用JSON格式输出日志更加方便。 依赖 增加Logbak JSON解析依赖。 另外需要注意的是JSON格式输出依赖Jackson&#xff0c;根据工程情况按需添加Jackson依赖。 <!--日志--><dependency><groupId>ch.qos.logback.contrib</grou…

嵌入式软件C/C++(技术面试题)

一&#xff0c;网络 1&#xff0c;TCP窗口机制 TCP&#xff08;传输控制协议&#xff09;是一种可靠的、面向连接的传输层协议。其中的窗口机制是TCP协议中的一项重要功能&#xff0c;用于控制数据在发送和接收之间的流程。 TCP窗口机制是利用滑动窗口的方式来进行拥塞控制和…

基于Python的Locust 性能测试指北(万字长文详解)

目录 Locust 我们为什么选择locust Locust的核心部件 Locust内部运行调用链路 locust 实践 检查点&#xff08;断言&#xff09; 权重比例 参数化 Tag 集合点 分布式 docker 运行locust 高性能 FastHttpUser 测试gRPC等其他协议 其他 资料获取方法 Locust Locu…

Squeeze-and-Excitation Networks阅读笔记一

文章目录 Abstract1 INTRODUCTION Abstract 卷积算子&#xff08;convolution operator&#xff09;是卷积神经网络&#xff08;cnn&#xff09;的核心组成部分&#xff0c;它使网络能够通过融合每层局部接受域内的空间和通道信息来构建信息特征。广泛的先前研究已经调查了这种…