linux head.s 详解
上一篇是boot.s,这是head.s。关于head.s的作用为head.s程序运行在32位保护模式下,其中主要包括初始设置的代码、时钟中断int 0x08的过程代码、系统调用中断int 0x80的过程代码以及任务A和任务B等的代码和数据。其中初始设置工作主要包括:①重新设置GDT表;②设置系统定时器芯片;③重新设置IDT表并且设 置时钟和系统调用中断门;④移动到任务A中执行。在虚
上一篇是boot.s,这是head.s。关于head.s的作用为
head.s程序运行在32位保护模式下,其中主要包括初始设置的代码、时钟中断int 0x08的过程代码、系统调用中断int 0x80的过程代码以及任务A和任务B等的代码和数据。其中初始设置工作主要包括:①重新设置GDT表;②设置系统定时器芯片;③重新设置IDT表并且设 置时钟和系统调用中断门;④移动到任务A中执行。
在虚拟地址空间中head.s程序的内核代码和任务代码分配图如图4-41所示。实际上,本内核示例中所有代码和数据段都对应到物理内存同一个区域 上,即从物理内存0开始的区域。GDT中全局代码段和数据段描述符的内容都设置为:基地址为0x0000;段限长值为0x07ff。因为颗粒度为1,所以 实际段长度为8MB。而全局显示数据段被设置成:基地址为0xb8000;段限长值为0x0002,所以实际段长度为8KB,对应到显示内存区域上。
图4-41 内核和任务在虚拟地址空间中的分配示意图 |
两个任务在LDT中代码段和数据段描述符的内容也都设置为:基地址为0x0000;段限长值为0x03ff,实际段长度为4MB。因此在线性地址空 间中这个"内核"的代码和数据段与任务的代码和数据段都从线性地址0开始并且由于没有采用分页机制,所以它们都直接对应物理地址0开始处。在head程序 编译出的目标文件中以及最终得到的软盘映像文件中,代码和数据的组织形式如图4-42所示。
图4-42 内核映像文件和内存中head代码和数据分布示意图 |
由 于处于特权级0的代码不能直接把控制权转移到特权级3的代码中执行,但中断返回操作是可以的,因此当初始化GDT、IDT和定时芯片结束后,我们就利用中 断返回指令IRET来启动第1个任务。具体实现方法是在初始堆栈init_stack中人工设置一个返回环境,即把任务0的TSS段选择符加载到任务寄存 器LTR中、LDT段选择符加载到LDTR中以后,把任务0的用户栈指针(0x17:init_stack)和代码指针(0x0f:task0)以及标志 寄存器值压入栈中,然后执行中断返回指令IRET。该指令会弹出堆栈上的堆栈指针作为任务0用户栈指针,恢复假设的任务0的标志寄存器内容,并且弹出栈中 代码指针放入CS:EIP寄存器中,从而开始执行任务0的代码,完成了从特权级0到特权级3代码的控制转移。
为了每隔10ms切换运行的任务,head.s程序中把定时器芯片8253的通道0设置成每经过10ms就向中断控制芯片8259A发送一个时钟中 断请求信号。PC的ROM BIOS开机时已经在8259A中把时钟中断请求信号设置成中断向量8,因此我们需要在中断8的处理过程中执行任务切换操作。任务切换的实现方法是查看 current变量中当前运行任务号。如果current当前是0,就利用任务1的TSS选择符作为操作数执行远跳转指令,从而切换到任务1中执行,否则 反之。
每个任务在执行时,会首先把一个字符的ASCII码放入寄存器AL中,然后调用系统中断调用int 0x80,而该系统调用处理过程则会调用一个简单的字符写屏子程序,把寄存器AL中的字符显示在屏幕上,同时把字符显示的屏幕的下一个位置记录下来,作为 下一次显示字符的屏幕位置。在显示过一个字符后,任务代码会使用循环语句延迟一段时间,然后又跳转到任务代码开始处继续循环执行,直到运行了10ms而发 生了定时中断,从而代码会切换到另一个任务去运行。对于任务A,寄存器AL中将始终存放字符"A",而任务B运行时AL中始终存放字符"B"。因此在程序 运行时我们将看到一连串的字符"A"和一连串的字符"B"连续不断地间隔显示在屏幕上,如图4-43所示。
(点击查看大图)图4-43 简单内核运行的屏幕显示情况 |
图4-43是我们在Bochs模拟软件中运行这个内核示例的屏幕显示情况。细心的读者会发现,在图中底端一行上显示出一个字符"C"。这是由于PC 偶然产生了一个不是时钟中断和系统调用中断的其他中断。因为我们已经在程序中给所有其他中断安装了一个默认中断处理程序。当出现一个其他中断时,系统就会 运行这个默认中断处理程序,于是就会在屏幕上显示一个字符"C",然后退出中断。
**********************************************************************************************************************************************
head.s
01 # head.s 包含32位保护模式初始化设置代码、时钟中断代码、系统调用中断代码和两个任务的代码。 02 # 在初始化完成之后程序移动到任务0开始执行,并在时钟中断控制下进行任务0和1之间的切换操作。 03 LATCH = 11930 # 定时器初始计数值,即每隔10ms发送一次中断请求。 04 SCRN_SEL = 0x18 # 屏幕显示内存段选择符。 05 TSS0_SEL = 0x20 # 任务0的TSS段选择符。 06 LDT0_SEL = 0x28 # 任务0的LDT段选择符。 07 TSS1_SEL = 0X30 # 任务1的TSS段选择符。 08 LDT1_SEL = 0x38 # 任务1的LDT段选择符。 09 .text 10 startup_32: 11 # 首先加载数据段寄存器DS、堆栈段寄存器SS和堆栈指针ESP。所有段的线性基地址都是0。 12movl $0x10,%eax # 0x10是GDT中数据段选择符。 13mov %ax,%ds 14lss init_stack,%esp 15 # 在新的位置重新设置IDT和GDT表。 16call setup_idt # 设置IDT。先把256个中断门都填默认处理过程的描述符。 17call setup_gdt # 设置GDT。 18movl $0x10,%eax # 在改变了GDT之后重新加载所有段寄存器。 19mov %ax,%ds 20mov %ax,%es 21mov %ax,%fs 22mov %ax,%gs 23lss init_stack,%esp 24 # 设置8253定时芯片。把计数器通道0设置成每隔10ms向中断控制器发送一个中断请求信号。 25movb $0x36, %al # 控制字:设置通道0工作在方式3、计数初值采用二进制。 26movl $0x43, %edx # 8253芯片控制字寄存器写端口。 27outb %al, %dx 28movl $LATCH, %eax # 初始计数值设置为LATCH(1193180/100),即频率100Hz。 29movl $0x40, %edx # 通道0的端口。 30outb %al, %dx # 分两次把初始计数值写入通道0。 31movb %ah, %al 32outb %al, %dx 33 # 在IDT表第8和第128(0x80)项处分别设置定时中断门描述符和系统调用陷阱门描述符。 34movl $0x00080000, %eax # 中断程序属内核,即EAX高字是内核代码段选择符0x0008。
08h | IRQ0: Implemented by the system timing component; called 18.2 times per second (once every 55 ms) by the PIC |
35movw $timer_interrupt, %ax # 设置定时中断门描述符。取定时中断处理程序地址。 36movw $0x8E00, %dx # 中断门类型是14(屏蔽中断),特权级0或硬件使用。 37movl $0x08, %ecx # 开机时BIOS设置的时钟中断向量号8。这里直接使用它。 38lea idt(,%ecx,8), %esi # 把IDT描述符0x08地址放入ESI中,然后设置该描述符。 39movl %eax,(%esi) 40movl %edx,4(%esi) 41movw $system_interrupt, %ax # 设置系统调用陷阱门描述符。取系统调用处理程序地址。 42movw $0xef00, %dx # 陷阱门类型是15,特权级3的程序可执行。 43movl $0x80, %ecx # 系统调用向量号是0x80。 44lea idt(,%ecx,8), %esi # 把IDT描述符项0x80地址放入ESI中,然后设置该描述符。 45movl %eax,(%esi) 46movl %edx,4(%esi) 47 # 好了,现在我们为移动到任务0(任务A)中执行来操作堆栈内容,在堆栈中人工建立中断返回时的场景。 48pushfl # 复位标志寄存器EFLAGS中的嵌套任务标志。 将堆栈指针递减 4(如果当前操作数大小属性为 32),并将 EFLAGS 寄存器的全部内容压入堆栈;或将堆栈指针递减 2(如果当前操作数大小属性为 16),并将 EFLAGS 寄存器的低 16 位(即 FLAGS 寄存器)压入堆栈。(这些指令执行 POPF/POPFD 指令的逆操作)。将整个 EFLAGS 寄存器复制到堆栈时,不会复制 VM 与 RF 标志(位 16 与 17);相反,在存储到堆栈的 EFLAGS 映像中,这些标志的值会被清除 49andl $0xffffbfff, (%esp) 50popfl 51movl $TSS0_SEL, %eax # 把任务0的TSS段选择符加载到任务寄存器TR。 52ltr %ax 53movl $LDT0_SEL, %eax # 把任务0的LDT段选择符加载到局部描述符表寄存器LDTR。 54lldt %ax # TR和LDTR只需人工加载一次,以后CPU会自动处理。 55movl $0, current # 把当前任务号0保存在current变量中。 56sti # 现在开启中断,并在栈中营造中断返回时的场景。 57pushl $0x17 # 把任务0当前局部空间数据段(堆栈段)选择符入栈。 58pushl $init_stack # 把堆栈指针入栈(也可以直接把ESP入栈)。 59pushfl # 把标志寄存器值入栈。 60pushl $0x0f # 把当前局部空间代码段选择符入栈。 61pushl $task0 # 把代码指针入栈。 62iret # 执行中断返回指令,从而切换到特权级3的任务0中执行。 63 64 # 以下是设置GDT和IDT中描述符项的子程序。 65 setup_gdt: # 使用6字节操作数lgdt_opcode设置GDT表位置和长度。 66lgdt lgdt_opcode 67ret # 这段代码暂时设置IDT表中所有256个中断门描述符都为同一个默认值,均使用默认的中断处理过程 # ignore_int。设置的具体方法是:首先在eax和edx寄存器对中分别设置好默认中断门描述符的0~3 # 字节和4~7字节的内容,然后利用该寄存器对循环往IDT表中填充默认中断门描述符内容。 68 setup_idt: # 把所有256个中断门描述符设置为使用默认处理过程。 69lea ignore_int,%edx # 设置方法与设置定时中断门描述符的方法一样。 70movl $0x00080000,%eax # 选择符为0x0008。 71movw %dx,%ax 72movw $0x8E00,%dx # 中断门类型,特权级为0。 73lea idt,%edi 74mov $256,%ecx # 循环设置所有256个门描述符项。 75 rp_idt: movl %eax,(%edi) 76movl %edx,4(%edi) 77addl $8,%edi 78dec %ecx 79jne rp_idt 80lidt lidt_opcode # 最后用6字节操作数加载IDTR寄存器。 81ret 82 83 # 显示字符子程序。取当前光标位置并把AL中的字符显示在屏幕上。整屏可显示80×25个字符。 84 write_char: 85push %gs # 首先保存要用到的寄存器,EAX由调用者负责保存。 86pushl %ebx 87mov $SCRN_SEL, %ebx # 然后让GS指向显示内存段(0xb8000)。 88mov %bx, %gs 89movl scr_loc, %bx # 再从变量scr_loc中取目前字符显示位置值。 90shl $1, %ebx # 因为在屏幕上每个字符还有一个属性字节,因此字符 91movb %al, %gs:(%ebx) # 实际显示位置对应的显示内存偏移地址要乘2。 92shr $1, %ebx # 把字符放到显示内存后把位置值除2加1,此时位置值对 93incl %ebx # 应下一个显示位置。如果该位置大于2000,则复位成0。 94cmpl $2000, %ebx 95jb 1f 96movl $0, %ebx 97 1:movl %ebx, scr_loc # 最后把这个位置值保存起来(scr_loc), 98popl %ebx # 并弹出保存的寄存器内容,返回。 99pop %gs 100ret 101 102 # 以下是3个中断处理程序:默认中断、定时中断和系统调用中断。 103 # ignore_int是默认的中断处理程序,若系统产生了其他中断,则会在屏幕上显示一个字符"C"。 104 .align 2 105 ignore_int: 106push %ds 107pushl %eax 108movl $0x10, %eax # 首先让DS指向内核数据段,因为中断程序属于内核。 109mov %ax, %ds 110movl $67, %eax # 在AL中存放字符"C"的代码,调用显示程序显示在屏幕上。 111call write_char 112popl %eax 113pop %ds 114iret 115 116 # 这是定时中断处理程序。其中主要执行任务切换操作。 117 .align 2 118 timer_interrupt: 119push %ds 120pushl %eax 121movl $0x10, %eax # 首先让DS指向内核数据段。 122mov %ax, %ds 123movb $0x20, %al # 然后立刻允许其他硬件中断,即向8259A发送EOI命令。 124outb %al, $0x20 125movl $1, %eax # 接着判断当前任务,若是任务1则去执行任务0,或反之。 126cmpl %eax, current 127je 1f 128movl %eax, current # 若当前任务是0,则把1存入current,并跳转到任务1 129ljmp $TSS1_SEL, $0 # 去执行。注意跳转的偏移值无用,但需要写上。 130jmp 2f 131 1:movl $0, current # 若当前任务是1,则把0存入current,并跳转到任务0 132ljmp $TSS0_SEL, $0 # 去执行。 133 2:popl %eax 134pop %ds 135iret 136 137 # 系统调用中断int 0x80处理程序。该示例只有一个显示字符功能。 138 .align 2 139 system_interrupt: 140push %ds 141pushl %edx 142pushl %ecx 143pushl %ebx 144pushl %eax 145movl $0x10, %edx # 首先让DS指向内核数据段。 146mov %dx, %ds 147call write_char # 然后调用显示字符子程序write_char,显示AL中的字符。 148popl %eax 149popl %ebx 150popl %ecx 151popl %edx 152pop %ds 153iret 154 155 /*********************************************/ 156 current:.long 0 # 当前任务号(0或1)。 157 scr_loc:.long 0 # 屏幕当前显示位置。按从左上角到右下角顺序显示。 158 159 .align 2 160 lidt_opcode: 161.word 256*8-1 # 加载IDTR寄存器的6字节操作数:表长度和基地址。 162.long idt 163 lgdt_opcode: 164.word (end_gdt-gdt)-1 # 加载GDTR寄存器的6字节操作数:表长度和基地址。 165.long gdt 166 167 .align 3 168 idt: .fill 256,8,0 # IDT空间。共256个门描述符,每个8字节,占用2KB。 169 170 gdt: .quad 0x0000000000000000 # GDT表。第1个描述符不用。 171.quad 0x00c09a00000007ff # 第2个是内核代码段描述符。其选择符是0x08。 172.quad 0x00c09200000007ff # 第3个是内核数据段描述符。其选择符是0x10。 173.quad 0x00c0920b80000002 # 第4个是显示内存段描述符。其选择符是0x18。 174.word 0x68, tss0, 0xe900, 0x0 # 第5个是TSS0段的描述符。其选择符是0x20 175.word 0x40, ldt0, 0xe200, 0x0 # 第6个是LDT0段的描述符。其选择符是0x28 176.word 0x68, tss1, 0xe900, 0x0 # 第7个是TSS1段的描述符。其选择符是0x30 177.word 0x40, ldt1, 0xe200, 0x0 # 第8个是LDT1段的描述符。其选择符是0x38 178 end_gdt: 179.fill 128,4,0 # 初始内核堆栈空间。 180 init_stack: # 刚进入保护模式时用于加载SS:ESP堆栈指针值。 181.long init_stack # 堆栈段偏移位置。 182.word 0x10 # 堆栈段同内核数据段。 183 184 # 下面是任务0的LDT表段中的局部段描述符。 185 .align 3 186 ldt0:.quad 0x0000000000000000 # 第1个描述符,不用。 187.quad 0x00c0fa00000003ff # 第2个局部代码段描述符,对应选择符是0x0f。 188.quad 0x00c0f200000003ff # 第3个局部数据段描述符,对应选择符是0x17。 189 # 下面是任务0的TSS段的内容。注意其中标号等字段在任务切换时不会改变。 190 tss0:.long 0 /* back link */ 191.long krn_stk0, 0x10 /* esp0, ss0 */ 192.long 0, 0, 0, 0, 0 /* esp1, ss1, esp2, ss2, cr3 */ 193.long 0, 0, 0, 0, 0 /* eip, eflags, eax, ecx, edx */ 194.long 0, 0, 0, 0, 0 /* ebx esp, ebp, esi, edi */ 195.long 0, 0, 0, 0, 0, 0 /* es, cs, ss, ds, fs, gs */ 196.long LDT0_SEL, 0x8000000 /* ldt, trace bitmap */ 197 198.fill 128,4,0 # 这是任务0的内核栈空间。 199 krn_stk0: 200 201 # 下面是任务1的LDT表段内容和TSS段内容。 202 .align 3 203 ldt1:.quad 0x0000000000000000 # 第1个描述符,不用。 204.quad 0x00c0fa00000003ff # 选择符是0x0f,基地址 = 0x00000。 205.quad 0x00c0f200000003ff # 选择符是0x17,基地址 = 0x00000。 206 207 tss1:.long 0 /* back link */ 208.long krn_stk1, 0x10 /* esp0, ss0 */ 209.long 0, 0, 0, 0, 0 /* esp1, ss1, esp2, ss2, cr3 */ 210.long task1, 0x200 /* eip, eflags */ 211.long 0, 0, 0, 0 /* eax, ecx, edx, ebx */ 212.long usr_stk1, 0, 0, 0 /* esp, ebp, esi, edi */ 213.long 0x17,0x0f,0x17,0x17,0x17,0x17 /* es, cs, ss, ds, fs, gs */ 214.long LDT1_SEL, 0x8000000 /* ldt, trace bitmap */ 215 216.fill 128,4,0 # 这是任务1的内核栈空间。其用户栈直接使用初始栈空间。 217 krn_stk1: 218 219 # 下面是任务0和任务1的程序,它们分别循环显示字符"A"和"B"。 220 task0: 221movl $0x17, %eax # 首先让DS指向任务的局部数据段。 222movw %ax, %ds # 因为任务没有使用局部数据,所以这两句可省略。 223movl $65, %al # 把需要显示的字符"A"放入AL寄存器中。 224int $0x80 # 执行系统调用,显示字符。 225movl $0xfff, %ecx # 执行循环,起延时作用。 226 1:loop 1b 227jmp task0 # 跳转到任务代码开始处继续显示字符。 228 task1: 229movl $66, %al # 把需要显示的字符"B"放入AL寄存器中。 230int $0x80 # 执行系统调用,显示字符。 231movl $0xfff, %ecx # 延时一段时间,并跳转到开始处继续循环显示。 232 1:loop 1b 233jmp task1 234 235.fill 128,4,0 # 这是任务1的用户栈空间。 236 usr_stk1:
更多推荐
所有评论(0)