标签:习惯 it! 注意 speed mips running tick 头部 size
还是有很多问题,但是我觉得我需要在查看更多资料后回来再理解,学这个也学了一周了,看了大量的资料。。。还是它们自己的80386手册和lab的指导手册觉得最准确,现在我就把这部分知识做一个汇总,也为之后的lab打下坚实的基础。80386真的难啊,比mips复杂多了。。顿时觉得我们学的都是小菜。。
下面这些知识来源于:
练习之所以被老师当做练习,一定有它重要的地方,所以我们先把练习有关的知识点汇总一下:
知识点包括:
首先是makefile 相关知识,然而这个makefile是真的复杂。。比我们的复杂多了。。下面说一说这里的知识。
老师视频里说了,重点掌握ucore.img的形成过程,老师教的方法是利用 make V= 命令把过程信息打印出来,打印如下:
+ cc kern/init/init.c gcc -Ikern/init/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/init/init.c -o obj/kern/init/init.o + cc kern/libs/readline.c gcc -Ikern/libs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/libs/readline.c -o obj/kern/libs/readline.o + cc kern/libs/stdio.c gcc -Ikern/libs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/libs/stdio.c -o obj/kern/libs/stdio.o + cc kern/debug/kdebug.c gcc -Ikern/debug/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/kdebug.c -o obj/kern/debug/kdebug.o + cc kern/debug/kmonitor.c gcc -Ikern/debug/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/kmonitor.c -o obj/kern/debug/kmonitor.o + cc kern/debug/panic.c gcc -Ikern/debug/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/panic.c -o obj/kern/debug/panic.o + cc kern/driver/clock.c gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/clock.c -o obj/kern/driver/clock.o + cc kern/driver/console.c gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/console.c -o obj/kern/driver/console.o + cc kern/driver/intr.c gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/intr.c -o obj/kern/driver/intr.o + cc kern/driver/picirq.c gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/picirq.c -o obj/kern/driver/picirq.o + cc kern/trap/trap.c gcc -Ikern/trap/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/trap.c -o obj/kern/trap/trap.o + cc kern/trap/trapentry.S gcc -Ikern/trap/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/trapentry.S -o obj/kern/trap/trapentry.o + cc kern/trap/vectors.S gcc -Ikern/trap/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/vectors.S -o obj/kern/trap/vectors.o + cc kern/mm/pmm.c gcc -Ikern/mm/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/mm/pmm.c -o obj/kern/mm/pmm.o + cc libs/printfmt.c gcc -Ilibs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -c libs/printfmt.c -o obj/libs/printfmt.o + cc libs/string.c gcc -Ilibs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -c libs/string.c -o obj/libs/string.o + ld bin/kernel ld -m elf_i386 -nostdlib -T tools/kernel.ld -o bin/kernel obj/kern/init/init.o obj/kern/libs/readline.o obj/kern/libs/stdio.o obj/kern/debug/kdebug.o obj/kern/debug/kmonitor.o obj/kern/debug/panic.o obj/kern/driver/clock.o obj/kern/driver/console.o obj/kern/driver/intr.o obj/kern/driver/picirq.o obj/kern/trap/trap.o obj/kern/trap/trapentry.o obj/kern/trap/vectors.o obj/kern/mm/pmm.o obj/libs/printfmt.o obj/libs/string.o + cc boot/bootasm.S gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootasm.S -o obj/boot/bootasm.o + cc boot/bootmain.c gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootmain.c -o obj/boot/bootmain.o + cc tools/sign.c gcc -Itools/ -g -Wall -O2 -c tools/sign.c -o obj/sign/tools/sign.o gcc -g -Wall -O2 obj/sign/tools/sign.o -o bin/sign + ld bin/bootblock ld -m elf_i386 -nostdlib -N -e start -Ttext 0x7C00 obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o ‘obj/bootblock.out‘ size: 488 bytes build 512 bytes boot sector: ‘bin/bootblock‘ success! dd if=/dev/zero of=bin/ucore.img count=10000 10000+0 records in 10000+0 records out 5120000 bytes (5.1 MB) copied, 0.0895198 s, 57.2 MB/s dd if=bin/bootblock of=bin/ucore.img conv=notrunc 1+0 records in 1+0 records out 512 bytes (512 B) copied, 0.000186759 s, 2.7 MB/s dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc 146+1 records in 146+1 records out 74923 bytes (75 kB) copied, 0.00184633 s, 40.6 MB/s
这是Makefile里生成ucore.img的代码,可以看到生成ucore.img需要kernel和bootblock
$(UCOREIMG): $(kernel) $(bootblock)
$(V)dd if=/dev/zero of=$@ count=10000
$(V)dd if=$(bootblock) of=$@ conv=notrunc
$(V)dd if=$(kernel) of=$@ seek=1 conv=notrunc
生成bootmain.o需要bootmain.c
实际命令为
gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc
-fno-stack-protector -Ilibs/ -Os -nostdinc
-c boot/bootmain.c -o obj/boot/bootmain.o
新出现的关键参数有
-fno-builtin 除非用__builtin_前缀,否则不进行builtin函数的优化
生成sign工具的makefile代码为
$(call add_files_host,tools/sign.c,sign,sign)
$(call create_target_host,sign,sign)
实际命令为
gcc -Itools/ -g -Wall -O2 -c tools/sign.c
-o obj/sign/tools/sign.o
gcc -g -Wall -O2 obj/sign/tools/sign.o -o bin/sign
首先生成bootblock.o
ld -m elf_i386 -nostdlib -N -e start -Ttext 0x7C00
obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o
其中关键的参数为
-m
-N 设置代码和数据段是可读可写的,对数据段不做page-align
-e
-Ttext 制定代码段开始位置
拷贝二进制代码bootblock.o到bootblock.out
objcopy -S -O binary obj/bootblock.o obj/bootblock.out
其中关键的参数为
-S 移除所有符号和重定位信息
-O
生成kernel的相关代码为
$(kernel): tools/kernel.ld
$(kernel): $(KOBJS)
@echo + ld $@
$(V)$(LD) $(LDFLAGS) -T tools/kernel.ld -o $@ $(KOBJS)
@$(OBJDUMP) -S $@ > $(call asmfile,kernel)
@$(OBJDUMP) -t $@ | $(SED) ‘1,/SYMBOL TABLE/d; s/ .* / /;
/^$$/d‘ > $(call symfile,kernel)
为了生成kernel,首先需要 kernel.ld init.o readline.o stdio.o kdebug.o
kmonitor.o panic.o clock.o console.o intr.o picirq.o trap.o
trapentry.o vectors.o pmm.o printfmt.o string.o
生成kernel的细节就不写了,就是.o文件的链接
生成一个有10000个块的文件,每个块默认512字节,用0填充
dd if=/dev/zero of=bin/ucore.img count=10000
把bootblock中的内容写到第一个块
dd if=bin/bootblock of=bin/ucore.img conv=notrunc
从第二个块开始写kernel中的内容
dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc
dd的一些参数的含义:
-if表示输入文件,如果不指定,那么会默认从stdin中读取输入
-of表示输出文件,如果不指定,那么会stdout
bs表示以字节为单位的块大小
count表示被赋值的块数
/dev/zero是一个字符设备,会不断返回0值字节\0
conv = notrunc 不截短输出文件
seek=blocks 从输出文件开头跳过blocks个块后再开始复制
这样我们就可以大致了解了这个内核ucore.img是如何被一步一步加载出来的,真的详细
tools/sign.c
按照这个文件的描述,需要检查以下几点:
输出的主引导扇区的最后两个字节是55AA
bootblock就是需要用到的主引导扇区
知识点:
gdb的使用(一直有问题)
要了解 makefile中的lab1-mon
我现在就记着个next nexti step stepi
x /10i $pc
知识点:
# Start the CPU: switch to 32-bit protected mode, jump into C.
# The BIOS loads this code from the first sector of the hard disk into
# memory at physical address 0x7c00 and starts executing in real mode
# with %cs=0 %ip=7c00.
.set PROT_MODE_CSEG, 0x8 # kernel code segment selector
.set PROT_MODE_DSEG, 0x10 # kernel data segment selector
.set CR0_PE_ON, 0x1 # protected mode enable flag
# start address should be 0:7c00, in real mode, the beginning address of the running bootloader
.globl start
start:
.code16 # Assemble for 16-bit mode
cli # Disable interrupts
cld # String operations increment
# Set up the important data segment registers (DS, ES, SS).
# this xorw can set %ax to zero regardless of what the initial value in this %ax is
# 使用 AT&T 样式的语法,所以其中的源和目的操作数和 Intel 文档中给出的顺序是相反的。
xorw %ax, %ax # Segment number zero
movw %ax, %ds # -> Data Segment
movw %ax, %es # -> Extra Segment
movw %ax, %ss # -> Stack Segment
# Enable A20:
# For backwards compatibility with the earliest PCs, physical
# address line 20 is tied low, so that addresses higher than
# 1MB wrap around to zero by default. This code undoes this.
seta20.1:
# 64 -> status reg , bit 1 is set when input reg has data
inb $0x64, %al # Wait for not busy(8042 input buffer empty).
# testb $0x2 AND %al , affected ZF
testb $0x2, %al
# jnz jump when ZF = 0,namely the result is not zero
jnz seta20.1
movb $0xd1, %al # 0xd1 -> port 0x64
outb %al, $0x64 # 0xd1 means: write data to 8042's P2 port
seta20.2:
inb $0x64, %al # Wait for not busy(8042 input buffer empty).
testb $0x2, %al
jnz seta20.2
// why don't read output buffer to get the original Output port?
movb $0xdf, %al # 0xdf -> port 0x60
outb %al, $0x60 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1
# Switch from real to protected mode, using a bootstrap GDT
# and segment translation that makes virtual addresses
# identical to physical addresses, so that the
# effective memory map does not change during the switch.
lgdt gdtdesc
movl %cr0, %eax
orl $CR0_PE_ON, %eax
movl %eax, %cr0
# Jump to next instruction, but in 32-bit code segment.
# Switches processor into 32-bit mode.
ljmp $PROT_MODE_CSEG, $protcseg
.code32 # Assemble for 32-bit mode
protcseg:
# Set up the protected-mode data segment registers
movw $PROT_MODE_DSEG, %ax # Our data segment selector
movw %ax, %ds # -> DS: Data Segment
movw %ax, %es # -> ES: Extra Segment
movw %ax, %fs # -> FS
movw %ax, %gs # -> GS
movw %ax, %ss # -> SS: Stack Segment
# Set up the stack pointer and call into C. The stack region is from 0--start(0x7c00)
movl $0x0, %ebp
movl $start, %esp
call bootmain
# If bootmain returns (it shouldn't), loop.
spin:
jmp spin
# Bootstrap GDT
.p2align 2
# force 4 byte alignment
# code seg and data seg base is equal?
gdt:
SEG_NULLASM # null seg
SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # code seg for bootloader and kernel
SEG_ASM(STA_W, 0x0, 0xffffffff) # data seg for bootloader and kernel
gdtdesc:
.word 0x17 # sizeof(gdt) - 1
.long gdt # address gdt
上面的这段代码,是执行bootmain之前bootloader所做的工作。我们一步步分析这段代码,并在此过程中把相关的知识点总结一下:
通过长跳转,更新CS寄存器的基地址
ljmp $PROT_MODE_CSEG, $protcseg
其中protcseg是一个label
这里还要注意PROT_MODE_CSEG和PROT_MODE_DSEG,这两者分别定义为0x8和0x10,表示代码段和数据段的选择子,注意段选择子的结构,前13位是index,正好这里分别对应1和2(和之前全局描述符表的顺序一致),然后后三位是0,表示全局的,而且dpl为0
设置段寄存器,并建立堆栈
注意这里建立堆栈,ebp寄存器按理来说是栈帧的,但是这里并不需要把它设置为0x7c00,因为这里0x7c00是栈的最高地址,它上面没有有效内容,而之后因为调用,ebp会被设置为被调用的那个函数的栈的起始地址,这里就不用管它了。
而且很重要的一点,这一点在下面的打印栈帧的练习中也用到了,就是用ebp是否为0来判断是否已经到达最初始的函数。
movw $PROT_MODE_DSEG, %ax
movw %ax, %ds
movw %ax, %es
movw %ax, %fs
movw %ax, %gs
movw %ax, %ss
movl $0x0, %ebp
movl $start, %esp
这就是bootasm.S中的代码的内容,它完成了bootloader的大部分功能,包括打开A20,初始化GDT,进入保护模式,更新段寄存器的值,建立堆栈
接下来bootmain完成bootloader剩余的工作,就是把内核从硬盘加载到内存中来,并把控制权交给内核,不过在此之前我们还需要了解一些基础知识。
这部分内容在指导书上的“bootloader的启动过程”中的“硬盘访问概述”中详细说明了,这里再仔细捋一遍
关于硬盘的读写,在我们的OS实验中也涉及到了,印象中就是在特定地址发命令,然后在特定地址读,或者在特定地址写,重复这个过程即可。
这里也是bootloader的硬盘访问都是通过CPU访问硬盘的IO地址寄存器来完成,大致读一个扇区(512字节)的流程和之前的设置A20的流程类似:
static void
readsect(void *dst, uint32_t secno)
从secno扇区读取一个扇区到dst
发出命令
outb(0x1F2, 1); // 设置读取扇区的数目为1
outb(0x1F3, secno & 0xFF);
outb(0x1F4, (secno >> 8) & 0xFF);
outb(0x1F5, (secno >> 16) & 0xFF);
outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0);
// 上面四条指令联合制定了扇区号
// 在这4个字节联合构成的32位参数中
// 29-31位强制设为1
// 28位(=0)表示访问"Disk 0"
// 0-27位是28位的偏移量
outb(0x1F7, 0x20); // 0x20命令,读取扇区
其中0x1f2是规定要读写的扇区数最后是读取到dst位置
insl(0x1F0, dst, SECTSIZE / 4); // 读取到dst位置,
注意insl命令,这个命令定义在x86.h中,其实就是从0x1f0读取SECTSIZE/4个双字到dst位置的汇编实现,注意这里是以双字为单位,即4个字节,所以才除以4,而且注意这里很多命令最后的“l”都对应了实际命令中的“d”
这个在之前《程序员的自我修养》中看过,然而现在全忘光了。。
这里只需要知道ELF是Linux系统下一种常用目标文件格式,有三种类型
ELFheader在文件开始处描述了整个文件的组织,elf文件头在elf.h中有定义,我们关注它的
可执行文件的程序头部是一个program header结构的数组,每个结构描述了一个段或者系统准备程序执行所必须的其他信息,下面我们看bootmain如何利用这些信息加载内核镜像
/* *
* readseg - read @count bytes at @offset from kernel into virtual address @va,
* might copy more than asked.
* */
static void
readseg(uintptr_t va, uint32_t count, uint32_t offset) {
uintptr_t end_va = va + count;
// round down to sector boundary
va -= offset % SECTSIZE;
// translate from bytes to sectors; kernel starts at sector 1
uint32_t secno = (offset / SECTSIZE) + 1;
// If this is too slow, we could read lots of sectors at a time.
// We'd write more to memory than asked, but it doesn't matter --
// we load in increasing order.
for (; va < end_va; va += SECTSIZE, secno ++) {
readsect((void *)va, secno);
}
}
就是把硬盘上的kernel,读取到内存中
void
bootmain(void) {
// 首先读取ELF的头部
readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
// 通过储存在头部的幻数判断是否是合法的ELF文件
if (ELFHDR->e_magic != ELF_MAGIC) {
goto bad;
}
struct proghdr *ph, *eph;
// ELF头部有描述ELF文件应加载到内存什么位置的描述表,
// 先将描述表的头地址存在ph
ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;
// 按照描述表将ELF文件中数据载入内存
for (; ph < eph; ph ++) {
readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
}
// ELF文件0x1000位置后面的0xd1ec比特被载入内存0x00100000
// ELF文件0xf000位置后面的0x1d20比特被载入内存0x0010e000
// 根据ELF头部储存的入口信息,找到内核的入口
((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
bad:
outw(0x8A00, 0x8A00);
outw(0x8A00, 0x8E00);
while (1);
}
至此,我们终于看完了bootloader的整个执行过程!这个过程理解了还是很清晰的。
我们现在其实可以自己根据代码总结一下我们的bootloader都干了什么
知识点:
栈这块我觉得很难理解,倒不是因为函数调用,而是后面的中断处理那里的栈处理,至今还不太明白。
所以这里仅仅先总结一下一般的函数调用,不涉及特权级切换,调用栈会发生什么。
在MIPS体系结构中,关于这个部分其实当时已经理解的很多了,而且编译中也涉及到了这方面内容,而在80386体系结构中,函数调用时的栈也是一样的,大致的顺序如下:
而此时的ebp指向哪里呢?此时的ebp指向上一层的ebp所在的地址,在执行pushl ebp之后,又会执行movl esp,ebp,把当前的esp给ebp作为被调用者的函数调用栈的栈帧(这里我习惯用栈帧来理解)
所以ebp寄存器很重要。
通过ebp寄存器的值,我们可以快速的得到调用者的ebp值,继而得到调用者的调用者的ebp值,这样可以建立一个调用链。这个很重要,我们在java里的exception.print(什么来着,忘了)这个就是依赖于ebp指针实现的,或者在遇到一个bad argument时,我们可以通过这个调用链来回溯检查
首先要注意ucore的实现中堆栈的建立是之前说的bootloader的bootasm.S中的把esp设置为0x7c00,ebp设置为0,然后就使用call bootmain来调用bootmain函数。
在执行call指令过程中,这个指令会执行:把返回地址push,然后把这一层的ebp push,所以此时esp指向的是0x7bf8(就是因为前面是一个ebp以及一个返回地址),这个也在之后的堆栈打印函数的执行结果中可以体现。然后ebp被赋予当前的esp,即0x7bf8,这也是最后一个合法的ebp。
void
print_stackframe(void) {
/* LAB1 YOUR CODE : STEP 1 */
/* (1) call read_ebp() to get the value of ebp. the type is (uint32_t);
* (2) call read_eip() to get the value of eip. the type is (uint32_t);
* (3) from 0 .. STACKFRAME_DEPTH
* (3.1) printf value of ebp, eip
* (3.2) (uint32_t)calling arguments [0..4] = the contents in address (unit32_t)ebp +2 [0..4]
* (3.3) cprintf("\n");
* (3.4) call print_debuginfo(eip-1) to print the C calling function name and line number, etc.
* (3.5) popup a calling stackframe
* NOTICE: the calling funciton's return addr eip = ss:[ebp+4]
* the calling funciton's ebp = ss:[ebp]
*/
uint32_t ebp = read_ebp(), eip = read_eip();
int i, j;
for (i = 0; ebp != 0 && i < STACKFRAME_DEPTH; i ++) {
cprintf("ebp:0x%08x eip:0x%08x args:", ebp, eip);
uint32_t *args = (uint32_t *)ebp + 2;
for (j = 0; j < 4; j ++) {
cprintf("0x%08x ", args[j]);
}
cprintf("\n");
print_debuginfo(eip - 1);
eip = ((uint32_t *)ebp)[1];
ebp = ((uint32_t *)ebp)[0];
}
}
在这个程序中要注意的是:
总之只要理解了之前的函数调用时,调用栈的变化,push的顺序,做出上面这段程序没有问题
知识点:
中断我感觉还有很多地方没有理解,这里先把我理解的部分好好总结一下,但是由于这部分内容实在是太多了,所以这里专挑练习问道的地方做总结
在中断之前,还有一种CPU和外设打交道的方式:轮询。但是这个方式太浪费CPU资源了,所以需要一种机制可以不让CPU主动询问,而是被动等待,等有需要的时候再处理中断事件
有三种
这个部分是重点,理解了这个部分,中断就没问题了。
CPU在收到中断事件后,打断当前任务的执行,根据某种机制跳到中断服务例程去执行的过程:
当中断处理工作完成后,需要通过iret指令恢复被打断的程序的执行,具体的执行过程如下:
所以说上面的就是宏观的,中断处理和返回的过程,具体到我们的代码,如何实现呢?
8259外设中断控制器
static void
serial_init(void) {
// Turn off the FIFO
outb(COM1 + COM_FCR, 0);
// Set speed; requires DLAB latch
outb(COM1 + COM_LCR, COM_LCR_DLAB);
outb(COM1 + COM_DLL, (uint8_t) (115200 / 9600));
outb(COM1 + COM_DLM, 0);
// 8 data bits, 1 stop bit, parity off; turn off DLAB latch
outb(COM1 + COM_LCR, COM_LCR_WLEN8 & ~COM_LCR_DLAB);
// No modem controls
outb(COM1 + COM_MCR, 0);
// Enable rcv interrupts,使串口1接手字符后产生中断
outb(COM1 + COM_IER, COM_IER_RDI);
// Clear any preexisting overrun indications and interrupts
// Serial port doesn't exist if COM_LSR returns 0xFF
serial_exists = (inb(COM1 + COM_LSR) != 0xFF);
(void) inb(COM1+COM_IIR);
(void) inb(COM1+COM_RX);
if (serial_exists) {
// IRQ_COM1 defined in trap.h,通过中断使能控制器使能串口1中断
pic_enable(IRQ_COM1);
}
}
这里细节,有时间再看,这不是重点
static void
kbd_init(void) {
// drain the kbd buffer
kbd_intr();
pic_enable(IRQ_KBD);
}
时钟这个外设很特殊,作用不仅仅是计时,正是因为有了规律的时钟中断,才使得无论当前CPU运行在哪里,操作系统都可以在预先确定的时间点上获得CPU的控制权,而且也影响一个应用程序的切换
/* *
* clock_init - initialize 8253 clock to interrupt 100 times per second,
* and then enable IRQ_TIMER.
* */
void
clock_init(void) {
// set 8253 timer-chip
// 100 times per second
outb(TIMER_MODE, TIMER_SEL0 | TIMER_RATEGEN | TIMER_16BIT);
outb(IO_TIMER1, TIMER_DIV(100) % 256);
outb(IO_TIMER1, TIMER_DIV(100) / 256);
// initialize time counter 'ticks' to zero
ticks = 0;
cprintf("++ setup timer interrupts\n");
pic_enable(IRQ_TIMER);
}
中断的初始化可以从vector.S说起。
vector.S文件,打开一看是两部分,第一部分是代码段,定义了vector0到vector255这256个标号所对应的代码段的起始位置,每个标号后的代码无非是两种:
然后是jmp __alltraps
第二部分是数据段,定义了__vectors数组,保存了每个中断向量的入口地址
而这些入口地址,就是当中断发生时,中断描述符中所对应的那个offset,所以一旦中断发生,中断处理程序首先是会跳到vector[i]所对应的代码
vector.S规定了每个中断处理例程的代码偏移,然后idt_init通过这些偏移,设置好idt表,然后再通过lidt,把idt表的初始地址保存到idtr寄存器中,这样中断相关的数据结构初始化完毕了
/* idt_init - initialize IDT to each of the entry points in kern/trap/vectors.S */
void
idt_init(void) {
/* LAB1 YOUR CODE : STEP 2 */
/* (1) Where are the entry addrs of each Interrupt Service Routine (ISR)?
* All ISR's entry addrs are stored in __vectors. where is uintptr_t __vectors[] ?
* __vectors[] is in kern/trap/vector.S which is produced by tools/vector.c
* (try "make" command in lab1, then you will find vector.S in kern/trap DIR)
* You can use "extern uintptr_t __vectors[];" to define this extern variable which will be used later.
* (2) Now you should setup the entries of ISR in Interrupt Description Table (IDT).
* Can you see idt[256] in this file? Yes, it's IDT! you can use SETGATE macro to setup each item of IDT
* (3) After setup the contents of IDT, you will let CPU know where is the IDT by using 'lidt' instruction.
* You don't know the meaning of this instruction? just google it! and check the libs/x86.h to know more.
* Notice: the argument of lidt is idt_pd. try to find it!
*/
extern uintptr_t __vectors[];
int i;
for (i = 0; i < sizeof(idt) / sizeof(struct gatedesc); i ++) {
SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL);
}
// set for switch from user to kernel
SETGATE(idt[T_SWITCH_TOK], 0, GD_KTEXT, __vectors[T_SWITCH_TOK], DPL_USER);
// load the IDT
lidt(&idt_pd);
}
注意:
至此,中断的初始化设置就结束了,接下来分析之前说的那个中断的处理过程如何用代码来实现
按照之前说的,因为idt_init中把__vector的元素作为中断描述符的offset设置好了,所以说,一旦中断发生,那么CPU会从__vector[i]所对应的代码开始执行。
然而,我们还需要仔细思考中断发生(INI 或者是 外设中断发生)后,这整个过程究竟是怎么样的,之前已经把这个过程宏观的(也不宏观,但是也没有代码细节)讲了一遍。
# vectors.S sends all traps here.
.text
.globl __alltraps
__alltraps:
# push registers to build a trap frame
# therefore make the stack look like a struct trapframe
pushl %ds
pushl %es
pushl %fs
pushl %gs
pushal
# load GD_KDATA into %ds and %es to set up data segments for kernel
movl $GD_KDATA, %eax
movw %ax, %ds
movw %ax, %es
# push %esp to pass a pointer to the trapframe as an argument to trap()
pushl %esp
# call trap(tf), where tf=%esp
call trap
# pop the pushed stack pointer
popl %esp
# return falls through to trapret...
.globl __trapret
__trapret:
# restore registers from stack
popal
# restore %ds, %es, %fs and %gs
popl %gs
popl %fs
popl %es
popl %ds
# get rid of the trap number and error code
addl $0x8, %esp
iret
可以从上面的代码中看到,在跳入__alltraps后,接下来的工作是:
进入了trap函数后,注意此时的参数是一个trapframe类型的指针,这个指针的值就是之前push的那个ebp,它指向的是trapframe结构体。然后会继续执行trap_dispatch函数,在这里,会根据trapno,不同情况做不同处理
处理之后,会继续回到__trapret这里继续执行,此时栈指针所指的就是那个参数!也就是之前的esp指针,恢复了它以后,按照之前压栈顺序陆续恢复这些寄存器,注意,恢复到ds寄存器之后,按照之前的思考,此时栈指针应该指向的是trapno,然而这里iret之前说过它不负责恢复trapno和errorno,所以此时我们需要手动把栈指针提高,跳过这两个,然后执行iret,按照之前的iret的描述,此时会陆续恢复eip cs eflags,还会根据是否特权级转换,恢复esp和ss,就和int的操作的逆过程一样
至此我们就把整个中断处理从初始化,到发生,执行,执行结束,这个过程弄清楚了!
知识点:
看了几天了,终于懂了这里的代码含义了,热泪盈眶!感受:指针得弄懂,堆栈也得彻底弄懂才能看懂
在从内核态,通过中断,切换为用户态时:
case T_SWITCH_TOU:
if (tf->tf_cs != USER_CS) {
switchk2u = *tf;
switchk2u.tf_cs = USER_CS;
switchk2u.tf_ds = switchk2u.tf_es = switchk2u.tf_ss = USER_DS;
switchk2u.tf_esp = (uint32_t)tf + sizeof(struct trapframe) - 8;
// set eflags, make sure ucore can use io under user mode.
// if CPL > IOPL, then cpu will generate a general protection.
switchk2u.tf_eflags |= FL_IOPL_MASK;
// set temporary stack
// then iret will jump to the right stack
*((uint32_t *)tf - 1) = (uint32_t)&switchk2u;
}
break;
static void
lab1_switch_to_user(void) {
//LAB1 CHALLENGE 1 : TODO
asm volatile (
"sub $0x8, %%esp \n"
"int %0 \n"
"movl %%ebp, %%esp"
:
: "i"(T_SWITCH_TOU)
);
}
上面说明了内核态通过切换到用户态时的过程,接下来解释,用户态通过中断到内核态的过程
case T_SWITCH_TOK:
if (tf->tf_cs != KERNEL_CS) {
tf->tf_cs = KERNEL_CS;
tf->tf_ds = tf->tf_es = KERNEL_DS;
tf->tf_eflags &= ~FL_IOPL_MASK;
switchu2k = (struct trapframe *)(tf->tf_esp - (sizeof(struct trapframe) - 8));
memmove(switchu2k, tf, sizeof(struct trapframe) - 8);
*((uint32_t *)tf - 1) = (uint32_t)switchu2k;
}
break;
static void
lab1_switch_to_kernel(void) {
//LAB1 CHALLENGE 1 : TODO
asm volatile (
"int %0 \n"
"movl %%ebp, %%esp \n"
:
: "i"(T_SWITCH_TOK)
);
}
struct trapframe {
struct pushregs tf_regs;
uint16_t tf_gs;
uint16_t tf_padding0;
uint16_t tf_fs;
uint16_t tf_padding1;
uint16_t tf_es;
uint16_t tf_padding2;
uint16_t tf_ds;
uint16_t tf_padding3;
uint32_t tf_trapno;
/* below here defined by x86 hardware */
uint32_t tf_err;
uintptr_t tf_eip;
uint16_t tf_cs;
uint16_t tf_padding4;
uint32_t tf_eflags;
/* below here only when crossing rings, such as from user to kernel */
uintptr_t tf_esp;
uint16_t tf_ss;
uint16_t tf_padding5;
} __attribute__((packed));
/* registers as pushed by pushal */
struct pushregs {
uint32_t reg_edi;
uint32_t reg_esi;
uint32_t reg_ebp;
uint32_t reg_oesp; /* Useless */
uint32_t reg_ebx;
uint32_t reg_edx;
uint32_t reg_ecx;
uint32_t reg_eax;
};
注意到在pushregs中有一个oesp,是useless的,注意把它与发生特权级切换时用户态的esp区分开
对于int指令,它在特权级改变时,会对栈进行这些操作:
特权级不变时,就在栈上:
push long pointer to return location
而call指令,相比int,少push一个eflags,也就是说call在长模式下只是push return address
pushal:
jmp只不过会影响CS寄存器,但不会对栈造成影响
有时间再做咯
标签:习惯 it! 注意 speed mips running tick 头部 size
原文地址:https://www.cnblogs.com/xxrxxr/p/9527344.html