1、操作系统镜像文件ucore.img是如何一步一步生成的?(需要比较详细地解释Makefile中每一条相关命令和命令参数的含义,以及说明命令导致的结果)
这个练习只是要大家熟悉makefile以及make,不必完整的掌握makefile里面的源码,只需要知道那些命令是什么意思就够了,了解就好。这东西要你记也记不住,学习makefile和linux系统命令一样,不是一朝一夕久能学好的,需要时间的累积和经验的积累。
下面来进行分析 首先我们查看lab1下的Makefile文件 找到create ucrore.image 大概在169行 看172行 用到了两个块分别叫做kernel和bootblock,我们先展示不管这两个块的内容是什么,假设我们已经有这两个块了,先直接分析下面的代码:
dd是一条linux命令指令: dd:用指定大小的块拷贝一个文件,并在拷贝的同时进行指定的转换。
if=文件名:输入文件名,缺省为标准输入。即指定源文件。< if=input file > of=文件名:输出文件名,缺省为标准输出。即指定目的文件。< of=output file > count=blocks:仅拷贝blocks个块,块大小等于ibs指定的字节数。 conv=conversion:用指定的参数转换文件。 conv=notrunc:不截短输出文件
这里的意思就是说先创建一个大小为10000字节的内存块儿,然后再将bootblock和kernel拷贝过去
然后我们再去看看kernel和bootblock的内容
在终端中执行输入 make V= 会看到ucore.image 的编译内容 正是我们之前说的先创建一个大小为10000字节的内存块儿,然后再将bootblock和kernel拷贝过去
2、一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?
查看lab1/tools/sign.c源代码
由31,32行代码看出、符合规范的硬盘主引导扇区的大小为512个字节,且最后两个字节为0x55、0xAA。
一、单步跟踪BIOS的执行。
1.首先修改 lab1/tools/gdbinit,内容为:
set architecture i8086 target remote :1234意思是与qemu建立连接 2.在 lab1目录下,执行make debug
3.然后在gdb进行单步调试
4.gdb界面下,可通过如下命令来看BIOS的代码
x /2i $pc二、在初始化位置0x7c00设置实地址断点,测试断点正常。 在tools/gdbinit结尾加上
b *0x7c00 //在0x7c00处设置断点。此地址是bootloader入口点地址,可看boot/bootasm.S的start地址处 c //continue简称,表示继续执行 x /5i $pc //显示当前eip处的汇编指令 set architecture i386 //设置当前调试的CPU是80386在lab1目录下,运行make debug,可发现gdb打印出 the target architecture is assumed to be i386。
三、从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和bootblock.asm进行比较 1.在tools/gdbinit结尾加上
b *0x7c00 c x /10i $pc2.在0x7c00会进行break,然后使用si和x/i $pc 进行单步调试。
si指定 进行单步调试 x/i $pc 将当前指令进行反汇编 0x00007c01 in ?? () (gdb) x/i $pc => 0x7c01: cld (gdb) si 0x00007c02 in ?? () (gdb) x/i $pc => 0x7c02: xor %eax,%eax (gdb) si 0x00007c04 in ?? () (gdb) x/i $pc => 0x7c04: mov %eax,%ds (gdb)bootblock.S 中的代码为:
.code16 # Assemble for 16-bit mode cli # Disable interrupts cld # String operations increment #Set up the important data segment registers (DS, ES, SS). xorw %ax, %ax # Segment number zero movw %ax, %ds # -> Data Segment movw %ax, %es # -> Extra Segment movw %ax, %ssbootblock.asm
start: .code16 cli #7c00: fa cli cld #7c01: fc cld xorw %ax, %ax #7c02: 31 c0 xor %eax,%eax movw %ax, %ds #7c04: 8e d8 mov %eax,%ds movw %ax, %es #7c06: 8e c0 mov %eax,%es movw %ax, %ss #7c08: 8e d0 mov %eax,%ss3.对比发现,上述反汇编指令与两个汇编文件相同。
1、关闭中断,将各个段寄存器重置 它先将各个寄存器置0
2、开启A20 然后就是将A20置1,这里简单解释一下A20,当 A20 地址线控制禁止时,则程序就像在 8086 中运行,1MB 以上的地是不可访问的。而在保护模式下 A20 地址线控制是要打开的,所以需要通过将键盘控制器上的A20线置于高电位,使得全部32条地址线可用。
3、加载GDT表
4、将CR0的第0位置1
5、长跳转到32位代码段,重装CS和EIP
6、重装DS、ES等段寄存器等
7、转到保护模式完成,进入boot主方法
首先要看bootmain.c的源码 ELF就是一个可执行文件的意思 代码分析 88L 读取第一个扇区 91L 判断是否是一个有效的elf 97L-103L 加载接下来的扇区 106L 利用强转为函数指针调用函数 一、bootloader如何读取硬盘扇区的? 查看 readsect函数 源码 各地址代表的寄存器意义如下:
0x1f0 读数据,当0x1f7不为忙状态时,可以读。 0x1F3 R/W,数据寄存器 0x1F2 R/W,扇区数寄存器,记录操作的扇区数 0x1F3 R/W,扇区号寄存器,记录操作的起始扇区号 0x1F4 R/W,柱面号寄存器,记录柱面号的低 8 位 0x1F5 R/W,柱面号寄存器,记录柱面号的高 8 位 0x1F6 R/W,驱动器/磁头寄存器,记录操作的磁头号,驱动器号,和寻道方式,前 4 位代表逻辑扇区号的高 4 位,DRV = 0/1 代表主/从驱动器,LBA = 0/1 代表 CHS/LBA 方式 0x1F7 R,状态寄存器,第 6、7 位分别代表驱动器准备好,驱动器忙 0x1F8 W,命令寄存器,0x20 命令代表读取扇区 代码分析
48L 设置读取扇区的数目为1 49L-52L 在这4个字节线联合构成的32位参数中,29-31位强制设为1,28位(=0)表示访问"Disk 0",0-27位是28位的偏移量 53L 0x20 命令 读取扇区 59L 从0x1F0读取SECTSIZE字节数到dst的位置,每次读四个字节,读取 SECTSIZE/ 4次。
从outb()可以看出这里是用LBA模式的PIO(Program IO)方式来访问硬盘的。从磁盘IO地址和对应功能表可以看出,该函数一次只读取一个扇区。 二、bootloader是如何加载ELF格式的OS的? 也就是bootmain函数内容 看上文的bootmain中elfhdr、proghdr相关的信息。 下面是关于elfhdr和proghdr的相关信息: #elfhdr:目标文件头 struct elfhdr { uint32_t e_magic; // must equal ELF_MAGIC uint8_t e_elf[12]; // 12 字节,每字节对应意义如下: // 0 : 1 = 32 位程序;2 = 64 位程序 // 1 : 数据编码方式,0 = 无效;1 = 小端模式;2 = 大端模式 // 2 : 只是版本,固定为 0x1 // 3 : 目标操作系统架构 // 4 : 目标操作系统版本 // 5 ~ 11 : 固定为 0 uint16_t e_type; // 1=relocatable, 2=executable, 3=shared object, 4=core image uint16_t e_machine; // 3=x86, 4=68K, etc. uint32_t e_version; // file version, always 1 uint32_t e_entry; // 程序入口地址 or 0 uint32_t e_phoff; // 程序段表头相对elfhdr偏移位置 uint32_t e_shoff; // 节头表相对elfhdr偏移量 uint32_t e_flags; // 处理器特定标志, usually 0 uint16_t e_ehsize; // size of this elf header uint16_t e_phentsize; // 程序头部长度 uint16_t e_phnum; // 段个数 uint16_t e_shentsize; // 节头部长度 uint16_t e_shnum; // 节头部个数 uint16_t e_shstrndx; // 节头部字符索引 }; struct proghdr { uint type; // 段类型 // 1 PT_LOAD : 可载入的段 // 2 PT_DYNAMIC : 动态链接信息 // 3 PT_INTERP : // 指定要作为解释程序调用的以空字符结尾的路径名的位置和大小 // 4 PT_NOTE : 指定辅助信息的位置和大小 // 5 PT_SHLIB : 保留类型,但具有未指定的语义 // 6 PT_PHDR : 指定程序头表在文件及程序内存映像中的位置和大小 // 7 PT_TLS : 指定线程局部存储模板 uint offset; // 段相对文件头的偏移值 uint va; // 段的第一个字节将被放到内存中的虚拟地址 uint pa; //段的第一个字节在内存中的物理地址 uint filesz; //段在文件中的长度 uint memsz; // 段在内存映像中占用的字节数 uint flags; //可读可写可执行标志位。 uint align; //段在文件及内存的对齐方式 };
根据函数堆栈的相关知识,我们可以直接写代码,过程中我多次参考了答案,最后写出的代码是这样的。 运行结果如下。 由此可知,结果正确。
一、中断描述符表(也可简称为保护模式下的中断向量表)中一个表项占多少字节?其中哪几位代表中断处理代码的入口?
一个表项占八个字节。入口地址为:gd_off_31_16 << 16 + gd_off_15_0 ;
struct gatedesc { unsigned gd_off_15_0 : 16; // 低16位为段内偏移 unsigned gd_ss : 16; // 段选择子占16位 unsigned gd_rsv1 : 3; // reserved(should be zero I guess) unsigned gd_type : 4; // type(STS_{TG,IG32,TG32}) interrupt or trap unsigned gd_s : 1; // must be 0 (system) unsigned gd_dpl : 2; // descriptor(meaning new) privilege level unsigned gd_p : 1; // Present unsigned gd_off_31_16 : 16; // 作为高16位的段内偏移。 };二、请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init。在idt_init函数中,依次对所有中断入口进行初始化。使用mmu.h中的SETGATE宏,填充idt数组内容。每个中断的入口由tools/vectors.c生成,使用trap.c中声明的vectors数组即可。
我的代码:
extern uintptr_t __vectors[]; int i; //初始化idt for(i=0;i<256;i++) { SETGATE(idt[i],0,GD_KTEXT,__vectors[i],DPL_KERNEL); } SETGATE(idt[T_SWITCH_TOK],0,GD_KTEXT,__vectors[T_SWITCH_TOK],DPL_USER); SETGATE(idt[T_SWITCH_TOU],0,GD_KTEXT,__vectors[T_SWITCH_TOU],DPL_KERNEL); lidt(&idt_pd);首先引入中断处理函数的入口地址__vectors[],这个变量在vector.s里面生成的 ,然后初始化idt中断描述符表,最后根据提示用lidt函数告知cpu IDT表的位置。
1.写完后,发现这中断向量表的代码贼短,其实里面涉及到的东西不少。
第一句,声明一个vectors数组,这个vector在vector.S里面定义的,意思是通过这个指针可以跳转到该中断处理的地点。
2.setgate这个函数的作用是设置正确的interrupt/trap gate 描述符。接下来,我来介绍一下该函数的参数。
//gate: Gate descriptors for interrupts and traps //istrap : 0 is interrupts ,else is traps //sel : the off's segment ,usual kernel text,the value is GD_KTEXT //off : offset in segment ,函数入口地址。 //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; \ } /* Gate descriptors for interrupts and traps */ struct gatedesc { unsigned gd_off_15_0 : 16; // low 16 bits of offset in segment unsigned gd_ss : 16; // segment selector unsigned gd_args : 5; // # args, 0 for interrupt/trap gates unsigned gd_rsv1 : 3; // reserved(should be zero I guess) unsigned gd_type : 4; // type(STS_{TG,IG32,TG32}) unsigned gd_s : 1; // must be 0 (system) unsigned gd_dpl : 2; // descriptor(meaning new) privilege level unsigned gd_p : 1; // Present unsigned gd_off_31_16 : 16; // high bits of offset in segment };3.注意需要对T_SWITCH_TOK的发生时机是在用户空间的,所以对应的dpl需要修改为DPL_USER。
4.lidt将idt的首地址和size装进idtr寄存器。