Linux 0.11 内核栈进程切换实战:5步手写switch_to汇编与fork改造

发布时间:2026/7/6 11:58:02
Linux 0.11 内核栈进程切换实战:5步手写switch_to汇编与fork改造 Linux 0.11 内核栈进程切换实战从理论到落地的五步拆解1. 环境准备与核心概念重塑在开始动手修改Linux 0.11内核代码前我们需要先搭建好实验环境并理解几个关键概念。不同于现代Linux发行版这个经典版本需要特定的工具链# 实验环境配置Ubuntu示例 sudo apt install gcc-3.4 bin86 libc6-dev-i386 wget https://oldlinux.org/Linux.old/bochs/linux-0.11.tar.gz tar zxvf linux-0.11.tar.gz内核栈与进程控制块(PCB)的独特关系是这个实验的基石。在Linux 0.11中每个进程的PCB和内核栈共享同一个4KB内存页--------------------- 0x0000 | PCB | | (task_struct) | --------------------- | | | 内核栈空间 | | (向下增长) | --------------------- 0x1000这种设计带来三个重要特性通过current指针可直接定位当前进程的内核栈内核栈指针可通过PAGE_SIZE (long)p计算获得p为PCB指针切换进程时只需保存/恢复ESP寄存器值2. switch_to汇编重写详解我们需要在kernel/system_call.s中实现新的switch_to函数以下是关键步骤的逐行解析.align 2 switch_to: pushl %ebp # 保存当前栈帧 movl %esp, %ebp # 建立新栈帧 pushl %ecx # 保存寄存器 pushl %ebx pushl %eax movl 8(%ebp), %ebx # 获取参数pnext目标PCB cmpl %ebx, current # 判断是否为目标进程 je 1f # 如果是则跳转到结尾 # PCB切换原子操作 movl %ebx, %eax xchgl %eax, current # 更新TSS中的ESP0仅0号进程的TSS movl tss, %ecx addl $4096, %ebx # 计算目标内核栈底 movl %ebx, 4(%ecx) # ESP0偏移量为4 # 内核栈切换 movl %esp, 12(%eax) # 保存当前ESP到PCB movl 8(%ebp), %ebx # 重新加载目标PCB movl 12(%ebx), %esp # 加载目标ESP # LDT切换 movl 16(%ebp), %ecx # 获取LDT选择子 lldt %cx # 加载LDTR movl $0x17, %ecx # 更新FS段寄存器 mov %cx, %fs 1: popl %eax # 恢复寄存器 popl %ebx popl %ecx popl %ebp ret关键修改点对比表原TSS切换方式内核栈切换方式通过ljmp指令触发CPU自动保存状态手动保存关键寄存器TR寄存器指向各进程的TSS所有进程共享0号进程TSS切换开销约200时钟周期切换开销大幅降低依赖硬件机制纯软件实现3. schedule函数适配改造在kernel/sched.c中我们需要调整schedule函数以适配新的切换方式// 修改后的核心逻辑 void schedule(void) { struct task_struct *pnext init_task; // 新增目标PCB指针 while (1) { // ...原有调度逻辑不变... if ((*p)-state TASK_RUNNING (*p)-counter c) { c (*p)-counter; next i; pnext *p; // 记录PCB指针 } } // 调用新switch_to增加LDT参数 switch_to(pnext, _LDT(next)); }PCB结构变更需要在include/linux/sched.h中添加内核栈指针struct task_struct { long state; long counter; long priority; long kernelstack; // 新增内核栈指针字段 // ...其他原有字段... };对应的0号进程初始化宏也需要更新#define INIT_TASK \ { 0,15,15, PAGE_SIZE(long)init_task, \ // 初始化kernelstack /* 其余初始化保持不变 */ }4. fork函数内核栈初始化改造kernel/fork.c中最关键的是正确初始化子进程内核栈。以下是改造后的核心代码int copy_process(...) { // ...原有代码... // 获取子进程内核栈指针 long *krnstack (long *)(PAGE_SIZE (long)p); // 构建中断返回帧 *(--krnstack) ss 0xffff; *(--krnstack) esp; *(--krnstack) eflags; *(--krnstack) cs 0xffff; *(--krnstack) eip; // 构建寄存器保存区 *(--krnstack) ds 0xffff; *(--krnstack) es 0xffff; *(--krnstack) fs 0xffff; *(--krnstack) gs 0xffff; *(--krnstack) esi; *(--krnstack) edi; *(--krnstack) edx; // 设置首次切换返回点 *(--krnstack) (long)first_return_from_kernel; *(--krnstack) ebp; *(--krnstack) ecx; *(--krnstack) ebx; *(--krnstack) 0; // eax0表示子进程 p-kernelstack krnstack; // 保存栈指针 // ...其余初始化代码... }父子进程内核栈对比图父进程内核栈布局 子进程内核栈布局 ---------------- ---------------- | SS | | SS | | ESP | | ESP | | EFLAGS | | EFLAGS | | CS | | CS | | EIP | | EIP | ---------------- ---------------- | DS | | DS | | ES | | ES | | FS | | FS | | GS | | GS | | ESI | | ESI | | EDI | | EDI | | EDX | | EDX | ---------------- ---------------- | 返回地址 | | first_return | | EBP | | EBP | | ECX | | ECX | | EBX | | EBX | | EAX | | 0 (EAX) | ---------------- ----------------5. 首次返回汇编与调试技巧在kernel/system_call.s中添加首次返回的汇编代码.align 2 first_return_from_kernel: popl %edx popl %edi popl %esi pop %gs pop %fs pop %es pop %ds iret # 关键指令返回到用户态常见问题排查指南内核崩溃无输出检查bochs配置文件中的magic_break设置在关键位置添加printk输出使用gdb远程调试bochs -q -f bochsrc-gdb.txt进程切换后寄存器值异常确认first_return_from_kernel的弹栈顺序检查fork中内核栈初始化是否漏掉某些寄存器验证LDT选择子是否正确传递系统调用后无法返回检查ret_from_sys_call标签是否保留确认system_call中保存了所有必要寄存器验证ESP0在TSS中的偏移量是否为4调试示例代码可在schedule()中添加printk(Switching from %d to %d\n, current-pid, pnext-pid); printk(Current stack: 0x%x, Next stack: 0x%x\n, current-kernelstack, pnext-kernelstack);实验完成后可以通过编写用户态测试程序验证效果#include unistd.h #include stdio.h int main() { int pid fork(); if (pid 0) { printf(Child process running\n); } else { printf(Parent process running\n); } return 0; }