仅作个人学习记录本章讲系统调用隔离启动过程实验准备了解系统调用流程阅读user.husy.splsyscall.hsyscall.csysproc.ctrace.c首先来看user.h保存着所用的系统调用函数和用户态的库函数struct stat; // 文件状态结构体存储文件大小、类型、权限、inode等元信息 struct rtcdate; // 实时时钟日期结构体存储年/月/日/时/分/秒时间信息 // 系统调用声明内核提供用户程序通过syscall触发 // 创建子进程返回值父进程返回子进程PID子进程返回0失败返回-1 int fork(void); // 终止当前进程noreturn标记表示该函数不会返回 int exit(int status) __attribute__((noreturn)); // 阻塞等待任意子进程退出将子进程退出状态存入传入指针返回退出子进程PID int wait(int* wstatus); // 创建管道参数int* fd为长度2的数组fd[0]读端文件描述符fd[1]写端成功返回0 int pipe(int* fd); // 文件写系统调用fd文件描述符buf写入数据缓冲区n写入字节数返回实际写入字节数 int write(int fd, const void* buf, int n); // 文件读系统调用fd文件描述符buf存储读出数据n期望读取字节数返回实际读取字节数 int read(int fd, void* buf, int n); // 关闭指定文件描述符fd释放进程占用的文件资源成功返回0 int close(int fd); // 向PID为pid的进程发送终止信号成功返回0失败返回-1 int kill(int pid); // 加载并执行ELF可执行文件 // path可执行文件路径argv命令行参数数组char*数组末尾以NULL结尾 int exec(char* path, char** argv); // 打开文件path文件路径flags打开标志(读/写/创建等)返回文件描述符 int open(const char* path, int flags); // 创建特殊设备节点文件path节点路径mode文件权限类型dev设备主次设备号 int mknod(const char* path, short mode, short dev); // 删除文件硬链接若链接计数为1则直接删除文件本体 int unlink(const char* path); // 获取fd对应文件的元数据存入stat结构体指针输出参数 int fstat(int fd, struct stat* stat_buf); // 创建文件硬链接oldpath原文件路径newpath新链接路径 int link(const char* oldpath, const char* newpath); // 创建目录path为目录路径 int mkdir(const char* path); // 修改当前进程工作目录至path指定路径 int chdir(const char* path); // 复制文件描述符fd返回新的文件描述符指向同一文件资源 int dup(int fd); // 获取当前进程PID返回进程ID整数值 int getpid(void); // 堆内存扩容函数传入增量字节数返回扩容前堆结束地址用于实现malloc底层 char* sbrk(int increment); // 阻塞进程sleep指定秒数 int sleep(int seconds); // 获取系统开机运行总时长返回开机到现在的秒数 int uptime(void); // ulib.c 用户层标准工具库函数封装系统调用/字符串内存操作 // 获取文件元数据path文件路径stat_buf输出文件状态信息 int stat(const char* path, struct stat* stat_buf); // 字符串拷贝将src字符串完整复制到dest缓冲区不做边界检查 char* strcpy(char* dest, const char* src); // 内存区域拷贝支持源与目标内存重叠场景count为拷贝字节数 void *memmove(void* dest, const void* src, int count); // 在字符串s中查找第一个字符c返回对应字符指针无匹配返回NULL char* strchr(const char* s, char c); // 字符串比较s1s2返回正数s1s2返回0s1s2返回负数 int strcmp(const char* s1, const char* s2); // 格式化输出至指定文件描述符fdfmt格式化字符串可变参数 void fprintf(int fd, const char* fmt, ...); // 格式化输出到标准输出(控制台)fmt格式化字符串可变参数 void printf(const char* fmt, ...); // 从标准输入读取一行字符串存入bufmax限制最大读取长度返回buf指针 char* gets(char* buf, int max); // 计算字符串s长度不含末尾\0返回字节长度 uint strlen(const char* s); // 将内存区域首地址dest的count字节全部填充为value值 void* memset(void* dest, int value, uint count); // 用户态堆内存分配分配size字节内存返回分配内存首地址 void* malloc(uint size); // 释放malloc分配的ptr堆内存 void free(void* ptr); // 将数字字符串s转换为对应十进制整型数字返回 int atoi(const char* s); // 内存内容比较前count字节逐字节对比相同返回0不同返回差值 int memcmp(const void *s1, const void *s2, uint count); // 内存拷贝不支持源与目标内存重叠count拷贝字节数 void *memcpy(void *dest, const void *src, uint count);有一个特别的函数int exec(char*, char**); 他的第二个参数是字符串指针数组char **argv char *argv[];字符串指针数组拆开两层理解char *单个字符串指针比如argv[0]存程序名字符串首地址char **指向一堆char*的指针也就是指针数组。exec()会遍历所有参数、拷贝到用户进程栈中再看usus.pl这个文件是一个Perl 脚本它的核心作用是自动生成一个汇编文件通常命名为usys.S这个汇编文件包含了系统调用的“存根Stubs”代码封装ecall陷入指令。#!/usr/bin/perl -w # Generate usys.S, the stubs for system calls. print # Generated by usys.pl - do not edit\n; print #include \kernel/syscall.h\\n sub entry {} my $name shift; print .global $name\n; print ${name}:\n; print li a7, SYS_${name}\n; print ecall\n; print ret\n; } entry(fork); entry(exit); entry(wait); entry(pipe); entry(read); entry(write); entry(close); entry(kill); entry(exec); entry(open); entry(mknod); entry(unlink); entry(fstat); entry(link); entry(mkdir); entry(chdir); entry(dup); entry(getpid); entry(sbrk); entry(sleep); entry(uptime); entry(trace); //添加的先来了解一下在系统调用时RISC-V的a0-a7寄存器寄存器用途a0/a1进入内核时前两个参数内核返回用户态系统调用返回值a2~a6第 3~7 个系统调用参数a7系统调用号专用寄存器在Makefile中usys.S会和用户态程序一起链接为可执行程序usys.pl运行时会拼接调用函数.global fork fork: li a7, SYS_fork会去syscall.h中索引 ecall ret 返回值放在a0a0-a1(如果返回俩个参数)ecall是特权指令触发陷阱陷入内核完成用户态到内核态切换系统调用当在用户态程序中调用read(1bufsizeof(buf)) 程序不能直接执行内核代码。它必须通过一种特殊的机制中断/异常切换到内核态。1、调用的时候会在usys.S中把文件拼接好调用号放在a7中执行ecall特权指令进入内核态此时 CPU 会把所有用户寄存器保存到p-trapframe陷阱帧内核态不能直接访问用户态的虚拟内存2、切到内核态后 进入trap.c的usertrap(), 异常原因会存在 scause 寄存器 用户态陷入ecall是编号8 由它判断是什么中断异常发现是ecall然后调用syscall()scause 值异常类型8User-mode ecall用户态执行 ecall系统调用9Supervisor-mode ecall内核态 ecall1,2,5 等缺页、非法指令、外设中断等其它异常3、执行syscall.c从头文件中找到SYS_read然后通过 copyin 拷贝一份用户态对应调用的虚拟地址读取陷阱帧中的寄存器拿到参数。之后跳转到对应的内核函数执行执行结束返回参数放到a0返回trap() 内核sret切回用户态usys.S代码retC 代码拿到 a0 里的返回值.任务一trace目标添加一个名为trace的新系统调用它可以控制内核记录并打印当前进程执行了哪些指定的系统调用。核心需求控制机制trace系统调用接收一个整数mask掩码作为参数通过比特位按位或来指定追踪哪些系统调用。例如系统调用号SYS_fork的值是 1用户代码调用trace(1 SYS_fork)就表示只追踪fork。传入的掩码转为二进制看那位是1就说明我要追踪哪一个系统调用掩码是32位4字节整数系统调用有21个足够了。打印格式当被追踪的进程执行了 mask 中指定的系统调用时内核必须在该系统调用即将返回时打印一行信息。格式进程ID: syscall 系统调用名称 - 返回值例如输出3: syscall read - 1023不需要打印系统调用的传入参数。继承特性如果一个进程启用了trace那么它此后通过fork()创建的所有子进程都必须自动继承该追踪掩码。也就是说fork()的子进程要拿到父进程的掩码参数隔离特性追踪功能不能影响系统中其他未开启trace的独立进程。实现路径关键点跑通整个系统调用注册流程修改你提到的user/usys.pl、user/user.h、kernel/syscall.h。修改kernel/proc.h中的进程控制块struct proc增加一个变量来保存掩码。修改kernel/proc.c中的fork()将父进程的掩码值赋给子进程。修改kernel/syscall.c中的syscall()函数所有系统调用的统一内核分发器在这里拦截系统调用完成后的时刻判断掩码并打印信息。看一下用户态的trace.c#include kernel/param.h #include kernel/types.h #include kernel/stat.h #include user/user.h int main(int argc, char *argv[]) { int i; char *nargv[MAXARG]; if(argc 3 || (argv[1][0] 0 || argv[1][0] 9)){ fprintf(2, Usage: %s mask command\n, argv[0]); exit(1); } if (trace(atoi(argv[1])) 0) { fprintf(2, %s: trace failed\n, argv[0]); exit(1); } for(i 2; i argc i MAXARG; i){ nargv[i-2] argv[i]; } exec(nargv[0], nargv); exit(0); }先检测传入参数是否合规字符串本质是字符数组如果argv[1] 142那么argv[1][0] 1 , argv[1][1] 4 ,为了检测第二个参数是不是字符。然后传入参数调用trace() 去内核中运行。usys.pl中添加trace系统调用proc.h头文件proc() 结构体中加入trace_mask参数在pro.c的fork函数中加入传参在syspro.c中添加trace在syscall.h头文件中添加掩码在syscall.c中添加打印的字符串并且做比对user.h头文件中添加系统调用函数最后运行结果