Linux0.11系统调用:预备知识

系统调用

预备知识

目标:了解系统调用的流程,在Linux 0.11上添加两个系统调用,并编写两个简单的应用程序测试它们。

对应章节:同济大学赵炯博士的《Linux内核0.11完全注释(修正版V3.0)》的第5.5节

下面就针对这一节做一些笔记

系统调用的概念

系统调用(通常称为 syscalls)是 Linux 内核与上层应用程序进行交互通信的唯一接口。这些接口为了应用级的代码能够在不同的操作系统上都可以运行,就需要有标准去限制系统调用接口的标准。不同操作系统的系统调用接口都要按照这个标准来。

POSIX(Portable Operating System Interface for Computing Systems)由IEEE开发,是一个标准族: 1003.1, 2003…于保证编制的应用程序可以在源代码一级上在多种操作系统上移植和运行。

image-20250429162117425

那用户态程序想要访问内核资源怎么办呢?

  • 从对中断机制的说明可知,用户程序通过直接或间接(通过库函数)调用中断 int 0x80
  • 并在 eax寄存器中指定系统调用功能号,即可使用内核资源,包括系统硬件资源。
  • 但是,这个太麻烦了还需要记住系统调用的功能号,还需设置中断,标准接口定义的 C 函数库中的函数间接地使用内核的系统调用,C 函数库已经帮封装好了
  • 系统调用的实现:(C 函数库已经给封装好了)
    • (1) 用户程序中写上一段包含int指令的代码
    • (2) OS写中断处理代码,获取想调程序的编号(系统调用编号)
    • (3) OS根据编号转去执行相应的代码

image-20250429163350968

上面提到的系统调用的功能号系统调用编号是什么?

  • 指的是同一个东西:在 Linux 内核中,每个系统调用都具有唯一的一个系统调用功能号。

  • 这些号定义在文件include/unistd.h 中第 60 行开始处。例如,write 系统调用的功能号是 4,定义为符号__NR_write。

    // 以下是内核实现的系统调用符号常数,用于作为系统调用函数表中的索引值。( include/linux/sys.h )
    #define __NR_setup 0		/* used only by init, to get system going */
    /* __NR_setup 仅用于初始化,以启动系统 */
    #define __NR_exit 1
    #define __NR_fork 2
    #define __NR_read 3
    #define __NR_write 4 
    
  • 这些系统调用号对应于 include/linux/sys.h 中定义的系统调用处理程序指针数组表 sys_call_table[]中项的索引值。因此 write()系统调用的处理程序指针就位于该数组的项 4 处。

    extern int sys_setup ();	// 系统启动初始化设置函数。 (kernel/blk_drv/hd.c,71)
    extern int sys_exit ();		// 程序退出。 (kernel/exit.c, 137)
    extern int sys_fork ();		// 创建进程。 (kernel/system_call.s, 208)
    extern int sys_read ();		// 读文件。 (fs/read_write.c, 55)
    extern int sys_write ();	// 写文件。 (fs/read_write.c, 83)
    // 系统调用函数指针表。用于系统调用中断处理程序(int 0x80),作为跳转表。
    fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,sys_write, sys_open,省略了
    };
    

这里有很多的系统调用处理函数,他们有几个相同点:

  • 系统调用执行的结果返回值。通常负值表示错误,而 0 则表示成功。

    在出错的情况下,错误的类型码被存放在全局变量 errno 中。通过调用库函数 perror(),我们可以打印出该错误码对应的出错字符串信息。

系统调用处理过程

image-20250429172248493

现在以read系统调用为例,来说明这个过程

首先,在include/unistd.h文件里面有个read函数的声明

// 对应各系统调用的函数原型定义。int read(int fildes, char * buf, off_t count);

但是,操作系统是负责写系统调用的,是要提供系统调用sys_read(),在文件fs/read_write.c中。接下来给出从read()->sys_read()的具体流程。

前面给出了read()的声明,下面得找到定义,Linux0.11的源码好像没有找到,但是在lib文件夹有一些其他的类似的函数定义例如write()。代码如下,这里用到了_syscall3宏,把这个宏展开之后,得到write()的定义。

write.s文件
#include <set_seg.h>
#define __LIBRARY__
// Linux 标准头文件。定义了各种符号常数和类型,并申明了各种函数。
// 如定义了__LIBRARY__,则还包括系统调用号和内嵌汇编_syscall0()等。
#include <unistd.h> 写文件系统调用函数。
// 该宏结构对应于函数:int write(int fd, const char * buf, off_t count)
// 参数:fd - 文件描述符;buf - 写缓冲区指针;count - 写字节数。
// 返回:成功时返回写入的字节数(0 表示写入0 字节);出错时将返回-1,并且设置了出错号。
_syscall3(int,write,int,fd,const char *,buf,off_t,count)展开之后:
int write(int fd, const char *buf, off_t count)
{long __res;__asm__ volatile ("int $0x80": "=a" (__res): "0" (__NR_write), "b" ((long)(fd)), "c" ((long)(buf)), "d" ((long)(count)));if (__res >= 0)return (int) __res;errno = -__res;return -1;
}

image-20250429202424816

接下来介绍宏_syscalln():这个宏完成了c标准库函数的定义,也就是对相关的系统调用sys_函数名的封装。

其中 n 代表携带的参数个数,可以分别 0 至 3。最多可以直接传递 3 个参数。其中寄存器 eax 中存放着系统调用号,而携带的参数可依次存放在寄存器 ebx、ecx 和 edx 中。所以用户程序能够向内核最多直接传递3个参数,当然也可以不带参数。下面给出_syscall3()的代码

#define _syscall3(type,name,atype,a,btype,b,ctype,c) \
type name(atype a,btype b,ctype c) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \: "=a" (__res) \: "0" (__NR_##name),"b" ((long)(a)),"c" ((long)(b)),"d" ((long)(c))); \
if (__res>=0) \return (type) __res; \
errno=-__res; \
return -1; \
}

依然以read为例来说明宏_syscall3()的作用

GCC 内联汇编(AT&T格式)学习资源:https://zhuanlan.zhihu.com/p/606376595

#define --LIBRARY-- 
#include <unistd.h>
-syscall3(int, read, int, fd, char *, buf, int, n) 展开之后int read(int fd, char *buf, int n)
{// 存储系统调用返回值long __res;// 内联汇编触发系统调用,volatile:编译器不优化__asm__ volatile ("int $0x80"          // 触发系统调用的中断指令: "=a" (__res)       // 输出,系统调用返回值存入 __res "=a" 表示使用寄存器 eax 来传递返回值,"=" 表示只写操作: "0" (__NR_read),   // 输入,"0" 表示使用和输出操作数列表中编号为 0 的操作数相同的寄存器(即 eax)"b" ((long)(fd)),  // ebx 传递文件描述符"c" ((long)(buf)), // ecx 传递缓冲区指针"d" ((long)(n))    // edx 传递要读取的字节数);// 系统调用成功,返回读取字节数if (__res >= 0)return (int)__res;// 系统调用失败,设置错误码errno = -__res;return -1;
}

接下来,将展开后的代码分成三部分

  • 首先,传入给eax寄存器一个系统调用号__NR_read,这个前文已经说明,__NR_read=3sys_read()这个系统调用函数在sys_call_table[]中项的索引值,有了这个才能定位到系统调用函数。输出的结果也使用寄存器 eax 来传递返回值

  • int $0x80:进入系统调用中断程序在kernel/system_call.s中。下面是中断代码:

    `kernel/system_call.s`中
    # ====================== 系统调用处理流程 ====================== #
    # 入口:用户态执行int 0x80后CPU跳转至此
    .align 2
    system_call:cmpl $nr_system_calls-1,%eax   # 1. 校验系统调用号范围ja bad_sys_call                # 超过最大值跳错误处理# ==== 保存用户态上下文 ==== #push %ds                       # 2. 保存用户数据段push %es                       # 保存用户附加段push %fs                       # 保存特殊用途段pushl %edx                     # 系统调用参数3pushl %ecx                     # 参数2pushl %ebx                     # 参数1# ==== 设置内核环境 ==== #movl $0x10,%edx                # 3. 内核数据段选择子(GDT[2], RPL=0)mov %dx,%ds                    # 设置ds/es为内核数据段mov %dx,%esmovl $0x17,%edx                # 用户数据段选择子(LDT[2], RPL=3)mov %dx,%fs                    # fs保持用户数据访问能力# ==== 执行系统调用 ==== #call *sys_call_table(,%eax,4)  # 4. 查系统调用表执行对应函数# eax*4因为函数指针占4字节# ==== 调度检查 ==== #pushl %eax                     # 保存系统调用返回值movl current,%eax              # 获取当前进程task_struct指针cmpl $0,state(%eax)           # 5. 检查进程状态(0=就绪态)jne reschedule                 # 非就绪态跳调度cmpl $0,counter(%eax)         # 检查时间片剩余je reschedule                  # 时间片耗尽跳调度# ==================== 系统调用返回路径 ==================== #
    ret_from_sys_call:movl current,%eax             cmpl task,%eax                # 6. 是否是初始任务0?je 3f                         # 是则跳过信号处理cmpw $0x0f,CS(%esp)           # 检查原CS是否是用户态代码段jne 3fcmpw $0x17,OLDSS(%esp)        # 检查原SS是否是用户态堆栈段jne 3f# ==== 信号处理 ==== #movl signal(%eax),%ebx        # 7. 获取待处理信号位图movl blocked(%eax),%ecx       # 获取阻塞信号掩码notl %ecx                     # 反转掩码(允许通过的信号)andl %ebx,%ecx                # 计算有效信号bsfl %ecx,%ecx                # 扫描最低有效位je 3f                         # 无信号则跳过btrl %ecx,%ebx                # 清除已处理信号位movl %ebx,signal(%eax)        # 更新信号位图incl %ecx                     # 信号编号转为1-basedpushl %ecx                    # 参数压栈call do_signal                # 8. 调用信号处理函数popl %eax# ==== 恢复上下文 ==== #
    3:  popl %eax                     # 恢复系统调用返回值popl %ebx                     # 9. 逆向弹出寄存器popl %ecxpopl %edxpop %fspop %espop %dsiret                          # 10. 中断返回用户态# ====================== 错误处理路径 ====================== #
    .align 2
    bad_sys_call:movl $-1,%eax                 # 返回-1表示错误iret                          # 直接返回用户态# ====================== 进程调度路径 ====================== #
    .align 2
    reschedule:pushl $ret_from_sys_call      # 将返回地址压栈jmp schedule                  # 跳转到调度函数

    这里的int 0x80是系统调用中断是在哪设置呢?为什么int 0x80就跳到_system_call执行呢?

    • 通常,异常中断处理过程(int0 --int 31)都在 traps.c 的初始化函数中进行了重新设置(kernl/traps.c,181)
    • 而系统调用中断 int128 则在调度程序初始化函数中进行了重新设置(kernel/sched.c,385)。
    // 调度程序的初始化子程序。
    void sched_init (void)
    {// 设置时钟中断处理程序句柄(设置时钟中断门)。set_intr_gate (0x20, &timer_interrupt);// 修改中断控制器屏蔽码,允许时钟中断。outb (inb_p (0x21) & ~0x01, 0x21);// 设置系统调用中断门。set_system_gate (0x80, &system_call);
    }
    
  • 上面的中断系统调用函数中call [_sys_call_table + eax * 4] ,直接跳转到sys_read()函数位置完成了系统调用。

下面给出整个read()->sys_read()的简易流程图

┌─────────────┐            ┌───────────────┐
│  用户态      │            │   内核态       │
├─────────────┤            ├───────────────┤
│ 1. read()   │            │               │
└──────┬──────┘            │               │▼                   │               │
┌──────┴───────┐           │               │
│ 2. int 0x80  ├───中断───▶ │3. system_call│
│ (NR=3 in EAX)│           │  - 检查NR有效性 │
└──────────────┘           │  - 保存寄存器   │▼               │┌─────┴─────┐         ││4.查系统调用表			|        │sys_call_table       │       │[NR=3] → sys_read    │└─────┬─────┘         ││               │▼               │┌─────┴─────┐         ││5.sys_read()         │└─────┬─────┘         ││               │▼               │┌─────┴─────┐         ││ 返回用户态  │         ││ (iret指令) │         │└───────────┘         │

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

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

相关文章

如何防止 ES 被 Linux OOM Killer 杀掉

当 Linux 系统内存不足时&#xff0c;内核会找出一个进程 kill 掉它释放内存&#xff0c;旨在保障整个系统不至于崩溃。如果 ES 按照最佳实践去实施部署&#xff0c;会保留一半的内存&#xff0c;不至于发生此类事情。但事情总有例外&#xff0c;有的朋友可能 ES 和其他的程序部…

swagger2升级至openapi3的利器--swagger2openapi

背景&#xff1a; 因为项目需要升级JDK&#xff0c;涉及到swagger2升级至openapi3的情况。由于swagger 2和openapi 3的语法差距太大&#xff0c;需要对yaml进行升级。无奈单个yaml文件的内容太大&#xff0c;高至4万多行&#xff0c;手动进行语法的转换肯定是不可能了&#xff…

在yolo中Ultralytics是什么意思呢?超越分析的智能

在YOLO&#xff08;You Only Look Once&#xff09;目标检测框架中&#xff0c;Ultralytics 是一家专注于计算机视觉和机器学习技术的公司&#xff0c;同时也是YOLO系列模型&#xff08;如YOLOv5、YOLOv8等&#xff09;的官方开发和维护团队。以下是关键点解析&#xff1a; 1. …

【阿里云大模型高级工程师ACP习题集】2.7 通过微调增强模型能力 (上篇)(⭐️⭐️⭐️ 重点章节!!!)

习题集: 【单选题】在大模型微调中,与提示工程和RAG相比,微调的独特优势在于( ) A. 无需外部工具即可提升模型表现 B. 能让模型学习特定领域知识,提升底层能力 C. 可以更高效地检索知识 D. 能直接提升模型的知识边界,无需训练 【多选题】以下关于机器学习和传统编程的说…

CuML + Cudf (RAPIDS) 加速python数据分析脚本

如果有人在用Nvidia RAPIDS加速pandas和sklearn等库&#xff0c;请看我这个小示例&#xff0c;可以节省你大量时间。 1. 创建环境 请使用uv&#xff0c;而非conda/mamba。 # install uv if not yetcurl -LsSf https://astral.sh/uv/install.sh | shuv init data_gpucd data_g…

2-SAT之完美塔防

小N最近喜欢玩一款塔防游戏。 题目描述 这款游戏的棋盘是一个 nm 的网格&#xff0c;每个格子上会有以下类型物件&#xff1a; A 型炮台&#xff1a;会向上下两个方向同时发射激光&#xff0c;符号为 |;B 型炮台&#xff1a;会向左右两个方向同时发射激光&#xff0c;符号为…

【android bluetooth 案例分析 03】【PTS 测试 】【PBAP/PCE/SSM/BV-02-C】

1. 测试介绍 PBAP/PCE/SSM/BV-02-C [PCE Closes a PBAP Session] 1. Test Purpose Verify that the PCE can terminate a PBAP session. 2. Initial Condition IUT: The IUT is engaged in a PBAP session with the Lower Tester.Lower Tester: The Lower Tester is engag…

ArcGIS:开启洪水灾害普查、评估与制图新征程

技术点目录 一、洪水普查技术规范解读二、ArcGIS介绍及数据管理三、空间数据的转换与处理四、洪水淹没专题地图制作五、矢量数据的采集与处理六、栅格数据的下载与处理七、ArcGIS水文分析八、ArcGIS洪水分析九、ArcGIS淹没分析了解更多 ———————————————————…

【系统参数合法性校验】spring-boot-starter-validation

JSR303校验 统一校验的需求 前端请求后端接口传输参数&#xff0c;是在controller中校验还是在Service中校验&#xff1f; 答案是都需要校验&#xff0c;只是分工不同。 Contoller中校验请求参数的合法性&#xff0c;包括&#xff1a;必填项校验&#xff0c;数据格式校验&…

[零基础]内网ubuntu映射到云服务器上,http访问(frp内网穿透)

阿里云服务器&#xff0c;高校教师可以半价&#xff0c; frp下载地址&#xff1a;https://github.com/fatedier/frp/releases&#xff0c;选amd64&#xff0c; 云服务器开放端口 选择网络与安全–>安全组->管理规则 配置开放端口&#xff0c;7000为支持frp开放的端口&…

第十六届蓝桥杯 2025 C/C++组 破解信息

目录 题目&#xff1a; 题目描述&#xff1a; 题目链接&#xff1a; 思路&#xff1a; 思路详解&#xff1a; 代码&#xff1a; 代码详解&#xff1a; 题目&#xff1a; 题目描述&#xff1a; 题目链接&#xff1a; P12344 [蓝桥杯 2025 省 B/Python B 第二场] 破解信息…

OpenAI Embedding 和密集检索(如 BERT/DPR)进行语义相似度搜索有什么区别和联系

OpenAI Embedding 和密集检索&#xff08;如 BERT/DPR&#xff09;其实是“同一种思想的不同实现”&#xff0c;它们都属于Dense Retrieval&#xff08;密集向量检索&#xff09;&#xff0c;只不过使用的模型、部署方式和调用方式不同。 &#x1f9e0; 首先搞清楚&#xff1a;…

Linux电源管理(3)_关机和重启的过程

原文&#xff1a;Linux电源管理&#xff08;3&#xff09;_Generic PM之重新启动过程 1.前言 在使用计算机的过程中&#xff0c;关机和重启是最先学会的两个操作。同样&#xff0c;这两个操作在Linux中也存在&#xff0c;可以关机和重启。这就是这里要描述的对象。在Linux Ke…

C# 继承详解

继承是面向对象程序设计&#xff08;OOP&#xff09;中的核心概念之一&#xff0c;它极大地增强了代码的重用性、扩展性和维护性。本篇文章将详细讲解C#中的继承机制&#xff0c;包括基础概念、语法特法、多重继承&#xff08;通过接口实现&#xff09;、继承的规则和实际应用示…

SQLAlchemy 2.x 异步查询方法比较

SQLAlchemy 2.x 异步查询中常用的 结果处理方法速查表&#xff0c;包含方法说明、使用场景、返回类型及典型用途。 SQLAlchemy 查询结果处理方法速查表&#xff08;适用于 AsyncSession&#xff09; 方法 说明 返回类型 示例 SQL 示例输出 scalars().all() 获取单列所有…

极客天成参与”AI助力智慧城市构建”主题演讲暨招商引智专题推介活动

4月7日下午&#xff0c;北京极客天成科技有限公司参加了天津市河东区数据局举办的“AI赋能智慧城市构建”主题演讲暨招商引智专题推介活动。 活动中&#xff0c;华为&#xff08;天津&#xff09;有限公司数字政府解决方案总监姜华庚围绕“政务大模型赋能智慧城市建设”&#x…

理解 EKS CloudWatch Pod CPU Utilization 指标:与 `kubectl top` 及节点 CPU 的关系

在使用 AWS EKS 时&#xff0c;CloudWatch Container Insights 提供了丰富的容器级别监控指标&#xff0c;帮助我们深入了解应用的运行状态。如下截图中的 ContainerInsights pod_cpu_utilization 指标就是一个非常重要的维度。本文将详细解释这个指标的含义&#xff0c;并将其…

使用pip3安装软件包报错`externally-managed-environment`的几种解决方式

1、pip3安装软件包报错 报错externally-managed-environment的原因&#xff1a; 从 Python 3.11 开始引入了 PEP 668 规范&#xff0c;该规范限制了在系统级 Python 环境中使用 pip 安装第三方包&#xff0c;以避免与系统包管理器&#xff08;如 apt&#xff09;产生冲突。 如…

spring security用户退出

Spring security默认实现了用户退出的功能&#xff0c;用户退出主要考虑退出后会话如何管理以及跳转到哪个页面。HttpSecurity类提供了logout()方法开启退出登录的支持&#xff0c;默认触发用户退出操作的URL为“/logout”&#xff0c;用户退出时同时也会清除Session等默认用户…

爱普生SG2520HHN晶振数据中心服务器的理想解决方案

在当今数字化时代&#xff0c;数据中心作为海量数据存储、处理与传输的核心枢纽&#xff0c;其服务器的高效稳定运行至关重要。服务器作为其核心设备&#xff0c;对时钟信号的精度和稳定性提出了严苛要求——微小的时序误差可能导致数据传输失败或系统宕机。爱普生 SG2520HHN 差…