# 操作系统实验报告:ucore-lab1

时间:2021-05-10 00:58:54

参考链接:
1. GDB 常用命令参考手册
2. 一篇优秀的gdb的总结
3. lab1实验报告
4. 清华大学操作系统实验lab1实验报告
5. elf文件格式总结

练习一: 理解通过 make 生成执行文件的过程

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

1. GCC相关编译选项

GCC
-g 增加gdb的调试信息
-Wall 显示告警信息
-O2 优化处理 (有 0,1,2,3,0是不优化)
-fno-builtin 只接受以"__"开头的内建函数
-ggdb 让gcc为gdb生成比较丰富的调试信息
-m32 编译32位程序
-gstabs 此选项以stabs格式生成调试信息,但是不包括gdb调试信息
-nostdinc 不在标准系统目录中搜索头文件,只在-l指定的目录中搜索
-fstack-protector-all 启用堆栈保护,为所有函数插入保护代码
-E 仅做预处理,不进行编译,汇编和链接
-x c 指明使用的语言为C语言

LDD Flags
-nostdlib 不连接系统标准启动文件和标准库文件,只把指定的文件传递给连接器
-m elf\_i386 使用elf_i386模拟器
-N 把text和data节设置为可读写,同时取消数据节的页对齐,取消对共享库的链接
-e func 以符号func的位置作为程序开始运行的位置
-Ttext addr 是连接时将初始地址重定向为addr (若不注明此,则程序的起始地址为0)

2. 编译bootloader

用于加载Kernel操作系统

先把bootasm.S,bootmain.c编译成目标文件
再使用连接器链接到一起,使用start符号作为入口,并且指定text段在程序中的绝对位置是0x7C00,
0x7c00 :这个是操作系统一开始加载的地址
//bootasm.o
+ cc boot/bootasm.S
gcc -Iboot/ -fno-builtin -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootasm.S -o obj/boot/bootasm.o

//生成bootmain.o
+ cc boot/bootmain.c
gcc -Iboot/ -fno-builtin -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootmain.c -o obj/boot/bootmain.o

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

'obj/bootblock.out' size: 468 bytes
build 512 bytes boot sector: 'bin/bootblock' success!

3. 编译Kernel

操作系统本身

先把.c文件和.S汇编文件生成目标文件,之后使用链接起生成Kernel
+ cc kern/init/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
+ cc kern/libs/readline.c
gcc -Ikern/libs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/libs/readline.c -o obj/kern/libs/readline.o
+ cc kern/libs/stdio.c
gcc -Ikern/libs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/libs/stdio.c -o obj/kern/libs/stdio.o
+ cc kern/debug/kdebug.c
gcc -Ikern/debug/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/kdebug.c -o obj/kern/debug/kdebug.o
+ cc kern/debug/kmonitor.c
gcc -Ikern/debug/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/kmonitor.c -o obj/kern/debug/kmonitor.o
+ cc kern/debug/panic.c
gcc -Ikern/debug/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/panic.c -o obj/kern/debug/panic.o
+ cc kern/driver/clock.c
gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/clock.c -o obj/kern/driver/clock.o
+ cc kern/driver/console.c
gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/console.c -o obj/kern/driver/console.o
+ cc kern/driver/intr.c
gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/intr.c -o obj/kern/driver/intr.o
+ cc kern/driver/picirq.c
gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/picirq.c -o obj/kern/driver/picirq.o
+ cc kern/trap/trap.c
gcc -Ikern/trap/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/trap.c -o obj/kern/trap/trap.o
+ cc kern/trap/trapentry.S
gcc -Ikern/trap/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/trapentry.S -o obj/kern/trap/trapentry.o
+ cc kern/trap/vectors.S
gcc -Ikern/trap/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/vectors.S -o obj/kern/trap/vectors.o
+ cc kern/mm/pmm.c
gcc -Ikern/mm/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/mm/pmm.c -o obj/kern/mm/pmm.o
+ cc libs/printfmt.c
gcc -Ilibs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -c libs/printfmt.c -o obj/libs/printfmt.o
+ cc libs/string.c
gcc -Ilibs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -c libs/string.c -o obj/libs/string.o

+ ld bin/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

编译sign

用于生成一个符合规范的硬盘主引导扇区。
+ cc tools/sign.c
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

生成ucore.img

  • dd - 转换和拷贝文件

    if  代表输入文件。如果不指定if,默认就会从stdin中读取输入。 
    of 代表输出文件。如果不指定of,默认就会将stdout作为默认输出。
    bs 代表字节为单位的块大小。
    count 代表被复制的块数。
    /dev/zero 是一个字符设备,会不断返回0值字节(\0)
    conv=notrunc 输入文件的时候,源文件不会被截断
    seek=blocks 从输出文件开头跳过 blocks(512字节) 个块后再开始复制
  • 过程
    生成一个空的软盘镜像,然后把bootloader以不截断的方式填充到开始的块中,然后kernel跳过bootloader所在的块,再填充

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. 一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?

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

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

1. 从 CPU 加电后执行的第一条指令开始, 单步跟踪 BIOS 的执行。
2. 在初始化位置 0x7c00 设置实地址断点,测试断点正常。
3. 在调用 qemu 时增加-d in_asm -D q.log 参数,便可以将运行的汇编指令保存在 q.log 中。
将执行的汇编代码与 bootasm.S 和 bootblock.asm 进行比较, 看看二者是否一致。


qemu-system-i386
-hda file 硬盘选项
-parallel dev 重定向虚拟并口到主机设备。最多可虚拟3个并口
-serial dev 重定向虚拟串口到主机设备
vc:虚拟控制台
pty:仅仅linux有效,虚拟tty(一个虚拟伪终端会被立刻分配)
none:没有设备被分配
null:无效设备
-S 启动的时候不直接从CPU启动,需要在窗口中按c来继续
-s shorthand for -gdb tcp::1234,打开端口1234,供gdb来调试

gdb
-x 从文件中执行gdb命令
-q 不要打印介绍和版权信息。
-tui 可以将终端屏幕分成原文本窗口和控制台的多个子窗口,可以一边看源码一边调试
  • -S –s是使得qemu在执行第一条指令之前停下来,。然后sleep 两秒应该是给qemu充分的时间准备等待连接。接下来使用GDB调试工具, -tui提供了代码与命令行分屏查看的界面,tools/gdbinit中存放的是gdb调试
qemu-system-i386 -S -s -parallel stdio -hda bin/ucore.img -serial null &
sleep 2
gnome-terminal -e "gdb -q -tui -x tool/gdbinit"

#tools/gdbinit
target remote :1234 #链接远端端口1234
使用make debug,调用debug建立qemu和gdb的链接
x/10i $pc:显示程序当前位置开始往后的10条汇编指令。
(gdb) x/10i $pc
=> 0xfff0: add %al,(%eax)
0xfff2: add %al,(%eax)
0xfff4: add %al,(%eax)
0xfff6: add %al,(%eax)
0xfff8: add %al,(%eax)
0xfffa: add %al,(%eax)
0xfffc: add %al,(%eax)
0xfffe: add %al,(%eax)
0x10000: add %al,(%eax)
0x10002: add %al,(%eax)
stepi 执行下一条汇编/CPU指令。
(gdb) stepi
0x0000e05b in ?? ()
(gdb) x/5i $pc
=> 0xe05b: add %al,(%eax)
0xe05d: add %al,(%eax)
0xe05f: add %al,(%eax)
0xe061: add %al,(%eax)
0xe063: add %al,(%eax)
break *address 在指定的地址处设置断点。一般在没有源代码时使用。
(gdb) b *0x7c00
Breakpoint 1 at 0x7c00
continue(c) 继续执行直到下一个断点或观察点。
(gdb) c
Continuing.
Breakpoint 1, 0x00007c00 in ?? ()

(gdb) x/5i $pc
=> 0x7c00: cli
0x7c01: cld
0x7c02: xor %eax,%eax
0x7c04: mov %eax,%ds
0x7c06: mov %eax,%es
//tools/gdbinit" 
file obj/bootblock.o
target remote :1234
set architecture i8086
b *0x7c00
continue
x /10i $pc

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

BIOS 将通过读取硬盘主引导扇区到内存,并转跳到对应内存中的位置执行 bootloader。 请分析bootloader 是如何完成从实模式进入保护模式的。
//在开启A20之前,BIOS还做了很多事:关中断、清除方向标志,给各个数据段清零。
.globl start
start:
.code16 # Assemble for 16-bit mode
cli # Disable interrupts
cld # String operations increment

xorw %ax, %ax # Segment number zero
movw %ax, %ds # -> Data Segment
movw %ax, %es # -> Extra Segment
movw %ax, %ss # -> Stack Segment

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

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

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

movb $0xdf, %al # 打开A20
outb %al, $0x60
如何初始化GDT表? 
#把gdt表的起始位置和界限装入GDTR寄存器

lgdt gdtdesc #把gdt表的起始位置和界限装入GDTR寄存器 movl %cr0, %eax orl $CR0_PE_ON, %eax
movl %eax, %cr0 #把保护模式位开启


工作在保护模式下。复位PE将返回到实模式工作。
此外,gdtdesc指出了全局描述符表在符号gdt处,

上面四句话实现了打开保护模式位。 3、如何使能进入保护模式? 通过长跳转指令
ljmp $PROT_MODE_CSEG, $protcseg 进入了保护模式。
进入保护模式之后还有一个步骤:把所有的数据段寄存器指向上面的GDT描述符表中的数据段(0x10)

练习 四:分析 bootloader 加载 ELF 格式的 OS 的过程。(要求在报告中写出分析)

1. bootloader如何读取硬盘扇区的?

//首先是读取一个扇区(512字节),参数是扇区号和写入地址
static void
readsect(void *dst, uint32_t secno) {
waitdisk(); // /* wait for disk to be ready */
outb(0x1F2, 1); // 设置读取扇区数的数目为1

/* 下面连续四句是说把secno的32位写入设备ID寄存器,
0~27 位是偏移量,
第28位的是主副通道,剩下的29~31被强制置1*/

outb(0x1F3, secno&0xFF);
outb(0x1F4, (secno>>8)&0xFF);
outb(0x1F5, (secno>>16)&0xFF);
outb(0x1F6, ((secno>>24)&0xF)|0xE0); //设置第28位,29~31强制置1

outb(0x1F7, 0x20); //cmd 0x20 -读取磁盘

waitdisk(); //等待磁盘准备好

/* 读到dst地址, 因为这里是以DW(双字)为单位,所以要/4*/
/* 1个字节 1byte=8bit,
1个字 == 2个字节
1个双字 == 2个字 == 4个字节 */

insl(0x1F0, dst, SECTSIZE/4);
}
//从设备中读count个字节到va这个地址,offset是指想读的位置距离开始的偏移
static void
readseg(uintptr_t va, uint32_t count, uint32_t offset) {
uintptr_t end_va = va + count;

//计算当前偏移在那个扇区的位置,并且让va地址向前偏移这些字节,
//然后读完之后,用户开始传入的地址va的内容就是偏移所在内容
va -= offset % SECTSIZE;

// translate from btyes to sectors; kernel starts at sector 1
uint32_t secno = (offset/ SECTSIZE) +1 ; //计算偏于所在的扇区,kernel是在起始就是第一个扇区


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

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


/* file header */
struct elfhdr {
uint32_t e_magic; // must equal ELF_MAGIC
uint8_t e_elf[12]; //描述信息
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; //程序入口的虚拟地址 entry point if executable
uint32_t e_phoff; // file position of program header or 0
uint32_t e_shoff; // file position of section header or 0
uint32_t e_flags; // architecture-specific flags, usually 0
uint16_t e_ehsize; // size of this elf header
uint16_t e_phentsize; // size of an entry in program header
uint16_t e_phnum; // number of entries in program header or 0
uint16_t e_shentsize; // size of an entry in section header
uint16_t e_shnum; // number of entries in section header or 0
uint16_t e_shstrndx; // section number that contains section name strings
};

/* program section header */
struct proghdr {
uint32_t p_type; // loadable code or data, dynamic linking info,etc.
uint32_t p_offset; // file offset of segment
uint32_t p_va; // virtual address to map segment
uint32_t p_pa; // physical address, not used
uint32_t p_filesz; // size of segment in file
uint32_t p_memsz; // size of segment in memory (bigger if contains bss)
uint32_t p_flags; // read/write/execute bits
uint32_t p_align; // required alignment, invariably hardware page size
};

void
bootmain(void) {
// read the 1st page off disk //512*8 = 4k
readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);

if (ELFHDR->e_magic != ELF_MAGIC) { // is this a valid ELF?
goto bad;
}

struct proghdr *ph, *eph;

// load each program segment (ignores ph flags)
ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff); //获取程序头部表格首地址
eph = ph + ELFHDR->e_phnum;

//把程序从硬盘中加载到内存,忽略p_flags
for (; ph < eph; ph ++) {
readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
}

// call the entry point from the ELF header
// note: does not return
((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();

bad:
outw(0x8A00, 0x8A00);
outw(0x8A00, 0x8E00);

/* do nothing */
while (1);
}

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

  • 前提知识

    栈相关的寄存器两个,ebp(基址寄存器)和esp(栈指针寄存器),栈的增长方向是由高到低
    eip是程序指令指针,当前程序运行的指令

    举例,main函数在调用sum(int a, int b)的时候
    高地址-—|--------------------------|--
    | 参数b |
    -—|--------------------------|--
    | 参数a |
    -—|--------------------------|--
    | sum函数下一条命令的地址 |
    -—|--------------------------|--
    | main函数栈的基址 |
    低地址-—|--------------------------|-- ebp

    此时ebp是sum函数栈的基址,然后eip里面是sum函数中的第一条指令
    sum函数执行完之后,sum函数栈的内容全部出栈,
    然后ebp重新变成main函数的函数栈基址,ebp=*((uint_t*)ebp)
    eip=*((uint_t*)ebp+1),就是sum函数之后的指令的地址,然后函数参数出栈
///kern/debug/kdebug.c
void print_stackframe(void) {
uint32_t t_ebp = read_ebp();
uint32_t t_eip = read_eip();

int i, j;
for( i = 0; i < STACKFRAME_DEPTH && t_ebp!=0; i++)
{
cprintf("ebp=%08x, eip=%08x, args:", t_ebp, t_eip);
for( j =0 ; j < 4; j++)
{
cprintf(" %08x ", *((uint32_t*)t_ebp+2+j));
}
cprintf("\n");
print_debuginfo(t_eip-1);
t_eip = *((uint32_t*)t_ebp+1);
t_ebp = *((uint32_t*)t_ebp);
}
}

练习 六:完善中断初始化和处理 (需要编程)

中断向量表调用关系
系统发生中断--->中断号(0~255)-->找到IDT,拿到段选择子和偏移-----> 拿着段选择子找GDT,找到段基址,再使用偏移跳转到程序入口地址

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

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

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

在 idt_init 函数中,依次对所有中断入口进行初始化。使用 mmu.h 中的 SETGATE 宏
#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:
istrap:陷阱门设为1,中断门设为0.
sel:段选择子,全局描述符表的代码段段选择子 //memlayout.h里面有宏定义GD_KTEXT
off:处理函数的入口地址,即__vectors[]中的内容。
dpl:特权级.从实验指导书中可知,ucore中的应用程序处于特权级3,内核态特权级为0.
注意除了系统调用中断(T_SYSCALL)以外,其它中断均使用中断门描述符,权限为内核态权限;而系统调用中断使用异常,权限为陷阱门描述符
void
idt_init(void) {
extern uintptr_t __vectors[];
for(int i = 0; i < sizeof(idt)/sizeof(struct gatedesc); i++)
{
SETGATE(idt[i], 0, GD_KTEXT, __vectors[i]; 0);
}
SETGATE(idt[T_SYSCALL], 0, GD_KTEXT, __vectors[T_SYSCALL]; 3);
lidt(&idt_pd);
}

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

case IRQ_OFFSET+IRQ_TIMER:
ticks++;
if (ticks % TICK_NUM == 0) {
print_ticks();
}
break;
/*
make
make qemu
*/