操作系统实验1.doc:用内核模块与kprobe观察系统调用和进程行为
简介这份操作系统实验报告文档面向计算机专业学生及操作系统初学者围绕OS Lab集成实验环境的使用展开帮助读者熟悉EOS操作系统内核与应用程序的编译、调试流程。文档完整记录了实验概述、实验环境、实验过程、思考题与实验体会等模块涵盖新建项目、生成与执行项目、断点终止、逐过程与逐语句单步调试、查看变量值与调用堆栈以及EOS内核项目、EOS应用程序项目的生成调试和软盘镜像文件查看等具体操作并附有对EOS SDK文件夹组织结构与头文件包含方式的思考分析。资源包内含1个doc文件大小约891KB结构完整、内容详实适合作为操作系统实验课程的参考范例与操作指引。目前已有57人学习可为读者理解EOS可执行文件生成机制、掌握OS Lab调试方法提供直接借鉴。1. 操作系统实验1.doc一份文档背后藏着的系统调用与进程观察入口很多人第一次看到“操作系统实验1.doc”这个文件名第一反应是去找这份文档本身。但真正做过操作系统课程实验的人都知道文档只是任务书真正要动手的是它背后那套“从用户态跨进内核态”的观察方法。你拿到的可能是一份实验指导书要求你写一个模块、调一个系统调用、观察一次进程创建或内存分配但核心问题始终是你怎么证明自己真的看懂了操作系统在做什么而不是抄了一段代码让它跑通。这个方向适合两类人一类是正在跟操作系统实验课、需要把实验报告写出技术含量的人另一类是想从应用层往下走一层、理解系统调用、进程调度、内存管理到底怎么落地的人。它解决的不是“学会写操作系统”而是“学会用最小可验证的手段观察操作系统行为”。我一般会把它拆成三件事先确认实验环境再写最小可加载模块或系统调用桩最后用可观测的输出证明行为符合预期。下面按这个路径展开。2. 实验环境与最小可运行骨架先把编译链和加载路径跑通2.1 为什么环境确认比写代码更重要操作系统实验翻车十次里有七次不是逻辑错而是环境不对。内核版本、头文件路径、编译器版本、模块签名策略、虚拟机配置任何一项不匹配都会让你在“编译通过但加载失败”或“加载成功但看不到输出”之间反复横跳。常见做法是先不写任何业务逻辑只写一个能加载、能打印、能卸载的空模块或空系统调用桩把整条链路跑通。这一步看起来慢但它是后面所有实验的后悔药。我一般会先确认三件事当前内核版本与头文件是否一致、模块加载是否被安全策略拦截、日志输出通道是否可用。在多数 Linux 实验环境里uname -r和/lib/modules/$(uname -r)/build必须指向同一版本否则编译出来的模块即使.ko文件生成了加载时也会报invalid module format。如果是系统调用实验还要确认你是用内核模块直接改sys_call_table还是用kprobe/tracepoint做观察前者风险高、后者更稳。2.2 最小内核模块的编译与加载命令下面这个骨架不实现任何业务功能只验证编译、加载、打印、卸载四个动作是否闭环。你可以把它当作所有操作系统实验1的起点。// minimal_mod.c #include linux/module.h #include linux/kernel.h #include linux/init.h static int __init minimal_init(void) { // 加载时打印用于确认模块真的进入了内核 printk(KERN_INFO minimal_mod: loaded\n); return 0; } static void __exit minimal_exit(void) { // 卸载时打印用于确认退出路径也正常 printk(KERN_INFO minimal_mod: unloaded\n); } module_init(minimal_init); module_exit(minimal_exit); MODULE_LICENSE(GPL); MODULE_AUTHOR(lab); MODULE_DESCRIPTION(minimal module for os lab);对应的 Makefile 只需要指向当前内核的构建目录obj-m minimal_mod.o KDIR : /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) all: make -C $(KDIR) M$(PWD) modules clean: make -C $(KDIR) M$(PWD) clean编译和加载按顺序执行make sudo insmod minimal_mod.ko dmesg | tail -n 20 sudo rmmod minimal_mod dmesg | tail -n 20逻辑说明module_init和module_exit分别指定加载和卸载时执行的函数printk是内核态打印输出到内核环形缓冲区用dmesg查看。参数说明KERN_INFO是日志级别实验阶段用它足够MODULE_LICENSE(GPL)必须写否则内核会标记为污染并可能拒绝某些符号。如果insmod报Operation not permitted先检查安全启动和模块签名策略不要急着改代码。2.3 系统调用观察的两种落地方式如果你的实验1要求“添加一个系统调用”或“观察某个系统调用的行为”先分清两种路径。第一种是改内核源码、重新编译内核适合有完整内核树和足够时间的场景第二种是用内核模块劫持sys_call_table或挂kprobe适合快速验证。前者干净但重后者轻但有风险。我一般会建议如果只是观察参数和返回值用kprobe如果要真正新增一个调用号才走改内核源码的路。用kprobe观察sys_openat的最小示例如下// trace_openat.c #include linux/module.h #include linux/kprobes.h #include linux/ptrace.h static int handler_pre(struct kprobe *p, struct pt_regs *regs) { // x86_64 下第一个参数在 di 寄存器 printk(KERN_INFO trace_openat: dfd%ld filename%p\n, regs-di, (void *)regs-si); return 0; } static struct kprobe kp { .symbol_name __x64_sys_openat, .pre_handler handler_pre, }; static int __init trace_init(void) { int ret register_kprobe(kp); if (ret 0) { printk(KERN_ERR trace_openat: register failed %d\n, ret); return ret; } printk(KERN_INFO trace_openat: registered\n); return 0; } static void __exit trace_exit(void) { unregister_kprobe(kp); printk(KERN_INFO trace_openat: unregistered\n); } module_init(trace_init); module_exit(trace_exit); MODULE_LICENSE(GPL);逻辑说明kprobe把pre_handler挂到目标符号执行前regs里保存了调用时的寄存器状态。参数说明symbol_name在不同内核版本里可能是sys_openat或__x64_sys_openat用grep openat /proc/kallsyms确认。注意直接读寄存器依赖架构x86_64 下前六个参数依次在di, si, dx, cx, r8, r9换架构就要改。3. 进程与内存观察实验把抽象概念变成可打印的数字3.1 进程创建路径的观察点选择操作系统实验1如果涉及进程通常要求你观察fork、exec、wait的行为或者统计进程创建次数。直接改调度器不现实常见做法是在copy_process或wake_up_new_task附近挂kprobe打印当前进程的pid、tgid和父进程pid。这样你能看到一次fork到底走了哪些关键函数而不是只背“fork 创建子进程”这句话。选择观察点时要注意copy_process调用频繁打印太多会拖慢系统wake_up_new_task更接近“子进程即将被调度”的时刻信息更干净。我一般会先用kprobe挂wake_up_new_task只打印pid和comm确认能抓到再决定是否加字段。3.2 用 kprobe 打印进程创建信息的代码与参数// trace_fork.c #include linux/module.h #include linux/kprobes.h #include linux/sched.h static int handler_pre(struct kprobe *p, struct pt_regs *regs) { struct task_struct *task (struct task_struct *)regs-di; if (task) { // 打印新任务的 pid、tgid 和进程名 printk(KERN_INFO trace_fork: pid%d tgid%d comm%s\n, task-pid, task-tgid, task-comm); } return 0; } static struct kprobe kp { .symbol_name wake_up_new_task, .pre_handler handler_pre, }; static int __init tf_init(void) { int ret register_kprobe(kp); if (ret 0) { printk(KERN_ERR trace_fork: register failed %d\n, ret); return ret; } return 0; } static void __exit tf_exit(void) { unregister_kprobe(kp); } module_init(tf_init); module_exit(tf_exit); MODULE_LICENSE(GPL);逻辑说明wake_up_new_task的第一个参数是struct task_struct *x86_64 下放在di。参数说明task-pid是进程号task-tgid是线程组号task-comm是进程名。注意task_struct字段在不同内核版本里位置可能变但pid、tgid、comm通常稳定。如果打印出来comm是乱码先检查task指针是否为空再检查内核版本对应的结构体定义。3.3 内存分配观察用 tracepoint 替代硬编码 kprobe内存实验里直接挂kmalloc或__alloc_pages的kprobe容易因为内联和符号改名失败。更稳的做法是用内核已有的tracepoint比如kmem:kmalloc和kmem:kfree。你不需要写内核模块直接用tracefs就能观察。# 挂载 tracefs如果尚未挂载 sudo mount -t tracefs nodev /sys/kernel/tracing # 打开 kmalloc 事件 echo 1 | sudo tee /sys/kernel/tracing/events/kmem/kmalloc/enable # 只看当前 shell 的分配过滤 pid echo $$ | sudo tee /sys/kernel/tracing/events/kmem/kmalloc/filter # 读取跟踪输出 sudo cat /sys/kernel/tracing/trace_pipe逻辑说明trace_pipe是实时流enable控制事件开关filter按pid过滤。参数说明kmalloc事件会打印调用地址、分配大小和gfp_flags你可以据此判断一次分配是原子上下文还是可睡眠上下文。如果看不到输出先确认tracefs挂载点是否正确再确认当前内核是否编译了CONFIG_TRACEPOINTS和CONFIG_FTRACE。4. 避坑与排查操作系统实验1最常见的五类翻车4.1 模块加载失败invalid module format现象insmod报invalid module formatdmesg里提示版本魔法不匹配。原因编译模块用的内核头文件版本与当前运行内核不一致或者编译器版本差异导致 vermagic 不匹配。解决用uname -r确认运行内核检查/lib/modules/$(uname -r)/build是否指向正确源码树必要时在目标机器上重新编译不要跨机器拷贝.ko。4.2 打印看不到printk 级别被过滤现象模块加载成功但dmesg里没有你的输出。原因printk默认控制台日志级别可能高于KERN_INFO或者dmesg限制只显示最近若干行。解决用dmesg -w实时观察或者临时调高控制台级别echo 8 /proc/sys/kernel/printk。注意生产环境不要长期开高日志级别。4.3 kprobe 注册失败符号找不到现象register_kprobe返回负数dmesg提示symbol not found。原因目标函数被内联、改名或者内核未导出该符号。解决用grep 目标名 /proc/kallsyms确认实际符号名优先选择带__x64_sys_前缀或tracepoint的稳定入口。如果符号存在但仍失败检查是否被kprobe黑名单限制。4.4 系统卡死在原子上下文里睡眠现象加载模块后系统无响应或直接重启。原因在kprobe的pre_handler里调用了可能睡眠的函数比如kmalloc(GFP_KERNEL)或copy_from_user。解决kprobe上下文是原子的只能做无锁、不睡眠的操作。需要复杂处理时用kprobe采集数据后交给工作队列或者改用tracepoint。4.5 实验报告写不出只有现象没有对照现象代码跑通了但报告里只有“我加载了模块看到了打印”。原因缺少对照实验和参数变化。解决至少做三组对照比如不挂kprobe时的基线、挂上后的输出、改变过滤条件后的差异。把dmesg时间戳、pid、分配大小这些可量化字段整理成表格比贴十张截图更有说服力。5. 从实验1到可复用的观察方法把一次作业变成长期工具操作系统实验1.doc 这个标题本身没有技术含量但它指向的能力很有含量你能不能在不改内核、不重启机器的前提下观察到一个系统调用、一次进程创建、一次内存分配的真实行为。这个能力一旦建立后面做性能分析、故障排查、安全审计都会用到。我自己的习惯是每做一个实验就把观察脚本和过滤条件整理成一个可复用的小工具而不是做完就删。进阶用法上你可以把kprobe和tracepoint的输出接到perf或bpftrace里做聚合。比如统计一分钟内kmalloc的分配大小分布# 用 bpftrace 统计 kmalloc 分配大小分布 sudo bpftrace -e tracepoint:kmem:kmalloc { bytes hist(args-bytes_alloc); } interval:s:60 { print(bytes); clear(bytes); exit(); }逻辑说明tracepoint:kmem:kmalloc是内核已有的稳定探针args-bytes_alloc是分配大小hist做直方图聚合。参数说明interval:s:60表示 60 秒后打印并退出适合做短时观察。如果bpftrace不可用用perf stat或ftrace的function_graph也能达到类似效果。验证方法上我一般会做两件事一是用strace在用户态看同一个系统调用的参数和返回值和内核态观察结果对照二是用time或perf看观察本身带来的开销确保没有因为探针过多导致结论失真。这两步做完你就能判断自己的观察是“看到了真实行为”还是“看到了探针引入的噪声”。最后说一个血泪教训不要一上来就改内核源码。我见过太多人为了加一个系统调用花两天编译内核最后卡在启动失败上连原来的实验环境都回不去。先用模块和tracepoint把行为看清楚确有必要再动内核树。虚拟机快照和版本控制是你的后悔药动手前先留一份干净环境。希望帮到你。本文还有配套的精品资源点击获取