ucore-lab1实验报告

练习一:

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

+ cc kern/init/init.c  //编译 init.c

+ cc kern/libs/readline.c //编译 readline.c

+ cc kern/libs/stdio.c //编译 stdio.c

+ cc kern/debug/kdebug.c//编译 kdebug.c 

+ cc kern/debug/kmonitor.c //编译 kmonitor

+ cc kern/debug/panic.c//编译 panic.c

+ cc kern/driver/clock.c //编译 clock.c

+ cc kern/driver/console.c //编译 console.c

+ cc kern/driver/intr.c//编译 intr.c

+ cc kern/driver/picirq.c //编译 picirq.c

+ cc kern/trap/trap.c //编译 trap.c

+ cc kern/trap/trapentry.S //编译 trapentry.S

+ cc kern/trap/vectors.S //编译 vector.S

+ cc kern/mm/pmm.c//编译 pmm.c

+ cc libs/printfmt.c // printgmt.c

+ cc libs/string.c //编译 string.c

+ ld bin/kernel//接下来用ld合并目标文件(object) 和 库文件(archive),生成kernel程序

+ cc boot/bootasm.S //编译 bootasm.S

+ cc boot/bootmain.c //编译 bootmain.c

+ cc tools/sign.c //编译 sign.c

+ ld bin/bootblock//接下来连接源文件与目标文件,生成bootblock程序

//最后将bootloader放入虚拟硬盘ucore.img中去。

dd if=/dev/zero of=bin/ucore.img count=10000

dd if=bin/bootblock of=bin/ucore.img conv=notrunc

dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc

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

tools/sign.c中的报错检查条件易知

大小必须为512字节并且以55AA标志结尾

练习二:使用qemu执行并调试lab1中的软件。

make lab1-mon                     //Makefile中设置调试

target remote localhost:1234   //qemugdb之间使用网络端口1234进行通讯

 


define hook-stop//强制反汇编x/i $pcend

next 单步到程序源代码的下一行,不进入函数。nexti 单步一条机器指令,不进入函数。step 单步到下一个不同的源代码行(包括进入函数)。stepi 单步一条机器指令。

练习三:分析bootloader进入保护模式的过程。

Bootasm.S代码段分析:

cli                       # (在16位下)关闭中断

cld                       # 设置字符串操作是递增方向

设置重要数据段计数器(DS, ES, SS).

xorw %ax, %ax           # eax寄存器(16位)设置为0

movw %ax, %ds           # -> 数据段寄存器

movw %ax, %es           # -> 扩展段寄存器

movw %ax, %ss           # -> 栈段寄存器

A20使能(打开A20地址线):

为了兼容早期的PC机,第20根地址线在实模式下不能使用  

#所以超过1MB的地址,默认就会返回到地址0,重新从0循环计数#下面的代码打开A20地址线  

seta20.1:

inb $0x64, %al   #0x64端口读入一个字节的数据到al

al寄存器为eax寄存器的低8位)

testb $0x2, %al  #test指令可以当作and指令,只不过它不会影响操作数  

jnz seta20.1     #如果上面的测试中发现al的第2位为0(代表键盘缓冲区为空),就不执行该指令  否则就循环检查

 

movb $0xd1, %al        #0xd1(edx8位)写入到al中 

outb %al, $0x64        # al中的数据写入到端口0x64

seta20.2:

inb $0x64, %al          # 0x64端口读取一个字节的数据到al中 

testb $0x2, %al        #测试al的第2位是否为0  

jnz seta20.2            #如果上面的测试中发现al的第2位为0,就不执行该指令  否则就循环检查  

movb $0xdf, %al        # 0xdf写入到al中 

outb %al, $0x60        # al中的数据写入到0x60端口中  将全局描述符表描述符加载到全局描述符表寄存器 

# cr0中的第0位为1表示处于保护模式 

# cr0中的第0位为0,表示处于实模式 

lgdt gdtdesc

movl %cr0, %eax         #把控制寄存器cr0加载到eax中  

orl $CR0_PE_ON, %eax   #eax中的第0位设置为1  

movl %eax, %cr0         #eax中的值装入cr0中  

跳转到32位模式中的下一条指令  

将处理器切换为32位工作模式 

#下面这条指令执行的结果会将$PROT_MODE_CSEG加载到cs中,cs对应的高速缓冲存储器会加载代码段描述符,同样将$protcseg加载到ip中  

ljmp $PROT_MODE_CSEG,$protcseg//PROT_MODE_CASE=0x8

gdt:

SEG_NULLASM                            # nullSEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # 代码段描述符  

SEG_ASM(STA_W, 0x0, 0xffffffff)    # 数据段描述符  

Bootload的启动过程可以概括如下:

首先,BIOS从主加载分区中物理地址为0x7c00的位置加载以下代码并开始执行实模式代码,段寄存器cs值为0,ip值为7c00。

    CLI屏蔽中断CLD使DF复位,即DF=0,串操作方向控制

设置寄存器 ax,ds,es,ss寄存器值清0;地址线20被封锁,高于1MB的地址都默认回卷到0。怎么激活A20呢,由于历史原因A20地址位由键盘控制器芯片8042管理。所以要给8042发命令激活A20

8042有两个IO端口:0x60和0x64, 激活流程位: 发送0xd1命令到0x64端口 --> 发送0xdf到0x60

    从实模式转换到保护模式,用到了全局描述符表和段表,使得虚拟地址和物理地址匹配,保证转换时有效的内存映射不改变;lgdt汇编指令把GDTR描述符表的大小和起始位置存入gdtr寄存器中将CR0的最后一位设置为1,进入保护模式指令跳转由代码段跳到protcseg的起始位置

设置保护模式下数据段寄存器;设置堆栈寄存器并调用main函数;对GDT作处理。

练习:分析bootloader加载ELF格式的OS的过程

 

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




void bootmain(void) {

// 从硬盘读取第一页(读到内存的位置,大小,ELF文件偏移)

readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);  

// 通过魔数判断是否为合法的ELF文件

if (ELFHDR->e_magic != ELF_MAGIC) {

goto bad;

}    

#加载每个程序头表中的段 (ignores ph flags)

//定义两个程序头表段                                       

struct proghdr *ph, *eph;

//ph表示ELF段表首地址;eph表示ELF段表末地址

ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);

eph = ph + ELFHDR->e_phnum;

//循环读每个段

for (; ph < eph; ph ++) {

readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);

}

 

// 调用头表中的内核入口地址实现内核链接地址转化为加载地址,无返回值

((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();

bad:                           //出现问题时的处理

outw(0x8A00, 0x8A00);

outw(0x8A00, 0x8E00);

/* do nothing */

while (1);

}

2、bootloader如何读取硬盘扇区的?

//检查硬盘是否就绪(检查0x1F7的最高两位,如果是01,则跳出循环;否则等待

static voiwaitdisk(void) {

while ((inb(0x1F7) & 0xC0) != 0x40)

/* do nothing */;

}

static void readsect(void *dst, uint32_t secno) {

// 等待磁盘准备就绪

  waitdisk();

  outb(0x1F2, 1); #count = 1   #读取一个扇区

  outb(0x1F3, secno & 0xFF);        #要读取的扇区编号

  outb(0x1F4, (secno >> 8) & 0xFF);  #用来存放读写柱面的低 8位字节  

  outb(0x1F5, (secno >> 16) & 0xFF);  #用来存放读写柱面的高 2位字节
  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);        //获取数据

}

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

完成函数print_stackframe的实现,观察输出,并解释最后一行各个数值的含义。

参考源代码如下
void print_stackframe(void) {

 uint32_t ebp = read_ebp();

    uint32_t eip = read_eip();

    int i, j;

for (i = 0; ebp != 0 && i < STACKFRAME_DEPTH;i++){

//打印当前ebp和eip的地址

        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];//eip为压到栈中的eip地址的内容

        ebp = ((uint32_t *)ebp)[0];//ebp为压入栈中的ebp所在地址的内容

    }

}

运行结果如下图:


分析最后一行为:

<unknow>: -- 0x00007d62 --

 显示的信息是“文件名、文件行数函数名称函数入口偏移量”

汇编语言在调用C函数的时候,先将参数按照倒序压到栈里,然后压入返回地址,即call语句的下一条指令的地址,然后将ebp的值压入栈中,之后将esp的值赋给ebp,然后再调整eip的值为函数入口地址。


练习六:

1、中断向量表中一个表项占用多少字节,其中哪几位代表中断处理代码的入口?

    中断向量表中一个表项struct gatedesc {}将bit位相加共64bit为8字节其第31-16位是段选择子,第63-48位和第15-0位分别是偏移量的高16位和低16位。通过段选择子和段偏移量,就可以找到中断处理代码入口。

2、完成初始化函数idt_init

查看mmu.h中的SETGATE宏

#define SETGATE(gate, istrap, sel, off, dpl) 

主要使用这个宏进行段选择符的构造

gate:为相应的idt数组内容

istrap:系统段设置为1,中断门设置为0

sel:段选择子

off:为__vectors数组内容

dpl:设置优先级

//保存在vectors.S中的256个中断处理例程的入口地址数组

extern long __vectors[];

int i;

//在中断门描述符表中通过建立中断门描述符,其中存储了中断处理例程的代码段GD_KTEXT和偏移量__vectors[i],特权级为DPL_KERNEL。这样通过查询idt[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);

//建立好中断门描述符表后,通过指令lidt把中断门描述符表的起始地址装入IDTR寄存器中,从而完成中段描述符表的初始化工作。

    lidt(&idt_pd);

}

3、完成中断处理函数trap();

设置时钟进行操作  
     case IRQ_OFFSET + IRQ_TIMER:
         ticks ++;          //一次中断累加1

             if (ticks % TICK_NUM == 0) { 

               print_ticks();

           }

        break;


看到了吗?你没有看错,时钟中断成功通过屏幕展示出来了,哈哈,还是比较简单的。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值