ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

SerenityOS syscall 命令详解:在 Shell 中直接测试内核系统调用

2026/9/10 22:31:44 拓冰建站 浏览量
SerenityOS syscall 命令详解:在 Shell 中直接测试内核系统调用 SerenityOS syscall 命令详解在 Shell 中直接测试内核系统调用【免费下载链接】serenityThe Serenity Operating System 项目地址: https://gitcode.com/GitHub_Trending/se/serenity导读syscall是 SerenityOS 自带的系统调用测试工具它允许你在 Shell 中绕过 libc 封装、以命令行参数的形式直接调用内核系统调用system call是内核开发调试、系统调用行为验证与底层机制学习的利器。本文以系统手册页 Base/usr/share/man/man1/syscall.md 为骨架结合其实现源码 Userland/Utilities/syscall.cpp 与内核侧的系统调用枚举定义 Kernel/API/Syscall.h完整讲解该工具的参数解析规则、选项、典型用法、参数缓冲区SC_*_params机制及其在 SerenityOS 内核架构中的位置。读完本文你将能够用syscall直接发起write、read、getpid、sleep、mkdir、exit乃至realpath等真实系统调用并理解其底层是如何工作的。命令概览与设计动机什么是 syscall 工具syscall是 SerenityOS 用户态提供的一个小工具其核心目的正如源码中args_parser.set_general_help(...)所写Enables you to do a direct syscall, even those that use a SC_*_params buffer.也就是说它能够让你直接发起一次系统调用包括那些需要传递SC_*_params参数缓冲区参数结构体指针的复杂系统调用而无需编写 C 程序或依赖 libc 的封装函数。这在内核开发、驱动调试、系统调用语义验证等场景下非常实用。工具名称与语法$ syscall [-o] [-l] [-h] syscall-name args... [bufBUFSIZ buffer]其中syscall-name是系统调用名字如write、getpidargs...是传递给该系统调用的最多 4 个参数。工具的编译入口与所有参数解析逻辑都集中在 Userland/Utilities/syscall.cpp 的serenity_main函数中。与 libc 封装的关系在 SerenityOS 中用户态程序通常通过 libc 的封装函数发起系统调用例如 Userland/Libraries/LibC/unistd.cpp 中大量出现的syscall(SC_getpid)、syscall(SC_execve, params)等调用它们会进一步触发内核中对应的Process::sys$xxx实现如 Kernel/Syscalls/write.cpp 中的sys$write。而syscall工具则把这一过程暴露到了命令行让每个系统调用都可以被独立、可重复地测试。选项说明syscall支持三个命令行选项选项长选项作用-o--output-buffer将作为参数传入的输出缓冲区buf内容写到 stdout。注意源码警告小心缓冲区中的多余零字节beware of stray zero bytes!。-l--list-syscalls打印所有 SerenityOS 系统调用的空格分隔列表实际为每行一个、带编号后退出。注意并非所有系统调用都可以通过本工具调用。-h打印帮助信息后退出。在源码中这三个选项分别对应output_buffer、list_syscalls两个布尔变量与 ArgsParser 自带的帮助选项args_parser.add_option(list_syscalls, List all existing syscalls, and exit, list-syscalls, l); args_parser.add_option(output_buffer, Output the contents of the buffer (beware of stray zero bytes!), output-buffer, o);-l的实现会遍历Syscall::Function::__Count枚举范围并借助 Kernel/API/SyscallString.h 中的to_string(Function)将枚举值格式化为名字输出同时打印出每个系统调用的编号if (list_syscalls) { outln(syscall list:); for (int sc 0; sc Syscall::Function::__Count; sc) { outln( \033[33;1m{}\033[0m - {}, sc, static_castSyscall::Function(sc)); } exit(0); }因此运行syscall -l可以直接看到内核当前注册的全部系统调用清单及其数字编号这对对照 Kernel/API/Syscall.h 中的ENUMERATE_SYSCALLS(S)宏展开结果非常直观。另外如果运行syscall而不带任何参数工具会打印用法信息args_parser.print_usage并退出码 1。参数解析规则五种输入类型syscall工具最灵活、也最容易让人困惑的部分是它的命令行参数解析。源码中的parse_from函数Userland/Utilities/syscall.cpp按优先级依次识别以下五类输入1. 强制字符串以逗号,开头以逗号开头的参数会被剥掉逗号、强制当作字符串指针传入。这是为了让那些形如数字、buf、[、]的字符串能够以字面字符串身份传递而不是被后续规则解释$ syscall write 1 ,0x0 2 # 向 fd 1 写入字符串 0x02 字节 $ syscall write 1 ,[ 1 # 向 fd 1 写入字符串 [1 字节2. 输出缓冲区buf参数buf会被替换为指向工具内部静态缓冲区outbuf的指针。outbuf的大小为BUFSIZstdio 定义的标准缓冲区大小这也是手册 Synopsis 中[bufBUFSIZ buffer]的含义。它与-o选项配合使用-o会在系统调用返回后把outbuf的内容整体写回 stdout。char outbuf[BUFSIZ]; // ... if (this_arg_string bufsv) return (FlatPtr)outbuf;3. 参数缓冲区[ ... ]syscall支持把[和]之间的参数打包成一个参数结构体缓冲区用来发起那些需要SC_*_params指针的系统调用如realpath、chown、execve。parse_parameter_buffer会递归解析[与]之间的所有参数把它们作为FlatPtr数组排列在内存中static FlatPtr parse_parameter_buffer(ArgIter iter) { VectorFlatPtr params_vec; while (!iter.is_end()) { if (*iter ]sv) { iter; return as_buf(params_vec); } params_vec.append(parse_from(iter)); } fprintf(stderr, Error: Unmatched [?!\n); exit(1); }随后as_buf会把该数组复制到一块通过mmap分配的、按页对齐的匿名内存中大小取sizeof(FlatPtr) * n 1向上取整到页大小并返回该内存地址作为参数指针。源码特意注释这块缓冲区故意不释放leak因为它必须存活到特殊系统调用执行完毕而工具紧接着就会退出。注意[和]必须是独立的命令行参数并且缓冲区可以递归嵌套Buffers can be used recursively即[内部还可以再出现[ ... ]。4. 数字参数若能被解析为无符号数则按数值传入。支持十进制1234与十六进制0xDEADC0DE两种写法if (auto l this_arg_string.to_numberunsigned(); l.has_value()) return *l;5. 普通字符串兜底以上规则都无法匹配时参数被当作字符串指针传入例如Hello, friends!。如果出现孤立的]工具会打印警告Warning: Treating unmatched ] as literal string并仍按字符串处理。数量上限与越界检查syscall最多接受4 个参数#define SC_NARG 4超过 4 个时会提示Too many arguments (did you want to use [ parameter buffers ]?)并返回 -1——这是为了提醒用户把参数放进[ ... ]缓冲区中。第一个参数会被当作系统调用编号如果它大于Syscall::Function::__Count则尝试按字符串在系统调用列表中进行匹配借助Syscall::to_string反向查表匹配失败则报Invalid syscall entry ...。实际发起调用的核心只有一行int rc syscall(arg[0], arg[1], arg[2], arg[3]);syscall是 libc 提供的通用系统调用入口区别于封装函数。调用结束后工具还会把返回值与错误信息打印到 stderrif (-rc 0 -rc EMAXERRNO) { warnln(Syscall return: {} ({}), rc, strerror(-rc)); } else { warnln(Syscall return: {} (?), rc); }也就是说如果返回值为负数则其绝对值对应 errno工具会直接给出可读的错误字符串如-2会显示为ENOENT的描述。当指定-o时还会额外执行fwrite(outbuf, 1, sizeof(outbuf), stdout)把缓冲区整体输出。常用示例详解手册页给出了 6 个可直接运行的示例下面逐一说明其含义与预期输出。向标准输出写入字符串$ syscall write 1 hello 5调用write(1, hello, 5)文件描述符1是 stdout字符串参数hello被识别为普通字符串并取其指针长度5为数字。运行后在终端打印hello。从标准输入读入并回显$ syscall -o read 0 buf 3调用read(0, buf, 3)从 stdinfd 0读取最多 3 字节到输出缓冲区-o随后把buf的BUFSIZ字节整体输出到 stdout注意是BUFSIZ一整块而不是实际读到的字节数这也是小心多余零字节警告的由来。获取当前进程 PID$ syscall getpid调用getpid()无参数。返回值为进程 PID。该调用在 Userland/Libraries/LibC/unistd.cpp 中也有对应封装cached_pid syscall(SC_getpid)工具输出格式类似Syscall return: 123 (Success)。休眠 3 秒$ syscall sleep 3调用sleep(3)数字参数3解析为毫秒数还是秒数取决于内核sys$sleep的实现语义手册示例定位为 3 秒等待结束后返回。创建目录$ syscall mkdir my-dir 0755调用mkdir(my-dir, 0755)第一个参数my-dir是字符串第二个参数0755按八进制解析为数字to_numberunsigned能正确处理0前缀的八进制写法对应权限rwxr-xr-x。以状态码 2 退出$ syscall exit 2调用exit(2)当前进程以退出码 2 终止。注意该调用在 Kernel/API/Syscall.h 的ENUMERATE_SYSCALLS中标记为NeedsBigProcessLock::Yes属于需要持有进程大锁的系统调用。进阶用法SC_*_params 参数缓冲区并非所有系统调用都只接受简单标量参数。SerenityOS 内核为realpath、chown、execve等系统调用定义了专门的参数结构体SC_xxx_params例如 Kernel/API/Syscall.h 中的struct SC_realpath_params。这些结构体由ENUMERATE_SYSCALLS宏配合SC_xxx_params定义生成内核侧通过UserspaceSC_realpath_params const*从用户态拷贝结构体内容见 Kernel/Syscalls/realpath.cpp 的Process::sys$realpath。syscall工具的[ ... ]语法正是为这类调用设计的。手册帮助文本给出了完整示例$ syscall -o realpath [ /usr/share/man/man2/getgid.md 1024 buf 1024 ]这条命令的含义是构造SC_realpath_params结构体其中依次为——路径字符串/usr/share/man/man2/getgid.md、路径缓冲区长度1024、输出缓冲区指针buf、输出缓冲区长度1024——然后发起realpath系统调用最后用-o把解析出的真实路径打印到 stdout。这里可以清楚地看到[和]必须作为独立参数传递内部元素可以是字符串、数字、buf甚至可以递归嵌套另一个[ ... ]。系统调用列表与内核侧对应关系syscall -l输出的每个名字都来自 Kernel/API/SyscallString.h 的to_string函数该函数利用ENUMERATE_SYSCALLS宏Kernel/API/Syscall.h展开出从SC_accept4到SC_*的全部枚举项。每个枚举项带有一个NeedsBigProcessLock标记Yes/No例如execve、exit、futex需要持锁而getpid、write等大多数调用不需要。内核侧每个系统调用的实现统一命名为Process::sys$xxx分布在 Kernel/Syscalls 目录下例如write→ Kernel/Syscalls/write.cpp 的Process::sys$write(int fd, Userspaceu8 const* data, size_t size)realpath→ Kernel/Syscalls/realpath.cpp 的Process::sys$realpath(UserspaceSyscall::SC_realpath_params const*)。因此当你用syscall write 1 hello 5发起调用时实际上走的是命令行参数解析 →syscall(SC_write, 1, hello, 5)→ 内核sys$write执行 → 返回值打印完整覆盖了用户态直接调用内核的全链路。注意事项与限制手册明确指出并非所有系统调用都能通过syscall工具调用。例如需要特殊上下文、文件描述符预置或无法通过 4 个FlatPtr标量参数表达的系统调用如fork、execve等有专门封装的调用就不适合直接测试。参数上限为 4 个更复杂的参数请使用[ ... ]参数缓冲区内部元素不受 4 个限制且可递归嵌套。输出缓冲区固定为BUFSIZ字节-o输出的是整块缓冲区内容含未写入的零字节读取结果时需结合实际返回的字节数判断。buf是一个全局静态缓冲区多次调用之间内容会保留配合-o使用前请先通过read等调用向其中写入数据。历史来源手册 History 一节说明syscall是直接移植自 Plan 9 操作系统中同名工具。Plan 9 以其一切皆文件与简洁的接口设计著称该工具的移植延续了这一传统——让系统调用本身成为可在命令行中直接试验的一等公民这也与 SerenityOS 注重教学性与可探索性的内核设计一脉相承。当前实现由 Andreas Kling 与 Alex Major 等人维护见 Userland/Utilities/syscall.cpp 文件头版权声明。【免费下载链接】serenityThe Serenity Operating System 项目地址: https://gitcode.com/GitHub_Trending/se/serenity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考