OS_lab3
OS_Lab3:从实模式到保护模式
要求
DDL:2025.4.6
提交内容:2+1个任务的代码和实验报告 传课程邮箱os_sysu_lab@163.com+实验报告pdf–提交到腾讯微云
材料的Example的代码放置在
src
目录下。
概述
学习如何从16位的实模式跳转到32位的保护模式,然后在平坦模式下运行32位系统。同时学习如何使用I/O端口和硬件交互,为后面保护模式编程打下基础
基础学习
突破512字节的限制
我们在之前学到了计算机在启动的最后只会自动加载MBR(512字节)到内存中运行,然而这个空间是很小的,所以在实际应用中,MBR只负责定义了一些基本信息,如磁盘大小、扇区大小等。
在运行操作系统前,需要将操作系统内核程序从外存加载到内存中,但这个空间大小无疑是较大的,因而在系统内核加载前,我们的MBR不再是输出“hello world”,而是将一段程序从外存加载到内存(大小无512Bytes限制)。这段程序(称为bootloader)在内存足够的前提下可以尽量大一点–可以实现:从实模式跳转到保护模式、加载操作系统内核等。
无论是MBR还是bootloader,其最开始都是存放在磁盘(外存)上的。而MBR和bootloader只有被加载到内存中才可以被执行,除了MBR外,计算机是不会去外存找程序执行的。MBR是在计算机启动时被计算机自动加载到0x7C00处执行。此后,计算机的任何行为都由我们的程序来控制。也就是说,我们需要自己从外存中加载程序到内存中运行。–学习如何读写硬盘!
LBA方式读写硬盘
读写I/O端口
- 硬盘属于外设,CPU和外设的交换通过I/O端口进行。
- I/O端口是一些寄存器,位于I/O接口电路中。当需要进行数据交换时,我们先将命令和数据放入到指定的I/O端口中,等待外设处理完后再从指定的端口取出处理结果。指定的端口也可以获取外设的状态。
(指定–数据交换的I/O端口已经被预先规定好了)
和寄存器编址很相似,每一个端口在I/O电路中都会被统一编址。
主硬盘分配的端口地址是0x1f0
0x1f7,从硬盘分配的端口地址是0x1700x177由于端口是独立编址的,因此我们无法使用mov指令来对端口赋值,可以使用in(读),out(写)指令。
值得注意的是:in指令的源操作数只能是立即数或dx,目的操作数只能是ax和al;out指令的源操作数只能是al或ax,目的操作数只能是立即数或dx
例子:
1
2
3
4
5
6
7
8
9
10
11
12; in指令
in al, 0x21 ; 表示从0x21端口读取一字节数据到al
in ax, 0x21 ; 表示从端口地址0x21读取1字节数据到al,从端口地址0x22读取1字节到ah
mov dx,0x379
in al, dx ; 从端口0x379读取1字节到al
; out指令
out 0x21, al ; 将al的值写入0x21端口
out 0x21, ax ; 将ax的值写入端口地址0x21开始的连续两个字节
mov dx, 0x378
out dx, ax ; 将ah和al分别写入端口0x379和0x378
读写硬盘
- 硬盘的读写是以块为单位的,块在硬盘中也被称为扇区,一般的扇区大小是512字节。
- LBA的全称是Logical Block Addressing, 逻辑块寻址模式。
- 硬盘的物理结构包括磁头、扇区和柱面(CHS),通过CHS就可以定位磁盘上的数据–在访问硬盘时手动指定磁头、磁道和柱面。
- 另一种方法:LBA模式。此模式下磁盘的地址空间被划分一个个的逻辑块,访问时只需要指定对应磁盘地址对应的逻辑块即可。
在实模式下,还有利用BIOS中断来读取硬盘的方式,称为CHS模式。需要手动将逻辑扇区号转化为磁盘对应的磁头、磁道和柱面,比较麻烦。这里不使用BIOS中断的原因是因为BIOS中断是16位程序,在保护模式下无法使用。有兴趣的同学可以自行探索“通过BIOS中断读取硬盘”
使用LBA读取硬盘的方式:
设置起始的逻辑扇区号。由于扇区的读写是连续的,因此只要给出第一个扇区的编号就好了。此处使用的是LBA28(28表示使用28位来表示逻辑扇区的编号)的方式来读取硬盘–但IO端口一次只能读取8位,所以要分四段写入端口。
- 逻辑扇区的0
7位被写入0x1F3端口,815位被写入0x1F4端口,16~23位被写入0x1F5端口,最后4位被写入0x1F6端口的低4位。
端口地址 数据位范围 描述 0x1F3 0~7 逻辑扇区号的低 8 位 0x1F4 8~15 逻辑扇区号的中间 8 位 0x1F5 16~23 逻辑扇区号的高 8 位 0x1F6 24~27 逻辑扇区号的最高 4 位 - 0x1F6的8个位表示如下
- 逻辑扇区的0
将要读取的扇区数量写入0x1F2端口。8位端口,每次最多只能读写255个扇区
向0x1F7端口写入0x20,请求硬盘读。
等待其他读写操作完成。
- 若在第四步中检测到其他操作已经完成,那么我们就可以正式从硬盘中读取数据。
1 |
|
保护模式
概述
保护模式,是一种在80286系列之后,基于x86架构的CPU操作模式。在80286及以后,保护模式的引入使得内存地址改为32位,程序至少可以访问到2^32=4G的内存空间
保护模式与实模式相比,主要有两个差别。
- 保护模式提供了段间的保护机制,防止程序间胡乱访问地址带来的问题。
- 保护模式访问的内存空间变大,32位地址线最大支持4G内存空间。
从实模式到保护模式
在保护模式下,所有的程序都会运行在自己的段中,一旦程序错误地访问其他段的地址空间,那么CPU就会产生异常来阻止程序访问。可以简单地理解为保护模式保护的是段地址空间,阻止程序越界访问。
CPU需要知道当前运行中程序的段地址空间信息,然后才能执行地址保护。段地址空间信息是通过段描述符(segment descriptor)来给出的,包含了段基地址(段的起始地址)、段界限(段的长度)等,共计64字节
- 段基地址。段基地址共32位,是段的起始地址,被拆分成三部分放置。
- G位。G表示粒度, G=0表示段界限以字节为单位, G=1表示段界限以4KB为单位。
- D/B位。D/B位是默认操作数的大小或默认堆栈指针的大小,在保护模式下,该位置为1,表示32位。
- L位。L位是 64 位代码段标志,由于这里我们使用的是32位的代码,所以L置0。
- AVL。AVL位是保留位。
- 段界限。段界限表示段的偏移地址范围,我们在后面详细讨论这个问题。
- P位。P位是段存在位, P=1表示段存在, P=0表示段不存在。
- DPL。DPL指明访问该段必须有的最低优先级,优先级从0-3依次降低,即0拥有最高优先级,3拥有最低优先级。
- S位。S位是描述符类型。S=0表示该段是系统段,S=1表示该段位代码段或数据段。
- TYPE。TYPE指示代码段或数据段的类型,如下所示。
第11位(X) | 第10位(E) | 第9位(W) | 第8位(A) | 含义 |
---|---|---|---|---|
0 | 0 | 0 | * | 只读,向上扩展 |
0 | 0 | 1 | * | 读写,向上扩展 |
0 | 1 | 0 | * | 只读,向下扩展 |
0 | 1 | 1 | * | 读写,向下扩展 |
1 | 0 | 0 | * | 只执行,非一致代码段 |
1 | 0 | 1 | * | 执行、可读,非一致代码段 |
1 | 1 | 0 | * | 只执行,一致代码段 |
1 | 1 | 1 | * | 执行、可读、一致代码段 |
A位表示是否被使用过,A=1表示使用,A=0表示未被使用,由CPU负责设置,我们不需要去管
向上扩展和向下扩展指的是段的线性基地址和段的线性尾地址的大小关系
保护模式的寻址过程:线性地址=base+offset
保护模式下,我们在指令中给出的都是偏移地址,偏移地址和段线性基地址相加后得到线性地址,线性地址通过地址变换部件MMU后得到实际的物理地址。
$$
物理地址=f(线性地址)
$$
但在此时我们并未开启分页机制,所以是恒等变换x=f(x) –>线性地址
对于一个向上扩展的段,如代码段和数据段,段界限给出的是最大的偏移量,寻址时满足下面的条件。
$$
0\le offset +length\le(段界限+1)*粒度
$$
对于一个向下扩展的段,如栈段,段界限给出的是最小的偏移量,寻址时满足如下条件。
$$
(段界限+1)*粒度\le offset-length\le\text{0xFFFFFFFF}
$$
在保护模式,所有段描述符都会被集中放置,这个集中放置的区域被称为全局描述符表(GDT)。
保护模式的内存地址扩展到32位,所以可以使用32位的寄存器。保护模式下段寄存器仍在使用,但保存的不再是段地址,而是段选择子(GDT的索引),用于指示给CPU寻址时使用的是哪个段。
- 第15-3位是段描述符的索引,表示选择子指向的段描述符是段描述符表中的第几个,编号从 0 开始。
- 第2位用来指示描述符表,0表示描述符表是 GDT。
- 第1-0位是请求特权级,特权级编号为 0-3,权限依次降低,0权限最高。
保护模式下的寻址:段地址+偏移地址–>表示为 选择子:偏移地址
每个段寄存器都会有一个64位的不可见的部分,这部分被称为描述符高速缓存器。当我们将选择子送入段寄存器时,CPU会自动从描述符表中加载对应的段描述符到描述符高速缓存器中。此后,当需要使用段寄存器时,CPU会直接从描述符高速缓存器中取出相应的内容,无需重新在描述符表中查找对应的段描述符。
CPU向下兼容,Intel 80286以后的CPU首先进入实模式,然后通过切换机制再进入到保护模式。也就是说在BIOS加电启动后,需要在实模式下的MBR中编写16位进入保护模式的代码,然后再跳转到保护模式,执行接下来的32位代码。
steps:
准备GDT,用lgdt指令加载GDTR信息。
打开第21根地址线。
实模式下,第21根地址线的值恒为0,使得当访问越界超过1MB时,自然溢出使得地址的值仍然小于1MB(取模)
所以要进入保护模式,需要打开第21根地址线–开关位于南桥芯片的端口A20,使用in、out指令可以对主板端口进行读写操作
1
2
3in al,0x92 ;南桥芯片内的端口
or al,0000_0010B
out 0x92,al ;打开A20
开启cr0的保护模式标志位。
真正的开关–CR0(32bit寄存器)包含了一系列用户控制CPU操作模式和运行的标志位
第0位是保护模式的开关位–PE位–PE位置1,CPU进入保护模式
1
2
3
4cli ;保护模式下中断机制尚未建立,应禁止中断
mov eax,cr0
or eax,1
mov cr0,eax ;设置PE位为1
远跳转,进入保护模式。
GDT的起始位置和大小由我们来确定,保存在寄存器GDTR中,GDTR:
全集描述符表边界–GDT的边界–段界限边界
$$
段描述符最大数量=\frac{2^{16}}{8}=8192
$$
每个段描述符是64bit(8字节)
Example1: bootloader的加载
内存地址安排:bootloader被安排在MBR之后,预留了5个扇区的空间
name | start | length | end |
---|---|---|---|
MBR | 0x7c00 | 0x200(512B) | 0x7e00 |
bootloader | 0x7e00 | 0xa00(512B *** 5**) | 0x8800 |
运行步骤:
编译bootloader后写入到硬盘起始编号为1的扇区,共有五个扇区。
- 编译:
nasm -f bin bootloader.asm -o bootloader.bin
- 加载:
dd if=bootloader.bin of=hd.img bs=512 count=5 seek=1 conv=notrunc
- 编译:
mbr也要重新编译并写入硬盘其实编号为0的扇区
- 编译:
nasm -f bin mbr.asm -o mbr.bin
- 加载:
dd if=mbr.bin of=hd.img bs=512 count=1 seek=0 conv=notrunc
回顾一下对应的参数设置:
- 编译:
运行
qemu-system-i386 -hda hd2.img -serial null -parallel stdio
运行结果:
Example2:进入保护模式
在进入保护模式之前,我们先对我们的内存地址进行规划。
Name | Start | Length | End |
---|---|---|---|
MBR | 0x7c00 | 0x200(512B) | 0x7e00 |
bootloader | 0x7e00 | 0xa00(512B * 5) | 0x8800 |
GDT | 0x8800 | 0x80(8B * 16) | 0x8880 |
- MBR自动加载到0x7c00
- bootloader在MBR中显式加载到0x7e00
- GDT显示加载到0x8800
equ
是汇编伪指令,e.g:编译器会在编译时将 LOADER_SECTOR_COUNT
出现的地方替换为5 (类似与宏
在操作系统内核设计的过程中,内存规划是一件令人苦恼的事情。从上面的例子可以看到,bootloader紧跟在MBR后面,GDT紧跟在bootloader后面,看起来非常紧凑。但是,只要其中一个发生变化,那么可能我们又要重新规划内存。也就是说,没有一种内存规划方案是完美的。
在bootloader中跳转到保护模式:
- 定义段描述符(代码段描述符、数据段描述符、栈段描述符和视频段描述符)
平坦模式:让代码段描述符、数据段描述符和栈段描述符中的段线性基地址为0 =>偏移地址和线性地址完全相同
段的存在是为了让CPU执行段保护,放置程序越界访问;
后续实验中会采用二级分页机制,此时页保护页可以组织程序越界访问 =》无需分段了
视频段描述符是显存所在的内存区域的段描述符。注意,GDT的第0个描述符必须是全0的描述符。接着,在GDT中依次放入0描述符,数据段描述符、堆栈段描述符、显存段描述符和代码段描述符
运行结果:
如何在gdb中使用info registers查看寄存器
- 要记得在qemu启动时加入
-s -S
参数:qemu-system-i386 -hda hd.img -serial null -parallel stdio -s -S
- 再启动gdb,并且连接端口
target remote:1234
- 在输入
info registers
可以查看
- 执行程序之后,发现寄存器发生了很大的变化
ps:
gdb指令 | 含义 | 实例 |
---|---|---|
break *adress或b *address | 在地址adress处设置断点。 | break *0x7c00 b *0x7c00 |
break symbol或b symbol | 在符号symbol处设置断点,例如symbol一般是函数名。 | break setup_kernel b setup_kernel |
break filename:line_number | 在文件filename处的第line_numer行设置断点 | b mbr.asm:12 |
add-symbol-file filename address | 加载符号表filename到地址address处 | add-symbol-file mbr.symbol 0x7c00 |
x/FMT address | address是内存地址,FMT格式是重复的单元个数+格式+大小。 重复的单元个数是一个数字,表示我们希望查看多少个单元。正数表示从address向后查看。负数表示从address向前查看。 格式是一个字符,可以是o(octal), x(hex), d(decimal), u(unsigned decimal), t(binary), f(float), a(address), i(instruction), c(char), s(string)。 大小是一个字符,可以是b(byte, 1 byte), h(halfword, 2 byte), w(word, 4 byte), g(giant, 8 bytes)。 | x/5xw 0x8032 x/10i 0x7c00 |
continue或c | 继续执行正在调试的程序到断点处暂停。 | |
step或s | 执行一条C语句,如果遇到函数调用语句,则会进入函数体中。 | |
next或n | 执行一条C语句,函数调用语句不会进入函数体,把函数当成一条语句执行。 | |
stepi或si | 执行一条汇编语句,如果遇到函数调用语句,则会进入函数体中。 | |
nexti或ni | 执行一条汇编语句,函数调用语句不会进入函数体,把函数当成一条语句执行。 | |
info registers | 查看所有寄存器的值 | |
layout layout_name | layout_name包含src,asm,split,regs。 src显示源代码窗口和命令窗口,asm显示汇编代码窗口和命令窗口,split显示源代码窗口、汇编代码窗口和命令窗口,regs显示寄存器窗口。 | layout split |
focus layout_window | 转换当前窗口到layout窗口,layout_window包含src,asm,regs,cmd。任何时刻gdb的当前窗口只有一个,并且使用方向键的效果只会在当前窗口处显示。 | focus cmd |
file symbol_file | 加载符号表,为gdb提供debug信息。 | file ../build/kernel.o |
set disassembly-flavor intel | 设置汇编代码格式为intel风格 | |
set architecture name | 设置指令对应的CPU架构,name包含i8086(16位),i386(32位) | set architecture i386 |
任务 1
1.1
复现Example 1,说说你是怎么做的并提供结果截图,也可以参考Ucore、Xv6等系统源码,实现自己的LBA方式的磁盘访问。
提示:部分需要的文件存放在src/example-1
下,请根据需要将其放置于自己创建的lab3文件夹下。
1.2
在Example1中,我们使用了LBA28的方式来读取硬盘。此时,我们只要给出逻辑扇区号即可,但需要手动去读取I/O端口。然而,BIOS提供了实模式下读取硬盘的中断,其不需要关心具体的I/O端口,只需要给出逻辑扇区号对应的磁头(Heads)、扇区(Sectors)和柱面(Cylinder)即可,又被称为CHS模式。现在,同学们需要==将LBA28读取硬盘的方式换成CHS读取==,同时给出逻辑扇区号向CHS的转换公式。最后说说你是怎么做的并提供结果截图。
勘误:《于渊:一个操作系统的实现2》P183-184给的是读取软盘的计算公式,而我们读取的是硬盘,因此不适用。参考资料变更如下。
其中,关键参数如下。
参数 | 数值 |
---|---|
驱动器号(DL寄存器) | 80h |
每磁道扇区数 | 63 |
每柱面磁头数(每柱面总的磁道数) | 18 |
LBA向CHS模式转换:
- LBA值需要通过运算间接的粗cylinder、head、sector这三个变量的值
术语
- cylinder:磁盘的柱面
- head:磁盘的磁头,每张磁片有两个磁头
- sector:磁盘扇区,这里指物理扇区,编号从 1 - 63,每条 track 的最大 sector 数 63
- SPT(sector_per_track):每磁道上的 sector 数
- HPC(head_per_cylinder):每个 cylinder 的 head 数量,这个数量应该是磁片数 * 2
LBA寻址
LBA的扇区从0开始计算。
==LBA = (cylinder * HPC + head) * SPT + sector - 1==
- 先计算track的数量,再加上物理扇区(从1开始编号),转换为LBA扇区需要-1
- track 的数量计算方式为:
cylinder * HPC(head_per_cylinder)+ head
LBA2CHS
- 计算cylinder:
cylinder=LBA/(SPT*HPC)
SPT*HPC
这部分计算的是每个cylinder有多少个扇区- 然后再计算在磁盘内需要多少个柱面表达
- 计算head:
head=(LBA/SPT)%HPC
- 在求出磁盘柱面的情况下,我们需要求得磁盘的磁头
LBA/SPT
这步在计算磁盘内有多少head- 然后再计算出在一个柱面中的磁头编号
- 计算sector:sector=LBA%SPT+1
LBA % SPT
这步计算可以得出逻辑的扇区号- 然后由于物理的扇区是从1开始的,所以要
+1
Resources:
代码实现之磁盘的 LBA 寻址转换为 CHS 寻址_lba和cly-CSDN博客
读取磁盘:CHS方式 - 猛练自然强 - 博客园
实现过程:
直接使用int 13h中断模式中的ah=02h功能(读扇区)
1 |
|
关键参数参考:
参数 | 数值 |
---|---|
驱动器号(DL寄存器) | 80h |
每磁道扇区数 | 63 |
每柱面磁头数(每柱面总的磁道数) | 18 |
扇区号(通过LBA地址+1得到物理地址CSH) | 2 |
柱面号 | 0 |
缓冲区地址(bootloader加载地址) | 0x7e00 |
代码:
1 |
|
结果截图:
任务 2
复现Example 2,使用gdb或其他debug工具在进入保护模式的4个重要步骤上设置断点,并结合代码、寄存器的内容等来分析这4个步骤,最后附上结果截图。gdb的使用可以参考appendix的“debug with gdb and qemu”部份。
提示:部分需要的文件存放在src/example-2
下,请根据需要将其放置于自己创建的lab3文件夹下。
复现
可以直接make run;
结果截图参见上面的Example2部分。
GDB调试方法
resources:
appendix/debug_with_gdb_and_qemu/README.md · young/sysu-2025-spring-operating-system - 码云 - 开源中国
Makefile(超详细一文读懂)-CSDN博客
回顾之前简单的调试:
- 在qemu启动的前提下,打开一个新的终端,并输入gdb进入gdb调试
- 在gdb下,连接已经启动的qemu进行调试:
target remote:1234
- 设置断点:
break func
- 输入
c
(continue)运行
生成符号表
- 可以使用gdb在debug过程中查看源代码=>提供相关的信息=>符号表(ELF格式)
- 制作符号表:
- 先编译汇编代码生成可重定向文件(ELF文件)
- 使用这个可重定向文件生成可执行文件和bin格式文件
以example-2为例:
- 删除
*.asm
中的org
语句,后续会在链接的过程中指定他们代码和数据的起始地址 - 编译mbr.asm生成.o文件 。
-g
参数是为了加上debug信息
1 |
|
- 为
mbr.o
文件指定起始地址为0x7c00
,分别链接生成可执行文件mbr.symbol
和mbr.bin
1 |
|
- 对bootloader.asm重复同样的工作
1 |
|
- 将mbr.bin和bootloader.bin都载入磁盘
1 |
|
执行完这些之后会发现生成了这些文件:
设置断点:
在mbr的第一条指令处设置断点
打开显示源代码的窗口:
加载MBR对应的符号表,可以在src窗口看到我们的源代码
==B+表示断点,白色窗口表示下一条要执行的指令==
窗口之间跳转:
注意,我们现在有两个窗口,一个是输入命令的cmd窗口,一个是显示源代码的src窗口。而方向键的效果只会在当前窗口起作用,并且当前窗口只有一个。在src窗口下,上下键的作用是上下滚动代码,在cmd窗口下,上键的作用是找到之前执行的命令。如果想要在两个窗口之前切换,可以使用focus
命令,例如切换当前窗口到cmd窗口。
设置断点
进入保护模式的四个步骤
- 准备GDT,用lgdt指令加载GDTR信息。
- 打开第21根地址线。
- 开启cr0的保护模式标志位。
- 远跳转,进入保护模式。
更加详细的基本调试思路
- qemu启动。我们先在一个Terminal下启动qemu,注意,qemu运行的参数需要加上
-s -S
参数,且在gdb启动之前不能关闭qemu。 - gdb启动。在另一个Terminal下启动gdb并连接上第1步启动的qemu。(如target remote:1234)
- 加载符号表。==符号表==会为gdb提供源代码和标识符等debug信息。
- 设置断点。gdb运行到我们设置的断点处会暂停,我们会在我们感兴趣的代码地址处设置断点,断点一般是我们认为bug出现的地方。
- 运行至断点处。使用命令跳过其他我们不感兴趣的代码,使代码一直执行到我们设置的断点处暂停。
- 查看寄存器或特定地址的值。我们可以在gdb暂停的地方查看寄存器或特定地址的值,并根据输出来判断前面执行的代码是否出现bug。
- 单步调试跟踪。gdb在断点处暂停后,我们可以一条一条语句地执行来跟踪程序的运行逻辑,gdb每执行条语句就会暂停。
- 重复3、4、5、6一直到bug解决。这个过程可能需要反复执行,但不一定是按照{3,4,5,6}{3,4,5,6}的顺序,可以是{3,4,6,5,6,6,6,5,3,4,5}{3,4,6,5,6,6,6,5,3,4,5}。
实验断点设置:
在mbr的初始地址设置断点:
键入c
之后,成功初始化
然后打开源代码窗口,并加载MBR对应的符号表
然后为bootloader设置断点,对应情况如下:
- 这里断点位置的查找(偏移量的计算)使用了
nasm -f bin -l bootloader.lst bootloader.asm
命令,可以生成一个对应的列表文件,会显示==每条指令的偏移量== - 另一种方法是使用反汇编(objdump)–输出结果会呈现在终端中
objdump -D -b binary -m i386 -M intel bootloader.bin
- 到达第一个起始点:
此时的寄存器内容:
栈顶指针指向0x7c00,下一条执行的指令
- 到达第二个断点–初始设置GDTR之前
寄存器内容:
- 第三个断点–GDTR设置后,打开第21根地址线之前
寄存器内容:
- 第四个断点–打开第21根地址线之后,设置保护模式PE位之前
- 第五个断点–设置PE位之后,进入保护模式之前
寄存器内容:
可以看到这里cr0位改变了,成功设置
进入保护模式前:
- 执行到最后(进入保护模式)
可以看到多呈现了一行输出–成功进入保护模式
Makefile
基本概念:
Makefile
是 make
命令所读取的配置文件,包含了构建项目的规则。其主要作用是检查项目文件的依赖关系,自动执行必要的命令,从而更新目标文件。一般来说,Makefile 主要包括以下三部分内容:
- 目标:需要生成的文件,例如可执行文件。
- 依赖:生成目标所依赖的文件或目标。
- 命令:构建目标时需要执行的命令。
基本语法:
1 |
|
- target:目标文件,可以是一个目标文件或一个动作名称(例如:
all
,clean
)。 - dependencies:生成目标所依赖的文件或其他目标。
- command:构建目标的命令,必须以
Tab
键开头。
变量的使用:
1 |
|
使用的时候:$(VarName)
例子:
1 |
|
常用的内置变量:
$@
:表示目标文件。$^
:表示所有的依赖文件。$<
:表示第一个依赖文件。
伪目标:一种==命令名称==
例子:
1 |
|
方式一:Makefile+直接编译链接(不推荐)
1 |
|
方式二:Makefile+编译+链接
1 |
|
方式三:Makefile+变量
知识点:
自定义变量:
- 变量名=变量值,如var=hello
预定义变量: - AR : 归档维护程序的名称,默认值为 ar
- CC : C 编译器的名称,默认值为 cc
- CXX : C++ 编译器的名称,默认值为 g++
- $@ : 目标的完整名称
- $< : 第一个依赖文件的名称
- $^: 所有的依赖文件
例子:
1 |
|
方式四:Makefile+模式匹配
方法五:Makefile + 函数
运行:
make
或make run
。使用qemu启动hd.img
,在此命令执行前应该执行make build
。make debug
。启动qemu并开启gdb调试。make build
。编译代码并写入hd.img
。make clean
。清除当前文件夹下以.bin
结尾的文件。(也可以自己设置,比如.o结尾,.exe结尾的文件等等)
gdb的命令也可以预先写到文件中,在启动gdb后会自动加载执行
比如:
1 |
|
任务 3(选做)
改造“Lab2-Assignment 4”为32位代码,即在保护模式后执行自定义的汇编程序。
resources:
x86保护模式——全局描述符表GDT详解_gdt全局描述符表 作用-CSDN博客
GDT介绍
GDT全称:全局描述符表
GDT的数据结构是一个描述符数组,每个描述符8个字节,可以存放在内存当中任意位置
实模式下的初始化代码,主要完成三件事:
- 初始化段描述符
- 初始化GDT的基址,并存放到GDTR寄存器
- 切换到保护模式(打开A20地址线,将CR0寄存器第0位设置为1),并跳转到负责打印字符串的代码段。
现在为止学到的:
从MBR加载bootloader,bootloader理论上是没有字节限制的,可以占据多个扇区,并完成很多功能,然后再将控制权交给操作系统
运行MBR是16位实模式,运行bootloader时通过执行对应的一组指令切换到32位保护模式,才开始加载内核进入内存,并且开始执行用户程序(本task要学习实现的)。
实现
参照原本的bootloader代码,
示例代码:
1 |
|
my_program:前面的部分和上面基本完全一样,下面只展示我的程序部分
- 初始化弹射字符变量–存储在内存中(保护模式下更倾向于使用内存变量而不是寄存器存储状态)
1 |
|
使用:mov <mem>, <con>
,将常数存储到==内存地址row==处,并通过byte
指定操作数大小为1字节。
2. 边界检查
1 |
|
这里将模板内存地址中存储的值取出到寄存器内,再进行比较。其余的内容差不多。
3. 显存位置计算
1 |
|
使用32位寄存器代替原本的16位寄存器。
4. 显存写入
区别是32位一次可以写入两个字节,所以我们分别对ah和al进行赋值之后,一起存储到gs中
1 |
|
- 位置更新
内存上的值要进行计算,需要提取到寄存器之后再计算。
1 |
|
- 延时实现
旧的代码中我使用的是bios int15h中断功能,但是这是实模式专属的,在保护模式无法使用。因此要切换成循环计数(纯软件延时)的方式(之前在实模式尝试过这种方式,但是延时效果比较差,应该是空间不够大)
1 |
|
整个程序:
1 |
|