🏴‍☠️
OS-Project-based-learning
  • Projects-for-x86
    • Project 1: Unix utilization
    • Project 2: reverse
    • Project 3: JOS lab1
    • Project 4: xv6-boot
    • Project 5: ucore lab1
      • Exercise-RTSRC
      • Exercise-Coding
    • Project 6: ucore lab2
  • Projects-for-Risc-V
Powered by GitBook
On this page
  • 练习1:
  • 练习2:
  • 练习3:
  • 练习4:
  1. Projects-for-x86
  2. Project 5: ucore lab1

Exercise-RTSRC

简单的任务练习......喂,气死了,简单个毛!

练习1:

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

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

利用make V=可以得到最终的编译顺序,可以看到:

1 + cc kern/init/init.c
2   gcc -Ikern/init/ -march=i686 -fno-builtin -fno-PIC -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

// -I: for directories.
// -fno-builtin: allow same name function declaration
// -march=i686: i686 is a subtype of i386 machine.
// -fno-PIC: possition independent code.
// -Wall: warning for all.
// -ggdb: allow gdb message.
// -m32: i386 is a 32-bit machine.
// -gstabs: more gdb message.
// -nostdinc: Don't search standard position, search directories with "I" marks.
// -fno-stack-protector: Don't provide stack protection.

通过RTFM阅读相关的标志,我们在code block底部简要解析如下,可以看到对于kern文件夹和libs文件夹下对源文件进行了向目标文件的转换。

之后,利用ld链接器将上述源文件链接在一起:链接脚本为kernel.ld.

  1 + ld bin/kernel
34  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/kd
    ebug.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/p
    icirq.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

// -T: read the files in the command line.
// -m: emulation linker.
// elf_i386: 
// -nostdlib: don't use standard libc.    
    

第三步,编译内核启动源码:方法类似,同时编译了工具sign.c.

第四步,将内核启动源码链接到0x7c00这个位置上,并对这个东西签名。

  1 + ld bin/bootblock
43  ld -m    elf_i386 -nostdlib -N -e start -Ttext 0x7C00 obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o

// -N: set the data/code segment to be readable and writable.
// -e: set the beginning of our code as the 'start' entry.
// -Ttext: the start of text segment will be 0x7c00. Which means the code will be placed here.

// Makefile scripts:
  5 $(bootblock): $(call toobj,$(bootfiles)) | $(call totarget,sign)
  4         @echo + ld $@
  3         $(V)$(LD) $(LDFLAGS) -N -e start -Ttext 0x7C00 $^ -o $(call toobj,bootblock)
  2         @$(OBJDUMP) -S $(call objfile,bootblock) > $(call asmfile,bootblock)
  1         @$(OBJCOPY) -S -O binary $(call objfile,bootblock) $(call outfile,bootblock)
166         @$(call totarget,sign) $(call outfile,bootblock) $(bootblock)

在完成上面的操作后,利用sign对生成的bootblock文件添上签名再写回。

  1     buf[510] = 0x55;
32      buf[511] = 0xAA;

这个数据很奇怪,不过我们还是找到了相关的资料,是主引导记录扇区的有效位。主引导扇区,即MBR,是计算机访问硬盘后读入的第一个扇区。

最后,采用下述操作:将生成的文件塞到ucore.img这个镜像中。

  1 dd if=/dev/zero of=bin/ucore.img count=10000
  2 dd if=bin/bootblock of=bin/ucore.img conv=notrunc
  3 dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc
  
  // dd: convert and copy a file.
  // for /dev/zero, Read operations from /dev/zero return as many null characters (0x00) as requested in the read operation.
  // if: file which we are read from.
  // of: file which we will write in.
  // count: copy only N input blocks.
  // conv: convert the file as per the comma separated symbol list.
  // seek: skip a block. Here will simply jump 512 bytes. For default block size is 512 bytes.

至于硬盘的主引导扇区的特征,我们已经在表格中提到了,即末尾的特殊字节。

练习2:

  1. 从CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行。

  2. 在初始化位置0x7c00设置实地址断点,测试断点正常。

  3. 从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和 bootblock.asm进行比较。

  4. 自己找一个bootloader或内核中的代码位置,设置断点并进行测试。

根据做JOS的经验以及RTFM的体会,加电后第一个位置是0xffff0是一件很了然的事情,然而这里在调试时是有点bug,首先不知道为什么这边的gdb非常操蛋的没有出现相关的指令,需要手动在里头编写一个函数来帮忙:

define nI # for next Instruction.
x/i $eip # print the next instruction.
end

其次,似乎在这种模式下没有ip寄存器,其对应的其实是eip这个东东。虽然e是extension的意思,但其实架构上是有点小冲突(这应该是386的机器,但是应该能让我去直接找ip寄存器才对)。箭头指向的是ip的位置,打印的也是那部分的地址,但按照常理这显然是出了问题,如果要看下一条指令到底是什么,我们应该把它做一下修正。这下终于可以打印出正确的指令了。

(gdb) nI
=> 0xfff0:	add    %al,(%eax) # 0xfff0 -> eip.

# 探索了一下下
(gdb) x/i $cs << 4 + $pc
Argument to arithmetic operation not a number or boolean.
(gdb) x/i $($cs << 4 + $pc)
The history is empty.
(gdb) x/i ($cs << 4 + $pc)
Argument to arithmetic operation not a number or boolean.
(gdb) x/i (($cs << 4) + $pc)
   0xfe05b:	cmpw   $0xffc8,%cs:(%esi)
   
# gdbinit function
define nI # for next Instruction.
x/i (($cs << 4) + $eip) # print the next instruction.
end

利用si和ni相关指令稍微跟一下BIOS加电后的指令即可,它们并没有什么太大的意思,而且就人力来说应该是跟不完的,在JOS Lab1中我也尝试跟了一小部分,可以参考,这一部分其实是很dirty的代码,真正开始起作用的其实是0x7c00之后的东东,它是我们之前装载进去的bootblock内容。

练习3:

  • 为何开启A20,以及如何开启A20

  • 如何初始化GDT表

  • 如何使能和进入保护模式

而且,在Makefile中装载是有顺序的,具体来说先装的是bootasm.S,然后再是bootmain.c这一部分。我们尝试对它们进行解读。

首先,我们来看一下A20的开启原因和方法:A20开启是让实模式可以访问高端地址,即高于自身16位的地址空间。如果不开启A20的话,访问高地址会自动回转回低地址,这类似于一个取mod操作。

方法的话参考下面这个网址所提的内容:写的很好

 39 # start address should be 0:7c00, in real mode, the beginning address of the running bootloader
 38 .globl start
 37 start:
 36 .code16                                             # Assemble for 16-bit mode
 35     cli                                             # Disable interrupts
 34     cld                                             # String operations increment
 33
 32     # Set up the important data segment registers (DS, ES, SS).
 31     xorw %ax, %ax                                   # Segment number zero
 30     movw %ax, %ds                                   # -> Data Segment
 29     movw %ax, %es                                   # -> Extra Segment
 28     movw %ax, %ss                                   # -> Stack Segment
 27
 26     # Enable A20:
 25     #  For backwards compatibility with the earliest PCs, physical
 24     #  address line 20 is tied low, so that addresses higher than
 23     #  1MB wrap around to zero by default. This code undoes this.
 22 seta20.1:
 21     inb $0x64, %al                                  # Wait for not busy(8042 input buffer empty).
 20     testb $0x2, %al                                 # bit 1: input buffer empty or not.
 19     jnz seta20.1
 18
 17     movb $0xd1, %al                                 # 0xd1 -> port 0x64
 16     outb %al, $0x64                                 # 0xd1 means: write data to 8042's P2 port
 15
 14 seta20.2:
 13     inb $0x64, %al                                  # Wait for not busy(8042 input buffer empty).
 12     testb $0x2, %al
 11     jnz seta20.2
 10
  9     movb $0xdf, %al                                 # 0xdf -> port 0x60
  8     outb %al, $0x60                                 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1
  
  # Process:
  # 1. start is the entry name, in Makefile process we inform ld to use start as the begin entry.
  # 2. cli, cld: please refer to Intel Manual. It disable the interrupt flag.
  # 3. clear segment register.
  
  # A20 port should be opened here.
  # How to open this port? -> 8042 port. 并且是output port上的某一位来控制
  # 标准的写output的方法:
  # 向64h发送0d1h命令,然后向60h写入Output Port的数据
  # 因此方案就很清楚了:
  # 1. 等待input buffer清空
  # 2. 向64h发送0d1h表示要写入的命令
  # 3. 再次等待input buffer清空
  # 4. 写入数据0xdfh,使得倒数第二位为1即可,上面的0xdf只是一个选择

我们根据80386手册对于初始化的描述章节可以得知,在从实模式转换为保护模式的时候,需要把全局描述符表也做好初始化。这意味着GDTR需要指向一个合法的GDT位置。

  4     # Switch from real to protected mode, using a bootstrap GDT
  3     # and segment translation that makes virtual addresses
  2     # identical to physical addresses, so that the
  1     # effective memory map does not change during the switch.
49      lgdt gdtdesc # define gdt before protected mode. the address is in gdtdesc.


## skip some code, simply check the gdt here. ##
  1 gdt:
80      SEG_NULLASM                                     # null seg
  1     SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff)           # code seg for bootloader and kernel
  2     SEG_ASM(STA_W, 0x0, 0xffffffff)                 # data seg for bootloader and kernel
  3
  4 gdtdesc:
  5     .word 0x17                                      # sizeof(gdt) - 1
  6     .long gdt                                       # address gdt
  
  

为了理解这个,我们先RTFM一下,在李忠老师的书《x86汇编语言:从实模式到保护模式》中也可以看到类似的表述:

也就是说,GDTR寄存器首先要规定好limit值,然后再把gdt的地址放在base处,至于limit的值,它应该被设置成8N-1这个值。我们看到此处的gdt共有三个项,所以设置limit为0x17是很合理的选择。

此外,和在前置知识中我们提到的那样,第一项是作为NULL特殊处理的,这边也就得到了一个不错的解释。

如要了解剩下的两个项干了什么,请参考这个资料。

 11 #ifndef __BOOT_ASM_H__
 10 #define __BOOT_ASM_H__
  9
  8 /* Assembler macros to create x86 segments */
  7
  6 /* Normal segment */
  5 #define SEG_NULLASM                                             \
  4     .word 0, 0;                                                 \
  3     .byte 0, 0, 0, 0
  2
  1 #define SEG_ASM(type,base,lim)                                  \
12      .word (((lim) >> 12) & 0xffff), ((base) & 0xffff);          \
  1     .byte (((base) >> 16) & 0xff), (0x90 | (type)),             \
  2         (0xC0 | (((lim) >> 28) & 0xf)), (((base) >> 24) & 0xff)
  3
  4
  5 /* Application segment type bits */
  6 #define STA_X       0x8     // Executable segment
  7 #define STA_E       0x4     // Expand down (non-executable segments)
  8 #define STA_C       0x4     // Conforming code segment (executable only)
  9 #define STA_W       0x2     // Writeable (non-executable segments)
 10 #define STA_R       0x2     // Readable (executable segments)
 11 #define STA_A       0x1     // Accessed
 12
 13 #endif /* !__BOOT_ASM_H__ */

我们尝试对SEG_ASM逐项分析:

首先,前16位是Limit的低16位,而输入的lim值是0xffffffff,严格意义上的lim是20 bit,所以需要作12位的右移工作,把多余的12位删掉。base则是同理,但它自然便是32 bit,所以右移便是不需要的了。

之后,第32-39位正好是一个字节,它反映的是Base的第16-23位。下一个字节则是Access Byte,其中按照下面的方式分成六个信息组件,我们已经在前置知识中提过了。这边仅给出表格结构。

Access Byte
7	6	5	4	3	2	1	0
P	DPL	        S	E	DC	RW	A

程序中让type与作0x90或操作,这很显然是把Present位和S位设置成1了。Present位自不必说,S位设置为1表示我们要做的是代码和数据段。

有了这个知识后,底下的几个Define基本是把功能写在了脸上。于是我们可以确定,GDT表中其中允许读和执行的描述符是代码段,而仅允许修改的是数据段。

对于第48-51位,这是Limit的高四位所在地,让它右移一下再与0xf与一下就行了,而52-55位,则是flag,这边设置成了1100这个样子

Flags
3	2	1	0
G	DB	L	Reserved

G: granularity flag, 0->Limit value One Byte. 1->Limit value 4 KiB.
DB: size flag. 1->32-bit protected mode segment. 0 for 16-bit.
L: for 64-bit. skip here.

于是我们清楚了Limit的粒度是4 KiB,这是一个很重要的信息。

对于最高的那个比特,太简单了,不提。

在完成了GDT表的装载之后,下一步是通过使能进入保护模式。在前置知识我们提到,CR0寄存器中有一位用于控制保护模式与否。

哦,对了,这个东西和刚刚的GDT写在一起:我们刚刚建立了GDT表,所以对于代码段和数据段的地址现在也非常清楚了。CR0的控制寄存器只要修改最后一位,就能通知硬件时刻准备好进入保护模式。

之后,只要轻轻一跃,我们就进入了保护模式。

  2 .set PROT_MODE_CSEG,        0x8                     # kernel code segment selector
  1 .set PROT_MODE_DSEG,        0x10                    # kernel data segment selector
10  .set CR0_PE_ON,             0x1                     # protected mode enable flag

 15     movl %cr0, %eax
 16     orl $CR0_PE_ON, %eax
 17     movl %eax, %cr0
 18
 19     # Jump to next instruction, but in 32-bit code segment.
 20     # Switches processor into 32-bit mode.
 21     ljmp $PROT_MODE_CSEG, $protcseg

练习4:

  • bootloader如何读取硬盘扇区的?

  • bootloader是如何加载ELF格式的OS?

我们先给上一节的小尾巴做一个切断。

  7 .code32                                             # Assemble for 32-bit mode
  8 protcseg:
  9     # Set up the protected-mode data segment registers
 10     movw $PROT_MODE_DSEG, %ax                       # Our data segment selector
 11     movw %ax, %ds                                   # -> DS: Data Segment
 12     movw %ax, %es                                   # -> ES: Extra Segment
 13     movw %ax, %fs                                   # -> FS
 14     movw %ax, %gs                                   # -> GS
 15     movw %ax, %ss                                   # -> SS: Stack Segment
 16
 17     # Set up the stack pointer and call into C. The stack region is from 0--start(0x7c00)
 18     movl $0x0, %ebp
 19     movl $start, %esp
 20     call bootmain

# PROT_MODE_DSEG: we have mentioned before. Set it for data segments.
# Stack: set the stack, with ebp->0x0, esp->0x7c00
# After the intialization of stack, call function bootmain.

现在我们进入函数bootmain,该文件位于bootmain.c中。

  3 /* bootmain - the entry of bootloader */
  2 void
  1 bootmain(void) {
88      // read the 1st page off disk
  1     readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
  2
  3     // is this a valid ELF?
  4     if (ELFHDR->e_magic != ELF_MAGIC) {
  5         goto bad;
  6     }
  7

该函数调用了readseg函数,同时检查ELFHDR头是否是合法的,具体是检查魔数。

先看到readseg函数,以及内部的readsect函数。我们在注释中做了更详细的解析工作。

 1 /* *
64   * readseg - read @count bytes at @offset from kernel into virtual address @va,
  1  * might copy more than asked.
  2  * */
  3 static void
  4 readseg(uintptr_t va, uint32_t count, uint32_t offset) {
  5     uintptr_t end_va = va + count;
  6
  7     // round down to sector boundary
  8     va -= offset % SECTSIZE; // when reading, va will start at the beginning of sector.
  9
 10     // translate from bytes to sectors; kernel starts at sector 1
 11     uint32_t secno = (offset / SECTSIZE) + 1; // sector number.
 12
 13     // If this is too slow, we could read lots of sectors at a time.
 14     // We'd write more to memory than asked, but it doesn't matter --
 15     // we load in increasing order.
 16     for (; va < end_va; va += SECTSIZE, secno ++) {
 17         readsect((void *)va, secno); // using readsect to read by sector.
 18     }
 19 }
 
 
  10 /* waitdisk - wait for disk ready */
 11 static void // for 0xC0 and 0x40, I don't know the details. It is also dirty knowledge.
 12 waitdisk(void) {
 13     while ((inb(0x1F7) & 0xC0) != 0x40) // 0x1f7 is the state and order register.
 14         /* inb is an inline-assembly function form of `inb port, data`*/;
 15 }
 
  17 /* readsect - read a single sector at @secno into @dst */
 18 static void
 19 readsect(void *dst, uint32_t secno) {
 20     // wait for disk to be ready
 21     waitdisk();
 22     // outb: load byte into ports. Secno is the abstract address in LEA.
 23     outb(0x1F2, 1);                         // count = 1. One sector every time.
 24     outb(0x1F3, secno & 0xFF);              // LEA: low 8
 25     outb(0x1F4, (secno >> 8) & 0xFF);       // LEA: mid 8
 26     outb(0x1F5, (secno >> 16) & 0xFF);      // LEA: high 8
 27     outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0); // LEA: 1110 [lba high 4].
 28     outb(0x1F7, 0x20);                      // cmd 0x20 - read sectors
 29
 30     // wait for disk to be ready
 31     waitdisk();
 32
 33     // read a sector
 34     insl(0x1F0, dst, SECTSIZE / 4);
 35 }

特别的,我们要注意按照先前makefile的解析,第二个扇区是kernel,第一个扇区是bootloader,因此,在计算secno之时,sector number会有一个加一。

readseg函数把从第二个扇区开始的内核ELF文件加载到地址0x10000处,并做相关的解析工作。查看源码如下:

96      struct proghdr *ph, *eph;
  1
  2     // load each program segment (ignores ph flags)
  3     ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
  4     eph = ph + ELFHDR->e_phnum;
  5     for (; ph < eph; ph ++) {
  6         readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
  7     }
  8
  9     // call the entry point from the ELF header
 10     // note: does not return
 11     ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();

对于ELF的格式,其实是很大的一个篇幅,不过我们这边简单看一下WIKI是怎么介绍的。

可以看到作为头结构的其实是两个文件,因此于lib/elf.h中的内容对应上了。在wiki上提到,为了能够把相关的代码段完整的装载进来,需要找到代码头表格的每一项的地址,并把他们拉进来。

因此,我们看到了前述代码的for loop + readseg将代码读进来,最后还进入ELF的起始运行地址entry,从而运行内核源码这一整个流程。

然而,针对这里的0xffffff我并没有太多的想法,如果你对这个东东有更多的理解,请联系我的邮箱:songlinke18@mails.ucas.ac.cn

PreviousProject 5: ucore lab1NextExercise-Coding

Last updated 2 years ago

ELF结构
Global Descriptor Table - OSDev Wiki
GDT项资料
Logo
关于A20 Gate - whowin的日志 - 网易博客
A20 relative info
MBR结构
GDT Info
ELF info