在大三上学期接触了操作系统这门课程,经过了小半个学期的理论讲解,我对操作系统的局部有了粗浅的了解。
紧接而来的这个实验是清华大学的操作系统实验课程,有一定难度,需要去学习很多课本中没有详谈的问题,需要通过自己的一次次尝试去了解操作系统开发实验环境,进一步熟悉命令行方式的编译、调试工程,同时还要求我们熟悉C语言编程和指针的概念。
本篇报告,详细地记录了我的整个实验过程,包括遇到新概念和软件后的学习笔记,也会记录在本文中,可以完整地反映我的学习过程。
实验环境配置
本次实验需要在LINUX环境下完成。
实验指导书中给出了多个配置环境的方法,我在这里选择了VMware中的Ubuntu 64位 虚拟机,版本为16.04.5。
Ubuntu是图形界面友好和易操作的linux发行版,有时只需执行几条简单的指令就可以完成繁琐的鼠标点击才能完成的操作,拥有强大的命令行操作。
编程开发调试的基本工具
1. gcc
- 安装gcc
$ sudo apt-get install gcc
安装完毕后是4:5.3.1-1ubuntu1的版本
更新:
sudo apt-get update
卸载:
sudo apt-get install <软件名>-
- 用gcc编译文件
$ gcc -Wall <代码文件名称> -o <可执行文件名称>
- 执行文件
$ ./<可执行文件名称>
实验一 系统软件启动过程
BIOS
Basic Input Output System,即基本输入/输出系统,实际上是一个被固化在计算机ROM(只读存储器)芯片上的特殊的软件,为上层软件提供最底层的、最直接的硬件控制与支持。
BIOS做完计算机硬件自检和初始化后,会选择一个启动设备,并且读取该设备的第一扇区,到内存一个特定的地址处,进一步的工作交给了ucore的bootloader。
BootLoader
在嵌入式操作系统中,BootLoader是在操作系统内核运行之前运行。可以初始化硬件设备、建立内存空间映射图,从而将系统的软硬件环境带到一个合适状态,以便为最终调用操作系统内核准备好正确的环境。
我通过网络上的资料对Bootloader这个软件进行了大概的了解,我把它理解为一个【导入管】。
计算机从开机到操作系统进入一个类似while(1)的永不退出的循环中,需要一个导入过程——在加电后执行第一段代码,在它完成CPU和相关硬件的初始化之后,再将操作系统映像或固化的嵌入式应用程序装在到内存中,然后跳转到操作系统所在的空间,启动操作系统运行。
实验中给的bootloader可以切换到X86保护模式,能够读磁盘并加载ELF执行文件格式。
我们再来看看实验指导书中对Bootloader这个软件的学习要求:
- List item
- 编译运行bootloader的过程
- 调试bootloader的方法
- PC启动bootloader的过程
- ELF执行文件的格式和加载
- 外设访问:读硬盘,在CGA上显示字符串
以下是头文件<asm.h>中的内容:
#ifndef __BOOT_ASM_H__
#ifndef 的意思是:如果_BOOT_ASM_H_这个标识未被定义,则编译下面的程序段。
接下来就是正式定义<asm.h>的内容,大致的操作含义是利用汇编宏(Assembler macros)定义了X86下的常规和应用程序的段类型,例如 STA_X为可执行段(Executable segment),STA_C为代码段(Conforming code segment)。
#define __BOOT_ASM_H__
/* Assembler macros to create x86 segments */
/* Normal segment */
#define SEG_NULLASM \
.word 0, 0; \
.byte 0, 0, 0, 0
#define SEG_ASM(type,base,lim) \
.word (((lim) >> 12) & 0xffff), ((base) & 0xffff); \
.byte (((base) >> 16) & 0xff), (0x90 | (type)), \
(0xC0 | (((lim) >> 28) & 0xf)), (((base) >> 24) & 0xff)
/* Application segment type bits */
#define STA_X 0x8 // Executable segment
#define STA_E 0x4 // Expand down (non-executable segments)
#define STA_C 0x4 // Conforming code segment (executable only)
#define STA_W 0x2 // Writeable (non-executable segments)
#define STA_R 0x2 // Readable (executable segments)
#define STA_A 0x1 // Accessed
#endif /* !__BOOT_ASM_H__ */
接下来是bootasm.S中的内容,大致的操作是:
- 启动CPU:切换到32位保护模式,并跳转到C;
- BIOS将这段代码从硬盘的第一个扇区,装入到物理地址为0x7c00处的内存,并以实模式开始执行。
实模式 是早期CPU运行的工作模式,这个模式下段基地址必须是16的整数倍。
保护模式 则是现代CPU运行的模式,可以实现更大空间的,更灵活的内存访问。
现代CPU在运行boot loader时仍旧要先进入实模式,是为了实现软件的向后兼容性。
#include <asm.h>
# Start the CPU: switch to 32-bit protected mode, jump into C.
# The BIOS loads this code from the first sector of the hard disk into
# memory at physical address 0x7c00 and starts executing in real mode
# with %cs=0 %ip=7c00.
.set PROT_MODE_CSEG, 0x8 # kernel code segment selector
.set PROT_MODE_DSEG, 0x10 # kernel data segment selector
.set CR0_PE_ON, 0x1 # protected mode enable flag
# start address should be 0:7c00, in real mode, the beginning address of the running bootloader
.globl start
start:
.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.
seta20.1:
inb $0x64, %al # Wait for not busy(8042 input buffer empty).
testb $0x2, %al
jnz seta20.1
movb $0xd1, %al # 0xd1 -> port 0x64
outb %al, $0x64 # 0xd1 means: write data to 8042's P2 port
seta20.2:
inb $0x64, %al # Wait for not busy(8042 input buffer empty).
testb $0x2, %al
jnz seta20.2
movb $0xdf, %al # 0xdf -> port 0x60
outb %al, $0x60 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1
# Switch from real to protected mode, using a bootstrap GDT
# and segment translation that makes virtual addresses
# identical to physical addresses, so that the
# effective memory map does not change during the switch.
lgdt gdtdesc
movl %cr0, %eax
orl $CR0_PE_ON, %eax
movl %eax, %cr0
# Jump to next instruction, but in 32-bit code segment.
# Switches processor into 32-bit mode.
ljmp $PROT_MODE_CSEG, $protcseg
.code32 # Assemble for 32-bit mode
protcseg:
# Set up the protected-mode data segment registers
movw $PROT_MODE_DSEG, %ax # Our data segment selector
movw %ax, %ds # -> DS: Data Segment
movw %ax, %es # -> ES: Extra Segment
movw %ax, %fs # -> FS
movw %ax, %gs # -> GS
movw %ax, %ss # -> SS: Stack Segment
# Set up the stack pointer and call into C. The stack region is from 0--start(0x7c00)
movl $0x0, %ebp
movl $start, %esp
call bootmain
# If bootmain returns (it shouldn't), loop.
spin:
jmp spin
# Bootstrap GDT
.p2align 2 # force 4 byte alignment
gdt:
SEG_NULLASM # null seg
SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # code seg for bootloader and kernel
SEG_ASM(STA_W, 0x0, 0xffffffff) # data seg for bootloader and kernel
gdtdesc:
.word 0x17 # sizeof(gdt) - 1
.long gdt # address gdt
练习1:理解通过make生成执行文件的过程。
Makefile 是一种常用于编译的脚本语言。它可以更好更方便的管理你的项目的代码编译,节约编译时间
使用GCC的命令进行程序编译时,当程序是单个文件时编译是比较方便的,但当工程中的文件数目增多,甚至非常庞大,并且目录结构关系复杂时,便需要通过makefile来进行程序的编译。
1.1 操作系统镜像文件ucore.img是如何一步一步生成的?(需要比较详细地解释Makefile中每一条相关命令和命令参数的含义,以及说明命令导致的结果)
我们先在Makefile文件中,找到ucore.img的部分
# create ucore.img
UCOREIMG := $(call totarget,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)
从第3行开始,代码表明了:
生成ucore需要kernel和bootblock文件;
将/dev/zero复制进一个新创建的地址为10000的块中去;
将bootblock复制到同一个位置(不截短输出文件 );
将kernel拷贝到同一位置(从输出文件开头跳过1个块后再开始拷贝 )
接下来,再看看kernel和bootblock的部分:
# create kernel target
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)
# create bootblock
bootfiles = $(call listf_cc,boot)
$(foreach f,$(bootfiles),$(call cc_compile,$(f),$(CC),$(CFLAGS) -Os -nostdinc))
bootblock = $(call totarget,bootblock)
$(bootblock): $(call toobj,$(bootfiles)) | $(call totarget,sign)
@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