简介
《Linux内核完全剖析》这本书在第4章4.9节给出了一个简单多任务内核示例程序,称之为Linux 0.00系统,给出实现这个简单内核的源程序:启动引导程序 boot.s
和保护模式多任务内核程序 head.s
,实现了两个任务分别称为任务 A 和任务 B(或任务 0 和任务 1),它们会调用这个显示系统调用在屏幕上分别显示出字符’A’和’B’,直到每个 10 毫秒切换到另一个任务。任务 A 连续循环地调用系统调用在屏幕上显示字符’A’;任务 B 则一直显示字符’B’。
代码下载地址:Linux.old
使用解压缩软件解开 linux-0.00-050613.zip
软件包后,可以看到这个软件包包含有以下几个文件:
- linux-0.00.tar.gz - 源程序压缩文件;
- linux-0.00-rh9.tar.gz - 源程序压缩文件;
- Image - 内核引导启动映像文件;
- bochsrc-0.00.bxrc - Bochs 配置文件;
- rawrite.exe - Windows 下把 Image 写入软盘的程序。
- README - 软件包说明文件;
解压缩linux-0.00.tar.gz
软件包,会在当前目录中生成一个 linux-0.00
子目录,包括三个文件,其中:
boot.s
,采用as86
语言编写,是引导启动程序,用于把映像文件中的内核代码加载到内存某个指定位置处,并在设置好临时GDT表等信息后,把处理器设置成保护模式,然后跳转到内核代码处运行。head.s
采用GNU as
汇编语言编写,实现了2个运行在特权级别3上的任务,他们在时钟中断控制下相互切换运行,一个在屏幕上打印“A”,另一个在屏幕上打印“B”。Makefile
文件是自动编译文件,方便编译使用。
接下来对这三个文件依次分析,并能在Ubuntu环境的Bochs中运行调试。
Makefile
代码解析
-
首先在原文件5行代码做如下修改:
# Makefile for the simple example kernel. AS86 =as86 -0 -a LD86 =ld86 -0 #对下面的3行代码新修改的结果 AS =as --32 LD =ld LDFLAGS =-m elf_i386 -Ttext 0 -e startup_32 -s -x -M all: Image #此处2行代码修改后如下 Image: boot system dd bs=32 if=boot of=Image skip=1 objcopy -O binary system head cat head >> Image # dd bs=512 if=system of=Image skip=2 seek=1 # sync disk: Image dd bs=8192 if=Image of=/dev/fd0 sync;sync;sync 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
-
对于boot.s文件,用的是
as86
汇编器和ld86
链接器。其中编译后的boot
大小是544B,包含着32B文件头,需要用dd命令去掉文件头,并将其余部分写入Image。 -
对于head.s文件,用的是GNU的
as
汇编器和ld
链接器。其中:- as --32:表示生成32位的代码
- -m elf_i386:指定仿真模式是
elf_i386
- -Ttext 0:指定
.text
的段地址为0; - -e startup_32:指定入口函数是
startup_32
; - -s:表示删除符号信息;
- -x:表示丢弃局部符号;
- -M:表示在标准输出上打印链接图文件;
head.o: head.s
:表示使用隐含规则,as -o head.o head.s
最后将elf格式的system转换成的二进制文件head追加到Image后面,此时的Image就是最终的镜像。
boot.s
代码解析
-
boot.s
代码不做任何修改!!! -
boot
程序的主要功能是把软盘或映像文件中的 head 内核代码加载到内存某个指定位置处,并在设置好临时GDT
表等信息后,把处理器设置成运行在保护模式下,然后跳转到head
代码处去运行内核代码。 -
实际上,
boot.s
程序会首先利用ROM BIOS
中断int 0x13
把软盘中的head
代码读入到内存0x10000 (64KB)
位置开始处,然后再把这段head
代码移动到内存 0 开始处。最后设置控制寄存器CR0
中的开启保护运行模式标志,并跳转到内存 0 处开始执行head
代码。
-
代码解析
! 首先利用 BIOS 中断把内核代码(head 代码)加载到内存 0x10000 处,然后移动到内存 0 处。 ! 最后进入保护模式,并跳转到内存 0(head 代码)开始处继续运行。 BOOTSEG = 0x07c0 ! 引导扇区(本程序)被 BIOS 加载到内存 0x7c00 处。不分配内存 SYSSEG = 0x1000 ! 内核(head)先加载到 0x10000 处,然后移动到 0x0 处。 SYSLEN = 17 ! 内核占用的最大磁盘扇区数。
entry start start: jmpi go,#BOOTSEG ! 段间跳转至 0x7c0:go 处。当本程序刚运行时所有段寄存器值 go: mov ax,cs ! 均为 0。该跳转语句会把 CS 寄存器加载为 0x7c0(原为 0)。 mov ds,ax ! 让 DS 和 SS 都指向 0x7c0 段。 mov ss,ax mov sp,#0x400 ! 设置临时栈指针。其值需大于程序末端并有一定空间即可。
标识符
entry
是保留关键字,用于迫使链接器ld86
在生成的可执行文件中包括其后指定的标号。通常在链接多个目标文件生成一个可执行文件时应该在其中一个汇编程序中用关键词entry
指定一个入口标号,以便调试。jmpi go,#BOOTSEG
,是一个段间跳转语句,跳转到0x07c0:go
处。当BIOS
把主引导扇区(也就是boot.s
生成的二进制镜像)加载到物理内存0x7c00
处并跳转到该处时,所有段寄存器(包括CS)的默认值均为0,即此时CS:IP=0x0000:0x7c00
。因此这里使用段间跳转语句就是为了给CS赋值0x07c0
。该语句执行后CS:IP = 0x07C0:go
。! 加载内核代码到内存 0x10000 开始处。 load_system: mov dx,#0x0000 ! 利用 BIOS 中断 int 0x13 功能 2 从启动盘读取 head 代码。 mov cx,#0x0002 ! DH - 磁头号;DL - 驱动器号;CH - 10 位磁道号低 8 位; mov ax,#SYSSEG ! CL - 位 7、6 是磁道号高 2 位,位 5-0 起始扇区号(从 1 计)。 mov es,ax ! ES:BX - 读入缓冲区位置(0x1000:0x0000)。 xor bx,bx ! AH - 读扇区功能号;AL - 需读的扇区数(17)。 mov ax,#0x200+SYSLEN int 0x13 jnc ok_load ! 若没有发生错误则跳转继续运行,否则死循环。 die: jmp die
int 0x13
是从磁盘上把一个或更多的扇区内容读进内存,在一个操作中读取的全部扇区必须在同一条磁道上。最后两行表示判断CF
标志,如果标志置位说明出错,则陷入死循环。否则跳转到ok_load
处。! 把内核代码移动到内存 0 开始处。共移动 8KB 字节(内核长度不超过 8KB)。 ok_load: cli ! 关中断。 mov ax, #SYSSEG ! 移动开始位置 DS:SI = 0x1000:0;目的位置 ES:DI=0:0。 mov ds, ax xor ax, ax mov es, ax mov cx, #0x1000 ! 设置cx表示共移动 4K 次,每次移动一个字(word)。 sub si,si sub di,di rep movw ! 执行重复移动指令。 ! 加载 IDT 和 GDT 基地址寄存器 IDTR 和 GDTR。 mov ax, #BOOTSEG mov ds, ax ! 让 DS 重新指向 0x7c0 段。 lidt idt_48 ! 加载 IDTR。6 字节操作数:2 字节表长度,4 字节线性基地址。 lgdt gdt_48 ! 加载 GDTR。6 字节操作数:2 字节表长度,4 字节线性基地址。
lidt是加载
IDTR
(中断描述符表寄存器),其后面要跟一个内存地址。在16位模式下,该地址是16位的;在32位模式下,该地址是32位的。该指令在实模式和保护模式下都可以用。地址指向一个6字节的内存区域,前16位是IDT的界限值,后32位是IDT的线性地址。lgdt是加载GDTR(全局描述符表寄存器),用法与lidt类似。gdt_48处定义了6个字节,前两个字节表示表的界限,0x7FF+1=2048(十进制),2048/8 = 256,也就是最多可以容纳256个段描述符;后四个字节是GDT的线性基地址,0x7c00+gdt,因为主引导扇区被BIOS加载到了0x7c00处,所以要加个偏移。
! 设置控制寄存器 CR0(即机器状态字),进入保护模式。段选择符值 8 对应 GDT 表中第 2 个段描述符。 mov ax,#0x0001 ! 在 CR0 中设置保护模式标志 PE(位 0)。 lmsw ax ! 然后跳转至段选择符值指定的段中,偏移 0 处。 jmpi 0,8 ! 注意此时段值已是段选择符。该段的线性基地址是 0。 ! 下面是全局描述符表 GDT 的内容。其中包含 3 个段描述符。第 1 个不用,另 2 个是代码和数据段描述符。 gdt: .word 0,0,0,0 ! 段描述符 0,不用。每个描述符项占 8 字节。 .word 0x07FF ! 段描述符 1。8Mb - 段限长值=2047 (2048*4096=8MB)。 .word 0x0000 ! 段基地址=0x00000。 .word 0x9A00 ! 是代码段,可读/执行。 .word 0x00C0 ! 段属性颗粒度=4KB,80386。 .word 0x07FF ! 段描述符 2。8Mb - 段限长值=2047 (2048*4096=8MB)。 .word 0x0000 ! 段基地址=0x00000。 .word 0x9200 ! 是数据段,可读写。 .word 0x00C0 ! 段属性颗粒度=4KB,80386。
lmsw
是加载机器状态字指令,后接16位寄存器或者内存地址。其功能是用源操作数的低4位加载CR0,也就是说仅会影响CR0的低4位——PE, MP, EM, TS。执行完后保护模式就开启了。jmpi 0,8
执行后,处理器一般会清空流水线并且串化执行;另一方面,会重新加载CS,并刷新描述符高速缓存寄存器的内容。这里的0是偏移地址,8是段选择子。jmpi 0,8
表示跳转到物理地址0处,这正是内核代码的起始位置,此后内核开始执行了。接下来定义了3个段描述符,第0个不用。
第1个描述符定义了一个代码段,其基地址为0,界限值是0x7FF(10进制2047),粒度4KB,DPL=0,非一致性,可读可执行。因为粒度是4KB,所以段长度是(2047+1)*4KB=8MB。
第2个描述符定义了一个数据段,其基地址为0,界限值是0x7FF(10进制2047),粒度4KB,DPL=0,向上扩展,可读可写。同上,段长度是8MB。
! 下面分别是 LIDT 和 LGDT 指令的 6 字节操作数。 idt_48: .word 0 ! IDT 表长度是 0。 .word 0,0 ! IDT 表的线性基地址也是 0。 gdt_48: .word 0x7ff ! GDT 表长度是 2048 字节,可容纳 256 个描述符项。 .word 0x7c00+gdt,0 ! GDT 表的线性基地址在 0x7c0 段的偏移 gdt 处。 .org 510 .word 0xAA55 ! 引导扇区有效标志。必须处于引导扇区最后 2 字节处。
伪指令
.org 510
表示在它之后的指令从地址510开始存放。遇到.org
,编译器会把其后的指令代码放到org
伪指令指定的偏移地址。如org
指定的地址和之前的指令地址有空洞,则用0填充。.word 0xAA55
是有效引导扇区的标志,第510字节必须是0x55,第511字节必须是0xAA.
head.s
代码解析
-
此时执行
make all
会报如下错误:as86 -0 -a -o boot.o boot.s ld86 -0 -s -o boot boot.o as --32 -o head.o head.s head.s: Assembler messages: head.s:103: 警告: using `%ebx' instead of `%bx' due to `l' suffix head.s:182: 错误: alignment not a power of 2 head.s:201: 错误: alignment not a power of 2 head.s:219: 错误: alignment not a power of 2 head.s:240: 错误: `%al' not allowed with `movl' head.s:249: 错误: `%al' not allowed with `movl' <builtin>: recipe for target 'head.o' failed make: *** [head.o] Error 1
-
根据错误信息,做如下修改:
- 将103行的
%bx
修改成%ebx
- 将182、201、219行的
.align 3
修改成.align 8
- 将240、249的
movl
修改成movb
- 还需要在一开始
.text
上面添加.code32
表示要编译成32位代码
- 将103行的
-
再次编译,通过:
as --32 -o head.o head.s ld -m elf_i386 -Ttext 0 -e startup_32 -s -x -M head.o -o system > System.map ld: 警告: 无法找到项目符号 startup_32; 缺省为 0000000000000000 dd bs=32 if=boot of=Image skip=1 记录了16+0 的读入 记录了16+0 的写出 512 bytes copied, 0.00130362 s, 393 kB/s dd bs=512 if=system of=Image skip=2 seek=1 记录了15+1 的读入 记录了15+1 的写出 8084 bytes (8.1 kB, 7.9 KiB) copied, 0.000434915 s, 18.6 MB/s sync
-
将生成的Image代替之前linux0.11环境下的镜像,然后
./run
运行该镜像如下:
-
head.s 程序运行在 32 位保护模式下,其中主要包括初始设置的代码、时钟中断 int 0x08 的过程代码、系统调用中断 int 0x80 的过程代码以及任务 A 和任务 B 等的代码和数据。其中初始设置工作主要包括:①重新设置 GDT 表;②设置系统定时器芯片;③重新设置 IDT 表并且设置时钟和系统调用中断门;④移动到任务 A 中执行。
-
实际上,本内核示例中所有代码和数据段都对应到物理内存同一个区域上,即从物理内存 0 开始的区域。GDT 中全局代码段和数据段描述符的内容都设置为:基地址为 0x0000;段限长值为 0x07ff。因为颗粒度为 1,所以实际段长度为8MB。而全局显示数据段被设置成:基地址为 0xb8000;段限长值为 0x0002,所以实际段长度为 8KB,对应到显示内存区域上。
-
代码分析如下
# head.s 包含 32 位保护模式初始化设置代码、时钟中断代码、系统调用中断代码和两个任务的代码。 # 在初始化完成之后程序移动到任务 0 开始执行,并在时钟中断控制下进行任务 0 和 1 之间的切换操作。 LATCH = 11930 # 定时器初始计数值,即每隔 10 毫秒发送一次中断请求。 SCRN_SEL = 0x18 # 屏幕显示内存段选择符。 TSS0_SEL = 0x20 # 任务 0 的 TSS 段选择符。 LDT0_SEL = 0x28 # 任务 0 的 LDT 段选择符。 TSS1_SEL = 0X30 # 任务 1 的 TSS 段选择符。 LDT1_SEL = 0x38 # 任务 1 的 LDT 段选择符。 .code32 # 代码要编译成32位代码。自己添加 .text # 标识正文段的开始,并切换到text段。 startup_32: #.global 表示标识符是外部的或者全局的。 # 首先加载数据段寄存器 DS、堆栈段寄存器 SS 和堆栈指针 ESP。所有段的线性基地址都是 0。 movl $0x10,%eax # 0x10 是 GDT 中数据段选择符。 mov %ax,%ds # 加载ds lss init_stack,%esp # 在新的位置重新设置 IDT 和 GDT 表。 call setup_idt # 设置 IDT。先把 256 个中断门都填默认处理过程的描述符。 call setup_gdt # 设置 GDT。 movl $0x10,%eax # 在改变了 GDT 之后重新加载所有段寄存器。 mov %ax,%ds mov %ax,%es mov %ax,%fs mov %ax,%gs lss init_stack,%esp # 设置 8253 定时芯片。把计数器通道 0 设置成每隔 10 毫秒向中断控制器发送一个中断请求信号。 movb $0x36, %al # 控制字:设置通道 0 工作在方式 3、计数初值采用二进制。 movl $0x43, %edx # 8253 芯片控制字寄存器写端口。端口是0x43 outb %al, %dx # 把al中的控制字写入端口0x43 movl $LATCH, %eax # 初始计数值设置为 LATCH(1193180/100),即频率 100HZ。 movl $0x40, %edx # 通道 0 的端口。端口是0x40 outb %al, %dx # 分两次把初始计数值写入通道 0。先写低字节 movb %ah, %al # 再写高字节 outb %al, %dx # 在 IDT 表第 8 和第 128(0x80)项处分别设置定时中断门描述符和系统调用陷阱门描述符。 movl $0x00080000, %eax # 中断程序属内核,即 EAX 高字是内核代码段选择符 0x0008。 movw $timer_interrupt, %ax # 设置定时中断门描述符。取定时中断处理程序地址。 movw $0x8E00, %dx # 中断门类型是 14(屏蔽中断),特权级 0 或硬件使用。 movl $0x08, %ecx # 开机时 BIOS 设置的时钟中断向量号 8。这里直接使用它。 lea idt(,%ecx,8), %esi # 把 IDT 描述符 0x08 地址放入 ESI 中,然后设置该描述符。 movl %eax,(%esi) movl %edx,4(%esi) movw $system_interrupt, %ax # 设置系统调用陷阱门描述符。取系统调用处理程序地址。 movw $0xef00, %dx # 陷阱门类型是 15,特权级 3 的程序可执行。 movl $0x80, %ecx # 系统调用向量号是 0x80。 lea idt(,%ecx,8), %esi # 把 IDT 描述符项 0x80 地址放入 ESI 中,然后设置该描述符。 movl %eax,(%esi) movl %edx,4(%esi) # 好了,现在我们为移动到任务 0(任务 A)中执行来操作堆栈内容,在堆栈中人工建立中断返回时的场景。 pushfl # 复位标志寄存器 EFLAGS 中的嵌套任务标志。 andl $0xffffbfff, (%esp) popfl movl $TSS0_SEL, %eax # 把任务 0 的 TSS 段选择符加载到任务寄存器 TR。 ltr %ax movl $LDT0_SEL, %eax # 把任务 0 的 LDT 段选择符加载到局部描述符表寄存器 LDTR。 lldt %ax # TR 和 LDTR 只需人工加载一次,以后 CPU 会自动处理。 movl $0, current # 把当前任务号 0 保存在 current 变量中。 sti # 现在开启中断,并在栈中营造中断返回时的场景。 pushl $0x17 # 把任务 0 当前局部空间数据段(堆栈段)选择符入栈。 pushl $init_stack # 把堆栈指针入栈(也可以直接把 ESP 入栈)。 pushfl # 把标志寄存器值入栈。 pushl $0x0f # 把当前局部空间代码段选择符入栈。 pushl $task0 # 把代码指针入栈。 iret # 执行中断返回指令,从而切换到特权级 3 的任务 0 中执行。 # 以下是设置 GDT 和 IDT 中描述符项的子程序。 setup_gdt: # 使用 6 字节操作数 lgdt_opcode 设置 GDT 表位置和长度。 lgdt lgdt_opcode ret # 这段代码暂时设置 IDT 表中所有 256 个中断门描述符都为同一个默认值,均使用默认的中断处理过程 # ignore_int。设置的具体方法是:首先在 eax 和 edx 寄存器对中分别设置好默认中断门描述符的 0-3 # 字节和 4-7 字节的内容,然后利用该寄存器对循环往 IDT 表中填充默认中断门描述符内容。 # IDT最多可有256个描述符,每个描述符占8个字节。 setup_idt: # 把所有 256 个中断门描述符设置为使用默认处理过程。 lea ignore_int,%edx # 设置方法与设置定时中断门描述符的方法一样。 movl $0x00080000,%eax # 选择符为 0x0008。 movw %dx,%ax movw $0x8E00,%dx # 中断门类型,特权级为 0。 lea idt,%edi mov $256,%ecx # 循环设置所有 256 个门描述符项。 rp_idt: movl %eax,(%edi) movl %edx,4(%edi) addl $8,%edi dec %ecx jne rp_idt lidt lidt_opcode # 最后用 6 字节操作数加载 IDTR 寄存器。 ret # 显示字符子程序。取当前光标位置并把 AL 中的字符显示在屏幕上。整屏可显示 80 X 25 个字符。 write_char: push %gs # 首先保存要用到的寄存器,EAX 由调用者负责保存。 pushl %ebx mov $SCRN_SEL, %ebx #SCRN_SEL是显存段的选择子 mov %bx, %gs # 然后让 GS 指向显示内存段(0xb8000)。 movl scr_loc, %bx # 再从变量 scr_loc 中取目前字符显示位置值。 shl $1, %ebx # 因为在屏幕上每个字符还有一个属性字节,因此字符 movb %al, %gs:(%ebx) # 实际显示位置对应的显示内存偏移地址要乘 2。 shr $1, %ebx # 把字符放到显示内存后把位置值除 2 加 1,此时位置值对 incl %ebx # ebx自增1算出下一个显示位置。若该位置大于2000,则复位成 0。 cmpl $2000, %ebx #比较ebx和2000 jb 1f #若 ebx < 2000 则跳转到1 movl $0, %ebx #说明ebx==2000,因为位置只有0~1999,所以把ebx置为0 1: movl %ebx, scr_loc # 最后把ebx这个位置值保存起来(scr_loc), popl %ebx # 并弹出保存的寄存器内容,返回。更新显示位置 pop %gs ret # 以下是 3 个中断处理程序:默认中断、定时中断和系统调用中断。 # ignore_int 是默认的中断处理程序,若系统产生了其他中断,则会在屏幕上显示一个字符'C'。 .align 2 ignore_int: push %ds pushl %eax movl $0x10, %eax # 首先让 DS 指向内核数据段,因为中断程序属于内核。 mov %ax, %ds movl $67, %eax # 在 AL 中存放字符'C'的代码,调用显示程序显示在屏幕上。 call write_char #调用过程 write_char popl %eax pop %ds iret # 这是定时中断处理程序。其中主要执行任务切换操作。 .align 2 timer_interrupt: push %ds pushl %eax movl $0x10, %eax # 首先让 DS 指向内核数据段。0x10是内核数据段的选择子 mov %ax, %ds movb $0x20, %al # 然后立刻允许其他硬件中断,即向 8259A 发送 EOI 命令。 outb %al, $0x20 movl $1, %eax # 接着判断当前任务,若是任务 1 则去执行任务 0,或反之。 cmpl %eax, current je 1f #相等跳转到1处 movl %eax, current # 若当前任务是 0,则把 1 存入 current,并跳转到任务 1 ljmp $TSS1_SEL, $0 # 去执行。注意跳转的偏移值无用,但需要写上。切换到任务1 jmp 2f 1: movl $0, current # 若当前任务是 1,则把 0 存入 current,并跳转到任务 0 ljmp $TSS0_SEL, $0 # 去执行。 2: popl %eax pop %ds iret # 系统调用中断 int 0x80 处理程序。该示例只有一个显示字符功能。 .align 2 system_interrupt: # 0x80系统调用,把AL中的字符打印到屏幕上 push %ds pushl %edx pushl %ecx pushl %ebx pushl %eax movl $0x10, %edx # 首先让 DS 指向内核数据段。 mov %dx, %ds call write_char # 然后调用显示字符子程序 write_char,显示 AL 中的字符。 popl %eax popl %ebx popl %ecx popl %edx pop %ds iret /*********************************************/ current:.long 0 # 当前任务号(0 或 1)。 scr_loc:.long 0 # 屏幕当前显示位置。按从左上角到右下角顺序显示。 .align 2 lidt_opcode: .word 256*8-1 # 加载 IDTR 寄存器的 6 字节操作数:表长度和基地址。 .long idt lgdt_opcode: .word (end_gdt-gdt)-1 # 加载 GDTR 寄存器的 6 字节操作数:表长度和基地址。 .long gdt .align 8 idt: .fill 256,8,0 # IDT 空间。共 256 个门描述符,每个 8 字节,占用 2KB。 gdt: .quad 0x0000000000000000 # GDT 表。第 1 个描述符不用。 .quad 0x00c09a00000007ff # 第 2 个是内核代码段描述符。其选择符是 0x08。 .quad 0x00c09200000007ff # 第 3 个是内核数据段描述符。其选择符是 0x10。 .quad 0x00c0920b80000002 # 第 4 个是显示内存段描述符。其选择符是 0x18。 .word 0x68, tss0, 0xe900, 0x0 # 第 5 个是 TSS0 段的描述符。其选择符是 0x20 .word 0x40, ldt0, 0xe200, 0x0 # 第 6 个是 LDT0 段的描述符。其选择符是 0x28 .word 0x68, tss1, 0xe900, 0x0 # 第 7 个是 TSS1 段的描述符。其选择符是 0x30 .word 0x40, ldt1, 0xe200, 0x0 # 第 8 个是 LDT1 段的描述符。其选择符是 0x38 end_gdt: .fill 128,4,0 # 初始内核堆栈空间。 init_stack: # 刚进入保护模式时用于加载 SS:ESP 堆栈指针值。 .long init_stack # 堆栈段偏移位置。 .word 0x10 # 堆栈段同内核数据段。 # 下面是任务 0 的 LDT 表段中的局部段描述符。 .align 8 ldt0: .quad 0x0000000000000000 # 第 1 个描述符,不用。 .quad 0x00c0fa00000003ff # 第 2 个局部代码段描述符,对应选择符是 0x0f。 .quad 0x00c0f200000003ff # 第 3 个局部数据段描述符,对应选择符是 0x17。 # 下面是任务 0 的 TSS 段的内容。注意其中标号等字段在任务切换时不会改变。 tss0: .long 0 /* back link */ .long krn_stk0, 0x10 /* esp0, ss0 */ .long 0, 0, 0, 0, 0 /* esp1, ss1, esp2, ss2, cr3 */ .long 0, 0, 0, 0, 0 /* eip, eflags, eax, ecx, edx */ .long 0, 0, 0, 0, 0 /* ebx esp, ebp, esi, edi */ .long 0, 0, 0, 0, 0, 0 /* es, cs, ss, ds, fs, gs */ .long LDT0_SEL, 0x8000000 /* ldt, trace bitmap */ .fill 128,4,0 # 这是任务 0 的内核栈空间。 krn_stk0: # 下面是任务 1 的 LDT 表段内容和 TSS 段内容。 .align 8 ldt1: .quad 0x0000000000000000 # 第 1 个描述符,不用。 .quad 0x00c0fa00000003ff # 选择符是 0x0f,基地址 = 0x00000。 .quad 0x00c0f200000003ff # 选择符是 0x17,基地址 = 0x00000。 tss1: .long 0 /* back link */ .long krn_stk1, 0x10 /* esp0, ss0 */ .long 0, 0, 0, 0, 0 /* esp1, ss1, esp2, ss2, cr3 */ .long task1, 0x200 /* eip, eflags */ .long 0, 0, 0, 0 /* eax, ecx, edx, ebx */ .long usr_stk1, 0, 0, 0 /* esp, ebp, esi, edi */ .long 0x17,0x0f,0x17,0x17,0x17,0x17 /* es, cs, ss, ds, fs, gs */ .long LDT1_SEL, 0x8000000 /* ldt, trace bitmap */ .fill 128,4,0 # 这是任务 1 的内核栈空间。其用户栈直接使用初始栈空间。 krn_stk1: # 下面是任务 0 和任务 1 的程序,它们分别循环显示字符'A'和'B'。 task0: movl $0x17, %eax # 首先让 DS 指向任务的局部数据段。 movw %ax, %ds # 因为任务没有使用局部数据,所以这两句可省略。 movl $65, %al # 把需要显示的字符'A'放入 AL 寄存器中。 int $0x80 # 执行系统调用,显示字符。 movl $0xfff, %ecx # 执行循环,起延时作用。 1: loop 1b jmp task0 # 跳转到任务代码开始处继续显示字符。 task1: movl $66, %al # 把需要显示的字符'B'放入 AL 寄存器中。 int $0x80 # 执行系统调用,显示字符。 movl $0xfff, %ecx # 延时一段时间,并跳转到开始处继续循环显示。 1: loop 1b jmp task1 .fill 128,4,0 # 这是任务 1 的用户栈空间。 usr_stk1:
-
参考链接: