操作系统实验报告 lab1

操作系统实验报告 lab1操作系统实验报告 lab1


练习1

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

利用make V= 查看执行了那些命令

生成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)

输出如下图

![enter description here][1]

由上描述可以看出,首先先创建一个大小为10000字节的块,然后再将bootblock,kernel拷贝过去。然而生成ucore.img需要先生成kernel和bootblock

1.生成bootblock的相关代码如下
$(bootblock): $(call toobj,$(bootfiles)) | $(call totarget,sign) @echo "========================$(call toobj,$(bootfiles))" @echo + ld $@ $(V)$(LD) $(LDFLAGS) -N -e start -Ttext 0x7C00 $^ -o $(call toobj,bootblock) @$(OBJDUMP) -S $(call objfile,bootblock) > $(call asmfile,bootblock) @$(OBJCOPY) -S -O binary $(call objfile,bootblock) $(call outfile,bootblock) @$(call totarget,sign) $(call outfile,bootblock) $(bootblock)

这里写图片描述

  1. 生成bootasm.o、bootmain.o、sign的相关代码为:

![enter description here][3]

kernel = $(call totarget,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) $(call create_target,kernel)

查看命令,生成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

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

查看sign.c源代码

buf[510] = 0x55; buf[511] = 0xAA; FILE *ofp = fopen(argv[2], "wb+"); size = fwrite(buf, 1, 512, ofp); if (size != 512) { fprintf(stderr, "write '%s' error, size is %d.\n", argv[2], size); return -1; }

从上述代码可以看出,要求硬盘主引导扇区的大小是512字节,还需要第510个字节是0x55,第511个字节为0xAA,也就是说扇区的最后两个字节内容是0x55AA


练习2

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

1 修改 lab1/tools/gdbinit,内容为:

set architecture i8086 target remote :1234

这里写图片描述

 x /2i $pc //显示当前eip处的汇编指令

这里写图片描述

 set architecture i8086 //设置当前调试的CPU是8086 b *0x7c00 //在0x7c00处设置断点。此地址是bootloader入口点地址,可看boot/bootasm.S的start地址处 c //continue简称,表示继续执行 x /2i $pc //显示当前eip处的汇编指令 set architecture i386 //设置当前调试的CPU是80386

这里写图片描述

所以断点正常

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

在tools/gdbinit结尾加上

 b *0x7c00 c x /10i $pc

在0x7c00处break,然后使用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, %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.

bootblock.asm

start: .code16 # Assemble for 16-bit mode cli # Disable interrupts 7c00: fa cli cld # String operations increment 7c01: fc cld # Set up the important data segment registers (DS, ES, SS). xorw %ax, %ax # Segment number zero 7c02: 31 c0 xor %eax,%eax movw %ax, %ds # -> Data Segment 7c04: 8e d8 mov %eax,%ds movw %ax, %es # -> Extra Segment 7c06: 8e c0 mov %eax,%es movw %ax, %ss # -> Stack Segment 7c08: 8e d0 mov %eax,%ss 

观察发现他们相同


练习3

从bootasm.s查看代码(在这里分析bootblock.asm也可以,二者源码相同),并分析过程

宏定义

.set PROT_MODE_CSEG, 0x8 #内核代码段选择子  .set PROT_MODE_DSEG, 0x10 #内核数据段选择子  .set CR0_PE_ON, 0x1 #保护模式使能标志 

1.关闭中断,将各个段寄存器重置

#CPU刚启动为16位模式  cli # 关中断 cld # 清方向标志  xorw %ax, %ax # 置零 movw %ax, %ds # -> 数据段寄存器 movw %ax, %es # -> 附加段寄存器 movw %ax, %ss # -> 堆栈段寄存器

2 .开启A20

seta20.1: inb $0x64, %al # 从0x64端口读入一个字节的数据到al中  testb $0x2, %al # test指令可以当作and指令,只不过它不会影响操作数  jnz seta20.1#如果上面的测试中发现al的第2位为0,就不执行该指令  否则就循环检查 movb $0xd1, %al # 将0xd1写入到al中  outb %al,$0x64 #将al中的数据写入到端口0x64中  seta20.2: inb $0x64, %al testb $0x2, %al jnz seta20.2 movb $0xdf, %al # 通过0x60写入数据 即将A20置1 outb %al, $0x60

初始化GDT表:一个简单的GDT表和其描述符已经静态储存在引导区中,载入即可

 lgdt gdtdesc #将全局描述符表描述符加载到全局描述符表寄存器 

进入保护模式:通过将cr0寄存器PE位置1便开启了保护模式

cr0中的第0位为1表示处于保护模式 cr0中的第0位为0,表示处于实模式 把控制寄存器cr0加载到eax中 movl %cr0, %eax orl $CR0_PE_ON, %eax movl %eax, %cr0

通过长跳转更新cs的基地址

ljmp $PROT_MODE_CSEG, $protcseg .code32 protcseg:

设置段寄存器,并建立堆栈

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

转到保护模式完成,进入boot主方法

call bootmain

练习4

bootmain 代码

bootmain(void) { readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0); if (ELFHDR->e_magic != ELF_MAGIC) { goto bad; } struct proghdr *ph, *eph; 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); while (1); }

readsect从设备的第secno扇区读取数据到dst位置

 static void readsect(void *dst, uint32_t secno) { waitdisk(); outb(0x1F2, 1); // 设置读取扇区的数目为1 outb(0x1F3, secno & 0xFF); outb(0x1F4, (secno >> 8) & 0xFF); outb(0x1F5, (secno >> 16) & 0xFF); outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0); // 上面四条指令联合制定了扇区号 // 在这4个字节线联合构成的32位参数中 // 29-31位强制设为1 // 28位(=0)表示访问"Disk 0" // 0-27位是28位的偏移量 outb(0x1F7, 0x20); // 0x20命令,读取扇区 waitdisk(); insl(0x1F0, dst, SECTSIZE / 4); // 读取到dst位置, // 幻数4因为这里以DW为单位 }

加载ELF文件

bootmain(void) { .......... 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); ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();//根据ELF头表中的入口信息,找到内核的入口并开始运行 bad: .......... }

练习5

什么是函数栈?

当函数被调用时,首先会把函数的参数依次入栈(这里指的是堆栈传参,当然也可以用寄存器传参)调用函数的栈底压栈到自己函数的栈中(push bp),然后将原来函数栈顶sp作为当前函数的栈底(mov bp,sp)。函数运行完成时,会将压入栈中的bp重新出栈到bp中(pop bp)。同时将计算的结果保存在寄存器中,返回原界面。

那么我们可以粗浅的建立一个栈模型

ss:[ebp-8] ;变量2 ss:[ebp-4] ;变量1 ss:[ebp] ;栈针 ss:[ebp+4] ;返回地址 ss:[ebp+8] ;第一个参数

函数实现

read_ebp()和read_eip()函数来获取当前ebp寄存器和eip 寄存器的信息。

查看print_stackframe函数注释

 /* 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] */
 for(i = 0; ebp!=0 && i < STACKFRAME_DEPTH; i++) { 
       //STACKFRAME_DEPTH = 20 一直向上循环找到所有的调用函数为止,一开始没有判断栈针为空的条件 cprintf("ebp:0x%08x eip:0x%08x ",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);//上一个函数的栈针  //循环直到没有调用函数停止 }

这里写图片描述

加了ebp!=0


练习6

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

这里写图片描述

其中015和4863分别为offset的低16位和高16位,16~31位是段选择子,通过段选择子得到段基址,再加上段内偏移量就可以得到中断处理代码的入口。

2.请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init

 extern uintptr_t __vectors[];//声明__vertors[] You can use "extern uintptr_t __vectors[];" to define this extern variable which will be used later. int i; for(i=0;i<256;i++) { SETGATE(idt[i],0,GD_KTEXT,__vectors[i],DPL_KERNEL);//对整个idt数组进行初始化 } SETGATE(idt[T_SWITCH_TOK],0,GD_KTEXT,__vectors[T_SWITCH_TOK],DPL_USER);//在这里先把所有的中断都初始化为内核级的中断 lidt(&idt_pd);//使用lidt指令加载中断描述符表 just google it! and check the libs/x86.h to know more.利用google找到了相关函数 } /* 传入的第一个参数gate是中断的描述符表 传入的第二个参数istrap用来判断是中断还是trap 传入的第三个参数sel的作用是进行段的选择 传入的第四个参数off表示偏移 传入的第五个参数dpl表示这个中断的优先级 */

3.程完善trap.c中的中断处理函数trap在对时钟中断进行处理的部分填写trap函数中处理时钟中断的部分,使操作系统每遇到100次时钟中断后,调用 print_ticks子程序,向屏幕上打印一行文字100 ticks

实验代码填写

 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 * (2) Every TICK_NUM cycle, you can print some info using a funciton, such as print_ticks(). * (3) Too Simple? Yes, I think so! */

代码如下:

ticks++; if(ticks%TICK_NUM == 0)//每次时钟中断之后ticks就会加一 当加到TICK_NUM次数时 打印并重新开始 print_ticks();//前面有定义 打印字符串

实验截图如下:

这里写图片描述

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/217005.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月18日 上午10:33
下一篇 2026年3月18日 上午10:34


相关推荐

  • 中标麒麟V6虚拟机安装和网络配置

    中标麒麟V6虚拟机安装和网络配置windows 中使用 VMware10 安装中标麒麟 V6linux 版本选用 linux2 6 x 内核 64 位 虚拟机的网络设置选用 桥接模式 不勾选 复制物理网络连接状态 安装过程中没有配置网络 安装完毕在图文界面编辑网络 添加 eth0 配置 IP 地址 子网掩码 网关和 DNS 勾选 自动连接 后网络连接成功 自动创建 etc sysconfig network scripts ifcfg

    2026年3月26日
    2
  • mysql第一二三范式_第一范式、第二范式、第三范式[通俗易懂]

    mysql第一二三范式_第一范式、第二范式、第三范式[通俗易懂]第一范式、第二范式、第三范式第一范式如果一个关系模式R的所有属性都是不可分的基本数据项,则R1NF(即R符合第一范式)。两点:一、每个字段都只能存放单一值课程有两个值,不符合第一范式,可改为如下二、每笔记录都要能利用一个惟一的主键来加以识别第一范式、第二范式、第三范式第一范式如果一个关系模式R的所有属性都是不可分的基本数据项,则R∈1NF(即R符合第一范式)。两点:一、每个字段都只能存…

    2022年5月23日
    45
  • nodejs安装淘宝镜像(配置淘宝镜像)

    强烈推荐30个原生JavaScript的demo,包括canvas时钟特效、自定义视频播放器、搜索栏快速匹配、fetch访问资源、console调试技巧等,先fork后学习,详见点击打开链接,欢迎点赞~~~谢谢,共同进步学习!将npm的注册表源设置为国内的镜像1、国内用户,建议将npm的注册表源设置为国内的镜像,可以大幅提升安装速度2、国内优秀npm镜像推荐及使用:http://rin…

    2022年4月18日
    945
  • [UVALive 6663 Count the Regions] (dfs + 离散化)[通俗易懂]

    [UVALive 6663 Count the Regions] (dfs + 离散化)

    2022年1月23日
    56
  • JMS activeMQ

    JMS activeMQ

    2022年3月3日
    37
  • arp命令详解

    arp命令详解显示和修改 地址解析协议 ARP 缓存中的项目 ARP nbsp 缓存中包含一个或多个表 它们用于存储 IP 地址及其经过解析的以太网或令牌环物理地址 计算机上安装的每一个以太网或令牌环网络适配器都有自己单独的表 如果在没有参数的情况下使用 则 nbsp arp nbsp 命令将显示帮助信息 nbsp 语法 nbsp arp a InetAddr NIfaceAddr g InetAddr NIfaceAdd

    2026年3月19日
    2

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号