当前位置: 首页 > news >正文

系统调用,库函数,exec系列函数,shell实现

一. 系统调用和库函数的区别

对比维度系统调用库函数
实现位置内核空间用户空间(如 glibc)
运行权限需要内核态权限用户态即可运行
调用方式通过软中断(int 0x80)或syscall指令普通函数调用(call指令)
上下文切换有(用户态 → 内核态 → 用户态)
执行效率较慢(切换开销)较快
可移植性依赖操作系统内核跨平台(遵循语言标准)
典型例子read(),write(),open(),fork()printf(),fread(),strlen(),sin()
是否能访问硬件能(通过驱动程序)不能直接访问,必须通过系统调用
是否缓存无缓存(直接操作内核缓冲区)可能有(如fread有用户态缓冲区)

1.1核心概念

1. 系统调用号

  • 每个系统调用都有一个唯一的编号(如#define __NR_open 5)。

  • 用户程序不直接写函数名,而是通过寄存器(如eax传递这个编号给内核。

2. 用户空间 vs 内核空间

  • 用户空间:应用程序运行的地方,受限制(不能直接访问硬件、内核数据)。

  • 内核空间:操作系统核心运行的地方,拥有最高权限。

  • 系统调用是从用户空间进入内核空间的唯一合法入口

3. 系统调用表

  • 内核维护一个函数指针数组(系统调用表)。

  • 表的下标 = 系统调用号。

  • 例如:

    • 表[1] →sys_exit()

    • 表[2] →sys_fork()

    • 表[3] →sys_read()

    • 表[4] →sys_write()

    • 表[5] →sys_open()

1.2执行流程(以open()为例)

这张图片讲的是系统调用的底层实现机制,特别是如何通过系统调用号和寄存器在用户空间和内核空间之间传递信息

1.步骤详解

步骤位置动作
1用户空间应用程序调用open()库函数
2用户空间库函数将系统调用号(5)存入eax寄存器
3用户空间触发软中断(int 0x80syscall指令)
4内核空间CPU 切换到内核态,根据eax中的编号查找系统调用表
5内核空间执行对应的内核函数sys_open()
6内核空间返回结果(如文件描述符)给用户空间

2.关键寄存器的作用

寄存器作用
eax存放系统调用号(如 open = 5)
ebx, ecx, edx存放系统调用的参数(如 open 的文件路径、标志位)
返回值也通过eax返回(如文件描述符或错误码)

1.3完整示例(逻辑流程)

text

应用程序: int fd = open("test.txt", O_RDONLY); ↓ libc 库函数: 将 __NR_open (5) 放入 eax 将参数放入 ebx, ecx 等 执行 syscall 指令 ↓ 内核: 根据 eax=5 查系统调用表 → sys_open() 执行 sys_open() 内核函数 ↓ 返回: eax = 文件描述符(如 3)或 -1(错误) ↓ 应用程序得到 fd = 3

总结:如何切换?(用户态 → 内核态)

切换机制

用户程序无法直接进入内核态,必须通过专门的指令或中断触发 CPU 特权级切换。

两种主要方式

方式说明平台
软中断int 0x80触发 0x80 号中断,CPU 查中断向量表进入内核老式 x86(32位)
syscall指令专门用于系统调用的快速指令,效率更高x86_64(现代系统)

详细切换步骤(以printf("hello")write()为例)

text

用户程序: printf("hello"); ↓ // printf 内部处理格式化字符串 // 最终调用write 系统调用↓ // 库函数将系统调用号(write = 4)放入 eax 寄存器 // 将参数(fd, buf, count)放入 ebx, ecx, edx ↓ //执行软中断或 syscall 指令 int 0x80 // 或 syscall↓ 【CPU 硬件自动完成】 1. 保存当前寄存器状态(包括用户栈、指令指针) 2.从用户态切换到内核态(特权级从 3 切换到 0)3. 跳转到内核中预设的入口点(如 system_call) ↓ 内核空间: 根据 eax 中的系统调用号(4)查系统调用表 执行 sys_write() 内核函数 ↓ 将结果放入 eax(返回值) 执行 iret 或 sysret 指令返回用户态 ↓ 【CPU 恢复用户态】 恢复寄存器 返回用户程序继续执行 ↓ 用户程序: printf 返回输出字符数

关键点总结

要素说明
触发方式int 0x80(老)或syscall(新)
寄存器传递eax= 系统调用号,ebx/ecx/edx= 参数
返回值也通过eax返回
切换代价~几十到几百个 CPU 周期(包括保存/恢复寄存器、TLB 可能失效)
为什么不能直接调用内核代码在独立地址空间,用户程序无法跳


二、exec系列函数

1.概念

exec系列函数的作用

  • 不创建新进程,而是用新程序替换当前进程的代码段、数据段、堆栈

  • 替换成功后,原进程的后续代码不再执行(除非替换失败)。

  • 常与fork()配合使用:fork()创建子进程,子进程调用exec()执行新程序。

  • 新程序运行在原进程的 PID下(getpid()返回值相同)

2.代码示例

3.6个exec函数的命名规律与区别

不必全记住,可以查询帮助手册 man exec

函数名路径参数参数传递方式是否搜索PATH环境变量
execl完整路径列表(可变参数)继承
execlp文件名列表(可变参数)继承
execle完整路径列表(可变参数)自定义(通过参数传递)
execv完整路径数组(char *argv[]继承
execvp文件名数组(char *argv[]继承
execve完整路径数组(char *argv[]自定义(通过参数传递)

命名规律

  • l:参数以列表(list)形式传递(可变参数,最后一个必须是(char*)0结尾)。

  • v:参数以向量(vector/array)形式传递(字符指针数组)。

  • p搜索 PATH环境变量,只需传文件名(如"ps"而非/usr/bin/ps)。

  • e:可以自定义环境变量(最后一个参数传envp[])。


4.参数格式说明

列表形式(l系列)

c

execl("/usr/bin/ps", "ps", "-f", (char*)0); // ↑路径 ↑第0个参数 ↑第1个参数 ↑结束标记
  • 每个参数单独写出,最后一个必须是(char*)0NULL

  • 第一个参数通常是程序名(可随意,但惯例是文件名)。

向量/数组形式(v系列)

c

char* myargv[] = {"ps", "-f", (char*)0}; execv("/usr/bin/ps", myargv); // ↑ 数组名(首地址)
  • 先构造一个char*数组,最后一项为NULL

  • 传给exec函数时,传数组名即可。


5.为什么需要(char*)0NULL

  • 告诉被调用的新程序:参数列表到此结束

  • 如果不加,函数无法知道参数有多少个,会一直往后读内存,导致错误。

三、如何实现一个迷你版的Shell(命令解释器)

1.定义

命令解释器是用户与操作系统内核之间的接口程序,它接收用户输入的命令,解析并转换为内核能理解的系统调用,最终让内核执行相应操作。命令解释器是这类程序的统称,bash是其中最流行的一种实现。

Shell 的本质 =fork()+exec()+wait()

核心流程

text

用户输入命令(如 "ls -l") ↓ Shell 解析命令 ↓ fork() 创建子进程 ↓ ┌─────────────────┐ │ 子进程 │ │ exec() 替换为 ls │ → 执行 ls -l → 输出结果 → 退出 └─────────────────┘ ↓ 父进程(Shell)wait() 等待子进程结束 ↓ 回到循环,打印提示符,等待下一条命令

为什么是 fork + exec?

步骤作用为什么必须这样
fork()复制出一个子进程不能直接用exec()替换 Shell 自身,否则 Shell 就没了
exec()将子进程替换为目标程序让子进程去执行用户命令
wait()父进程等待子进程结束避免僵尸进程,且 Shell 需要等待命令执行完再继续

2.代码

#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<sys/wait.h> #include<string.h> int main() { while(1) { printf("\033[1;32mxatu@localhost\033[0m:\033[1;34m~$\033[0m"); fflush(stdout); // 强制刷新输出缓冲区 char buff[128] = {0}; fgets(buff, 128, stdin); // "ls\n" buff[strlen(buff) - 1] = '\0'; // "ls" if(buff[0] == '\0') { continue; } if(strcmp(buff, "exit") == 0) { break; } if(strcmp(buff, "clear") == 0) { printf("\033[2J\033[H"); // \033[2J → 清屏 // \033[H → 将光标移动到左上角 continue; } pid_t pid = fork(); if(pid == 0) { execlp(buff, buff, NULL); printf("exec err\n"); exit(0); } wait(NULL); } return 0; }
http://www.cnnetsun.cn/news/1884260.html

相关文章:

  • 国产连接器是否能全面替代 Adam Tech 进口连接器?深度解析与评估
  • C语言位运算实战:从奇偶校验到循环移位,5个嵌入式开发必会技巧(附完整代码)
  • openclaw卸载与重装
  • 八大网盘直链下载终极方案:LinkSwift开源工具深度解析
  • 3分钟掌握VideoDownloadHelper:全网视频下载的终极神器
  • Claude Code全解析:去哪找、怎么用、如何快速获取
  • 如何用TMSpeech实现本地实时语音转文字:3个实战案例
  • 从3D相机数据到三维模型:Halcon实战深度图、亮度图与点云转换全流程
  • Qt程序打包避坑指南:从Release编译到单文件封装的完整流程
  • 从零到一:构建一个支持无障碍访问的现代Slider组件
  • 我用 AI 做了一个PDF转Word神器:解决排版烦恼
  • 2026 最新:10个高质量免费 PPT 网站(附官网链接)
  • 语雀文档导出终极指南:5分钟搞定知识库完整迁移
  • 智慧点餐系统|亿坊·扫码点餐——正餐/快餐/茶饮,一套源码全搞定!
  • MobileNetSSD_deploy.caffemodel下载地址
  • 科研数据处理:结合MATLAB信号分析与Qwen3-ASR-0.6B语音识别
  • 告别网盘龟速下载:5分钟掌握LinkSwift直链下载神器的终极指南
  • 中兴光猫终极解锁指南:zteOnu工具快速获取隐藏权限
  • fsadfd
  • AKShare金融数据接口库:新手必学的5个实战技巧与避坑指南
  • 保姆级教程:用Livox Viewer搞定多台Mid-70雷达的外参标定与ROS驱动配置
  • 会议纪要效率提升:FUTURE POLICE自动生成带时间戳的文本
  • React Fiber 调度优先级优化方案
  • 深度解析:针对科研人员的工作特性,评估不同学习方式与考试形式的利弊
  • Rust的闭包语法分析
  • nlp_structbert_sentence-similarity_chinese-large实操案例:构建企业内部文档语义知识图谱
  • 手把手教你用STM32F103C8T6打造百元级环境监测手表(含BME280传感器驱动避坑指南)
  • 湿度计算常数γ:气象学中的隐藏变量及其在农业灌溉中的实际应用
  • 3分钟掌握TrollInstallerX:iOS 14-16.6.1设备安装TrollStore终极指南
  • 全面战争模组编辑器终极指南:5个技巧快速掌握RPFM工具