概述

《只是为了好玩》书中,林纳斯描述过他最早的试验性程序就是执行两个不同的任务(一个不断输出A,另一个输出B),同时不断地让 CPU 在两个任务间做切换。结合《Linux 内核完全注释》提供的一个多任务切换示例程序,本篇将就多任务切换程序的执行流程进行详述,并提供当下汇编工具下的适配。

关于运行环境的说明,欢迎参考 Bochs 仿真器使用简介

引导程序

理解 Linux Kernel (1) 中已经描述过 BIOS 加载/执行引导程序的全流程。操作系统的概念对于处理器、内存等底层硬件来说,并不是必须的。处理器永远只是忠实地执行指令指针(IP, instruction pointer)指向的机器指令。那么,机器上电之后,第一个 IP 是如何提供给处理器的呢?由硬件来直接完成初始化的工作(至于细节,没实际操作过,不表)。

在机器上电启动之后,存储在非易失性存储器/只读存储器上的 BIOS 程序将被加载到内存,并执行(至于细节,不甚了解,不表)。随后,BIOS 从指定磁盘(软盘、硬盘等)读取首个扇区 512 字节的内容(称为 boot 引导程序),载入到预定的内存地址(0x7c000 开始的内存块)。同时处理器 JMP 到 CS:IP = 0x07c00:0x0000 的位置。从而触发引导程序。

随后处理器将忠实地执行 boot 引导程序描述的指令。至于引导程序的实现是让处理器执行一个操作系统程序,还是执行一个用户程序。这完全取决于编写引导程序的人,而对处理器来说,完全没有差别。

BIOS -> boot 引导程序 -> 操作系统引导程序 -> 操作系统 这就构成了一个宏观的操作系统启动的一个流程。

boot.s 引导程序 主体代码来自《Linux 内核完全注释》,进行了一定量的改写

BOOTSEG = 0x07c0
SYSSEG = 0x0100
SYSLEN = 17

entry start
start:
    jmpi    go,#BOOTSEG
go:
    mov ax,cs
    mov ds,ax
    mov ss,ax
    mov sp,#0x0400

load_system:
    xor dx,dx       ! 开始位置, 磁头:硬盘号
    mov cx,#0x0002  ! 开始位置, 磁道:扇区
    mov ax,#0x0100
    mov es,ax       ! 载入到, ES 段
    xor bx,bx       ! 载入到, 偏移量 
    mov ax,#0x0211  ! AH: 读取扇区子功能, AL: 读取多少个扇区
    int 0x13        ! BIOS 13 号中断
    jnc continue_load   ! JUMP if CF = 0
die:
    jmp die

continue_load:
    cli             ! 清除中断允许位标志
    mov ax,#SYSSEG 
    mov ds,ax       ! 设置数据段寄存器位置 0x1000
    xor ax,ax
    mov es,ax       ! 设置扩展段寄存器 0x0000
    mov cx,#0x1000  ! 计数器
    sub si,si
    sub di,di
    rep 
    movsw

    mov ax,#BOOTSEG
    mov ds,ax       ! 重新设置数据段寄存器到当前数据段基地址
    lidt idt_48     ! 设置中断描述符表寄存器
    lgdt gdt_48     ! 设置全局描述符表寄存器

    mov ax,#0x0001
    lmsw ax         ! 设置 CR0, 进入保护模式
    jmpi 0,8

gdt:
    .word   0,0,0,0
    .word   0x07FF,0x0000,0x9A00,0x00C0
    .word   0x07FF,0x0000,0x9200,0x00C0

idt_48:
    .word   0,0,0
gdt_48:
    .word   0x07FF,0x7C00+gdt,0

.org 510
    .word   0xAA55

这段汇编程序,通过 load_system 标识符标识的这段程序表明需要加载0号磁盘,0号磁头,0号磁道,从第2扇区开始,连续17个扇区的内容(这里将存储支持任务切换的程序),加载到内存以 0x1000 开始的物理地址处。

continue_load 标识将 0x1000 物理地址开始的 4096 字 (即 8192 字节) 的内容依次复制到以 0x0000 开始的物理地址处。

其后,设置 IDT (中断描述符表)、IDTR(中断描述符表寄存器) 及 GDT(全局描述符表)、GDTR(全局描述符表寄存器),将 CPU 运行模式改成 保护模式 ,继而将控制权转交给这个被加载进来的程序。

多任务程序

主体内容来自《Linux 内核完全注释》,经过一定量改变以适应当前运行环境

# head.s
.code32
LATCH = 11930
SCRN_SEL = 0x18
TSS0_SEL = 0x20
LDT0_SEL = 0x28
TSS1_SEL = 0x30
LDT1_SEL = 0x38

.text
.globl startup_32
startup_32:

    movl $0x00000010,%eax       # 段选择符 2
    mov %ax,%ds                
    lss init_stack,%esp         # Load Far Pointer 加载到 SS:ESP 

    call setup_idt              # 设置中断描述符表
    call setup_gdt              # 设置全局描述符表
    movl $0x00000010,%eax
    mov %ax,%ds
    mov %ax,%es
    mov %ax,%fs
    mov %ax,%gs
    lss init_stack,%esp         # Load Far Pointer 加载到 SS:ESP

# 设置 8253 定时芯片 10s 一个中断
    movb $0x36,%al  
    movl $0x00000043,%edx
    outb %al,%dx
    movl $LATCH,%eax
    movl $0x40,%edx
    outb %al,%dx
    movb %ah,%al
    outb %al,%dx

    movl $0x00080000,%eax       # 重新设置 int 0x08 时钟中断
    movw $timer_interrupt,%ax
    movw $0x8E00,%dx
    movl $0x08,%ecx
    lea idt(,%ecx,8),%esi
    movl %eax,(%esi)
    movl %edx,4(%esi)
    movw $system_interrupt,%ax  # 重新设置 int 0x80 系统中断
    movw $0xef00,%dx
    movl $0x80,%ecx
    lea idt(,%ecx,8),%esi
    movl %eax,(%esi)
    movl %edx,4(%esi)

    pushfl                      # 重置 EFLAGS 嵌套任务标志位
    andl $0xffffbfff,(%esp)
    popfl
    movl $TSS0_SEL,%eax
    ltr %ax                     # Load Task Register
    movl $LDT0_SEL,%eax
    lldt %ax                    # Load Local Descriptor Register
    movl $0,current
    sti                         # set interrupt flag
    pushl $0x17
    pushl $init_stack
    pushfl
    pushl $0x0f
    pushl $task0
    iret


setup_gdt:
    lgdt lgdt_opcode
    ret
setup_idt:
    lea ignore_int,%edx         # 预先把中断处理程序的偏移地址 ignore_int 存到 EDX
    movl $0x00080000,%eax       # 预存 0x0008 - 段选择符
    movw %dx,%ax                # 补上 0-15 位偏移地址
    movw $0x8E00,%dx            # DX 补上标志位
    lea idt,%edi
    mov $256,%ecx
rp_idt: movl %eax,(%edi)        # 循环 256 遍处理 IDT
    movl %edx,4(%edi)
    addl $8,%edi
    dec %ecx
    jne rp_idt
    lidt lidt_opcode
    ret


write_char:
    push %gs
    pushl %ebx
    mov $SCRN_SEL,%ebx
    mov %bx,%gs
    movl scr_loc,%ebx
    shl $1,%ebx
    movb %al,%gs:(%ebx)
    shr $1,%ebx
    incl %ebx
    cmpl $2000,%ebx
    jb 1f
    movl $0,%ebx
1:  movl %ebx,scr_loc
    popl %ebx
    pop %gs
    ret



.align 4
ignore_int:                 # 默认的中断处理程序
    push %ds
    pushl %eax
    movl $0x10,%eax
    mov %ax,%ds
    movl $67,%eax
    call write_char
    popl %eax
    pop %ds
    iret


.align 4
timer_interrupt:            # 定时中断处理程序
    push %ds
    pushl %eax
    movl $0x10,%eax
    mov %ax,%ds
    movb $0x20,%al
    outb %al,$0x20
    movl $1,%eax
    cmpl %eax,current
    je 1f
    movl %eax,current
    jmp $TSS1_SEL, $0
    jmp 2f
1:  movl $0,current
    jmp $TSS0_SEL, $0
2:  popl %eax
    pop %ds
    iret


.align 4
system_interrupt:           # 系统调用中断处理程序
    push %ds
    pushl %edx
    pushl %ecx
    pushl %ebx
    pushl %eax
    movl $0x10,%edx
    mov %dx,%ds
    call write_char
    popl %eax
    popl %ebx
    popl %ecx
    popl %edx
    pop %ds
    iret


current:.long 0
scr_loc:.long 0

.align 4
lidt_opcode:
    .word 256*8-1
    .long idt
lgdt_opcode:
    .word (end_gdt-gdt)-1
    .long gdt

.align 8
idt:    .fill 256,8,0

gdt:    .quad 0x0000000000000000
        .quad 0x00c09a00000007ff
        .quad 0x00c09200000007ff
        .quad 0x00c0920b80000002
        .word 0x68,tss0,0xe900,0x0
        .word 0x40,ldt0,0xe200,0x0
        .word 0x68,tss1,0xe900,0x0
        .word 0x40,ldt1,0xe200,0x0
end_gdt:
        .fill 128,4,0
init_stack:
    .long init_stack
    .word 0x0010


.align 8
ldt0:   .quad 0x0000000000000000
        .quad 0x00c0fa00000003ff
        .quad 0x00c0f200000003ff

tss0:   .long 0
        .long krn_stk0, 0x10
        .long 0,0,0,0,0
        .long 0,0,0,0,0
        .long 0,0,0,0,0
        .long 0,0,0,0,0,0
        .long LDT0_SEL,0x8000000

        .fill 128,4,0
krn_stk0:


.align 8
ldt1:   .quad 0x0000000000000000
        .quad 0x00c0fa00000003ff
        .quad 0x00c0f200000003ff

tss1:   .long 0
        .long krn_stk1,0x10
        .long 0,0,0,0,0
        .long task1,0x200
        .long 0,0,0,0
        .long usr_stk1,0,0,0
        .long 0x17,0x0f,0x17,0x17,0x17,0x17
        .long LDT1_SEL,0x8000000

        .fill 128,4,0
krn_stk1:


task0:
    movl $0x17,%eax
    movw %ax,%ds
    mov $65,%al
    int $0x80
    movl $0xfff,%ecx
1:  loop 1b
    jmp task0
task1:
    mov $66,%al
    int $0x80
    movl $0xfff,%ecx
1:  loop 1b
    jmp task1

    .fill 128,4,0
usr_stk1:

上面的这个程序内容不再详述,想了解细节请参考 《Linux 内核完全注释》

下面提供编译 boot.s 以及 head.s 的可用 Makefile

首先描述一下额外的工具版本

# Makefile for the simple example kernel.
AS86	=as86 -0 -a
LD86	=ld86 -0
AS	=as
ASFLAGS =-32
LD	=ld
LDFLAGS	=-s -x -M -m elf_i386 -e startup_32 -Ttext 0x0

all:	Image

Image: boot system
	dd bs=32 if=boot of=Image skip=1
	dd bs=512 if=system of=Image skip=8 seek=1
	sync

disk: Image
	dd bs=8192 if=Image of=/dev/fd0
	sync;sync;sync

head.o: 
	$(AS) $(ASFLAGS) -o head.o head.s

system:	head.o 
	$(LD) $(LDFLAGS) head.o  -o system > System.map

boot:	boot.s
	$(AS86) -o boot.o boot.s
	$(LD86) -s -o boot boot.o

clean:
	rm -f Image System.map core boot *.o system

运行结果

想了解更多细节的请自行实操查看吧!

附件

程序源码

  __                    __                  
 / _| __ _ _ __   __ _ / _| ___ _ __   __ _ 
| |_ / _` | '_ \ / _` | |_ / _ \ '_ \ / _` |
|  _| (_| | | | | (_| |  _|  __/ | | | (_| |
|_|  \__,_|_| |_|\__, |_|  \___|_| |_|\__, |
                 |___/                |___/