一. 系统调用和库函数的区别对比维度系统调用库函数实现位置内核空间用户空间如 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 0x80或syscall指令4内核空间CPU 切换到内核态根据eax中的编号查找系统调用表5内核空间执行对应的内核函数sys_open()6内核空间返回结果如文件描述符给用户空间2.关键寄存器的作用寄存器作用eax存放系统调用号如 open 5ebx, ecx, edx等存放系统调用的参数如 open 的文件路径、标志位返回值也通过eax返回如文件描述符或错误码1.3完整示例逻辑流程text应用程序 int fd open(test.txt, O_RDONLY); ↓ libc 库函数 将 __NR_open (5) 放入 eax 将参数放入 ebx, ecx 等 执行 syscall 指令 ↓ 内核 根据 eax5 查系统调用表 → sys_open() 执行 sys_open() 内核函数 ↓ 返回 eax 文件描述符如 3或 -1错误 ↓ 应用程序得到 fd 3总结:如何切换用户态 → 内核态切换机制用户程序无法直接进入内核态必须通过专门的指令或中断触发 CPU 特权级切换。两种主要方式方式说明平台软中断int 0x80触发 0x80 号中断CPU 查中断向量表进入内核老式 x8632位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 切换到 03. 跳转到内核中预设的入口点如 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系列cexecl(/usr/bin/ps, ps, -f, (char*)0); // ↑路径 ↑第0个参数 ↑第1个参数 ↑结束标记每个参数单独写出最后一个必须是(char*)0或NULL。第一个参数通常是程序名可随意但惯例是文件名。向量/数组形式v系列cchar* myargv[] {ps, -f, (char*)0}; execv(/usr/bin/ps, myargv); // ↑ 数组名首地址先构造一个char*数组最后一项为NULL。传给exec函数时传数组名即可。5.为什么需要(char*)0或NULL告诉被调用的新程序参数列表到此结束。如果不加函数无法知道参数有多少个会一直往后读内存导致错误。三、如何实现一个迷你版的Shell命令解释器1.定义命令解释器是用户与操作系统内核之间的接口程序它接收用户输入的命令解析并转换为内核能理解的系统调用最终让内核执行相应操作。命令解释器是这类程序的统称bash是其中最流行的一种实现。Shell 的本质 fork()exec()wait()核心流程text用户输入命令如 ls -l ↓ Shell 解析命令 ↓ fork() 创建子进程 ↓ ┌─────────────────┐ │ 子进程 │ │ exec() 替换为 ls │ → 执行 ls -l → 输出结果 → 退出 └─────────────────┘ ↓ 父进程Shellwait() 等待子进程结束 ↓ 回到循环打印提示符等待下一条命令为什么是 fork exec步骤作用为什么必须这样fork()复制出一个子进程不能直接用exec()替换 Shell 自身否则 Shell 就没了exec()将子进程替换为目标程序让子进程去执行用户命令wait()父进程等待子进程结束避免僵尸进程且 Shell 需要等待命令执行完再继续2.代码#includestdio.h #includeunistd.h #includestdlib.h #includesys/wait.h #includestring.h int main() { while(1) { printf(\033[1;32mxatulocalhost\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; }