ucore实验lab0-lab1

ucore实验lab0-lab1

lab0

make和makefile
在阅读实验指导书的同时,这里也记录我学习make的心得

makefile的规则
make命令是GNU的工程化编译工具,它用于编译大量互相关联的源代码,使用它可以实现项目的工程化管理,提高开发效率。

综上,编译链接的过程是:

首先源文件会通过编译阶段生成中间目标文件,再由中间目标文件生成执行文件。
编译时编译器只检测程序的语法、函数,变量是否声明以及一些预处理。如果函数只是声明但是未实现则编译器只会警告(不是所有的编译器都是这样),仍然可以生成中间目标文件。
在链接程序时,链接器会在所有的中间目标文件中寻找函数的实现,如果找不到,那就会提示链接错误。

target … : prerequisites …
command

target也就是一个目标文件,可以是Object 文件,也可以是执行文件。还可以是一个标签(Label),对于标签这种特性,将在后面叙述。
prerequisites就是,要生成那个target所需要的文件或是目标。
command也就是make需要执行的命令。(任意的Shell命令)
这个规则可以这么看,目标文件target的生成需要依赖prerequisites中的一些文件,而target文件的生成规则是在command中定义的。
基于硬件模拟器实现源码级调试
安装硬件模拟器QEMU
1
sudo apt-get install qemu-system
结合gdb和qemu源码级调试ucore
ucore 代码编译

ucore.img:被qemu访问的虚拟硬盘文件
kernel: ELF格式的toy ucore kernel执行文,被嵌入到了ucore.img中
bootblock: 虚拟的硬盘主引导扇区(512字节),包含了
bootloader执行代码,被嵌入到了ucore.img中
sign:外部执行程序,用来生成虚拟的硬盘主引导扇区
使用远程调试
通过gdb可以对ucore代码进行调试,以lab1中调试memset函数为例:

(1) 运行 qemu -S -s -hda ./bin/ucore.img -monitor stdio
(2) 运行 gdb并与qemu进行连接
(3) 设置断点并执行
(4) qemu 单步调试。

使用gdb配置文件
在lab1/tools目录下,执行完make后,我们可以创建文件gdbinit,并输入下面的内容:

target remote 127.0.0.1:1234
file bin/kernel

lab1
练习1:理解通过make生成执行文件的过程
操作系统镜像文件ucore.img是如何一步一步生成的?
首先查看makefile文件

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>

int
main(int argc, char *argv[]) {
struct stat st;
if (argc != 3) {
fprintf(stderr, “Usage: \n”);
return -1;
}
if (stat(argv[1], &st) != 0) {
fprintf(stderr, “Error opening file ‘%s’: %s\n”, argv[1], strerror(errno));
return -1;
}
printf("’%s’ size: %lld bytes\n", argv[1], (long long)st.st_size);
if (st.st_size > 510) {
fprintf(stderr, “%lld >> 510!!\n”, (long long)st.st_size);
return -1;
}
char buf[512];
memset(buf, 0, sizeof(buf));
FILE *ifp = fopen(argv[1], “rb”);
int size = fread(buf, 1, st.st_size, ifp);
if (size != st.st_size) {
fprintf(stderr, “read ‘%s’ error, size is %d.\n”, argv[1], size);
return -1;
}
fclose(ifp);
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;
}
fclose(ofp);
printf(“build 512 bytes boot sector: ‘%s’ success!\n”, argv[2]);
return 0;
}
查看细节的代码

int size = fread(buf, 1, st.st_size, ifp);
if (size != st.st_size) {
fprintf(stderr, “read ‘%s’ error, size is %d.\n”, argv[1], size);
return -1;
}

buf[510] = 0x55;
buf[511] = 0xAA;
FILE *ofp = fopen(argv[2], “wb+”);
size = fwrite(buf, 1, 512, ofp);
一个磁盘主引导扇区只有512字节。且 第510个(倒数第二个)字节是0x55,第511个(倒数第一个)字节是0xAA。

练习2: 使用qemu执行并调试lab1中的软件
从CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行。

$ cd labcodes_answer/lab1_result/
$ make lab1-mon

在初始化位置0x7c00设置实地址断点,测试断点正常

b *0x7c00
c

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

可以发现代码是一样的

练习3:分析bootloader进入保护模式的过程
实模式
在bootloader接手BIOS的工作后,当前的PC系统处于实模式(16位模式)运行状态,在这种状态下软件可访问的物理内存空间不能超过1MB,且无法发挥Intel 80386以上级别的32位CPU的4GB内存管理能力。实模式将整个物理内存看成分段的区域,程序代码和数据位于不同区域,操作系统和用户程序并没有区别对待,而且每一个指针都是指向实际的物理地址。这样,用户程序的一个指针如果指向了操作系统区域或其他用户程序区域,并修改了内容,那么其后果就很可能是灾难性的。通过修改A20地址线可以完成从实模式到保护模式的转换。有关A20的进一步信息可参考附录“关于A20 Gate”。

保护模式
只有在保护模式下,80386的全部32根地址线有效,可寻址高达4G字节的线性地址空间和物理地址空间,可访问64TB(有2^ 14个段,每个段最大空间为2^32字节)的逻辑地址空间,可采用分段存储管理机制和分页存储管理机制。这不仅为存储共享和保护提供了硬件支持,而且为实现虚拟存储提供了硬件支持。通过提供4个特权级和完善的特权检查机制,既能实现资源共享又能保证代码数据的安全及任务的隔离。保护模式下,有两个段表:GDT(Global Descriptor Table)和LDT(Local Descriptor Table),每一张段表可以包含8192 (2^13)个描述符,因而最多可以同时存在2 * 2^13 = 2 ^ 14个段。虽然保护模式下可以有这么多段,逻辑地址空间看起来很大,但实际上段并不能扩展物理地址空间,很大程度上各个段的地址空间是相互重叠的。目前所谓的64TB(2^ (14+32)=2^ 46)逻辑地址空间是一个理论值,没有实际意义。在32位保护模式下,真正的物理空间仍然只有2^32字节那么大。

关于A20 Gate
分段存储管理机制
只有在保护模式下才能使用分段存储管理机制。分段机制将内存划分成以起始地址和长度限制这两个二维参数表示的内存块,这些内存块就称之为段(Segment)。编译器把源程序编译成执行程序时用到的代码段、数据段、堆和栈等概念在这里可以与段联系起来,二者在含义上是一致的。分段机涉及4个关键内容:逻辑地址、段描述符(描述段的属性)、段描述符表(包含多个段描述符的“数组”)、段选择子(段寄存器,用于定位段描述符表中表项的索引)。转换逻辑地址(Logical Address,应用程序员看到的地址)到物理地址(Physical Address, 实际的物理内存地址)分以下两步:
分段地址转换:CPU把逻辑地址(由段选择子selector和段偏移offset组成)中的段选择子的内容作为段描述符表的索引,找到表中对应的段描述符,然后把段描述符中保存的段基址加上段偏移值,形成线性地址(Linear Address)。如果不启动分页存储管理机制,则线性地址等于物理地址。分页地址转换,这一步中把线性地址转换为物理地址。(注意:这一步是可选的,由操作系统决定是否需要。在后续试验中会涉及。
上述转换过程对于应用程序员来说是不可见的。线性地址空间由一维的线性地址构成,线性地址空间和物理地址空间对等。线性地址32位长,线性地址空间容量为4G字节。分段地址转换的基本过程如下图所示。

段描述符
在分段存储管理机制的保护模式下,每个段由如下三个参数进行定义:段基地址(Base Address)、段界限(Limit)和段属性(Attributes)。在ucore中的kern/mm/mmu.h中的struct segdesc 数据结构中有具体的定义。
段基地址:规定线性地址空间中段的起始地址。在80386保护模式下,段基地址长32位。因为基地址长度与寻址地址的长度相同,所以任何一个段都可以从32位线性地址空间中的任何一个字节开始,而不象实方式下规定的边界必须被16整除。
段界限:规定段的大小。在80386保护模式下,段界限用20位表示,而且段界限可以是以字节为单位或以4K字节为单位。
段属性:确定段的各种性质。
段属性中的粒度位(Granularity),用符号G标记。G=0表示段界限以字节位位单位,20位的界限可表示的范围是1字节至1M字节,增量为1字节;G=1表示段界限以4K字节为单位,于是20位的界限可表示的范围是4K字节至4G字节,增量为4K字节。
类型(TYPE):用于区别不同类型的描述符。可表示所描述的段是代码段还是数据段,所描述的段是否可读/写/执行,段的扩展方向等。
描述符特权级(Descriptor Privilege Level)(DPL):用来实现保护机制。
段存在位(Segment-Present bit):如果这一位为0,则此描述符为非法的,不能被用来实现地址转换。如果一个非法描述符被加载进一个段寄存器,处理器会立即产生异常。图5-4显示了当存在位为0时,描述符的格式。操作系统可以任意的使用被标识为可用(AVAILABLE)的位。
已访问位(Accessed bit):当处理器访问该段(当一个指向该段描述符的选择子被加载进一个段寄存器)时,将自动设置访问位。操作系统可清除该位。

查看源码

gedit bootasm.S

#start address should be 0:7c00, in real mode, the beginning address of the running bootloader
起始地址应为0:7c00,在实模式下,是正在运行的引导程序的起始地址

start:
.code16 # Assemble for 16-bit mode
cli # Disable interrupts
cld # String operations increment
CLI 全称 Clear Interupt 屏蔽中断,CLD 全称 Clear Director 字行块传送方向从低地址到高地址

启用A20:为了与最早的PC向后兼容,物理地址线20在低电平,因此地址高于1MB默认回零。 此代码撤消了此操作。

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

inb指令是给al寄存器做一个输入,test指令本质上和and一样,但是会置位ZF标志位,test结果为0时,ZF会置为1。jnz为jump not zero也就是ZF为0。上面的代码含义为使用8042键盘控制器控制A20,这样为切换到保护模式做好准备,使得32位地址线全部可用。

movl %cr0, %eax
orl $CR0_PE_ON, %eax
movl %eax, %cr0
进入保护模式,将CR0寄存器最低位PE置1

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
设置5个段寄存器与栈帧地址,进入bootmain继续执行

练习4:分析bootloader加载ELF格式的OS的过程
阅读bootmain.c源码
这是一个简单的启动装载程序,唯一的工作就是启动来自第一个IDE硬盘的ELF内核映像。

磁盘布局
此程序(bootasm.S和bootmain.c)是引导加载程序.应该存储在磁盘的第一个扇区中。第二个扇区开始保存内核映像。内核映像必须为ELF格式。

启动步骤
当CPU启动时,它将BIOS加载到内存中并执行,BIOS初始化设备,中断例程集以及读取引导设备的第一个扇区(例如,硬盘驱动器)进入内存并跳转到它。假设此引导加载程序存储在该引导程序的第一个扇区中控制从bootasm.S开始-设置保护模式,和一个堆栈,然后运行C代码,然后调用bootmain()该文件中的bootmain()会接管,读取内核并跳转到该内核。

static void
readsect(void *dst, uint32_t secno) {
// wait for disk to be ready
waitdisk();

outb(0x1F2, 1);                         // count = 1
outb(0x1F3, secno & 0xFF);
outb(0x1F4, (secno >> 8) & 0xFF);
outb(0x1F5, (secno >> 16) & 0xFF);
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);

}
在linux的驱动程序中,都会使用大量的outb、outw、inb、inw等等宏来訪问硬件或寄存器。outb() I/O 上写入 8 位数据 ( 1 字节 )。inb() I/O 上读入 8 位数据 ( 1 字节 )。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值