05-一个简单的多任务内核实例(part2:head.s)

0.head.s功能简介

  1. 初始化设置(时钟、GDT/LDT、寄存器等)

  2. 时钟中断、系统调用、字符显示

  3. 任务A和B的代码和数据

    程序执行流程如下图:
    head.s执行流程图

1.head.s汇编代码(AT&T风格)及注解

# head.s 包含32位保护模式初始化设置代码、时钟中断代码、系统调用中断代码
# 和两个任务的代码。在初始化完成后程序移动到任务0开始执行,并在时钟中断控制
# 下进行任务01直接的切换操作
LATCH  = 11930  #定时器初始计算值,即每隔10ms发送一次中断请求
SCRN_SEL = 0x18  #屏幕显示内存段选择符
TSS0_SEL = 0x20  #任务0的TSS段选择符
LDT0_SEL = 0x28  #任务0的LDT段选择符
TSS1_SEL = 0x30  #任务1的TSS段选择符
LDT1_SEL = 0x38  #任务1的LDT段选择符
/*备注:一个任务(进程)在GDT表中对应两项段选择符:LDT和TSS
 这些段选择符怎么定的值?
[15~3]为段索引,[2]为0表示GDT,1表示LDT,[1~0]:RPL(请求特权级)
举例:0x18-->[00011 0 00] 表示GDT表的第3个选择符,RPL=0
8253时钟芯片的输入频率是1.193180Mhz,1193180/100=11930,即频率100Hz
*/
.text
startup_32:    #告知链接器,以下是32位代码
# 首先加载数据段寄存器DS、堆栈段寄存器SS和堆栈指针ESP。所有段的线性基地址都是0
 mov $0x10,%eax  #0x10是GDT中数据段选择符
 mov %ax,%ds
 lss init_stack,%esp #给SS和ESP同时赋值,即SS:ESP执行init_stack地址
# 在新的位置重新设置IDT和GDT表(主要为了结构清晰,此外为与Linux0.12内核源码一致)
 call setup_idt  # 设置IDT,把256个中断门都填默认处理过程的描述符
 call setup_gdt  # 设置GDT
 movl $0x10,%eax  # setup_gdt时改变了gdt的位置,故这里需重新加载所有段寄存器
 mov  %ax,%ds
 mov  %ax,%es
 mov  %ax,%fs
 mov  %ax,%gs
 lss  init_stack,%esp
 /*
 LSS指令: 第一个操作数是一个有效地址,第二个是一个通用寄存器。该指令将有效地址的前32位
   装入寄存器中,同时将后面16位的选择符装入SS堆栈段寄存器。
 init_stack:
  .long init_stack #四字节地址(32位)
  .word 0x10       #段选择符(16位),同内核数据段选择符
 最终效果:SS:ESP = 0x10:init_stack (注意,标号表示标号第一条指令的地址)
 拓展:LDS、LFS、LGS、LES 指令用法一样,只是装入的段寄存器不同而已
*/
# 设置8253定时芯片。把计数器通道0设置成每隔10ms向中断控制器发送一个中断请求信号
 movb $0x36,%al   # 控制字:设置通道0工作在方式3、计数初值采用二进制
 movl $0x43,%edx  # 8253芯片控制字寄存器写端口
 outb %al,  %dx   # 往0x43端口写0x36
 
 movl $LATCH,%eax # 分两次往0x40端口写入LATCH,即计数器初值
 movl $0x40, %edx 
 outb %al, %dx 
 mov  %ah, %al
 mov  %al, %dx 
# 在IDT表第8和第128(0x80)项处分别设置定时中断门描述符和系统调用陷阱门描述符
 movl $0x00080000,%eax  #%eax存放IDT表第8项的低4字节,timer_interrupt地址
 movw $timer_interrupt,%ax  
 movw $0x8E00, %dx    #%edx存放IDT表第8项的高4字节,中断门类型,特权级0
 movl $0x08, %ecx    #利用开机时BIOS设置的时钟中断向量号8(注:实际内核不用BIOS中断向量号)
 lead idt(,%ecx,8),%esi 
 movl %eax,(%esi)
 movl %edx,4(%esi)
 movw $system_interrupt,%ax  #同一的方法设置系统调用陷阱门描述符
 movl $0xef00, %dx
 movl $0x80, %ecx 
 led  idt(,%ecx,8), %esi 
 movl %eax,(%esi)
 movl %edx,4(%esi)
# 现在我们为移动到任务0(任务A)中执行来操作堆栈内容,在堆栈中人工建立中断返回时的场景
 pushfl     # 复位标志寄存器EFLAGS中的嵌套任务标志NT
 andl $0xffffbfff,(%esp)
 popfl
 movl $TSS0_SEL, %eax # 把任务0的TSS段选择符加载到任务寄存器TR
 ltr  %ax     # 注意,加载时段选择符时,TSS段描述符也会自动加载进TR
 movl $LDT0_SEL, %eax # 把任务0的LDT段选择符加载到局部描述符表寄存器LDTR
 lldt %ax    # TR和LDTR只需人工加载一次,以后CPU会自动处理
 sti      # 现在开启中断,并在栈中营造中断返回时的场景
 pushl $0x17    # 把任务0当前局部空间数据段(堆栈段)选择符入栈
 pushl $init_stack    # 把堆栈指针入栈(也可以把ESP入栈)
 pushfl     # 把标志寄存器入栈
 pushl $0x0f    # 把当前局部空间代码段选择符入栈
 pushl $task0   # 把代码指针入栈
 iret     # 执行中断返回指令,从而切换到特权级3的任务0中执行
 /* 备注:不能直接从特权级0跳转到特权级3执行。但中断返回操作是可以的,所以这里营造的中断返回时的场景以
  切换到特权级3开始执行
 
 问题1:上面为什么要复位NT标志位呢?
  NT是嵌套任务标志,它控制被中断任务和调用任务之间的“链接”关系,使用CALL、中断或
 异常执行任务调用时,处理器会设置该标志。在通过使用IRET指令从一个任务返回时,处理器会检查并
 修改这个NT标志。使用POPF/POPFD指令也可以修改这个标志,但是在应用程序中改变这个标志的状态
 会产生不可意料的异常
 NT = 0,用堆栈中保存的值恢复EFLAGS、CS和EIP,执行常规的中断返回操作
 NT = 1,通过任务转换实现中断返回
 因此,复位NT即通过恢复堆栈中内容实现中断返回
 
 问题2:为什么人工加载一次LDTR和TR就行了呢?(任务切换的问题)
  一方面,正如上面讲到的NT标志位,使用CALL、中断或异常调用是通过“任务转换”
 实现的,这个转换是CPU自动完成的。
  另一方面,当CALL指令、中断或异常造成任务切换,处理器会自动把当前TSS段的
 选择符复制到新任务TSS段的前一任务链接字段中(以能返回前一任务)。TSS0段的布局相当重要,在下文
 会给出图示。
*/
setup_gdt:
 lgdt lgdt_opcode
 ret
setup_idt:
 lea  ignore_int,%edx  #取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)  #把ignore_int的地址送往idt地址处,一项的前4字节
  movl %edx,4(%edi) #把0x8E00送往(idt+4)地址处,一项的后4字节
  addl $8,%edi   # edi+8
  dec  %ecx
  jne  rp_idt    # ecx≠0则跳转到rp_idt,直到设置完256项描述符
  lidt lidt_opcode  # 最后用6字节操作数加载IDTR寄存器
  ret
# 显示字符子程序。取当前光标位置并把AL中的字符显示在屏幕上。整屏可显示80x25个字符
write_char:
 push %gs     # 首先保存要用到的寄存器,EAX由调用者负责保存
 pushl %ebx
 mov   $SCRN_SEL, %ebx # 然后让GS执行显示内存段(0xb800)
 mov  %bx,%gs 
 movl scr_loc,%bx   # 在从变量scr_loc中取目前字符显示位置值
 shl  $1,%ebx    # 因为屏幕上每个字符还有一个属性字符,因此字符实际
        # 显示位置对应的显示内存偏移地址要乘2
 movb %al,%gs:(%ebx)   # 把字符放到显示内存后把位置值除21,此时位置对应
        # 下一个显示位置。如果该位置>2000,则复位成0
 shr  $1,%ebx 
 incl %ebx
 cmpl $2000,%ebx 
 jb   1f
 movl $0,%ebx 
1:  movl %ebx, scr_loc   # 最后把这个位置保存起来(scr_loc)
 popl %ebx      # 并弹出保存的寄存器内容,返回
 pop  %gs 
 ret 
 # 以下是3个中断处理程序:默认中断、定时中断和系统调用中断
# ignore_int 是默认的中断处理程序,若系统产生了其他中断,则会在屏幕上显示一个字符C
.align 2
ignore_int:
 push %ds
 pushl %eax 
 movl $0x10, %eax   # 首先让DS执行内核数据段,因为中断程序属于内核
 movl %ax, %ds 
 movl $67, %eax 
 call write_char
 popl %eax
 pop  %ds 
 iret 
# 备注:iret 和 ret的区别,以及何时使用iret,何时用ret?
# iret 和 ret 对寄存器的影响
# iret用于中断处理程序(内核程序)(特权级0)的返回,会根据特权级变换决定是否切换堆栈
# 而ret用于用户程序(特权级3)的返回

# 这是定时中断处理程序。其中主要执行任务切换操作
.align 2
timer_interrupt:
 push %ds
 pushl %eax 
 movl $0x10, %eax    # 首先让DS执行内核数据段
 mov  %ax, %ds
 movb $0x20,%al    # 然后立刻允许其它硬件中断,即向8259A发送EOI命令
 outb %al, $0x20
 movl $1,%eax     # 接着判断当前任务,若是任务1则去执行任务0,或反之
 cmpl %eax, current
 je   1f      # 1f是执行任务0
 movl %eax, current   # 若当前任务是0,则把1存入current,并跳转到任务1去执行
 ljmp $TSS1_SEL, %0    # 注意,跳转的偏移值(0)无用,但需要写上
 jmp  2f      
1:  movl $0, current
 ljmp $TSS0_SEL, $0
2:  popl %eax
 pop  %ds
 iret
 
# 系统调用中断 int 0x80 处理程序,该示例只有一个显示字符串功能
.align 2
system_interrupt:
 push %ds
 pushl %edx 
 pushl %ecx 
 pushl %ebx
 pushl %eax
 movl  $0x10,%edx   # 首先让DS执行内核数据段
 movl  %dx, %ds 
 call  write_char
 popl  %eax
 popl  %ebx
 popl  %ecx 
 popl  %edx 
 pop   %ds 
 iret
/***********************************************************/
current:.long 0     # 当前任务号(01)
scr_loc:.long 0     # 屏幕当前显示位置。按从左上角到右下角顺序显示

.align 2
lidt_opcode:
 .word 256*8-1    # IDT表长度
 .long idt      # IDT表线性基地址
lgdt_opcode:
 .word (end_gdt-gdt)-1 # GDT表长度
 .long gdt      # GDT表线性基地址

.align 3       # 2^3=8B,要求 addr mod 8 == 0 进行对齐
idt: .fill 256,8
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-gdt:
  .fill 128,4,0
init_stack:
 .long init_stack # 堆栈段偏移位置,即设置ESP
 .word 0x10    # 堆栈段同内核数据段,即设置SS

# 下面是任务0的LDT表段中的局部段描述符
.align 3
ldt0: .quad 0x0000000000000000
  .quad 0x00c0fa00000003ff
  .quad 0x00c0f200000003ff
tss0: .long 0
  .long krn_stk0, 0x10
  .long 0, 0, 0, 0, 0, 0
  .long 0, 0, 0, 0, 0, 0
  .long 0, 0, 0, 0, 0, 0 #注意这里没设置任务0的用户栈,实际指向0x10(内核数据段):init_stack
  .long 0, 0, 0, 0, 0, 0
  .long LDT0_SEL, 0x8000000
  
  .fill 128,4,0  #这是任务0的内核栈空间
krn_stk0:

# 下面是任务1的LDT表段内容和TSS段内容
.align 3
ldt1: .quad 0x0000000000000000
  .quad 0x00c0fa00000003ff
  .quad 0x00c0f200000003ff
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 /* ldt, trace bitmap */
  .long LDT1_SEL, 0x8000000
  
  .fill 128,4,0
krn_stk1:

# 下面是任务0和任务1的程序,它们分别循环显示字符‘A’ 和‘B’
task0:
 mov $0x17, %eax  #首先让DS指向任务的局部数据段
 movw %ax, %ds 
 movl $65, %al
 int  $0x80
 movl $0xfff,%ecx # 延迟一段时间,并跳转到开始处继续循环显示
1:  loop 1b    # loop 会检查ecx,若为0,则结束1循环
 jmp  task0 
task1:
 movl $66,%al
 int  $0x80
1: loop 1b
 jmp task1
 
 .fill 128,4,0
usr_stk1:

head.s在内存中分布如下:
head代码和数据内存分布示意图

2.补充知识:

#1.在16位CPU,只有4个段寄存器(CS DS SS ES),内存寻址为“段值:偏移量”
而在32位CPU,有6个段寄存器(+FS GS),此时装入段寄存器的不再是段值,
而是段选择符(或称为"选择子"),通过段选择符索引对应表(LDT/GDT/IDT)
项存储的“段描述符”,而段描述符中存储了对应段的“线性”基地址和长度等其它属性
#2.段描述符类型:
1.数据段描述符
2.代码段描述符
3.系统段描述符(包括TSS、LDT(表项为进程的代码段/数据段)\调用门/中断门/陷阱门/任务门描述符)

#3.三种地址空间:
1.逻辑地址空间(即可执行程序中看到的地址值)
2.线性地址空间(基于“段”进行划分,段长度不定)
3.物理地址空间(基于“页”划分,段长度一定(一般4K))
三者转换关系:开启分段、分页机制时:1->2->3
开启分段、没有分页时:1->2<=>3(线性地址直接线性映射到物理地址空间,两者是一 一对应的)

3 编译、链接、运行

环境:Ubuntu14.04-LTS

  1. 编译链接boot.s
    as86 -0 -a -o boot.o boot.s
    ld86 -0 -s -o boot boot.o

  2. 编译链接head.s
    as --32 -o head.o head.s //注意一定要–32选项,编译成32位代码,head.s修改如下
    head.s的修改
    (.code32指定下面指令按32位编译,.global startup_32 声明一个全局标识符 startup_32供链接时指定入口地址)
    ld -s -m elf_i386 -Ttext 0 -e startup_32 -o head head.o
    注释:-m elf_i386:顾名思义,生成i386平台下的elf格式的可执行文件
    -s:去掉符号信息 -Ttext 0:不加这个选项,ld默认会给代码内所有的偏移加上0x08048000,而使用之后则是+0
    保留了符号引用的原值(即代码内偏移量——LC,更详细注解请参考:https://www.linuxidc.com/Linux/2014-11/108829.htm
    -e: 即entry的缩写,指定入口地址(前面的.global声明目的在此)

  3. 生成软盘映像文件
    dd bs=32 if=boot of=Image skip=1
    dd bs=512 if=head of=Image skip=8 seek=1
    备注:生成的Image为5524字节(大小很重要,能判断是否正确生成!)
    skip=xxx是在备份时对if=原文件跳过多少块再开始备份;
    seek=xxx则是在备份时对of=目标文件跳过多少块再开始写。

    对于该实例更详细的编译和运行解释,推荐一篇博文:
    https://blog.csdn.net/wangnanjun/article/details/7080318

  4. bochsrc-0.00.bxrc配置文件

megs: 16
romimage: file=$BXSHARE/BIOS-bochs-latest, address=0xf0000
vgaromimage: file=$BXSHARE/VGABIOS-lgpl-latest
floppya: 1_44="Image", status=inserted
boot: a
log: bochsout.txt

5)运行效果
一个简单多任务内核实例的运行效果图待完成:
该简单的多任务内核实例涉及任务切换、特权级转换、系统调用等相关知识。因内容比较多且复杂,会在后文展开时探讨。
参考资料:
《Linux内核完全剖析——基于0.12内核》第4章、第17章

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值