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

news/2025/2/9 3:45:58/
在应用程序和硬件间设置一个额外层优点:
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.ppmy.cn/news/1009827.html

相关文章

在服务器上搭建gitlab

最终效果展示: 官方文档: 安装部署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 …

OSPF 动态路由协议 路由传递

影响OSPF路由选择的因素: 1.OSPF路由的开销值:宽带参考值默认为100. COST1000/接口带宽。此时接口 带宽的值可更改,更改后只改变参考数值,带宽仍然为初始值。 注意:更改COST需要 在路由的入方向,数据的出方…

vue2-vue项目中你是如何解决跨域的?

1、跨域是什么? 跨域本质是浏览器基于同源策略的一种安全手段。 同源策略(sameoriginpolicy),是一种约定,它是浏览器最核心也是最基本的安全功能。 所谓同源(即指在同一个域)具有以下三个相同点…

Godot 4 源码分析 - Path2D与PathFollow2D

学习演示项目dodge_the_creeps,发现里面多了一个Path2D与PathFollow2D 研究GDScript代码发现,它主要用于随机生成Mob var mob_spawn_location get_node(^"MobPath/MobSpawnLocation")mob_spawn_location.progress randi()# Set the mobs dir…

如何搭建一个成功的家具小程序

家具行业近年来发展迅猛,越来越多的消费者开始选择在小程序商城上购买家具。因此,制作一款家具小程序商城成为了许多家具商家的必然选择。那么,如何制作一款个性化、功能齐全的家具小程序商城呢?下面将为大家介绍一种简单且高效的…

TypeScript初学

文章转载:https://blog.csdn.net/weixin_46185369/article/details/121512287 写的很详细,适合初学者看看。 一、TypeScript是什么? 1.TypeScript简称:TS,是JavaScript的超集。简单来说就是:JS有的TS都有…

Servlet是什么和创建、配置第一个servlet

Servlet是什么和创建、配置第一个servlet servlet是什么 2、创建servlet 方式一: 方式二: 方式三:

vue获取近七天、月份、年份的起始日和结束日

vue获取近七天的起始日和结束日 例如:startDate: 2023-07-29 endDate: 2023-08-04 data() {return {startDate: null,endDate: null} }, mounted() {this.calculateDateRange(); }, methods: {calculateDateRange() {var currentDate new Date();var startDate …