ucore lab1

练习1:理解通过make生成执行文件的过程

问题一:操作系统镜像文件ucore.img是如何一步一步生成的?(需要比较详细地解释Makefile中每一条相关命令和命令参数的含义,以及说明命令导致的结果)

在Makefile中生成ucore.img的代码如下:

# create ucore.img
UCOREIMG	:= $(call totarget,ucore.img)

$(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

$(call create_target,ucore.img)

首先先创建一个大小为10000字节的块儿,然后再将bootblock拷贝过去。
生成ucore.img需要先生成kernelbootblock

通过make V=指令得到执行的具体命令如下:

# 编译 init.c 文件,生成 init.o 
+ cc kern/ern/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

# 编译 readline.c 文件,生成 readline.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

# 编译 stdio.c 文件,生成 stdio.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

# 编译 kdebug.c 文件,生成 kdebug.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

# 编译 kmonitor.c 文件,生成 kmonitor.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

# 编译 panic.c 文件,生成 painc.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

# 编译一系列 .c 文件
+ 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

# 链接生成的所有目标文件,并生成 kernel 二进制文件
+ 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

# 编译 bootasm.S / bootmain.c / sign.c / 
+ 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

# 生成 sign 文件
gcc -g -Wall -O2 obj/sign/tools/sign.o -o bin/sign

# 链接生成 bootblock 二进制文件
+ 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!

# 生成ucore.img文件
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.0325965 s, 157 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.000118495 s, 4.3 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.00055858 s, 134 MB/s

根据其中可以看到,要生成kernel,需要用GCC编译器将kern目录下所有的.c文件全部编译生成的.o文件的支持。同样,要生成bootblock,首先需要生成bootasm.o、bootmain.o、sign

GCC 编译选项详解:

编译选项 含义
-I 指定库文件包含路径(① 指定值 ② 环境变量 ③ 标准系统搜索路径)
-fno-builtin 只识别以 __builtin_为前缀的 GCC 內建函数,禁用大多数內建函数,防止与其重名
-Wall 编译后显示所有警告信息
-ggdb 使用 GDB 加入调试信息
-m32 生成 32位机器代码,int long pointer 都是 32 位,指定x86处理器特定选项处理器依赖选项
-gstabs 产生 stabs 格式的调试信息,不包含 GDB 扩展
-nostdinc 不搜索标准系统目录的头文件,只搜索 -I / -iquote / -isystem / -dirafter指定的头文件, 目录选项
-fno-stack-protector 禁用堆栈保护机制,工具选项
-c 编译或汇编源文件,但是不进行链接。将.c/.i/.s等后缀的文件编译成 .o 后缀。输出类型控制
-O 优化生成的代码,-Os 仅仅是优化生成代码的大小,它开启了所有的-O2优化选项,除了那些会使代码尺寸增大的选项。 优化选项

LD 编译选项详解:

编译选项 含义
-m 指定生成文件的格式,默认使用 LDEMULATION环境变量,如果没有这个环境变量,则依赖与linker 的默认配置。通过 ld -V 可以查看它支持的 emulation
-nostdlib 只搜索命令行中显示制定的库目录,链接脚本里面制定的目录被忽略,包括命令行中制定的链接脚本。
-N 设置 text and data section 可读写,数据段不进行页对其,不链接动态链接库
-e entry 指定程序开始执行的入口函数,而不是默认的入口点。
-Tbss=org / -Tdata=org / -Ttext=org 通过 org 制定一个 section 在输出文件中的绝对地址。

dd 磁盘维护命令详解:

Linux dd命令用于读取、转换并输出数据。dd可从标准输入或文件中读取数据,根据指定的格式来转换数据,再输出到文件、设备或标准输出。

if = 文件名:输入文件名,缺省为标准输入。
of = 文件名:输出文件名,缺省为标准输出。
	ibs = bytes:一次读入bytes个字节,即指定一个块大小为bytes个字节。(默认 512 字节)
	obs = bytes:一次输出bytes个字节,即指定一个块大小为bytes个字节。(默认 512 字节)
	bs = bytes:同时设置读入/输出的块大小为bytes个字节。
	cbs = bytes:一次转换bytes个字节,即指定转换缓冲区大小。
skip = blocks:从输入文件开头跳过blocks个块后再开始复制。
seek = blocks:从输出文件开头跳过blocks个块后再开始复制。
count = blocks:仅拷贝blocks个块,块大小等于ibs指定的字节数。
conv = <关键字>,关键字可以有以下11种:
	conversion:用指定的参数转换文件。
	ascii:转换ebcdic为ascii
	ebcdic:转换ascii为ebcdic
	ibm:转换ascii为alternate ebcdic
	block:把每一行转换为长度为cbs,不足部分用空格填充
	unblock:使每一行的长度都为cbs,不足部分用空格填充
	lcase:把大写字符转换为小写字符
	ucase:把小写字符转换为大写字符
	swab:交换输入的每对字节
	noerror:出错时不停止
	notrunc:不截短输出文件
	sync:将每个输入块填充到ibs个字节,不足部分用空(NUL)字符补齐。

N and BYTES may be followed by the following multiplicative suffixes:
c =1, w =2, b =512, kB =1000, K =1024, MB =1000*1000, M =1024*1024, xM =M
GB =1000*1000*1000, G =1024*1024*1024, and so on for T, P, E, Z, Y.

问题二:一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?

回答:硬盘主引导扇区的 size = 512 Bytes,并且最后两个字节为0x55AA

在 sign.c 作了如下检查:

    //检查主引导扇区大小
		if (size != 512) {
        fprintf(stderr, "write '%s' error, size is %d.\n", argv[2], size);
        return -1;
    }

练习2:使用qemu执行并调试lab1中的软件。(要求在报告中简要写出练习过程)

练习3:分析bootloader进入保护模式的过程。(要求在报告中写出分析)

bootloader做的事情:

1.使能保护模式&断机制

2.从硬盘读取kernel in ELF 格式的ucore kernel(跟在MBR后面的扇区)并放到内存中固定位置

3.跳转到ucore的入口点(entry point)执行,这时控制权到了ucore OS中

1.为何开启A20,如何开启A20?

Intel早期的8086 CPU提供了20根地址线,可寻址空间范围即02^20(00000HFFFFFH)的 1MB内存空间。但8086的数据处理位宽位16位,无法直接寻址1MB内存空间,所以8086提供了段地址加偏移地址的地址转换机制。PC机的寻址结构是segment:offset,segment和offset都是16位的寄存器,最大值是0ffffh,换算成物理地址的计算方法是把segment左移4位,再加上offset,所以segment:offset所能表达的寻址空间最大应为0ffff0h + 0ffffh = 10ffefh(前面的0ffffh是segment=0ffffh并向左移动4位的结果,后面的0ffffh是可能的最大offset),这个计算出的10ffefh是多大呢?大约是1088KB,就是说,segment:offset的地址表示能力,超过了20位地址线的物理寻址能力。**所以当寻址到超过1MB的内存时,会发生“回卷”(不会发生异常)。但下一代的基于Intel 80286 CPU的PC AT计算机系统提供了24根地址线,这样CPU的寻址范围变为 2^24=16M,同时也提供了保护模式,可以访问到1MB以上的内存了,此时如果遇到“寻址超过1MB”的情况,系统不会再“回卷”了,这就造成了向下不兼容。**为了保持完全的向下兼容性,IBM决定在PC AT计算机系统上加个硬件逻辑,来模仿以上的回绕特征,于是出现了A20 Gate。他们的方法就是把A20地址线控制和键盘控制器的一个输出进行AND操作,这样来控制A20地址线的打开(使能)和关闭(屏蔽\禁止)。一开始时A20地址线控制是被屏蔽的(总为0),直到系统软件通过一定的IO操作去打开它(参看bootasm.S)。很显然,在实模式下要访问高端内存区,这个开关必须打开,在保护模式下,由于使用32位地址线,如果A20恒等于0,那么系统只能访问奇数兆的内存,即只能访问0–1M、2-3M、4-5M…,这样无法有效访问所有可用内存。所以在保护模式下,这个开关也必须打开。

2.如何初始化GDT表?

分段机涉及4个关键内容:逻辑地址、段描述符(描述段的属性)、段描述符表(包含多个段描述符的“数组”)、段选择子(段寄存器,用于定位段描述符表中表项的索引)

全局描述符表全局描述符表是一个保存多个段描述符的“数组”,其起始地址保存在全局描述符表寄存器GDTR中。GDTR长48位,其中高32位为基地址,低16位为段界限。由于GDT不能有GDT本身之内的描述符进行描述定义,所以处理器采用GDTR为GDT这一特殊的系统段。注意,全局描述符表中第一个段描述符设定为空段描述符。GDTR中的段界限以字节为单位。对于含有N个描述符的描述符表的段界限通常可设为8*N-1。在ucore中的boot/bootasm.S中的gdt地址处和kern/mm/pmm.c中的全局变量数组gdt[]分别有基于汇编语言和C语言的全局描述符表的具体实现。

gdtdesc指出了全局描述符表在符号gdt处,将gdt表装入GDTR中。

# 把gdt表的起始位置和界限装入GDTR寄存器
lgdt gdtdesc
# 将CR0的第0位置1,以开启保护模式
movl %cr0, %eax
orl $CR0_PE_ON, %eax
movl %eax, %cr0

cr0寄存器的第0位为保护模式位PE,设置PE将让处理器工作在保护模式下。

3.如何进入保护模式?

通过长跳转指令ljmp $PROT_MODE_CSEG, $protcseg进入保护模式。

具体分析bootmain.S代码如下:

bios从硬盘的第一个扇区加载bootmain.S,并将它加载到内存的物理地址0x7c00,然后开始以实模式运行。

#include <asm.h>

# 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). 将各个寄存器置0
    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.
    #  关于A20 Gate: https://chyyuu.gitbooks.io/ucore_os_docs/content/lab1/lab1_appendix_a20.html
    #  理论上讲,我们只要操作8042芯片的输出端口(64h)的bit 1,就可以控制A20 Gate,
    #  但实际上,当你准备向8042的输入缓冲区里写数据时,可能里面还有其它数据没有处理,
    #  所以,我们要首先禁止键盘操作,同时等待数据缓冲区中没有数据以后,才能真正地去操作8042打开或者关闭A20 Gate。
    #  打开A20 Gate的具体步骤大致如下(参考bootasm.S):
    #  等待8042 Input buffer为空;
    #  发送Write 8042 Output Port (P2)命令到8042 Input buffer;
    #  等待8042 Input buffer为空;
    #  将8042 Output Port(P2)得到字节的第2位置1,然后写入8042 Input buffer;
seta20.1:
    inb $0x64, %al                                  # Wait for not busy(8042 input buffer empty). 等待8042键盘控制器不忙
    testb $0x2, %al                                 # 判断输入缓存是否为空
    jnz seta20.1

    movb $0xd1, %al                                 # 0xd1 -> port 0x64,0xd1表示写输出端口命令,参数随后通过0x60端口写入
    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

    movb $0xdf, %al                                 # 0xdf -> port 0x60,通过0x60写入数据11011111 即将A20置1
    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.
    # 加载GDT表
    lgdt gdtdesc
    # 将CR0的第0位置1,以打开保护模式
    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.
    # 长跳转到32位代码段,重装CS和EIP
    ljmp $PROT_MODE_CSEG, $protcseg

.code32                                             # Assemble for 32-bit mode
protcseg:
    # Set up the protected-mode data segment registers
    # 设置DS、ES等段寄存器
    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)
    # 转到保护模式完成,进入boot主方法
    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
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

练习4:分析bootloader加载ELF格式的OS的过程。(要求在报告中写出分析)

通过阅读bootmain.c,了解bootloader如何加载ELF文件。通过分析源代码和通过qemu来运行并调试bootloader&OS,

  • bootloader如何读取硬盘扇区的?
  • bootmain读硬盘的代码如下:
unsigned int    SECTSIZE  =      512 ;
struct elfhdr * ELFHDR    =      ((struct elfhdr *)0x10000) ;     // scratch space
/* bootmain - the entry of bootloader */
void
bootmain(void) {
    // read the 1st page off disk
  	// 将八个扇区读进内存
    readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);

    // is this a valid ELF?
  	// 检查是否合法的elf
    if (ELFHDR->e_magic != ELF_MAGIC) {
        goto bad;
    }
  	//do something else...
}

根据上述bootmain的分析,bootmain调用readseg函数读取硬盘扇区,而readseg则调用了readsect每次读取一个扇区:

/* waitdisk - wait for disk ready */
static void
waitdisk(void) {
    while ((inb(0x1F7) & 0xC0) != 0x40)
        /* do nothing */;
}

/*
读一个扇区的流程(可参看boot/bootmain.c中的readsect函数实现)大致如下
1。等待磁盘准备好
2。发出读取扇区的命令
3。等待磁盘准备好
4。把磁盘扇区数据读到指定内存
 */
/* readsect - read a single sector at @secno into @dst */
static void
readsect(void *dst, uint32_t secno) {
    // wait for disk to be ready
    waitdisk();
    // 写地址0x1f2~0x1f5,0x1f7,发出读取磁盘的命令
    outb(0x1F2, 1);                         // count = 1
    outb(0x1F3, secno & 0xFF);
    outb(0x1F4, (secno >> 8) & 0xFF);
    outb(0x1F5, (secno >> 16) & 0xFF);
    outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0);
    outb(0x1F7, 0x20);                      // cmd 0x20 - read sectors

    // wait for disk to be ready
    waitdisk();

    // read a sector
  	// 读取一个扇区
    insl(0x1F0, dst, SECTSIZE / 4);
}

/* *
 * 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;

    // 将逐个扇区读出
    for (; va < end_va; va += SECTSIZE, secno ++) {
        readsect((void *)va, secno);
    }
}
  • bootloader是如何加载ELF格式的OS?

bootmain读取完磁盘后开始加载ELF文件,bootmain中加载ELF格式的OS代码如下:

/* bootmain - the entry of bootloader */
void
bootmain(void) {
  
    //read from disk

    struct proghdr *ph, *eph;

    // load each program segment (ignores ph flags)
    // 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);
    }

    // call the entry point from the ELF header
    // note: does not return
    // 根据ELF头表中的入口信息,找到内核的入口并开始运行
    ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();

bad:
    outw(0x8A00, 0x8A00);
    outw(0x8A00, 0x8E00);

    /* do nothing */
    while (1);
}

练习5:实现函数调用堆栈跟踪函数 (需要编程)

一些寄存器的名称

General Register(通用寄存器):EAX/EBX/ECX/EDX/ESI/EDI/ESP/EBP这些寄存器的低16位就是8086的 AX/BX/CX/DX/SI/DI/SP/BP,对于AX,BX,CX,DX这四个寄存器来讲,可以单独存取它们的高8位和低8位 (AH,AL,BH,BL,CH,CL,DH,DL)。它们的含义如下:

	EAX:累加器
	EBX:基址寄存器
	ECX:计数器
	EDX:数据寄存器
	ESI:源地址指针寄存器
	EDI:目的地址指针寄存器
	EBP:基址指针寄存器
	ESP:堆栈指针寄存器

Segment Register(段寄存器,也称 Segment Selector,段选择符,段选择子):除了8086的4个段外(CS,DS,ES,SS),80386还增加了两个段FS,GS,这些段寄存器都是16位的,用于不同属性内存段的寻址,它们的含义如下:

	CS:代码段(Code Segment)
	DS:数据段(Data Segment)
	ES:附加数据段(Extra Segment)
	SS:堆栈段(Stack Segment)
	FS:附加段
	GS 附加段

Instruction Pointer(指令指针寄存器):EIP的低16位就是8086的IP,它存储的是下一条要执行指令的内存地址,在分段地址转换中,表示指令的段内偏移地址。

对栈的理解

理解调用栈最重要的两点是:栈的结构,EBP寄存器(EBP:基址指针寄存器)的作用。一个函数调用动作可分解为:零到多个PUSH指令(用于参数入栈),一个CALL指令。CALL指令内部其实还暗含了一个将返回地址(即CALL指令下一条指令的地址)压栈的动作(由硬件完成,CALL指令下一条指令的地址即当前eip的值)。几乎所有本地编译器都会在每个函数体之前插入类似如下的汇编指令:

pushl   %ebp	# 将ebp压栈
movl   %esp , %ebp	# esp -> ebp, esp:堆栈指针寄存器

这样在程序执行到一个函数的实际指令前,已经有以下数据顺序入栈:参数、返回地址、ebp寄存器。由此得到类似如下的栈结构(参数入栈顺序跟调用方式有关,这里以C语言默认的CDECL为例):

+|  栈底方向		| 高位地址
 |    ...		|
 |    ...	  |
 |  参数3		|
 |  参数2		|
 |  参数1		|
 |  返回地址		|
 |  上一层[ebp]	| <-------- [ebp]
 |  局部变量		|  低位地址

这两条汇编指令的含义是:首先将ebp寄存器入栈,然后将栈顶指针esp赋值给ebp。“mov ebp esp”这条指令表面上看是用esp覆盖ebp原来的值,其实不然。因为给ebp赋值之前,原ebp值已经被压栈(位于栈顶),而新的ebp又恰恰指向栈顶。此时ebp寄存器就已经处于一个非常重要的地位,该寄存器中存储着栈中的一个地址(原ebp入栈后的栈顶),从该地址为基准,向上(栈底方向)能获取返回地址、参数值,向下(栈顶方向)能获取函数局部变量值,而该地址处又存储着上一层函数调用时的ebp值。

一般而言,ss:[ebp+4]处为返回地址,ss:[ebp+8]处为第一个参数值(最后一个入栈的参数值,此处假设其占用4字节内存),ss:[ebp-4]处为第一个局部变量,ss:[ebp]处为上一层ebp值。由于ebp中的地址处总是“上一层函数调用时的ebp值”,而在每一层函数调用中,都能通过当时的ebp值“向上(栈底方向)”能获取返回地址、参数值,“向下(栈顶方向)”能获取函数局部变量值。如此形成递归,直至到达栈底。这就是函数调用栈。

函数调用栈的变化过程

函数调用大概包括以下几个步骤:

1、参数入栈:将参数从右向左(或从右向左)依次压入系统栈中。

2、返回地址入栈:将当前代码区调用指令的下一条指令地址压入栈中,供函数返回时继续执行。

3、代码区跳转:处理器从当前代码区跳转到被调用函数的入口处。

4、栈帧调整

4.1保存当前栈帧状态值,已备后面恢复本栈帧时使用(EBP入栈)。

4.2将当前栈帧切换到新栈帧(将ESP值装入EBP,更新栈帧底部)。

4.3给新栈帧分配空间(把ESP减去所需空间的大小,抬高栈顶)。

函数返回大概包括以下几个步骤:

1、保存返回值,通常将函数的返回值保存在寄存器EAX中。

2、弹出当前帧,恢复上一个栈帧。

2.1在堆栈平衡的基础上,给ESP加上栈帧的大小,降低栈顶,回收当前栈帧的空间

2.2将当前栈帧底部保存的前栈帧EBP值弹入EBP寄存器,恢复出上一个栈帧。

2.3将函数返回地址弹给EIP寄存器。

3、跳转:按照函数返回地址跳回母函数中继续执行。

kern/debug/kdebug.c的注释及实现如下:

/* *
 * print_stackframe - print a list of the saved eip values from the nested 'call'
 * instructions that led to the current point of execution
 * 打印一系列嵌套调用到当前执行位置的eip值
 *
 * The x86 stack pointer, namely esp, points to the lowest location on the stack
 * that is currently in use. Everything below that location in stack is free. Pushing
 * a value onto the stack will involve decreasing the stack pointer and then writing
 * the value to the place that stack pointer points to. And popping a value does the
 * opposite.
 * 堆栈指针寄存器esp指向当前正在用的栈的最低地址,在该地址以下的都是栈的空闲空间
 * 压栈会导致堆栈指针指向更低的地址,并将值写到堆栈指针指向的位置;出栈操作相反。
 *
 * The ebp (base pointer) register, in contrast, is associated with the stack
 * primarily by software convention. On entry to a C function, the function's
 * prologue code normally saves the previous function's base pointer by pushing
 * it onto the stack, and then copies the current esp value into ebp for the duration
 * of the function. If all the functions in a program obey this convention,
 * then at any given point during the program's execution, it is possible to trace
 * back through the stack by following the chain of saved ebp pointers and determining
 * exactly what nested sequence of function calls caused this particular point in the
 * program to be reached. This capability can be particularly useful, for example,
 * when a particular function causes an assert failure or panic because bad arguments
 * were passed to it, but you aren't sure who passed the bad arguments. A stack
 * backtrace lets you find the offending function.
 * ebp(基址寄存器)利用软件来与堆栈相关联,当进入一个c函数时,
 * 1。函数的初始化代码通常将前一函数的ebp压栈保存
 * 2。然后将esp的值传到ebp,以此保存函数调用信息。
 *
 * The inline function read_ebp() can tell us the value of current ebp. And the
 * non-inline function read_eip() is useful, it can read the value of current eip,
 * since while calling this function, read_eip() can read the caller's eip from
 * stack easily.
 * read_ebp()告知得到目前ebp的值,read_eip()可以得到目前eip的值
 *
 * In print_debuginfo(), the function debuginfo_eip() can get enough information about
 * calling-chain. Finally print_stackframe() will trace and print them for debugging.
 * debuginfo_eip()可以得到函数调用链的足够信息,print_stackframe()将跟踪、打印这些信息
 *
 * Note that, the length of ebp-chain is limited. In boot/bootasm.S, before jumping
 * to the kernel entry, the value of ebp has been set to zero, that's the boundary.
 * ebp链条的长度是有限的,在bootmain.S中,ebp的值被设为0,这就是它的界限。
 * */
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 (uint32_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();  // (1) call read_ebp() to get the value of ebp. the type is (uint32_t);
    uint32_t eip = read_eip();  // (2) call read_eip() to get the value of eip. the type is (uint32_t);

    int i, j;
    for (i = 0; ebp != 0 && i < STACKFRAME_DEPTH; i ++) {
        cprintf("ebp:0x%08x eip:0x%08x args:", ebp, eip);   // (3.1) printf value of ebp, eip
        uint32_t *args = (uint32_t *)ebp + 2;
        for (j = 0; j < 4; j ++) {
            cprintf("0x%08x ", args[j]);   // (3.2) (uint32_t)calling arguments [0..4] = the contents in address (uint32_t)ebp +2 [0..4]
        }
        cprintf("\n");  // (3.3) cprintf("\n");
        print_debuginfo(eip - 1);   //  (3.4) call print_debuginfo(eip-1) to print the C calling function name and line number, etc.
        eip = ((uint32_t *)ebp)[1]; // eip转到caller的CALL指令下一条指令的地址
        ebp = ((uint32_t *)ebp)[0]; // (3.5) popup a calling stackframe 因为ebp指向的是caller's ebp的指针
    }
}

练习6:完善中断初始化和处理 (需要编程)

中断描述符表(也可简称为保护模式下的中断向量表)中一个表项占多少字节?其中哪几位代表中断处理代码的入口?

中断描述符表一个表项占8字节。其中0~ 15位和48~ 63位分别为offset的低16位和高16位。16~ 31位为段选择子。通过段选择子获得段基址,加上段内偏移量即可得到中断处理代码的入口。

请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init。在idt_init函数中,依次对所有中断入口进行初始化。使用mmu.h中的SETGATE宏,填充idt数组内容。每个中断的入口由tools/vectors.c生成,使用trap.c中声明的vectors数组即可。

关于中断描述符表idt:中断号–>中断描述符表表项–>中断处理例程

中断描述符表每一表项记录了一个中断处理例程的地址,包括段选择子、偏移。而段选择子则可以在GDT中查找段描述符。得到段描述符就得到了有关这个中断服务例程的基址,加上offset就得到了中断服务例程的起始地址。

上述查找过程由硬件完成,但是有关的表由软件建立,我们只需用lidt指令让cpu得知中断描述符表的地址。

/* 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.
      *     中断服务例程的入口地址被保存在__vectors中,__vectors数组位于 kern/trap/vector.S
      *     将__vectors[]声明为外部的指针数组
      *
      * (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
      *     idt数组就是中断描述符表,利用宏SETGATE来设置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!
      *     设置好IDT后,利用lidt来让cpu得知IDT的位置
      */
    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);
}

SETGATE宏位于kern/mm/mmu.h,注释如下:

/* *
 * Set up a normal interrupt/trap gate descriptor
 * 设置中断/陷阱门描述符
 *   - istrap: 1 for a trap (= exception) gate, 0 for an interrupt gate
 *   - sel: Code segment selector for interrupt/trap handler
 *   - off: Offset in code segment for interrupt/trap handler
 *   - dpl: Descriptor Privilege Level - the privilege level required
 *          for software to invoke this interrupt/trap gate explicitly
 *          using an int instruction.
 *   gate:为相应的idt[]数组内容,处理函数的入口地址
 *   istrap: 1是trap门,0是中断门
 *   sel: 中断/陷阱门处理器的代码段选择子
 *   off: 代码段的偏移量
 *   dpl: 描述符的优先级,用户程序触发这个中断/陷阱需要的优先级
 * */
#define SETGATE(gate, istrap, sel, off, dpl) {            \
    (gate).gd_off_15_0 = (uint32_t)(off) & 0xffff;        \
    (gate).gd_ss = (sel);                                \
    (gate).gd_args = 0;                                    \
    (gate).gd_rsv1 = 0;                                    \
    (gate).gd_type = (istrap) ? STS_TG32 : STS_IG32;    \
    (gate).gd_s = 0;                                    \
    (gate).gd_dpl = (dpl);                                \
    (gate).gd_p = 1;                                    \
    (gate).gd_off_31_16 = (uint32_t)(off) >> 16;        \
}
请编程完善trap.c中的中断处理函数trap,在对时钟中断进行处理的部分填写trap函数中处理时钟中断的部分,使操作系统每遇到100次时钟中断后,调用print_ticks子程序,向屏幕上打印一行文字”100 ticks”。

时钟是一种有着特殊作用的外设,其作用并不仅仅是计时。在后续章节中将讲到,正是由于有了规律的时钟中断,才使得无论当前CPU运行在哪里,操作系统都可以在预先确定的时间点上获得CPU控制权。

当跳转到中断处理例程的入口地址时,如:

vector2:
  pushl $0
  pushl $2
  jmp __alltraps
.globl vector3

可见该中断处理例程跳转到__alltraps__alltraps位于kern/trap/trapentry.S中:

#include <memlayout.h>

# vectors.S sends all traps here.
.text
.globl __alltraps
__alltraps:
    # push registers to build a trap frame 构造trap frame结构
    # therefore make the stack look like a struct trapframe 让栈看起来像一个结构体trapframe
    pushl %ds
    pushl %es
    pushl %fs
    pushl %gs
    pushal

    # load GD_KDATA into %ds and %es to set up data segments for kernel
    # 将GD_KDATA加载到ds和es寄存器来为内核设置数据段
    movl $GD_KDATA, %eax
    movw %ax, %ds
    movw %ax, %es

    # push %esp to pass a pointer to the trapframe as an argument to trap()
    # 将esp压栈以将一个指向trapframe的指针传给trap()函数
    pushl %esp

    # call trap(tf), where tf=%esp
    # 转到trap函数
    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

上述汇编代码最终调用了kern/trap/trap.c中的trap函数:

/* *
 * trap - handles or dispatches an exception/interrupt. if and when trap() returns,
 * the code in kern/trap/trapentry.S restores the old CPU state saved in the
 * trapframe and then uses the iret instruction to return from the exception.
 * trap函数处理、分发中断/异常,
 * 当trap函数返回时,trapentry.S恢复保存在trapframe中的原来的cpu状态,
 * 然后用iret指令来返回一个异常。
 * */
void
trap(struct trapframe *tf) {
    // dispatch based on what type of trap occurred
    trap_dispatch(tf);
}

其中trapframe结构体定义如下:

/* 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;
};

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));

trap_dispatch的实现如下:

/* trap_dispatch - dispatch based on what type of trap occurred */
static void
trap_dispatch(struct trapframe *tf) {
    char c;

    switch (tf->tf_trapno) {
    case IRQ_OFFSET + IRQ_TIMER:	// 时钟中断
        /* LAB1 YOUR CODE : STEP 3 */
        /* handle the timer interrupt */
        // 处理时钟中断
        /* (1) After a timer interrupt, you should record this event using a global variable (increase it), such as ticks in kern/driver/clock.c
         * 在一个时钟中断后,应该让ticks变量自增来记录它
         * (2) Every TICK_NUM cycle, you can print some info using a function, such as print_ticks().
         * 每经过TICK_NUM次时钟中断,应该用print_ticks()打印一些信息
         * (3) Too Simple? Yes, I think so!
         */
        ticks ++;	// 每次时钟中断计数器加一
        if (ticks % TICK_NUM == 0) {
            print_ticks();  // 每TICK_NUM次时钟中断调用一次
        }
        break;
    }
}
上一篇:UE4 编辑器脚本工具Editor Scripting Utilities


下一篇:OS-lab1实验报告