lab1的实验练习答案

# Lab1 report

## [练习1]

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

bin/tos.img

| 生成tos.img的相关代码为

| (TOSIMG):(kernel) (bootblock)|(V)dd if=/dev/zero of=@count=10000|(V)dd if=(bootblock)of=@ conv=notrunc

| (V)ddif=(kernel) of=@seek=1conv=notrunc||为了生成tos.img,首先需要生成bootblock、kernel||>bin/bootblock||生成bootblock的相关代码为||(bootblock): (calltoobj,(bootfiles)) | (calltotarget,sign)||@echo+ld@

| | (V)(LD) (LDFLAGS)?N?estart?Ttext0x7C00^ \

| | -o (calltoobj,bootblock)||@(OBJDUMP) -S (callobjfile,bootblock)> ||(call asmfile,bootblock)

| | @(OBJCOPY)?S?Obinary(call objfile,bootblock) \

| | (calloutfile,bootblock)||@(call totarget,sign) (calloutfile,bootblock)(bootblock)

| |

| | 为了生成bootblock,首先需要生成bootasm.o、bootmain.o、sign

| |

| |> obj/boot/bootasm.o, obj/boot/bootmain.o

| | | 生成bootasm.o,bootmain.o的相关makefile代码为

| | | bootfiles = (calllistfcc,boot)|||(foreach f,(bootfiles),(call cc_compile,(f),(CC),\

| | | $(CFLAGS) -Os -nostdinc))

| | | 实际代码由宏批量生成

| | |

| | | 生成bootasm.o需要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

| | | 其中关键的参数为

| | | -ggdb 生成可供gdb使用的调试信息。这样才能用qemu+gdb来调试bootloader or tos。

| | | -m32 生成适用于32位环境的代码。我们用的模拟硬件是32bit的80386,所以tos也要是32位的软件。

| | | -gstabs 生成stabs格式的调试信息。这样要tos的monitor可以显示出便于开发者阅读的函数调用栈信息

| | | -nostdinc 不使用标准库。标准库是给应用程序用的,我们是编译tos内核,OS内核是提供服务的,所以所有的服务要自给自足。

| | | -fno-stack-protector 不生成用于检测缓冲区溢出的代码。这是for 应用程序的,我们是编译内核,tos内核好像还用不到此功能。

| | | -Os 为减小代码大小而进行优化。根据硬件spec,主引导扇区只有512字节,我们写的简单bootloader的最终大小不能大于510字节。

| | | -I

添加搜索头文件的路径

| | |

| | | 生成bootmain.o需要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

| | | 新出现的关键参数有

| | | -fno-builtin 除非用_builtin前缀,

| | | 否则不进行builtin函数的优化

| |

| |> bin/sign

| | | 生成sign工具的makefile代码为

| | | (calladdfileshost,tools/sign.c,sign,sign)|||(call create_target_host,sign,sign)

| | |

| | | 实际命令为

| | | gcc -Itools/ -g -Wall -O2 -c tools/sign.c \

| | | -o obj/sign/tools/sign.o

| | | gcc -g -Wall -O2 obj/sign/tools/sign.o -o bin/sign

| |

| | 首先生成bootblock.o

| | ld -m elf_i386 -nostdlib -N -e start -Ttext 0x7C00 \

| | obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o

| | 其中关键的参数为

| | -m 模拟为i386上的连接器

| | -nostdlib 不使用标准库

| | -N 设置代码段和数据段均可读写

| | -e 指定入口

| | -Ttext 制定代码段开始位置

| |

| | 拷贝二进制代码bootblock.o到bootblock.out

| | objcopy -S -O binary obj/bootblock.o obj/bootblock.out

| | 其中关键的参数为

| | -S 移除所有符号和重定位信息

| | -O 指定输出格式

| |

| | 使用sign工具处理bootblock.out,生成bootblock

| | bin/sign obj/bootblock.out bin/bootblock

|

|> bin/kernel

| | 生成kernel的相关代码为

| | (kernel):tools/kernel.ld||(kernel): (KOBJS)||@echo+ld@

| | (V)(LD) (LDFLAGS)?Ttools/kernel.ld?o@ (KOBJS)||@(OBJDUMP) -S @>(call asmfile,kernel)

| | @(OBJDUMP)?t@ | (SED) ‘1,/SYMBOL TABLE/d; s/ .* / /; \  
|   |       /^/d′>(call symfile,kernel)

| |

| | 为了生成kernel,首先需要 kernel.ld init.o readline.o stdio.o kdebug.o

| | kmonitor.o panic.o clock.o console.o intr.o picirq.o trap.o

| | trapentry.o vectors.o pmm.o printfmt.o string.o

| | kernel.ld已存在

| |

| |> obj/kern//.o

| | | 生成这些.o文件的相关makefile代码为

| | | (calladdfilescc,(call listf_cc,(KSRCDIR)),kernel, |||(KCFLAGS))

| | | 这些.o生成方式和参数均类似,仅举init.o为例,其余不赘述

| |> obj/kern/init/init.o

| | | 编译需要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

| |

| | 生成kernel时,makefile的几条指令中有@前缀的都不必需

| | 必需的命令只有

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

| | 其中新出现的关键参数为

| | -T 让连接器使用指定的脚本

|

| 生成一个有10000个块的文件,每个块默认512字节,用0填充

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

|

| 把bootblock中的内容写到第一个块

| dd if=bin/bootblock of=bin/tos.img conv=notrunc

|

| 从第二个块开始写kernel中的内容

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


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

从sign.c的代码来看,一个磁盘主引导扇区只有512字节。且
第510个(倒数第二个)字节是0x55,
第511个(倒数第一个)字节是0xAA。

## [练习2]

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

练习2可以单步跟踪,方法如下:

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

set architecture i8086

target remote :1234


2 在 lab1目录下,执行

make debug


3 在看到gdb的调试界面(gdb)后,在gdb调试界面下执行如下命令

si

即可单步跟踪BIOS了。

4 在gdb界面下,可通过如下命令来看BIOS的代码

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


> [进一步的补充]

改写Makefile文件

debug: (TOSIMG)(V)(TERMINAL)?e“(QEMU) -S -s -d in_asm -D (BINDIR)/q.log?parallelstdio?hda< -serial null”

(V)sleep2(V)$(TERMINAL) -e “gdb -q -tui -x tools/gdbinit”


在调用qemu时增加`-d in_asm -D q.log`参数,便可以将运行的汇编指令保存在q.log中。
为防止qemu在gdb连接后立即开始执行,删除了`tools/gdbinit`中的`continue`行。

[练习2.2] 在初始化位置0x7c00 设置实地址断点,测试断点正常。

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

运行"make debug"便可得到
Breakpoint 2, 0x00007c00 in ?? ()
=> 0x7c00:      cli
   0x7c01:      cld
   0x7c02:      xor    %eax,%eax
   0x7c04:      mov    %eax,%ds
   0x7c06:      mov    %eax,%es
   0x7c08:      mov    %eax,%ss
   0x7c0a:      in     $0x64,%al
   0x7c0c:      test   $0x2,%al
   0x7c0e:      jne    0x7c0a
   0x7c10:      mov    $0xd1,%al

[练习2.3] 在调用qemu 时增加-d in_asm -D q.log 参数,便可以将运行的汇编指令保存在q.log 中。
将执行的汇编代码与bootasm.S 和 bootblock.asm 进行比较,看看二者是否一致。

在tools/gdbinit结尾加上
b *0x7c00
c
x /10i $pc

便可以在q.log中读到"call bootmain"前执行的命令
----------------
IN:
0x00007c00:  cli    

----------------
IN:
0x00007c01:  cld
0x00007c02:  xor    %ax,%ax
0x00007c04:  mov    %ax,%ds
0x00007c06:  mov    %ax,%es
0x00007c08:  mov    %ax,%ss

----------------
IN:
0x00007c0a:  in     $0x64,%al

----------------
IN:
0x00007c0c:  test   $0x2,%al
0x00007c0e:  jne    0x7c0a

----------------
IN:
0x00007c10:  mov    $0xd1,%al
0x00007c12:  out    %al,$0x64
0x00007c14:  in     $0x64,%al
0x00007c16:  test   $0x2,%al
0x00007c18:  jne    0x7c14

----------------
IN:
0x00007c1a:  mov    $0xdf,%al
0x00007c1c:  out    %al,$0x60
0x00007c1e:  lgdtw  0x7c6c
0x00007c23:  mov    %cr0,%eax
0x00007c26:  or     $0x1,%eax
0x00007c2a:  mov    %eax,%cr0

----------------
IN:
0x00007c2d:  ljmp   $0x8,$0x7c32

----------------
IN:
0x00007c32:  mov    $0x10,%ax
0x00007c36:  mov    %eax,%ds

----------------
IN:
0x00007c38:  mov    %eax,%es

----------------
IN:
0x00007c3a:  mov    %eax,%fs
0x00007c3c:  mov    %eax,%gs
0x00007c3e:  mov    %eax,%ss

----------------
IN:
0x00007c40:  mov    $0x0,%ebp

----------------
IN:
0x00007c45:  mov    $0x7c00,%esp
0x00007c4a:  call   0x7d0d

----------------
IN:
0x00007d0d:  push   %ebp

其与bootasm.S和bootblock.asm中的代码相同。

## [练习3]
分析bootloader 进入保护模式的过程。

从`%cs=0 $pc=0x7c00`,进入后

首先清理环境:包括将flag置0和将段寄存器置0
.code16
    cli
    cld
    xorw %ax, %ax
    movw %ax, %ds
    movw %ax, %es
    movw %ax, %ss

开启A20:通过将键盘控制器上的A20线置于高电位,全部32条地址线可用,
可以访问4G的内存空间。
seta20.1:               # 等待8042键盘控制器不忙
    inb $0x64, %al      #
    testb $0x2, %al     #
    jnz seta20.1        #

    movb $0xd1, %al     # 发送写8042输出端口的指令
    outb %al, $0x64     #

seta20.1:               # 等待8042键盘控制器不忙
    inb $0x64, %al      #
    testb $0x2, %al     #
    jnz seta20.1        #

    movb $0xdf, %al     # 打开A20
    outb %al, $0x60     #

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

进入保护模式:通过将cr0寄存器PE位置1便开启了保护模式
    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]
分析bootloader加载ELF格式的OS的过程。

首先看readsect函数,
`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为单位
}

readseg简单包装了readsect,可以从设备读取任意长度的内容。
static void
readseg(uintptr_t va, uint32_t count, uint32_t offset) {
    uintptr_t end_va = va + count;

    va -= offset % SECTSIZE;

    uint32_t secno = (offset / SECTSIZE) + 1;
    // 加1因为0扇区被引导占用
    // ELF文件从1扇区开始

    for (; va < end_va; va += SECTSIZE, secno ++) {
        readsect((void *)va, secno);
    }
}

在bootmain函数中,
void
bootmain(void) {
    // 首先读取ELF的头部
    readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);

    // 通过储存在头部的幻数判断是否是合法的ELF文件
    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);
    }
    // ELF文件0x1000位置后面的0xd1ec比特被载入内存0x00100000
    // ELF文件0xf000位置后面的0x1d20比特被载入内存0x0010e000

    // 根据ELF头部储存的入口信息,找到内核的入口
    ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();

bad:
    outw(0x8A00, 0x8A00);
    outw(0x8A00, 0x8E00);
    while (1);
}


## [练习5]
实现函数调用堆栈跟踪函数 

ss:ebp指向的堆栈位置储存着caller的ebp,以此为线索可以得到所有使用堆栈的函数ebp。
ss:ebp+4指向caller调用时的eip,ss:ebp+8等是(可能的)参数。

输出中,堆栈最深一层为
ebp:0x00007bf8 eip:0x00007d68     args:0x00000000 0x00000000 0x00000000 0x00007c4f
    <unknow>: -- 0x00007d67 --

其对应的是第一个使用堆栈的函数,bootmain.c中的bootmain。
bootloader设置的堆栈从0x7c00开始,使用"call bootmain"转入bootmain函数。
call指令压栈,所以bootmain中ebp为0x7bf8。

## [练习6]
完善中断初始化和处理

[练习6.1] 中断向量表中一个表项占多少字节?其中哪几位代表中断处理代码的入口?

中断向量表一个表项占用8字节,其中2-3字节是段选择子,0-1字节和6-7字节拼成位移,
两者联合便是中断处理程序的入口地址。

[练习6.2] 请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init。

见代码

[练习6.3] 请编程完善trap.c中的中断处理函数trap,在对时钟中断进行处理的部分填写trap函数

见代码

## [练习7]

增加syscall功能,即增加一用户态函数(可执行一特定系统调用:获得时钟计数值),
当内核初始完毕后,可从内核态返回到用户态的函数,而用户态的函数又通过系统调用得到内核态的服务

在idt_init中,将用户态调用SWITCH_TOK中断的权限打开。
    SETGATE(idt[T_SWITCH_TOK], 1, KERNEL_CS, __vectors[T_SWITCH_TOK], 3);

在trap_dispatch中,将iret时会从堆栈弹出的段寄存器进行修改
    对TO User
    tf->tf_cs = USER_CS;
    tf->tf_ds = USER_DS;
    tf->tf_es = USER_DS;
    tf->tf_ss = USER_DS;
    对TO Kernel
    tf->tf_cs = KERNEL_CS;
    tf->tf_ds = KERNEL_DS;
    tf->tf_es = KERNEL_DS;

在lab1_switch_to_user中,调用T_SWITCH_TOU中断。
注意从中断返回时,会多pop两位,并用这两位的值更新ss,sp,损坏堆栈。
所以要先把栈压两位,并在从中断返回后修复esp。
asm volatile (
    "sub $0x8, %%esp \n"
    "int %0 \n"
    "movl %%ebp, %%esp"
    :
    : "i"(T_SWITCH_TOU)
);

在lab1_switch_to_kernel中,调用T_SWITCH_TOK中断。
注意从中断返回时,esp仍在TSS指示的堆栈中。所以要在从中断返回后修复esp。
asm volatile (
    "int %0 \n"
    "movl %%ebp, %%esp \n"
    :
    : "i"(T_SWITCH_TOK)
);

但这样不能正常输出文本。根据提示,在trap_dispatch中转User态时,将调用io所需权限降低。
tf->tf_eflags |= 0x3000;


时间: 2024-10-28 10:02:18

lab1的实验练习答案的相关文章

C语言程序设计 第3版 课后习题答案 苏小红 王宇颖 孙志岗 版 实验题答案 高等教育出版社 课后答案 解析 第3章 课后答案

C语言程序设计 第3版 课后习题答案  苏小红 王宇颖 孙志岗  实验题答案 高等教育出版社 课后答案 解析 第3章 课后答案 C语言程序设计 苏小红 王宇颖 孙志岗 版 习题3 课后习题答案 前辅文第1章 为什么要学C 语言 课后习题答案1.1 引言1.2 游戏?黑客和C 语言1.3 C 语言,不老的传说1.4 C 语言的爱与恨1.5 C 语言教给我们的事1.6 什么是“编程”1.7 本章小结习题1第2章 C 数据类型 课后答案2.1 常量与变量2.1.1 常量2.1.2 变量2.2 简单的屏

[HIT操作系统][lab1]熟悉实验环境 文件交互遇到的小盲区

实验链接: https://www.shiyanlou.com/courses/115 课程链接 http://mooc.study.163.com/course/HIT-1000002004#/info 本实验主要是熟悉实验环境目录结构,如何编译Linux源文件,并在bochs模拟器下运行. 要感谢实验楼提供的环境,我在Ubuntu 16.04下自行编译和Bochs运行非常不顺利,而实验楼非常的顺滑,可以专注于课程知识的学习. 实验楼环境里按步骤操作并没有遇到什么大问题,不过我觉得老师的实验步

王爽 实验7 答案

可算是写出来了, assume cs:code,ds:data data segment db '1975','1976','1977','1978','1979' db '1980','1981','1982','1983','1984' db '1985','1986','1987','1988','1989' db '1990','1991','1992','1993','1994' db '1995' dd 16,22,382,1356,2390,8000,16000,24486 dd

MIT 操作系统实验 MIT JOS lab1

JOS lab1 嘿嘿,实验环境还是相当的友好的. 很多东西都准备好了.把重点放在理论的印证上面. MIT才是改变并引领世界的牛校,心神往之,吾身不能至啊~ 国内的北大,上交等学校的OS实验都是直接用的JOS,这点证据还是容易找的...说明什么,不言而喻咯... ----------------------------------------------------------------------------------------------------------------------

Ubuntu 使用文件 casper-rw 镜像文件 保存变更内容

yumi工具本可以制作基于u盘的persistent启动盘 casper-rw是ubuntu特有的一种功能,支持liveCD启动的ubuntu系统保存用户的变更内容 那我们想同iso光盘从硬盘上启动,并使用casper-rw镜像文件是否可行呢?经过长期研究.实验,答案是可行,但有一定的限制 可行性: 通过grub4dos支持Ubuntu iso文件启动到live模式,并可以使用casper-rw文件保存变更内容 限制: iso文件 和 casper-rw文件必须在同一个磁盘分区上,且该分区必须是

iptables防火墙 --Linux详解

在实际运维工作中,哪里还有单纯的服务器执行工作的,一般都是服务器提供一定的网络服务来让一些客户机进行访问.那么,这时候网络问题是不是就显得尤为重要了呢??? 既然服务器暴露在互联网上,我们是不是要防止服务器被攻击?是不是要限制访问服务器的客户端用户?是不是要设置一定的规则来管理我们的访客呢?没错,这就用到了我接下来要讲解的一项Linux运维人员不得不知的技术--iptables防火墙. 如上图所示:防火墙分为软件防火墙和硬件防火墙两种,其中,硬件防火墙是由厂商设计好的主机硬件,这台硬件防火墙内的

第8章 Iptables与Firewalld防火墙

章节简述: 红帽RHEL7系统已经用firewalld服务替代了iptables服务,新的防火墙管理命令firewall-cmd与图形化工具firewall-config. 本章节基于数十个防火墙需求,使用规则策略完整演示对数据包的过滤.SNAT/SDAT技术.端口转发以及负载均衡等实验. 不光光学习iptables命令与firewalld服务,还新增了Tcp_wrappers防火墙服务小节,简单配置即可保证系统与服务的安全. 本章目录结构 [收起] 8.1 了解防火墙管理工具 8.2 Ipta

第7章 Iptables与Firewalld防火墙。

第7章 Iptables与Firewalld防火墙. Chapter7_听较强节奏的音乐能够让您更长时间的投入在学习中. <Linux就该这么学> 00:00/00:00 104:01 204:13 304:12 402:47 502:57 605:39 702:52 803:55 903:59 1003:59 1102:56 1203:44 1303:19 1403:08 章节简述: 红帽RHEL7系统已经用firewalld服务替代了iptables服务,新的防火墙管理命令firewall

为什么计算机的学生要学习 Linux 开源技术

by falcon of TinyLab.org 2013/08/25 Linux开源相关技术对于学生来说,特别是计算机专业的学生来说,非常重要,下面就几个方面进行讨论: 研究平台 因为开源的优势,有非常多的开放的文案可以参考,有很多有趣的点子可以拿来做深入的研究.任何一个点挖进去都是一片天地. 专业视野 通过那些开放的项目,你可以通过邮件列表.Linkedin.Google Group接触到来自全球各地的天才,不仅可以提升英文读写能力,认识国际友人,还可以把握领域前沿,甚至还有机会得到大佬们的