清华大学OS操作系统实验lab1练习知识点汇总

时间:2018-08-24 01:50:29   收藏:0   阅读:230

lab1知识点汇总

还是有很多问题,但是我觉得我需要在查看更多资料后回来再理解,学这个也学了一周了,看了大量的资料。。。还是它们自己的80386手册和lab的指导手册觉得最准确,现在我就把这部分知识做一个汇总,也为之后的lab打下坚实的基础。80386真的难啊,比mips复杂多了。。顿时觉得我们学的都是小菜。。

下面这些知识来源于:

lab1练习汇总

练习之所以被老师当做练习,一定有它重要的地方,所以我们先把练习有关的知识点汇总一下:

练习1

知识点包括:

Makefile gcc dd ld 等相关知识以及ucore.img的生成过程

首先是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

  1. 这是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

  2. 为了生成bootblock,首先需要生成bootasm.o、bootmain.o、sign
    1. 生成bootasm.o需要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
      其中关键的参数为
      -ggdb 生成可供gdb使用的调试信息
      -m32 生成适用于32位环境的代码
      -gstabs 生成stabs格式的调试信息
      -nostdinc 不在标准系统文件夹寻找头文件,只在-I等参数指定的文件夹中搜索头文件
      -fno-stack-protector 不生成用于检测缓冲区溢出的代码
      -Os 为减小代码大小而进行优化
      -I 添加搜索头文件的路径,优先查找头文件的地方
    2. 生成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函数的优化

    3. 生成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

    4. 首先生成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 模拟为i386上的连接器
      -N 设置代码和数据段是可读可写的,对数据段不做page-align
      -e 指定入口
      -Ttext 制定代码段开始位置

    5. 拷贝二进制代码bootblock.o到bootblock.out
      objcopy -S -O binary obj/bootblock.o obj/bootblock.out
      其中关键的参数为
      -S 移除所有符号和重定位信息
      -O 指定输出格式

    6. 使用sign工具处理bootblock.out,生成bootblock
      bin/sign obj/bootblock.out bin/bootblock
  3. 生成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文件的链接

  4. 生成一个有10000个块的文件,每个块默认512字节,用0填充
    dd if=/dev/zero of=bin/ucore.img count=10000

  5. 把bootblock中的内容写到第一个块
    dd if=bin/bootblock of=bin/ucore.img conv=notrunc

  6. 从第二个块开始写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
按照这个文件的描述,需要检查以下几点:

知识点:

要了解 makefile中的lab1-mon

熟悉gdb的调试命令

我现在就记着个next nexti step stepi

x /10i $pc

练习 3

根据代码分析bootloader的作用,同时重点是bootloader进入保护模式的过程

知识点:

bootasm.S的内容

# 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所做的工作。我们一步步分析这段代码,并在此过程中把相关的知识点总结一下:

  1. 第一步:屏蔽中断,设置串地址增长方向,把ds,es,ss寄存器置0
    1. cli:中断允许标志IF为1时CPU能响应外部的可屏蔽中断请求,但是对非屏蔽不起作用 ,设置用STI 清除是CLI,这里清除,表示不响应可屏蔽中断
    2. CLD:DF方向标志 为1 串按减 为0 串按加 STD置 CLD清DF
    3. 设置ds,es,ss寄存器为0.这里用了一个技巧,不管ax寄存器初始化为什么内容,通过xor ax,ax我都可以让ax置0
  2. 第二步:开启A20
    这部分知识,在实验指导书上有详细说明,这里对这部分内容进行详细阐述:
    • 首先,先不要管A20是什么,我们先了解一下8086结构的历史。之前的内存空间是比较小的,然后一开始的8086的地址线是20位,所以按照2的20次方是1M,所以按理来说,“按理”是指这20位二进制数的值就作为byte的地址的话,可以的寻址范围就是0-1M的范围,然而当时的寄存器是16位,所以不得不采用另外一种寻址方式:一个16位寄存器表示基地址 * 16 + 另一个16位寄存器表示的偏移地址,这样计算,最大的寻址空间是 0xffff0(0xffff左移了四位) + 0xffff,最后结果是0x10ffef,大约是1088KB,那么这就比1024KB还要大,举个例子,当你的地址通过上面的计算方式得到0x100001这个地址,那么因为只有20根地址线,所以这个地址的最高位1根本无法表示出来,会发生“回卷”,也就是会获取地址为0x00001处的值。但是当时这个问题对于使用没有影响
    • 但是后来,随着内存空间的不断增大,地址线也逐渐增加到32位,为了保持向下兼容(至今未理解这个向下兼容是具体表示什么意思),他们采取的做法是在第20根地址线(A20)上做了一个开关,当A20被使能时,它是一个正常的地址线,当他被disable时,它永远为0,所以这就引入了在一开始A20使能的问题,在保护模式下,要访问高端内存,一定要打开这个开关,否则第21个bit总是为0,那只能访问奇数M的空间了
    • 在了解A20如何打开之前,先对这个体系的地址空间做一个了解:在一开始只有1M内存时,这个部分内存是被分为低端的640KB的常规内存,和高端的384KB的内存,这部分内存一开始是被设计用来作为ROM和系统设备的地址区域。(好像是IBM当时认为内存不会到现在这么大,才把高地址的384KB作这样用)这个设计为之后内存容量的增大带来了麻烦。因为这384KB是ROM和系统设备的地址空间,那么内存会被这部分分开,0-640KB ,1M-最大内存,不连续了,为了解决这个问题,采用了这样的办法:系统加电后,先让ROM有效(即这部分地址空间是给ROM的),此时取出ROM的内容,然后再让RAM有效,把这部分内容保存到RAM的这部分地址空间中,这就是所谓的ROM shadowing
    • 接下来讲A20的相关操作。之前说A20是第21根地址线的值,实际上,是由一个8042键盘控制器来控制的A20 Gate(据说是找不到其他可以控制的地方了),而8042芯片内部有三个端口,其中一个是Output Port,而A20Gate就是Output Port端口的bit 1,所以要控制A20使能,其实就是通过读写端口数据,使得这个bit的值为1
    • 还是需要介绍这个芯片的读写方式:
      • 首先,这个芯片有两个外部端口,0x60h和0x64h,就相当于读写操作的地址了。
      • 读Output Port,需要向64h发送0d0h命令,然后从60h读取Output port的内容
      • 写Output Port,需要先向64h发送0d1h命令,然后向60h写入Output Port的内容
      • 同时我们还需要检查当前缓冲区是否有数据,如果有正在处理的数据,那么肯定需要等待数据处理完才可以,所以还需要知道我们可以通过读取0x64h的数据,获取这个芯片的状态,如果这个状态为0x2(这是规定),说明还有数据没有处理完
      • 实际上还需要知道更多的命令。包括关闭键盘输入等,但是在ucore的实现中,并没有这么麻烦
    • 所以我们可以看这部分代码,很容易就理解,这部分代码就是按照下面的顺序打开A20的:
      • inb $0x64, %al 就是读取当前状态到 al寄存器,然后testb $0x2, %al,就是检查它当前状态是否标志位0x2被设置了,配合下面这个跳转,当这个标志位为0,或者说是当前输入缓冲区没有数据了,就不跳转继续执行了
      • movb $0xd1, %al outb %al, $0x64,按照之前说的,先向64h发送0xd1命令, 表示要写
      • 其实不太清楚,这里为什么还要等待8042的input buffer没有数据了,中断已经关闭了呀,应该没有什么会影响到input buffer了吧
      • movb $0xdf, %al outb %al, $0x60,把0xdf写入0x60,这样A20就打开了
  3. 初始化GDT表,使用lgdt gdtdesc 即可,gdtdesc的定义了解一下,定义了空描述符,数据段和代码段
  4. 进入保护模式,就是让cr0寄存器的PE为1
    movl %cr0, %eax
    orl $CR0_PE_ON, %eax
    movl %eax, %cr0
  5. 通过长跳转,更新CS寄存器的基地址
    ljmp $PROT_MODE_CSEG, $protcseg
    其中protcseg是一个label
    这里还要注意PROT_MODE_CSEG和PROT_MODE_DSEG,这两者分别定义为0x8和0x10,表示代码段和数据段的选择子,注意段选择子的结构,前13位是index,正好这里分别对应1和2(和之前全局描述符表的顺序一致),然后后三位是0,表示全局的,而且dpl为0

  6. 设置段寄存器,并建立堆栈
    注意这里建立堆栈,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

  1. 最后进入bootmain方法

这就是bootasm.S中的代码的内容,它完成了bootloader的大部分功能,包括打开A20,初始化GDT,进入保护模式,更新段寄存器的值,建立堆栈

bootmain的内容

接下来bootmain完成bootloader剩余的工作,就是把内核从硬盘加载到内存中来,并把控制权交给内核,不过在此之前我们还需要了解一些基础知识。

硬盘的读写

这部分内容在指导书上的“bootloader的启动过程”中的“硬盘访问概述”中详细说明了,这里再仔细捋一遍

关于硬盘的读写,在我们的OS实验中也涉及到了,印象中就是在特定地址发命令,然后在特定地址读,或者在特定地址写,重复这个过程即可。

readsect

这里也是bootloader的硬盘访问都是通过CPU访问硬盘的IO地址寄存器来完成,大致读一个扇区(512字节)的流程和之前的设置A20的流程类似:

        static void
        readsect(void *dst, uint32_t secno)
        从secno扇区读取一个扇区到dst
ELF文件格式

这个在之前《程序员的自我修养》中看过,然而现在全忘光了。。

这里只需要知道ELF是Linux系统下一种常用目标文件格式,有三种类型

ELFheader在文件开始处描述了整个文件的组织,elf文件头在elf.h中有定义,我们关注它的

可执行文件的程序头部是一个program header结构的数组,每个结构描述了一个段或者系统准备程序执行所必须的其他信息,下面我们看bootmain如何利用这些信息加载内核镜像

bootmain函数
readseg
/* *
 * 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,读取到内存中

最后就是bootmain
    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的作用

我们现在其实可以自己根据代码总结一下我们的bootloader都干了什么

  1. 关闭中断,
  2. A20 使能
  3. 全局描述符表初始化
  4. 保护模式启动
  5. 设置段寄存器(长跳转更新CS,根据设置好的段选择子更新其他段寄存器)
  6. 设置堆栈,esp 0x700 ebp 0
  7. 进入bootmain后读取内核映像到内存,检查是否合法,并启动操作系统,控制权交给它

练习5 实现函数调用堆栈跟踪函数

知识点:

必须先理解函数调用栈

栈这块我觉得很难理解,倒不是因为函数调用,而是后面的中断处理那里的栈处理,至今还不太明白。

所以这里仅仅先总结一下一般的函数调用,不涉及特权级切换,调用栈会发生什么。
在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的顺序,做出上面这段程序没有问题

练习6 完善中断初始化和处理

知识点:

中断和异常

中断我感觉还有很多地方没有理解,这里先把我理解的部分好好总结一下,但是由于这部分内容实在是太多了,所以这里专挑练习问道的地方做总结

中断引入

在中断之前,还有一种CPU和外设打交道的方式:轮询。但是这个方式太浪费CPU资源了,所以需要一种机制可以不让CPU主动询问,而是被动等待,等有需要的时候再处理中断事件

中断分类

有三种

中断描述符表IDT

中断处理中硬件完成的工作

这个部分是重点,理解了这个部分,中断就没问题了。
CPU在收到中断事件后,打断当前任务的执行,根据某种机制跳到中断服务例程去执行的过程:

  1. 之前一直觉得中断是一个神秘的过程,这里解释了:CPU在执行完当前程序的每一条指令后,都会去确认在执行刚才的指令过程中中断控制器(如:8259A)是否发送中断请求过来,如果有那么CPU就会在相应的时钟脉冲到来时从总线上读取中断请求对应的中断向量;
  2. 根据这个中断向量号,利用IDTR寄存器,经过检查后,得到该向量号所对应的中断描述符
  3. 中断描述符中保存着offset,还保存着中断例程的所在段的段选择子,根据这个段选择子,从GDT中取得相应的代码段描述符,在代码段描述符中保存了中断服务例程的基地址,根据这里得到的基地址和中段描述符中的offset,我们可以得到中断服务例程的起始地址
  4. 接下来进行特权级转换的判断,CPU会根据当前的CPL和中断服务例程的段描述符DPL信息,确认是否发生了特权级的转换。具体的判断逻辑:
    1. 首先明确参与判断的三个特权级表示:①当前代码段寄存器的段选择子中存储的CPL,表示当前代码的特权级,②中断描述符的中断门描述符或者陷阱门描述符中存储的中断例程代码段的选择子中的DPL,表示目标代码段的DPL,然后也是中断门描述符或者陷阱门描述符的标志位中的DPL,表示门中的DPL
    2. 要求:CPL>=目标代码段的DPL(作为结果的CPL一定要等于目标代码段的DPL) 对于软件产生的中断(用户态程序中的指令触发,比如INT n),要求:CPL <= gateDPL
    3. 如果这些检查失败,那么会产生一个一般的保护异常
    4. 是否发生特权级的转换,我觉得就是看CPL是否被改变,或者说是目标代码段的DPL是否与CPL相等
    5. 当发生CPL的改变,一个堆栈切换操作就会完成!按照指导书,这个切换操作是这样的:这时CPU会从当前程序的TSS信息里取得改程序的内核栈地址,即包括内核态的ss和esp的值,并立即将系统当前使用的栈切换成新的内核栈,这个栈就是即将运行的中断服务程序所要使用的栈,紧接着就要把当前用户态程序使用的ss和esp先压入内核栈中(所以根据试验指导书,如果发生特权级转换,那么相比于没发生特权级转换,栈(不管是新栈还是旧栈)会先多压入一个ss和一个esp,剩下的和没发生特权级转换一样)
  5. 刚刚检查完是否进行了特权级的转换,接下来,CPU就需要开始保存当前被打断的程序的现场,不管现在是内核栈还是用户栈,都会压入:eflags cs eip errorcode
  6. 这些现场保护工作做完后,CPU就利用之前的中断服务例程里记录的offset和根据段选择子得到的段描述符里记录的base address,设置好当前的cs和eip寄存器,开始执行中断服务例程

当中断处理工作完成后,需要通过iret指令恢复被打断的程序的执行,具体的执行过程如下:

  1. 首先弹出eip,cs eflags
  2. 然后如果存在特权级转换(内核态到用户态?如何判断),那么还需要从内核栈中弹出用户态的ss和esp,此时栈也恢复为用户态的栈了
  3. 对于错误码,需要自己通过指令主动弹出,也就是说,iret指令在执行时自动的按照eip cs eflags弹出的,所以为了保证弹出正确,需要在iret指令执行之前自己写指令弹出errorcode

所以说上面的就是宏观的,中断处理和返回的过程,具体到我们的代码,如何实现呢?

具体的中断实现

外设的基本初始化设置

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 规定了中断的入口地址

vector.S文件,打开一看是两部分,第一部分是代码段,定义了vector0到vector255这256个标号所对应的代码段的起始位置,每个标号后的代码无非是两种:

然后是jmp __alltraps

第二部分是数据段,定义了__vectors数组,保存了每个中断向量的入口地址
而这些入口地址,就是当中断发生时,中断描述符中所对应的那个offset,所以一旦中断发生,中断处理程序首先是会跳到vector[i]所对应的代码

idt_init 初始化中断向量表

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后,接下来的工作是:

至此我们就把整个中断处理从初始化,到发生,执行,执行结束,这个过程弄清楚了!

扩展练习1中关于特权级转换的具体实现

知识点:

看了几天了,终于懂了这里的代码含义了,热泪盈眶!感受:指针得弄懂,堆栈也得彻底弄懂才能看懂

在从内核态,通过中断,切换为用户态时:

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)
    );
}
trapframe的具体规定
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指令,它在特权级改变时,会对栈进行这些操作:

特权级不变时,就在栈上:

pushl的堆栈操作

pushal:

  1. Temp <- ESP
  2. push EAX
  3. PUSH ECX
  4. PUSH EDX
  5. PUSH EBX
  6. PUSH TEMP
  7. PUSH EBP
  8. PUSH ESI
  9. PUSH EDI

jmp只不过会影响CS寄存器,但不会对栈造成影响

扩展练习2

有时间再做咯

原文:https://www.cnblogs.com/xxrxxr/p/9527344.html

评论(0
© 2014 bubuko.com 版权所有 - 联系我们:wmxa8@hotmail.com
打开技术之扣,分享程序人生!