编程环境:Ubuntu Kylin 16.04
代码仓库:https://gitee.com/AprilSloan/linux0.11-project
工程结构:每一个目录对应于一章的内容,如chapter_01对应于第一章。每一章包含多个小节,如chapter_01/1st代表第一章第一节(简单的启动盘),所有的shell命令都默认当前目录为小节的目录。
linux0.11源码下载(不能直接编译,需进行修改)
原本linux0.11是用gas汇编编写bootloader,我认为大家对gas汇编不太熟悉,所以用Intel汇编编写bootloader。部分汇编知识和计算机知识并不会详细讲述,毕竟这是博客不是写书,还请大家见谅。
来写一个简单的启动盘吧。启动盘有两个最基本的要求,一是大小必须为512字节,二是最后两个字节必须是0x55和0xaa,不然这不会被识别为启动盘。
以下为bootsect.s的内容。
start:
jmp start ; 死循环
times 0x1fe - ($ - $$) db 0 ; 填写0,直到0x1fe
dw 0xaa55 ; 启动盘标识
第1、2行是死循环,第4行是让jmp指令后到0x1fe的空间全部填充为0。第五行是让地址0x1fe和0x1ff分别为0x55和0xaa。
利用如下指令编译汇编文件。(没有安装nasm的话,用sudo apt install nasm
安装nasm)
nasm boot/bootsect.s -o boot/bootsect.bin
此时,bootsect.bin就可以用来仿真调试了,但为了规范,我们还是将bootsect.bin写入软盘中,再用软盘启动。所以,我们要创建一个软盘。这里使用bximage创建软盘,安装bochs仿真器的过程中就会安装bximage,大家可以看看我的另一篇博客安装bochs:Linux下bochs的安装与使用,里面也有使用bochs的方法。
接下来要将bootsect.bin写入kernel.img中,使用如下命令:
dd if=boot/bootsect.bin of=kernel.img bs=512 count=1 conv=notrunc
现在就可以用kernel.img进行仿真调试了。但还别急,要使用bochs仿真还需要配置文件固定仿真使用的cpu、内存大小、软盘启动设置等等,这个文件在仓库中有,我命名为bochsrc,内容如下。
plugin_ctrl: unmapped=1, biosdev=1, speaker=1, extfpuirq=1, parallel=1, serial=1, iodebug=1
config_interface: textconfig
#使用GUI调试
display_library: x, options="gui_debug"
cpu: model=core2_penryn_t9600, count=1, ips=50000000, reset_on_triple_fault=1, ignore_bad_msrs=1, msrs="msrs.def"
cpu: cpuid_limit_winnt=0
cpuid: x86_64=1, mmx=1, sep=1, simd=sse4_2, apic=xapic, aes=1, movbe=1, xsave=1
cpuid: family=6, model=0x1a, stepping=5
romimage: file=$BXSHARE/BIOS-bochs-latest
vgaromimage: file=$BXSHARE/VGABIOS-lgpl-latest
vga: extension=vbe, update_freq=5
keyboard: type=mf, serial_delay=250, paste_delay=100000, user_shortcut=none
mouse: type=ps2, enabled=0, toggle=ctrl+mbutton
pci: enabled=1, chipset=i440fx
clock: sync=none, time0=local, rtc_sync=0
private_colormap: enabled=0
#软盘启动的配置,kernel.img为软盘名
floppya: type=1_44, 1_44=kernel.img, status=inserted, write_protected=0
ata0: enabled=1, ioaddr1=0x1f0, ioaddr2=0x3f0, irq=14
ata1: enabled=0, ioaddr1=0x170, ioaddr2=0x370, irq=15
ata2: enabled=0, ioaddr1=0x1e8, ioaddr2=0x3e0, irq=11
ata3: enabled=0, ioaddr1=0x168, ioaddr2=0x360, irq=9
# 硬盘启动配置,kernel.img为硬盘名,根据硬盘大小不同需改变后三个参数的值
#ata0-master: type=disk, mode=flat, path=kernel.img, cylinders=130, heads=16, spt=63
#ata0-slave: type=none
#ata0-master: type=none
#ata1-slave: type=none
# 软盘启动/硬盘启动
boot: floppy
# boot: disk
floppy_bootsig_check: disabled=0
log: -
logprefix: %t%e%d
panic: action=ask
error: action=report
info: action=report
debug: action=ignore
debugger_log: -
com1: enabled=1, mode=null
com2: enabled=0
com3: enabled=0
com4: enabled=0
parport1: enabled=1, file=none
parport2: enabled=0
speaker: enabled=1, mode=system
magic_break: enabled=0
print_timestamps: enabled=0
port_e9_hack: enabled=0
megs: 2048
好了,开始仿真调试吧!
bochs -qf bochsrc
输入以上命令会出现两个界面,如下所示:
第一个界面会显示操作系统的汇编代码,当前的寄存器数值,可以查看内存内容,输入指令控制程序运行。第二个程序是操作系统运行时用于显示的界面。
在第一个界面中分别输入以下两个指令:
b 0x7c00
c
第一个指令用于在0x7c00地址打断点,第二个指令让程序继续运行,程序会在断点处停止。界面会变成如下所示:
这是我们编写的代码。我们可以看到,程序被加载到了0x7c00地址处,那为什么是在这个地址呢?这是约定俗成的,电脑开机,会将磁盘的前512字节内容拷贝到0x7c00地址,如果这512字节不是以0x55和0xaa结束,会报错说找不到启动盘。
这一节的内容就到此为止了,下一节让我来打印Hello World吧。
学习编程必不可少的是什么?是语法或数据结构吗?不,是Hello World!
我们对bootsect.s进行修改。
start:
mov ax, 0x07c0
mov es, ax
mov dx, 0 ; 光标位置为(0,0)
mov cx, 16 ; 写16个字符
mov bx, 0x0007 ; 页面0,颜色模式7
mov bp, msg ; 字符串地址
mov ax, 0x1301 ; 写字符串,光标随之移动
int 0x10 ; 进入BIOS中断
jmp $ ; 死循环
msg: ; 要打印的字符串
db 13, 10
db "Hello World!"
db 13, 10
times 0x1fe - ($ - $$) db 0 ; 填写0,直到0x1fe
dw 0xaa55 ; 启动盘标识
这段程序打印Hello World!后进入死循环。虽然我把程序执行结果告诉你了,但你还是想知道第2-8行是什么意思对不对?推荐下载BIOS接口技术参考手册,这是我的资源,不收C币,免费下载。查阅手册,找到int 0x10下的如下内容:
虽然是英文的,但也不难。汇编程序中改变了ax,bx,cx,dx,es,bp等寄存器的值,改变这些寄存器的目的都在上图中指出。
最后得到的结果如图所示。
有没有觉得每次在终端里敲命令很繁琐?就不能一条命令就编译内核然后启动仿真器吗?当然可以!现在轮到脚本和Makefile出场了。
使用bximage制作软盘虽然只需要敲几下键盘就可以了,但本着能偷懒就偷懒的精神,我用脚本(在Makefile中出错了)实现了制作软盘的步骤,命名为mkimg.sh。
#!/bin/bash
RED_COLOR='\E[41m'
BLUE_COLOR='\E[44m'
RESET='\E[0m'
echo -e "${BLUE_COLOR}=== env check ===${RESET}"
if [ ! -e bochsrc ];then
echo -e "${RED_COLOR}=== no bochsrc ===${RESET}"
exit 1
fi
if [ ! -e /usr/local/bin/bochs ];then
echo "${RED_COLOR}=== no bochs ===${RESET}"
exit 1
fi
if [ ! -e /usr/local/bin/bximage ];then
echo "${RED_COLOR}=== no bximage ===${RESET}"
exit 1
fi
if [ -e kernel.img ]; then
rm kernel.img
fi
echo -e "1\nfd\n\nkernel.img\n" | bximage
我在这里面添加了不少提示性的信息,最后一句才是精华,好好品味吧。
Makefile要完成所有的工作,之后,一条make
指令就可以开始仿真调试了。
default: all
all: Image
Image: mkimg boot/bootsect.bin
dd if=boot/bootsect.bin of=kernel.img bs=512 count=1 conv=notrunc
bochs -qf bochsrc
boot/bootsect.bin: boot/bootsect.s
nasm boot/bootsect.s -o boot/bootsect.bin
mkimg:
./mkimg.sh
clean:
rm -rf boot/*.bin kernel.img
这个Makefile并不难理解(每次有人说**不难理解,我都想打人,我终究活成了自己讨厌的摸样:-),不做过多赘述。
这节内容到此结束,下节会详细讲讲启动盘的任务。
bootloader的作用是什么?它主要完成加载内核以及系统初始化,把加载内核的工作称为boot,把系统初始化称为loader。bootsect.s完成boot部分,setup.s完成loader部分。
系统一开始只会将bootsect.s的内容,而setup.s的内容就需要让bootsect.s加载到内存中。我们首先将bootsect.s的内容从0x7c00移动到0x90000,再将setup.s的内容加载到0x90200。前面0x00000~0x90000的空间之后会用来存放内核。这里我们假设setup.s的内容会占用4个扇区。
下面会小小地改动bootsect.s的内容。
SETUPLEN equ 4
BOOTSEG equ 0x07c0
INITSEG equ 0x9000
SETUPSEG equ 0x9020
start:
mov ax, BOOTSEG
mov ds, ax
mov ax, INITSEG
mov es, ax
mov cx, 256
sub si, si
sub di, di
rep
movsw ; 将bootsect.s从0x7c00移动到0x90000
jmp INITSEG:go
go: mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0xff00
load_setup:
mov dx, 0x00
mov cx, 0x02
mov bx, 0x0200
mov ax, 0x0200 + SETUPLEN
int 0x13 ; 加载setup.s到0x90200
jnc ok_load_setup
mov dx, 0x00
mov ax, 0x00
int 0x13
jmp load_setup ; 加载失败则复位软盘,重新加载
ok_load_setup:
mov ah, 0x03
xor bh, bh
int 0x10 ; 获取光标位置
mov cx, 24
mov bx, 0x0007
mov bp, msg
mov ax, 0x1301
int 0x10 ; 打印字符串
jmp SETUPSEG:0 ; 跳转到setup.s的内容
msg:
db 13, 10
db "Loading system ..."
db 13, 10, 13, 10
times 0x1fe - ($ - $$) db 0 ; 填写0,直到0x1fe
dw 0xaa55 ; 启动盘标识
这个内容不算多吧。。。前4行定义了4个宏定义,方便以后修改或维护。第7~15行将移动bootsect.s的内容,rep movsw
会将[ds:si]地址的两个字节移动到[es:di]处,每次移动si和di都加1,一共移动cx次。然后是跳转到移动后的地址。
跳转后要重新设置段寄存器和sp,这些寄存器在之后都会用到,sp的值只要远大于512即可。
做完这些就开始加载setup.s的内容,setup.s的内容保存在第2~5扇区内容。查BIOS接口技术参考手册的int 0x13可以知道:
dh=0,dl=0,ch=0,cl=2代表第2扇区,al=4代表读取4扇区,将内容读到es:bx(0x9000:0x200)地址处。读取成功,CF=0,跳转到ok_load_setup;读取失败,CF=1,复位软盘,重新读取扇区。
下面是读取光标位置到dx寄存器中。在第2节中,打印Hello World的地方原本有字符,不大好看。读取光标位置后打印字符串会打印在空白地区,更好看一点。
打印字符串的程序之前已经说过,就不多说了。打印Loading system …之后就会跳转到setup.s的内容中。
这次把setup.s写简单一点,还是一个死循环。
start:
jmp start
弄完bootsect.s和setup.s后,就开始更改Makefile。
default: all
all: Image
Image: mkimg boot/bootsect.bin boot/setup.bin
dd if=boot/bootsect.bin of=kernel.img bs=512 count=1 conv=notrunc
dd if=boot/setup.bin of=kernel.img bs=512 count=4 seek=1 conv=notrunc
bochs -qf bochsrc
boot/bootsect.bin: boot/bootsect.s
nasm boot/bootsect.s -o boot/bootsect.bin
boot/setup.bin: boot/setup.s
nasm boot/setup.s -o boot/setup.bin
mkimg:
./mkimg.sh
clean:
rm -rf boot/*.bin kernel.img
dd指令的使用可以在linux中使用dd --help
进行查看,其余就没什么好讲的了。
那就开始仿真调试吧。
make完运行了jmp INITSEG:go
指令之后,地址发生了变化。
我们可以一直使用n
命令一步一步运行。
这次打印的字符串明显要更好一些。
咦?说好的setup.s的内容呢?别慌,点击上面的Refresh键。
这次可以看到死循环了。
这一节的内容结束了,下一节会开始完善setup.s,bootsect.s的内容会在开始写内核的时候再完善。
setup.s要本要完成移动内核到指定位置的任务,但现在还没有写内核,就只完成初始化的功能就可以了。
目前,CPU还处于16位模式,能够使用的寄存器也都是16位的,而我们接下来要进入32位保护模式,setup.s会为进入32位做准备。(我们都知道32位比16位好,那这是为什么呢?)
要进入保护模式需要做什么呢?
最后还需要跳转到保护模式的地址,但由于没写内核代码,这步将由死循环代替(老惯例了)。
以下是修改后的setup.s的代码:
INITSEG equ 0x9000
SYSSEG equ 0x1000
SETUPSEG equ 0x9020
cli ; 保护模式下中断机制尚未建立,应禁止中断
start:
mov ax, SETUPSEG
mov ds, ax
lgdt [gdt_48]
mov al, 2
out 0x92, al
mov ax, 0x0001
lmsw ax
jmp $
gdt:
dw 0, 0, 0, 0
dw 0x07ff, 0x0000, 0x9a00, 0x00c0
dw 0x07ff, 0x0000, 0x9200, 0x00c0
gdt_48:
dw 0x800
dw 512 + gdt, 0x9
gdt包含操作系统内存分段管理的相关知识,在setup.s中只是临时设置gdt,在内核中会重新设置gdt。gdt的结构如下所示。如果对gdt不感兴趣就跳过下面的图表吧:-)
gdt结构图
gdt各字段解释
gdt各字段 | 意义 |
---|---|
基地址 | 段在内存中的起始地址 |
段界限 | 段长度=(段界限+1)*段界限单位(段界限单位与下方的G字段有关) |
G | 粒度位,用于解释段界限的含义。G=0,段界限以字节为单位,段的扩展范围为1B到1MB(描述符的界限值为20位);G=1,段界限以4KB为单位,段的扩展范围为4KB到4GB |
D/B | 默认的操作数大小。主要是为了能够在32位处理器上兼容运行16位保护模式的程序。D/B=0,表示指令中的偏移地址或者操作数是16位的;D/B=1,表示指令中的偏移地址或者操作数是32位的。 |
L | 64位代码段标志,保留此位给64位处理器使用,目前将此位置0即可。 |
AVL | 软件可利用位。80386对该位未作规定,且与80386兼容的处理器都不会对该位的使用做任何规定。 |
P | 段存在位,用于表示描述符所对应的段是否存在。P=0,表示段不在内存中。P=1,表示段在内存中。 |
DPL | 表示描述符的特权级。处理器支持的特权级别有4种,分别是0,1,2,3,其中0是最高特权级别。 |
S | 用于指定描述符的类型。S=0,表示这是一个系统段;S=1,表示这是一个代码段或数据段(堆栈段也是特殊的数据段)。 |
TYPE | 由于介绍过长,放在下面解释 |
gdt中TYPE字段的介绍
数据段 | 代码段 | ||||||||
---|---|---|---|---|---|---|---|---|---|
X | E | W | A | 含义 | X | C | R | A | 含义 |
0 | 0 | 0 | X | 只读 | 1 | 0 | 0 | X | 只执行 |
0 | 0 | 1 | X | 读和写 | 1 | 0 | 1 | X | 读和可执行 |
0 | 1 | 0 | X | 只读,向下扩展 | 1 | 1 | 0 | X | 只执行,依从的代码段 |
0 | 1 | 1 | X | 读写,向下扩展 | 1 | 1 | 1 | X | 可执行,读,依从的代码段 |
上表中第2行各字母的含义
字母 | 含义 |
---|---|
X | 表示是否可执行。X=0,表示不可执行,数据段总是不可执行的;X=1,表示可执行,代码段总是可执行的。 |
E | 段的扩展方向。E=0,向上扩展,即向高地址方向扩展,是普通的数据段;E=1,向下扩展,即向低地址方向扩展,通常是堆栈段。 |
W | 段的写属性。W=0,不允许写入,此时写入的话会引发异常中断;W=1,允许写入。 |
A | 已访问位。用于表示它指向的段最近是否被访问过。在描述符创建的时候应该清0。之后每当该段被访问时,处理器将该位置1。 |
C | 段特权级依从。C=0,表示非依从的代码段,可以与特权级相同的代码段调用,或者通过门调用;C=1,表示运行从低特权级的程序转移到该段执行。 |
R | 段是否允许读出。R=0,表示代码段不能读出,此时读出会引发处理器异常中断;R=1,表示代码段可以读出。 |
通过上面的图表,我们可以知道代码第18-20行都干了什么。每个gdt占8个字节,所以每一行就是一个gdt。第一个gdt内容必须是0,对应于第18行代码。第二个gdt代表系统代码段,这8个字符的意思是:该段在内存的起始地址是0,段界限为0x7FF,段界限以4KB为单位,段长度为8MB,该段指令中的偏移地址或者操作数是32位的,段在内存中,特权级为0,这是一个代码段或数据段,该段可读可执行。这么介绍确实有些繁琐,不过应该挺易懂的。第三个gdt代表系统数据段,相关内容还请各位自行观看。
第8行代码是加载gdt的地址及长度到gdtr中。LGDT指令是将源操作数中的值加载到全局描述符表格寄存器(GDTR)。源操作数指定6字节内存位置,它包括了GDT的基址和界限。如果操作数大小属性是32位,则将16位限制(操作数的2个低位字节)与32位基址(操作数的4个高位字节)加载到寄存器。第24行的基址也可表示为0x90200+gdt或0x9020:gdt。
第10-11行是为了开启A20,这样就可以访问1MB以上的内容地址了。关于A20的更多知识请自行百度。开启A20的方法有几种,这里使用的是最简单的方法,与linux0.11的代码并不相同。
第13-14行是为了设置cr0寄存器的PE位为1(PE位位于cr0寄存器的bit0)。这段代码与如下代码的意思相同。
mov ax, cr0
or ax, 1
mov cr0, ax
下面来运行一下代码吧。
这好像没什么可以展示的啊。在运行了lgdt和lmsw指令之后可以看看gdtr和cr0寄存器的变化。
进入保护模式的准备已经做好了,下面就可以写内核,但是写完内核就需要更新bootsect.s和setup.s的内容,好复杂啊。。。
这章内容有点多,感觉应该写得更细一点,但写多了又感觉会变得很臃肿,就这样了,下章的内容也不少啊。
文章浏览阅读5.1k次,点赞2次,收藏7次。一. RoseHA的工作原理 RoseHA双机系统的两台服务器(主机)都与磁盘阵列(共享存储)系统直接连接,用户的操作系统、应用软件和RoseHA高可用软件分别安装在两台主机上,数据库等共享数据存放在存储系统上,两台主机之间通过私用心跳网络连接。配置好的系统主机开始工作后,RoseHA软件开始监控系统,通过私用网络传递的心跳信息,每台主机上的RoseHA软件都可监控另一台主机的状态。当工作主机..._rose主备切换
文章浏览阅读2.7k次。一、前言上2个章节已经实现了mysql和MongoDB的多租户切实现方案,本章将继续学习Redis的多数据源切换。Redis服务器默认有16个database,我们可以将每个租户的数据放到其中一个database中,也可以部署多台Redis服务器,每个租户使用一个Redis服务器,也可以把两者结合起来,Redis服务器部署多台,先在一台的16个Database上放,放满了16个Database然后再往下一台Redis服务器上放。这种方式需要有一个MySQL数据库表存储每台Redis服务器的Databa
文章浏览阅读2.3k次。win10触摸键盘通过::SendMessage隐藏方式没有效果HWND hWnd = ::FindWindow(L"OSKMainClass", NULL);if ( hWnd ){::SendMessage(hWnd, WM_SYSCOMMAND, SC_MINIMIZE, 0);} win10触摸键盘无法找到状态窗口状态,isWidowsVisible,GetWindowPlacement,GetWindowLong,状态没有变化 ..._tabtip
文章浏览阅读1.5k次。9月25日 通过波形图比较声音的特性中考频度:★★☆☆☆难易程度:★☆☆☆☆(2019·广东初二期末)把频率为256 Hz音叉发出的声音信号输入示波器,示波器展现的波形如图甲所示。若把频率为512 Hz音叉发出的声音信号输入同一设备的示波器,其波形可能图中的__________。【参考答案】丁【试题解析】由题知,原来音叉发出声音的频率为256 Hz,现在音叉发出声音的频率为512 Hz..._规格为256hz音叉声音波形如图所示,将512hz音叉的声音输入同一设置的示波器后,其波
文章浏览阅读8.1k次。前两天刚在win10中安装完SqlServer 2008 R2 ,安装步骤可以参见这篇文章,装完之后打开 Sql Management Studio,登陆、查询、用Navicat连接都没有问题,于是开始转移以前的项目到本机。然后就出现问题了,在访问项目的时候只要是关于查询数据库的程序全都卡半天,最后报个(org.hibernate.exception.GenericJDBCException: C..._msvcr120 sqlservr windows 无非是访问
文章浏览阅读5.3w次,点赞2次,收藏16次。本文详细介绍了 JSON 文件格式化的方法。通过深入探讨,文中提供了多种有效的方式来对 JSON 文件进行格式化,以提高其可读性和可维护性。这些方法涵盖了使用特定工具或的相关技巧和要点。读者可以从中了解到如何快速、准确地对 JSON 文件进行格式化,以便更好地理解和处理其中的数据。_json格式化
文章浏览阅读499次。数学:求欧拉函数算法模板求欧拉函数求欧拉函数int phi(int x){ int res = x; for (int i = 2; i <= x / i; i ++ ) if (x % i == 0) { res = res / i * (i - 1); while (x % i == 0) x /= i; } if (x > 1) res = res / x * .
文章浏览阅读8.8k次,点赞3次,收藏8次。横纵坐标字体大小调节:通过fontsize可以进行调节ax1.set_ylabel("AUC",fontsize=20)ax2.set_ylabel("Logloss",fontsize=20)图例字体大小调节:在plt.legend中加一个prop={"size":18,"weight":"black"}即可_matplotlib绘图横纵坐标设置
文章浏览阅读1w次,点赞2次,收藏19次。作为前端开发者,我们肯定都使用过非常多的jQuery插件,毋庸置疑,jQuery非常流行,尤其是结合HTML5和CSS3以后,让这些jQuery插件有了更多地动画效果,更为绚丽多彩。下面分享了一些超炫酷的jQuery/HTML5应用,一起来看看。1、HTML5/CSS3一组可爱的3D按钮这是一款利用HTML5和CSS3制作而成的按钮组合,这款CSS按钮非常具有个性化。该CSS3按钮_html5 特效按钮
文章浏览阅读1.8w次,点赞4次,收藏43次。参考连接: link.1、查看树莓派蓝牙开启状态_树莓派连接蓝牙耳机并使用麦克风
文章浏览阅读3.3k次,点赞3次,收藏12次。介绍了输入(input)、输出(print),及字符串格式化(F-string、format与%)方式_%s.%d' %()
文章浏览阅读9.3k次。问题:EL表达式比较字符串或是数字格式的数值是否相等,为true,却不执行为true时的代码。示例:true原因:有可能是test="${ 1 == 1}(这里多个空格)",即大括号与双引号之间多了空格,这个时候,就不会打印true。去掉多余的空格就可以了_el 表达式 判断字符串和数字相等