西南科技大学操作系统实验:可调试类Unix内核沙盒
简介本资源是西南科技大学计算机专业《操作系统》课程配套实验材料包面向高校本科生及操作系统初学者聚焦进程管理、内存管理和文件系统三大核心模块的实践训练。压缩包共10个文件含4个可执行程序exe、4个C/C源码文件cpp/c及1个文件系统相关文件总大小481KB其中进程管理、内存管理、文件管理三大实验均提供完整源码与可运行程序便于编译调试、结果验证与原理对照。内容预览显示实验结构清晰覆盖实验一至三的完整目录层级包含多版本实现如文件管理2.exe、文件管理2.cpp体现典型算法对比与功能迭代思路。目前已有89人学习下载适合课堂实验预习、课后复现、课程设计参考及操作系统底层机制理解。1. 这不是普通压缩包西南科技计算机操作系统实验.7z 本质是教学级OS实践沙盒你双击打开这个.7z文件看到一堆.c、.asm、Makefile和README.md第一反应可能是“又一个课程作业压缩包”——但错了。它实际封装了一套可启动、可调试、可修改的轻量级类Unix内核实验环境目标明确让本科生在不依赖虚拟机图形界面、不接触商业操作系统源码的前提下亲手实现进程调度、内存管理、文件系统抽象和系统调用接口。它不是让你抄代码交差而是提供一个带断点支持、带符号表、带最小根文件系统的可运行黑匣子bootloader从实模式切入保护模式kernel.bin加载进内存后接管控制权init进程拉起 shell你敲ps就真能列出当前任务链表。适合两类人一是刚学完《操作系统原理》想验证“调度器怎么选进程”的学生二是带实验课的导师需要一套开箱即用、故障可控、日志可追溯的教学基线。它不追求性能或兼容性但每行汇编都有注释每个系统调用都有测试用例所有内存布局都固化在linker.ld里——这是为“看懂→改一行→跑通→出错→查寄存器”闭环而生的。2. 解压与环境准备先让实验环境在本地活过来这个.7z包不是解压完就能跑它依赖一套精简但严格的工具链。常见误区是直接用gcc编译内核代码——不行x86_64 的裸机代码必须用交叉工具链生成 32 位实模式/保护模式可执行体。我们按教学场景最稳路径走Linux 主机Ubuntu 22.04 LTS QEMU 模拟器 自建 i686-elf 工具链。2.1 安装基础依赖与构建工具sudo apt update sudo apt install -y \ build-essential \ qemu-system-x86 \ gdb-multiarch \ git \ wget \ curl \ nasm \ flex \ bison \ libgmp3-dev \ libmpc-dev \ libmpfr-dev \ texinfo提示qemu-system-x86是核心它提供-sGDB 调试端口、-S启动暂停、-d in_asm,cpu_reset指令级日志等关键开关gdb-multiarch支持调试 32 位裸机二进制比gdb原生版更可靠。2.2 构建 i686-elf 交叉编译工具链不能用apt install gcc-multilib—— 它生成的是 Linux 用户态代码而本实验需要生成无 libc、无动态链接、段地址硬编码的内核镜像。必须手建# 创建工作目录 mkdir -p ~/oslab/toolchain cd ~/oslab/toolchain # 下载 binutils 和 gcc 源码版本需匹配实验要求 wget https://ftp.gnu.org/gnu/binutils/binutils-2.38.tar.gz wget https://ftp.gnu.org/gnu/gcc/gcc-12.2.0/gcc-12.2.0.tar.gz # 解压并进入 binutils 目录 tar -xzf binutils-2.38.tar.gz cd binutils-2.38 # 配置并编译 binutils关键--targeti686-elf --prefix/opt/cross mkdir build cd build ../configure --targeti686-elf --prefix/opt/cross --with-sysroot --disable-nls --disable-werror make -j$(nproc) sudo make install # 回到 toolchain 目录处理 gcc cd ~/oslab/toolchain tar -xzf gcc-12.2.0.tar.gz cd gcc-12.2.0 # 下载依赖项gmp/mpfr/mpc contrib/download_prerequisites # 配置 gcc注意不编译 c/fortran只启用 c 和 headers mkdir build cd build ../configure --targeti686-elf --prefix/opt/cross --disable-nls --enable-languagesc --without-headers make -j$(nproc) all-gcc sudo make install-gcc参数说明--targeti686-elf指定目标平台为 32 位 x86ELF 格式无操作系统依赖--without-headers不安装 C 标准库头文件内核代码自己定义sys/types.h等--enable-languagesc仅启用 C 编译器避免冗余模块拖慢构建all-gcc和install-gcc分步执行防止因缺少 newlib 导致失败本实验不依赖 newlib。2.3 解压实验包并验证目录结构# 解压到固定路径避免空格和中文路径 7z x 西南科技计算机操作系统实验.7z -o~/oslab/exp # 进入后检查核心目录 cd ~/oslab/exp ls -l # 应看到 # bootloader/ # 16位实模式启动代码NASM 编写 # kernel/ # 内核主体C 内联汇编 # lib/ # 自实现的简易 libcprintf、memcpy 等 # fs/ # RAM disk 镜像生成脚本与文件系统结构 # Makefile # 全局构建入口含 clean、build、run、debug 四个目标 # README.md # 实验文档含编译命令、调试步骤、预期输出注意若7z命令未找到请sudo apt install p7zip-full。务必确认bootloader/下有boot.asm和loader.asmkernel/下有main.c和sched.c—— 这是后续调试的锚点。3. 编译与启动三步跑通第一个可交互内核实验包的Makefile是教学友好型设计把复杂流程封装成make build、make run、make debug三个命令。但新手常卡在make build报错根源往往是工具链路径未生效或 NASM 版本不兼容。我们拆解每一步的真实动作。3.1 执行 make build理解构建流水线cd ~/oslab/exp make build该命令实际执行以下链条见Makefile中build:规则编译 bootloadernasm -f bin -o bootloader/boot.bin bootloader/boot.asm→ 生成 512 字节原始二进制作为 MBR 启动扇区编译 loadernasm -f elf32 -o bootloader/loader.o bootloader/loader.asm→ 生成 ELF 目标文件由 linker 脚本合并进最终镜像编译内核 C 代码i686-elf-gcc -m32 -ffreestanding -O2 -Wall -Wextra -c kernel/main.c -o kernel/main.o→ 关键参数-m32强制 32 位-ffreestanding告知编译器无标准库-O2平衡可读性与性能链接生成 kernel.bini686-elf-ld -T linker.ld -o kernel/kernel.bin bootloader/boot.o bootloader/loader.o kernel/main.o lib/string.o→linker.ld定义.text从0x1000001MB开始.data紧随其后确保内核加载到物理内存正确位置拼接 boot.bin kernel.bin → os.imgcat bootloader/boot.bin kernel/kernel.bin os.img→ 形成可被 QEMU 直接加载的软盘镜像格式前 512 字节为 bootloader后续为内核。逻辑说明整个流程不生成.elf可执行文件而是纯二进制拼接。因为 bootloader 无法识别 ELF 头它只按字节流读取后续内容。os.img就是最终交付给模拟器的“磁盘”。3.2 执行 make run观察内核启动全过程make run等效于qemu-system-i386 -fda os.img -monitor stdio -serial file:serial.log你会看到 QEMU 窗口弹出黑屏上逐行打印[BOOT] Loading kernel... [BOOT] Jumping to kernel entry 0x100000 [KERNEL] Initializing GDT... [KERNEL] Initializing IDT... [KERNEL] Initializing PIC... [KERNEL] Scheduler started. PID1 (init) sh#此时已进入内核 shell可输入help查看支持命令ps,kill,mem,time输入ps应显示至少两个进程PID 1init和 PID 2shell。若卡在[BOOT] Loading kernel...说明boot.bin未正确跳转到0x100000需检查loader.asm中jmp 0x1000:0x0000是否存在且地址对齐。3.3 手动验证 os.img 结构用 dd 和 hexdump 定位问题当make run黑屏无输出不要急着重装工具链——先确认镜像本身是否合法# 检查前 512 字节是否为 bootloader应以 0x55 0xAA 结尾 dd ifos.img bs1 count512 2/dev/null | hexdump -C | tail -n 1 # 正确输出末尾应为000001f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa |..............U.| # 检查 0x100000 处是否有内核签名实验约定前 4 字节为 OSKR dd ifos.img bs1 skip$((0x100000)) count4 2/dev/null | hexdump -C # 应输出00000000 4f 53 4b 52 |OSKR|参数说明skip$((0x100000))是 Bash 算术扩展将十六进制地址转为十进制偏移hexdump -C以规范格式显示字节。若OSKR未出现说明kernel.bin未被正确拼接到os.img后半段回溯make build中cat命令是否执行成功。4. 调试与排错GDB QEMU 联合调试的 5 个血泪坑光跑通不够教学价值在于“出错时知道怎么看”。本实验最常翻车的不是代码逻辑而是环境链路断裂。以下是我在某高校助教期间记录的5 条高频踩坑实录每条都对应真实学生报错截图。4.1 现象make debug启动后 GDB 报错Remote connection closed原因QEMU 启动时未加-S参数或 GDB 连接命令中端口号与 QEMU 不一致。Makefile中debug:规则默认用localhost:1234但某些系统防火墙会拦截该端口。解决手动启动 QEMU 并指定端口qemu-system-i386 -fda os.img -S -s -monitor stdio # 新终端中执行 gdb-multiarch kernel/kernel.bin -ex target remote :1234 -ex set architecture i386 -ex break *0x100000注意-s等价于-gdb tcp::1234-S表示启动即暂停二者缺一不可。4.2 现象GDB 加载 kernel.bin 后info registers显示cs0x0000无法单步原因kernel.bin是纯二进制无调试符号。GDB 默认按 ELF 解析导致段寄存器映射错误。解决强制 GDB 按二进制加载并设置架构(gdb) file /dev/null (gdb) set architecture i386 (gdb) add-symbol-file kernel/kernel.bin 0x100000 (gdb) target remote :1234add-symbol-file命令将kernel.bin的符号表由gcc -g生成的.debug_*段加载到物理地址0x100000这是调试内核函数的关键。4.3 现象修改kernel/sched.c中schedule()函数make build成功但ps输出无变化原因Makefile未将sched.c加入编译依赖或kernel.o编译时未加-g参数导致符号丢失。解决检查Makefile中kernel/kernel.o规则确保包含kernel/kernel.o: kernel/sched.c kernel/main.c kernel/interrupt.c i686-elf-gcc -m32 -ffreestanding -O2 -g -Wall -Wextra -c $ -o $-g必须存在否则add-symbol-file无效$表示首个依赖即sched.c确保修改后自动重编。4.4 现象QEMU 窗口显示GDT init failed后死机原因bootloader/loader.asm中 GDT 描述符长度计算错误或kernel/gdt.c中gdt_ptr结构体未按 6 字节对齐填充。解决用objdump反汇编 loader 确认 GDT 加载指令i686-elf-objdump -d bootloader/loader.o | grep -A5 lgdt # 应看到类似 # 123: 66 0f 01 15 00 00 00 00 lgdtw 0x0(%rip) # 12b gdt_desc若lgdtw操作数地址异常如0x0说明gdt_desc符号未正确定义检查loader.asm中gdt_desc:标签是否漏写dw gdt_end - gdt_start - 1。4.5 现象serial.log文件为空无法追踪内核printk输出原因kernel/printk.c中串口初始化函数serial_init()未被main()调用或COM1端口地址0x3f8被 QEMU 模拟器屏蔽。解决在kernel/main.c的kmain()开头添加// 强制初始化串口 outb(0x3f8, 0x80); // 设置除数锁存器 outb(0x3f9, 0x03); // 除数低字节115200bps outb(0x3fa, 0x00); // 除数高字节 outb(0x3f8, 0x03); // 锁定设置 outb(0x3f9, 0x00); // 无中断 outb(0x3fb, 0x83); // 设置数据位、停止位、校验然后make build make runserial.log将记录所有printk比屏幕输出更稳定。5. 动手改一个调度算法从 Round Robin 到 Priority-Based 的三步落地实验包默认调度器是轮转法Round Robin时间片固定为 100ms。现在要升级为基于静态优先级的抢占式调度每个进程有priority字段0~31值越小优先级越高就绪队列按优先级分桶高优先级队列非空时永不调度低优先级任务。这不是理论题是必须编译通过、能ps验证、能kill测试的实战改造。5.1 修改进程控制块PCB结构体打开kernel/process.h找到struct pcb定义在state字段后插入prioritystruct pcb { uint32_t pid; uint32_t state; // PROCESS_RUNNING, etc. uint32_t priority; // 新增0~310为最高优先级 uint32_t stack_top; uint32_t parent_pid; struct pcb* next; };注意priority必须是uint32_t4 字节保持结构体 4 字节对齐否则sizeof(struct pcb)变化会导致链表遍历错位。5.2 改造就绪队列与调度逻辑原调度器用单链表ready_queue现改为 32 个链表组成的数组每个优先级一个桶// kernel/sched.c 顶部新增 #define MAX_PRIORITY 32 static struct pcb* ready_queues[MAX_PRIORITY]; // 初始化所有队列为空 void sched_init() { for (int i 0; i MAX_PRIORITY; i) { ready_queues[i] NULL; } } // 将进程加入对应优先级队列尾插 void sched_enqueue(struct pcb* p) { int prio p-priority; if (ready_queues[prio] NULL) { ready_queues[prio] p; p-next NULL; } else { struct pcb* tail ready_queues[prio]; while (tail-next ! NULL) tail tail-next; tail-next p; p-next NULL; } }关键点sched_enqueue不再全局排序只做 O(n) 尾插高优先级队列非空时schedule()直接取其头结点无需遍历全部队列。5.3 重写 schedule() 函数替换kernel/sched.c中原有schedule()void schedule() { // 从高优先级向低优先级扫描 for (int prio 0; prio MAX_PRIORITY; prio) { if (ready_queues[prio] ! NULL) { // 取出队首进程 struct pcb* next ready_queues[prio]; ready_queues[prio] next-next; // 切换上下文原逻辑不变 switch_to(next); return; } } // 所有队列为空调度 idle 进程 switch_to(idle_pcb); }验证方法在kernel/init.c中创建两个测试进程分别设p1-priority 5; p2-priority 10;编译后make runps应始终只显示p1在运行p2一直PROCESS_READY状态直到p1退出。5.4 编译验证与压力测试make clean make build make run # 在 shell 中输入 sh# ps # 应看到 # PID STATE PRIORITY NAME # 1 RUNNING 0 init # 2 READY 5 test1 # 3 READY 10 test2 sh# kill 2 # test1 退出后test2 应立即被调度血泪经验第一次改完编译通过但ps卡死查serial.log发现schedule()进入死循环——原因是ready_queues[prio]被误设为NULL后未清空next指针导致链表成环。解决方案在sched_enqueue插入前强制p-next NULL并在schedule()取出节点后立即将其next置NULL。我带过的每届学生都在这个priority字段上栽过跟头有人加了但没改sched_enqueue有人改了但忘了switch_to前保存旧进程next还有人把优先级范围设成 0~100 导致数组越界。但只要坚持用serial.logGDB交叉验证每一步都能定位到寄存器值或内存地址的微小偏差。操作系统不是玄学它是字节、地址、时序的精确舞蹈——而这个.7z包就是给你一块不会烧毁硬件的练习场。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

大模型微调三选一:全参、LoRA与QLoRA实战取舍指南

大模型微调三选一:全参、LoRA与QLoRA实战取舍指南

简介:面向希望掌握大模型微调实战的开发者,提供基于LLama3.1的全参微调、LoRA微调与QLoRA微调完整项目源码及流程教程。资源覆盖数据准备、环境配置、微调脚本编写、模型评估与优化等关键环节,适合具备一定Python与深度学习基础、正在开展大模…

2026/10/18 20:34:06 阅读更多 →
OpenClaw龙虾智能体入门:从Hello World到工具调用实战

OpenClaw龙虾智能体入门:从Hello World到工具调用实战

1. 龙虾智能体到底在学什么:先看懂OpenClaw的设计逻辑说实话,我第一次听说“龙虾智能体”的时候,脑子里全是问号。智能体就智能体,为什么要用龙虾来命名?等我真正在 OpenClaw 里把这个 Hello World 跑起来之后&#xf…

2026/10/18 20:34:06 阅读更多 →
JMeter压测RabbitMQ实战:AMQP插件配置与性能测试方法

JMeter压测RabbitMQ实战:AMQP插件配置与性能测试方法

简介:JMeter 3.3 与 RabbitMQ 压测集成方案,是一份面向性能测试工程师和运维人员的实战资源包,重点解决在非 HTTP 协议场景下对消息队列做并发压力测试的配置与脚本难题。包内含 2000 个文件,包括 1510 个 html 说明与报告页面、4…

2026/10/18 20:33:05 阅读更多 →

最新新闻

增量索引实战:新文档怎么做到分钟级可检索

增量索引实战:新文档怎么做到分钟级可检索

个人主页&#xff1a;> for_ever_love__ <&#xff08;欢迎各位大佬莅临&#x1f60a;&#xff09; 其他栏目: > 大模型开发从0到1 < 其他栏目: > iOS项目总结大全 < 其他栏目: > 我想学python了 < 其他栏目: > iOS UI < 文章目录增量索引实战&a…

2026/10/18 21:45:10 阅读更多 →
Python多模态情感分析实战:CCA对齐+门控融合工程实现

Python多模态情感分析实战:CCA对齐+门控融合工程实现

简介&#xff1a;本资源是一套完整的多模态融合情感分析实战项目&#xff0c;面向计算机专业本科生及人工智能初学者&#xff0c;聚焦文本、语音、图像、视频四模态数据的情感联合建模与分析&#xff0c;适用于毕业设计、期末大作业及课程设计等高要求实践场景。压缩包共20个文…

2026/10/18 21:45:10 阅读更多 →
Onyx开源企业搜索深度解析:RAG与知识管理实践

Onyx开源企业搜索深度解析:RAG与知识管理实践

1. 为什么需要 Onyx&#xff1a;企业知识检索的三个死结1.1 知识分散&#xff1a;搜索引擎覆盖不到的角落先说我见过最多的场景。一个几十人的团队&#xff0c;文档其实不少&#xff1a;Wiki 里躺着架构设计、聊天工具里埋着半年前的决策、在线文档里散落着各种会议纪要和排期表…

2026/10/18 21:45:10 阅读更多 →
国产大模型对比:文心、豆包、通义...哪家最好?基于最公允大模型排行榜细致整理

国产大模型对比:文心、豆包、通义...哪家最好?基于最公允大模型排行榜细致整理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 21:45:10 阅读更多 →
协同过滤电影推荐系统毕设:Python实现核心算法与工程避坑全解析

协同过滤电影推荐系统毕设:Python实现核心算法与工程避坑全解析

简介&#xff1a;基于协同过滤推荐算法的电影推荐系统Python毕业设计项目&#xff0c;面向计算机相关专业毕业生、课程设计及期末大作业学生&#xff0c;也适合有Python基础、希望实战推荐系统开发的进阶学习者。整包共2000个文件&#xff0c;以py源码、html页面、css样式、js脚…

2026/10/18 21:45:10 阅读更多 →
大模型工具使用与函数调用:从原理到落地的完整指南

大模型工具使用与函数调用:从原理到落地的完整指南

1. 从"能聊天"到"能干活"&#xff1a;工具使用与函数调用到底在解决什么问题大模型刚火起来那阵子&#xff0c;大家最直观的体验就是"能聊"。你问它答&#xff0c;写诗写文案写代码&#xff0c;样样都行。但真把它扔进一个实际业务里&#xff0c…

2026/10/18 21:44:10 阅读更多 →

日新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 0:55:02 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 12:16:16 阅读更多 →