云计算百科
云计算领域专业知识百科平台

唱着歌谣把内核学(卷二)

内核学习第二卷 · 进程

(1.)文档说明

本文档是独立实现内核,并且记录内核学习过程,以帮助后来的学习者来理解和实现内核,如果有讲解不清楚的地方请见谅。

(2.)组件分析

#ifndef PROCESS_DEFS_H
#define PROCESS_DEFS_H

// 前置声明
struct task_struct;
struct exec_domain;

// 完整定义 restart_block
struct restart_block {
long fn;
unsigned long arg0, arg1, arg2, arg3;
};

typedef unsigned int __u32;
typedef unsigned long mm_segment_t;

struct thread_info {
struct task_struct *task;
struct exec_domain *exec_domain;
__u32 flags;
__u32 status;
__u32 cpu;
int preempt_count;
mm_segment_t addr_limit;
struct restart_block restart_block;
void *sysenter_return;
int uaccess_err;
};

#endif // PROCESS_DEFS_H

(2.1)头文件保护宏:#ifndef / #define / #endif

作用:防止同一个头文件被多次 include,造成重复定义编译错误。

(2.2)前置声明 struct task_struct; struct exec_domain;

作用:可以暂时不需要写出进程描述符内部成员。

(2.3)typedef 自定义类型

作用:给已有的数据类型起别名,不是创建新类型。

(3.)NASM汇编

(3.1)struct restart_block 拆解 + NASM 伪指令

struct restart_block {
long fn; // long,8字节 → dq
unsigned long arg0; // unsigned long,8字节 → dq
unsigned long arg1; // unsigned long,8字节 → dq
unsigned long arg2; // unsigned long,8字节 → dq
unsigned long arg3; // unsigned long,8字节 → dq
};

NASM:

; struct restart_block
RB_FN dq 0
RB_ARG0 dq 0
RB_ARG1 dq 0
RB_ARG2 dq 0
RB_ARG3 dq 0

(3.2)struct thread_info 逐成员标注 NASM 伪指令

struct thread_info {
struct task_struct *task; // 64位指针,8B → dq
struct exec_domain *exec_domain; // 64位指针,8B → dq
__u32 flags; // __u32,4B → dd
__u32 status; // __u32,4B → dd
__u32 cpu; // __u32,4B → dd
int preempt_count; // int,4B → dd
mm_segment_t addr_limit; // unsigned long,8B → dq
struct restart_block restart_block; // 内嵌结构体,40B(5个dq)
void *sysenter_return; // 64位指针,8B → dq
int uaccess_err; // int,4B → dd
};

NASM 静态定义(.data 段,db/dw/dd/dq)

section .data
ti:
ti_task dq 0 ; struct task_struct *task; 8B
ti_exec_domain dq 0 ; struct exec_domain *exec_domain; 8B
ti_flags dd 0 ; __u32 flags; 4B
ti_status dd 0 ; __u32 status;4B
ti_cpu dd 0 ; __u32 cpu;4B
ti_preempt_cnt dd 0 ; int preempt_count;4B
ti_addr_limit dq 0 ; mm_segment_t addr_limit;8B

; struct restart_block 内嵌实体(40字节)
ti_rb_fn dq 0
ti_rb_arg0 dq 0
ti_rb_arg1 dq 0
ti_rb_arg2 dq 0
ti_rb_arg3 dq 0

ti_sysenter_ret dq 0 ; void *sysenter_return;8B
ti_uaccess_err dd 0 ; int uaccess_err;4B

(4.)描述进程状态

进程状态是放在 task_struct(进程描述符)里面的成员。

// 类型别名(和NASM保持一致,不依赖内核头)
typedef unsigned int __u32;
typedef unsigned long mm_segment_t;

// ===================== 1. 进程状态常量(状态开关)=====================
#define TASK_RUNNING 0 // 就绪/正在运行,调度器可以调度上CPU
#define TASK_INTERRUPTIBLE 1 // 可中断睡眠,等待事件,收到信号可唤醒
#define TASK_UNINTERRUPTIBLE 2 // 不可中断睡眠,等待IO,不响应信号
#define TASK_STOPPED 4 // 进程暂停
#define TASK_ZOMBIE 8 // 僵尸进程,已退出,等待父进程回收

// ===================== 2. restart_block 结构体(内嵌在thread_info)=====================
struct restart_block {
long fn; // 重启函数指针
unsigned long arg0, arg1, arg2, arg3; // 函数参数
};

// ===================== 3. 进程描述符 task_struct(保存进程核心信息,含state状态)=====================
struct task_struct {
long pid; // 进程ID
long state; // ✅进程状态开关(核心)
char comm[16]; // 进程名称,16字节字符串
};

// ===================== 4. thread_info 线程信息结构体 =====================
struct thread_info {
struct task_struct *task; // 指向进程描述符task_struct
struct exec_domain *exec_domain; // 执行域指针
__u32 flags;
__u32 status;
__u32 cpu;
int preempt_count;
mm_segment_t addr_limit;
struct restart_block restart_block; // 内嵌restart_block实体
void *sysenter_return;
int uaccess_err;
};

(4.1)每个成员 → NASM 字节伪指令映射(x86-64)

1、struct restart_block

struct restart_block {
long fn; // long 8B → dq
unsigned long arg0; // 8B → dq
unsigned long arg1; // 8B → dq
unsigned long arg2; // 8B → dq
unsigned long arg3; // 8B → dq
};

2、struct task_struct(进程描述符,新增 state)

struct task_struct {
long pid; // 8B → dq
long state; // 8B → dq ✅进程状态
char comm[16]; // 16B → db 16(连续16个字节)
};

3、struct thread_info

struct thread_info {
struct task_struct *task; // 指针8B → dq
struct exec_domain *exec_domain; // 指针8B → dq
__u32 flags; // 4B → dd
__u32 status; // 4B → dd
__u32 cpu; // 4B → dd
int preempt_count; // int 4B → dd
mm_segment_t addr_limit; // unsigned long 8B → dq
struct restart_block restart_block; // 内嵌40字节
void *sysenter_return; // 指针8B → dq
int uaccess_err; // int 4B → dd
};

(4.2)NASM EQU 偏移

; ========== restart_block 偏移 ==========
RB_FN equ 0
RB_ARG0 equ 8
RB_ARG1 equ 16
RB_ARG2 equ 24
RB_ARG3 equ 32

; ========== thread_info 偏移 ==========
TI_TASK equ 0
TI_EXEC_DOMAIN equ 8
TI_FLAGS equ 16
TI_STATUS equ 20
TI_CPU equ 24
TI_PREEMPT_CNT equ 28
TI_ADDR_LIMIT equ 32
TI_RESTART_BLOCK equ 40
TI_SYSENTER_RET equ 80
TI_UACCESS_ERR equ 88

; ==========【新增】task_struct 进程描述符偏移 ==========
TASK_PID equ 0
TASK_STATE equ 8 ; 进程状态state偏移
TASK_COMM equ 16

EQU 就是给数字起别名,方便阅读记忆;更大价值是统一管理常量,修改只改一处,避免硬编码数字带来的错位 bug。

(5.)汇编test.asm

; test.asm
; NASM 64位 Linux 简易进程状态调度演示
; 编译:nasm -f elf64 test.asm -o test.o && ld test.o -o test
; 运行:./test

; ====================== 全部EQU常量【放在文件最开头】======================
; ——– 进程状态常量 ——–
TASK_RUNNING equ 0
TASK_INTERRUPTIBLE equ 1
TASK_UNINTERRUPTIBLE equ 2
TASK_STOPPED equ 4
TASK_ZOMBIE equ 8

; ——– restart_block 成员偏移 ——–
RB_FN equ 0
RB_ARG0 equ 8
RB_ARG1 equ 16
RB_ARG2 equ 24
RB_ARG3 equ 32

; ——– thread_info 成员偏移 ——–
TI_TASK equ 0
TI_EXEC_DOMAIN equ 8
TI_FLAGS equ 16
TI_STATUS equ 20
TI_CPU equ 24
TI_PREEMPT_CNT equ 28
TI_ADDR_LIMIT equ 32
TI_RESTART_BLOCK equ 40
TI_SYSENTER_RET equ 80
TI_UACCESS_ERR equ 88
TI_SIZE equ 92 ; thread_info总字节

; ——– task_struct 进程描述符偏移 ——–
TASK_PID equ 0
TASK_STATE equ 8
TASK_COMM equ 16
TASK_SIZE equ 32
; ========================================================================

section .bss
; task_struct 整块预留,起始标签 task1
task1 resb TASK_SIZE

; thread_info 整块预留,起始标签 ti1 【修复关键点!】
ti1 resb TI_SIZE

section .data
msg_run db "进程:可运行状态,准备调度",10,0
msg_sleep db "进程:睡眠状态,跳过调度",10,0

section .text
global _start

_start:
; 初始化:ti1.task = &task1 (关联thread_info 和进程描述符)
mov rdi, ti1
mov qword [rdi + TI_TASK], task1

; 设置PID=1001
mov rbx, [rdi + TI_TASK]
mov qword [rbx + TASK_PID], 1001

; 设置进程状态:TASK_INTERRUPTIBLE(睡眠态)
mov qword [rbx + TASK_STATE], TASK_INTERRUPTIBLE

; ========== 调度判断:跳转逻辑 ==========
mov rax, [rbx + TASK_STATE]
cmp rax, TASK_RUNNING ; 判断state是否等于运行态
je SCHED_RUN ; 相等则跳转到SCHED_RUN
jmp SCHED_SKIP ; 否则跳到跳过调度

SCHED_RUN:
mov rsi, msg_run
call print_str
jmp EXIT_PROG

SCHED_SKIP:
mov rsi, msg_sleep
call print_str

EXIT_PROG:
; 系统调用退出程序
mov rax,60
mov rdi,0
syscall

; 子函数 print_str(rsi=字符串首地址)
print_str:
push rsi
xor rdx,rdx
.count:
cmp byte [rsi+rdx],0
je .out
inc rdx
jmp .count
.out:
mov rax,1
mov rdi,1
syscall
pop rsi
ret

(6.)裸机boot.asm

; boot.asm 主引导扇区 512字节
[bits 16]
[org 0x7c00]

start:
mov ax, 0
mov es, ax
mov bx, 0x1000 ; 把第二阶段加载到物理地址0x1000
mov ah, 0x02 ; BIOS读扇区
mov al, 1 ; 读取1个扇区(512字节)
mov ch, 0 ; 磁道0
mov cl, 2 ; 从第2扇区读取(扇区1是当前引导扇区)
mov dh, 0 ; 磁头0
int 0x13
jc disk_error ; 读盘失败跳转

cli
lgdt [gdt_desc]

mov eax, cr0
or eax, 1
mov cr0, eax

jmp 0x08:pm_entry ; 远跳转,刷新CS进入保护模式

[bits 32]
pm_entry:
mov ax, 0x10
mov ds, ax
mov es, ax
mov ss, ax
mov esp, 0x7c00

; 清空页目录 0x2000
mov edi, 0x2000
mov ecx, 1024
xor eax, eax
rep stosd

; 页目录第0项:指向页表0x3000,Present+RW
mov dword [0x2000], 0x3003

; 清空页表 0x3000
mov edi, 0x3000
mov ecx, 1024
xor eax, eax
rep stosd

; 填充页表:映射虚拟地址0~4MB 到物理地址0~4MB
mov ebx, 0x3000
mov eax, 0x00000003
mov ecx, 1024
fill_pt:
mov [ebx], eax
add ebx, 4
add eax, 0x1000
loop fill_pt

mov eax, 0x2000
mov cr3, eax ; CR3 = 页目录物理地址

mov eax, cr0
or eax, 0x80000000
mov cr0, eax ; 开启分页

jmp 0x08:0x1000 ; 跳到第二阶段内核代码!

disk_error:
cli
hlt

gdt_start:
dq 0
gdt_code:
dw 0xffff
dw 0
db 0
db 0x9a
db 0xcf
db 0
gdt_data:
dw 0xffff
dw 0
db 0
db 0x92
db 0xcf
db 0
gdt_end:

gdt_desc:
dw gdt_end – gdt_start – 1
dd gdt_start

times 510 – ($-$$) db 0
dw 0xaa55

(6.1)kernel.asm

; kernel.asm 第二阶段裸机内核,加载到0x1000
[bits 32]

; ===================== EQU常量 对应C结构体 =====================
; 进程状态
TASK_RUNNING equ 0
TASK_INTERRUPTIBLE equ 1
TASK_UNINTERRUPTIBLE equ 2
TASK_STOPPED equ 4
TASK_ZOMBIE equ 8

; restart_block
RB_FN equ 0
RB_ARG0 equ 4
RB_ARG1 equ 8
RB_ARG2 equ 12
RB_ARG3 equ 16

; thread_info
TI_TASK equ 0
TI_EXEC_DOMAIN equ 4
TI_FLAGS equ 8
TI_STATUS equ 12
TI_CPU equ 16
TI_PREEMPT_CNT equ 20
TI_ADDR_LIMIT equ 24
TI_RESTART_BLOCK equ 28
TI_SYSENTER_RET equ 48
TI_UACCESS_ERR equ 52
TI_SIZE equ 56

; task_struct 进程描述符
TASK_PID equ 0
TASK_STATE equ 4
TASK_COMM equ 8
TASK_SIZE equ 24

; 显存文本模式 0xB8000,每个字符 2字节:ASCII + 颜色
VID_MEM equ 0xB8000
; 颜色:0x0F = 黑底白字
WHITE_ON_BLACK equ 0x0F
; 颜色:0x0A = 黑底绿字
GREEN_ON_BLACK equ 0x0A
; 颜色:0x04 = 黑底红字
RED_ON_BLACK equ 0x04

; ============= 全局数据:两个进程的thread_info、task_struct ===========
; 放在静态内存,裸机直接固定地址
; task1 PCB
task1 equ 0x1200
ti1 equ 0x1300

; task2 PCB
task2 equ 0x1400
ti2 equ 0x1500

KERNEL_START:
; ======== 初始化进程1 =========
mov dword [ti1 + TI_TASK], task1 ; ti1->task = &task1
mov dword [task1 + TASK_PID], 1
mov dword [task1 + TASK_STATE], TASK_RUNNING
mov byte [task1 + TASK_COMM], 'p'
mov byte [task1 + TASK_COMM+1], '1'

; ======== 初始化进程2 =========
mov dword [ti2 + TI_TASK], task2
mov dword [task2 + TASK_PID], 2
mov dword [task2 + TASK_STATE], TASK_INTERRUPTIBLE
mov byte [task2 + TASK_COMM], 'p'
mov byte [task2 + TASK_COMM+1], '2'

; 清屏
call clear_screen

; ========== 简易调度循环 ==========
SCHED_LOOP:
; 检查进程1
mov esi, ti1
call check_process

; 检查进程2
mov esi, ti2
call check_process

; 简单延时循环
mov ecx,0x080000
delay:
loop delay
jmp SCHED_LOOP

; 函数 check_process(esi = thread_info指针)
; 读取task->state,判断状态,输出彩色文字到显存
check_process:
push ebx
push eax
push edx

mov ebx, [esi + TI_TASK] ; ebx = ti->task (task_struct)
mov eax, [ebx + TASK_STATE] ; eax = task->state
mov edx, [ebx + TASK_PID] ; edx = pid

; 显存位置:pid*80*2,每行80字符,2字节一个字符
mov edi, VID_MEM
imul edx, 160
add edi, edx

cmp eax, TASK_RUNNING
je .proc_run
cmp eax, TASK_INTERRUPTIBLE
je .proc_sleep
jmp .proc_other

.proc_run:
; 绿字:运行态
mov byte [edi], 'P'
mov byte [edi+1], GREEN_ON_BLACK
mov byte [edi+2], 'I'
mov byte [edi+3], GREEN_ON_BLACK
mov byte [edi+4], 'D'
mov byte [edi+5], GREEN_ON_BLACK
mov byte [edi+6], ':'
mov byte [edi+7], GREEN_ON_BLACK
add edi,8
mov al, [ebx + TASK_COMM]
mov [edi],al
mov byte [edi+1],GREEN_ON_BLACK
mov al, [ebx + TASK_COMM+1]
mov [edi+2],al
mov byte [edi+3],GREEN_ON_BLACK
mov byte [edi+4], ' '
mov byte [edi+5],GREEN_ON_BLACK
mov byte [edi+6],'R'
mov byte [edi+7],GREEN_ON_BLACK
mov byte [edi+8],'U'
mov byte [edi+9],GREEN_ON_BLACK
mov byte [edi+10],'N'
mov byte [edi+11],GREEN_ON_BLACK
jmp .proc_done
.proc_sleep:
; 红字:睡眠态
mov byte [edi], 'P'
mov byte [edi+1], RED_ON_BLACK
mov byte [edi+2], 'I'
mov byte [edi+3], RED_ON_BLACK
mov byte [edi+4], 'D'
mov byte [edi+5], RED_ON_BLACK
mov byte [edi+6], ':'
mov byte [edi+7], RED_ON_BLACK
add edi,8
mov al, [ebx + TASK_COMM]
mov [edi],al
mov byte [edi+1],RED_ON_BLACK
mov al, [ebx + TASK_COMM+1]
mov [edi+2],al
mov byte [edi+3],RED_ON_BLACK
mov byte [edi+4], ' '
mov byte [edi+5],RED_ON_BLACK
mov byte [edi+6],'S'
mov byte [edi+7],RED_ON_BLACK
mov byte [edi+8],'L'
mov byte [edi+9],RED_ON_BLACK
mov byte [edi+10],'E'
mov byte [edi+11],RED_ON_BLACK
.proc_other:
.proc_done:
pop edx
pop eax
pop ebx
ret

; 清屏:把整个屏幕填充黑底空白
clear_screen:
mov edi, VID_MEM
mov ecx, 80*25
mov ah, WHITE_ON_BLACK
mov al, ' '
.cls_loop:
mov [edi], ax
add edi, 2
loop .cls_loop
ret

; 补齐到512字节扇区
times 512 – ($ – KERNEL_START) db 0

(6.2)编译、打包成镜像(Linux 下)

# 编译引导扇区
nasm -f bin boot.asm -o boot.bin
# 编译第二阶段内核
nasm -f bin kernel.asm -o kernel.bin

# 生成软盘镜像,1个扇区boot + 1扇区kernel,其余填充0
dd if=/dev/zero of=os.img bs=512 count=2880
dd if=boot.bin of=os.img conv=notrunc
dd if=kernel.bin of=os.img bs=512 seek=1 conv=notrunc

(6.3)用 qemu 模拟器启动

qemu-system-i386 -drive format=raw,file=os.img

(7.)32 位裸机多进程内核

一个用 NASM + C 写的 32 位裸机内核:从软盘镜像启动,进入保护模式、建立自己的 GDT 和分页,然后运行两个进程(proc1 / proc2),用协作式上下文切换轮流执行,并在 VGA 文本模式下打印带有自增计数的运行信息。

(7.1)环境安装与要求

1、运行环境

    64 位 Linux(Debian / Ubuntu / Kali 等),或 Windows 上的 WSL2。终端里能执行 make。

2、依赖工具

    nasm                          汇编器,汇编 boot.asm、loader.asm
    gcc                             C 编译器,需要支持 -m32 编译(能生成 32 位目标文件即可)
    ld (binutils)                链接器,用 -m elf_i386 按 kernel.ld 链接
    make                          构建工具,执行 Makefile
    qemu-system-i386    模拟器,运行生成的 os.img

3、安装命令(Debian / Ubuntu / Kali)

    sudo apt-get update
    sudo apt-get install -y nasm gcc make binutils qemu-system-x86

    # 可选:想在系统里另外链接普通 32 位程序时再装
    sudo apt-get install -y gcc-multilib

    说明:本项目是 freestanding 链接(不连 libc、直接用 ld),所以不装
    gcc-multilib 也能构建成功,它只是方便你做别的 32 位实验。

(7.2)构建与运行

    make                生成 os.img
    make run         用 QEMU 启动并显示内核画面
    make debug    启动 QEMU 并停在起点等待 gdb(gdb: target remote :1234)
    make clean     删除全部编译产物
    make rebuild   先 clean 再重新编译

   没有图形界面时,可用无界面方式启动:

    qemu-system-i386 -drive format=raw,file=os.img -display none

(7.3)文件说明

boot.asm

主引导扇区(512 字节)。用 INT 13h 读 16 个扇区到 0x1000;设置 GDT;
建立 0x10000 页目录 + 0x11000 页表(恒等映射 0~4MB);
开启分页后跳到 0x1000

; boot.asm 主引导扇区 512字节
[bits 16]
[org 0x7c00]

start:
mov ax, 0
mov es, ax
mov bx, 0x1000 ; 把第二阶段加载到物理地址0x1000
mov ah, 0x02 ; BIOS读扇区
mov al, 16 ; 读取16个扇区(8KB),足够装下整个内核镜像
mov ch, 0 ; 磁道0
mov cl, 2 ; 从第2扇区读取(扇区1是当前引导扇区)
mov dh, 0 ; 磁头0
int 0x13
jc disk_error ; 读盘失败跳转

cli
lgdt [gdt_desc]

mov eax, cr0
or eax, 1
mov cr0, eax

jmp 0x08:pm_entry ; 远跳转,刷新CS进入保护模式

[bits 32]
pm_entry:
mov ax, 0x10
mov ds, ax
mov es, ax
mov ss, ax
mov esp, 0x7c00

; 清空页目录 0x10000
mov edi, 0x10000
mov ecx, 1024
xor eax, eax
rep stosd

; 页目录第0项:指向页表0x11000,Present+RW
mov dword [0x10000], 0x11003

; 清空页表 0x11000
mov edi, 0x11000
mov ecx, 1024
xor eax, eax
rep stosd

; 填充页表:映射虚拟地址0~4MB 到物理地址0~4MB
mov ebx, 0x11000
mov eax, 0x00000003
mov ecx, 1024
fill_pt:
mov [ebx], eax
add ebx, 4
add eax, 0x1000
loop fill_pt

mov eax, 0x10000
mov cr3, eax ; CR3 = 页目录物理地址

mov eax, cr0
or eax, 0x80000000
mov cr0, eax ; 开启分页

jmp 0x08:0x1000 ; 跳到第二阶段内核代码!

disk_error:
cli
hlt

gdt_start:
dq 0
gdt_code:
dw 0xffff
dw 0
db 0
db 0x9a
db 0xcf
db 0
gdt_data:
dw 0xffff
dw 0
db 0
db 0x92
db 0xcf
db 0
gdt_end:

gdt_desc:
dw gdt_end – gdt_start – 1
dd gdt_start

times 510 – ($-$$) db 0
dw 0xaa55

loader.asm

保护模式下的入口 start。把栈设为 0x6000 并调用 C 的 kernel_main;
返回后死循环停机。

; loader.asm [bits 32]
[bits 32]
global start
extern kernel_main

section .text
start:
; 设置C代码栈
mov esp, 0x6000
call kernel_main

; 死循环,内核返回就停机
hang_loop:
hlt
jmp hang_loop

kernel.c

内核主体:进程描述符 task_struct、上下文切换 cpu_switch / switch_to、
两个进程 proc1 / proc2、显存输出与整数显示。

// kernel.c
// 32 位裸机 C 内核(freestanding,没有 libc)
// 目标:让两个进程 proc1 / proc2 通过 switch_to 做上下文切换,轮流运行。
//
// 物理内存布局(boot.asm 已建立 0~4MB 的恒等分页):
// 0x00001000 内核镜像 (.text/.rodata/.data/.bss)
// 0x00003000 proc1 内核栈(栈顶 0x4000,向下增长)
// 0x00004000 proc2 内核栈(栈顶 0x5000,向下增长)
// 0x00005000 内核主线程栈(栈顶 0x6000,向下增长)
// 0x00010000 页目录 PD(boot.asm 用,内核不碰)
// 0x00011000 页表 PT(boot.asm 用,内核不碰)

#define TASK_RUNNING 0
#define TASK_INTERRUPTIBLE 1
#define TASK_UNINTERRUPTIBLE 2
#define TASK_STOPPED 4
#define TASK_ZOMBIE 8

#define VID_MEM 0xB8000
#define WHITE_ON_BLACK 0x0F
#define GREEN_ON_BLACK 0x0A
#define RED_ON_BLACK 0x04

// 进程上下文:只需保存栈指针即可,因为所有通用寄存器都已经
// 压在这个任务自己的栈上了。
struct context {
unsigned int eax;
unsigned int ebx;
unsigned int ecx;
unsigned int edx;
unsigned int esi;
unsigned int edi;
unsigned int ebp;
unsigned int esp; // 关键字段:被切换出去那一刻的栈指针
unsigned int eip;
};

// 进程描述符
struct task_struct {
int pid;
int state;
char comm[16];
struct context ctx;
};

struct task_struct p1;
struct task_struct p2;
struct task_struct init_task; // 内核主线程:第一次切换时的“当前任务”
struct task_struct *current;

// 每个任务独立的内核栈顶(栈向下增长)
#define STACK_P1 0x4000
#define STACK_P2 0x5000

// cpu_switch 在栈上保存/恢复 7 个通用寄存器 + 1 个返回地址
#define CTX_SLOTS 8

// —————- 显存输出(C 直接写 0xB8000) —————-
void print_str(int row, char *s, char color)
{
char *v = (char *)VID_MEM + row * 160;
while (*s) {
*v++ = *s++;
*v++ = color;
}
}

// 把无符号整数按十进制写到第 row 行、第 col 列,至少 width 位(右对齐补 0)
// 没有 libc,所以自己把整数拆成字符
void print_uint(int row, int col, unsigned int val, int width, char color)
{
char *v = (char *)VID_MEM + row * 160 + col * 2;
char buf[12];
int n = 0;

do {
buf[n++] = (char)('0' + val % 10);
val /= 10;
} while (val && n < (int)sizeof(buf));
while (n < width && n < (int)sizeof(buf))
buf[n++] = '0';

while (n > 0) {
*v++ = buf[–n];
*v++ = color;
}
}

void clear_screen(void)
{
char *v = (char *)VID_MEM;
for (int i = 0; i < 25 * 80; i++) {
*v++ = ' ';
*v++ = WHITE_ON_BLACK;
}
}

// 初始化任务,并构造“任务第一次被调度”时的初始栈。
// 栈布局(低地址 -> 高地址),正好对应 cpu_switch 里的 pop 顺序:
// [esp+0]=ebp [esp+4]=edi [esp+8]=esi [esp+12]=edx
// [esp+16]=ecx [esp+20]=ebx [esp+24]=eax [esp+28]=入口地址
// 这样 switch_to 恢复完寄存器后执行 ret,就会跳到任务入口函数。
static void task_init(struct task_struct *t, int pid, const char *name,
void (*entry)(void), unsigned int stack_top)
{
unsigned int *sp = (unsigned int *)(stack_top – CTX_SLOTS * 4);
int i;

t->pid = pid;
t->state = TASK_RUNNING;
for (i = 0; i < 15 && name[i]; i++)
t->comm[i] = name[i];
t->comm[i] = '\\0';

for (i = 0; i < CTX_SLOTS – 1; i++)
sp[i] = 0; // 7 个通用寄存器的初值
sp[CTX_SLOTS – 1] = (unsigned int)entry; // ret 的目标 = 任务入口

t->ctx.esp = (unsigned int)sp; // 第一次切换时从这里开始 pop
t->ctx.eip = (unsigned int)entry;
}

// —————- 上下文切换 —————-
// 真正干活的是 cpu_switch:
// 1) 把当前任务的 7 个通用寄存器压到它自己的栈上,并保存此时的栈指针
// 2) 换上目标任务的栈指针
// 3) 恢复目标任务的寄存器并 ret —— 回到它上次被切走的“下一条指令”
// 关键:cpu_switch 必须是 naked(无栈帧)函数。
// 普通 C 函数会在寄存器与返回地址之间插入编译器自己的栈帧(push ebp / sub esp),
// 这样任务第一次启动还能跑,但被“重新调度回来”时 ret 会弹到栈帧里的垃圾地址。
// task_init 是按“寄存器紧跟返回地址”的布局预置初始栈的,两者必须保持一致。
// 注意:naked 函数体只能写“基本汇编”,寄存器直接写 %eax,不用写成 %%eax。
__attribute__((naked))
void cpu_switch(unsigned int *save_esp, unsigned int new_esp)
{
__asm__ __volatile__(
"pushl %ebp\\n\\t"
"pushl %edi\\n\\t"
"pushl %esi\\n\\t"
"pushl %edx\\n\\t"
"pushl %ecx\\n\\t"
"pushl %ebx\\n\\t"
"pushl %eax\\n\\t"
"movl 32(%esp), %eax\\n\\t" /* save_esp(跳过 7 个寄存器 + 返回地址)*/
"movl %esp, (%eax)\\n\\t" /* *save_esp = 当前栈指针 */
"movl 36(%esp), %eax\\n\\t" /* new_esp */
"movl %eax, %esp\\n\\t" /* 切到目标任务栈 */
"popl %eax\\n\\t"
"popl %ebx\\n\\t"
"popl %ecx\\n\\t"
"popl %edx\\n\\t"
"popl %esi\\n\\t"
"popl %edi\\n\\t"
"popl %ebp\\n\\t"
"ret\\n\\t" /* 回到目标任务上次切走的返回地址 */
);
}

// 进程代码调用这个:更新 current,再把“当前栈地址”和“目标栈指针”交给 cpu_switch
void switch_to(struct task_struct *next)
{
struct task_struct *prev = current;
current = next; // 必须在切换前更新,因为切走后不会再回来执行
cpu_switch(&prev->ctx.esp, next->ctx.esp);
}

// —————- 两个进程 —————-
// counter 是各自的局部变量,存放在各自的内核栈上,所以两个进程互不影响。
void proc1(void)
{
unsigned int counter = 0;
while (1) {
print_str(1, "PID1 Running ", GREEN_ON_BLACK);
print_uint(1, 13, counter, 10, GREEN_ON_BLACK);
counter++;
p1.state = TASK_INTERRUPTIBLE;
switch_to(&p2); // 主动让出 CPU 给 proc2
p1.state = TASK_RUNNING; // 再次被调度回来时从这里继续
}
}

void proc2(void)
{
unsigned int counter = 0;
while (1) {
print_str(2, "PID2 Running ", RED_ON_BLACK);
print_uint(2, 13, counter, 10, RED_ON_BLACK);
counter++;
p2.state = TASK_INTERRUPTIBLE;
switch_to(&p1); // 主动让出 CPU 给 proc1
p2.state = TASK_RUNNING;
}
}

// —————- C 内核入口 —————-
void kernel_main(void)
{
clear_screen();

task_init(&p1, 1, "proc1", proc1, STACK_P1);
task_init(&p2, 2, "proc2", proc2, STACK_P2);

// 内核主线程也是一个任务:第一次 switch_to 需要一个“from”
init_task.pid = 0;
init_task.state = TASK_RUNNING;
current = &init_task;

switch_to(&p1); // 启动第一个进程

// 只有当内核主线程被重新调度回来才会执行到这里
while (1)
__asm__ __volatile__("hlt");
}

kernel.ld

链接脚本。所有段从 0x1000 起连续排布,整体保持在 0x3000 以下,
避免与栈、页表冲突。

/* kernel.ld 链接脚本
* 所有段从 0x1000 开始连续排布,整体保持在 0x3000 以下,
* 避免和内核对栈(0x3000~0x6000)、页目录/页表(0x10000/0x11000)冲突。
*/
ENTRY(start);

SECTIONS {
. = 0x1000; /* 加载到物理地址0x1000,和boot的jmp目标匹配 */

.text : {
*(.boot)
*(.text)
*(.text.*)
}

.rodata : {
*(.rodata)
*(.rodata.*)
}

.eh_frame : {
*(.eh_frame)
}

.data : {
*(.data)
*(.data.*)
}

.bss : {
*(COMMON)
*(.bss)
*(.bss.*)
}
}

Makefile

构建脚本:汇编、编译、链接、打包镜像,以及运行 / 调试 / 清理。

# Makefile —— 32 位裸机内核(加油)
# 依赖:nasm / gcc(支持 -m32 编译) / ld(binutils) / make / qemu-system-i386

CC = gcc
AS = nasm
LD = ld
QEMU = qemu-system-i386

# freestanding:不连 libc;-m32 生成 32 位代码
# -fno-stack-protector 裸机没有 __stack_chk_fail,必须关掉栈保护
# -fno-asynchronous-unwind-tables 不生成 .eh_frame,减小镜像体积
CFLAGS = -m32 -ffreestanding -c -g -fno-pic -fno-pie -fno-plt \\
-fno-stack-protector -fno-asynchronous-unwind-tables
ASFLAGS_BIN = -f bin
ASFLAGS_ELF32 = -f elf32
LDSCRIPT = kernel.ld

OBJS = loader.o kernel.o
IMG = os.img

# 所有由构建生成的产物(make clean 时一并删除)
GENERATED = boot.bin loader.o kernel.o kernel.elf kernel.bin $(IMG)

.PHONY: all clean rebuild run debug

all: $(IMG)

kernel.bin kernel.elf: $(OBJS) $(LDSCRIPT)
$(LD) -m elf_i386 -T $(LDSCRIPT) $(OBJS) -o kernel.elf
$(LD) -m elf_i386 -T $(LDSCRIPT) –oformat binary $(OBJS) -o kernel.bin

loader.o: loader.asm
$(AS) $(ASFLAGS_ELF32) loader.asm -o loader.o

kernel.o: kernel.c
$(CC) $(CFLAGS) kernel.c -o kernel.o

boot.bin: boot.asm
$(AS) $(ASFLAGS_BIN) boot.asm -o boot.bin

$(IMG): boot.bin kernel.bin
dd if=/dev/zero of=$(IMG) bs=512 count=2880
dd if=boot.bin of=$(IMG) conv=notrunc
dd if=kernel.bin of=$(IMG) bs=512 seek=1 conv=notrunc

# 删除全部编译产物(.o / .bin / .elf / os.img)
clean:
rm -f $(GENERATED)

# 一步完成:先清干净,再重新编译
rebuild:
$(MAKE) clean
$(MAKE) all

# 启动 QEMU 运行内核
run: $(IMG)
$(QEMU) -drive format=raw,file=$(IMG)

# 启动 QEMU 并停在起点等待 gdb 连接(gdb: target remote :1234)
debug: $(IMG)
$(QEMU) -s -S -drive format=raw,file=$(IMG)

生成的文件(make clean 会全部删除)
    boot.bin      boot.asm 汇编出的 512 字节引导扇区。
    loader.o      loader.asm 的 32 位 ELF 目标文件。
    kernel.o      kernel.c 的 32 位目标文件。
    kernel.elf    带符号信息的 ELF,用于 objdump / gdb 调试。
    kernel.bin    纯二进制内核镜像,被写入磁盘。
    os.img        1.44MB 软盘镜像:第 0 扇区是 boot.bin,第 1 扇区起是 kernel.bin。

赞(0)
未经允许不得转载:网硕互联帮助中心 » 唱着歌谣把内核学(卷二)
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!