台风夜基本是不会睡觉的,写点有意思的。此时,2019年8月10日 2:20.
酷爱历史,于是在主音吉他手的推荐下,在历史的垃圾堆里找到了Freedos。这个从1994年开始的dos兼容开源操作系统。实际上,它就是dos,可以说它是MS-DOS的续命者。
非常令人震惊,2016年至今,这个Freedos竟然依然活跃。我以前竟然不知道这个。
我一直以为1994年和现在的201X年对于计算机这个特定的领域而言简直就是两个时代,中间的技术代差堪比希腊火,伏火雷这种和现代洲际导弹之间的差异,没想到1994年的系统,竟然在今天还能玩到原汁原味的延续下来的东西,不错!同样的1990年代初的Linux,你看看5.3和0.01之间的差异就知道了。
索性就装了一个。先是在VirtualBox上装的,后来改到了bochs。
对于可以执行指令的机器而言,最纯粹的玩法就是眼睁睁看着这些指令被执行,而不是去折腾什么库,SDK,中间件,编译器之类,这是原教旨主义的看法,当然,我只是说随便玩玩,不提赚钱,那是程序员的事。
我一直都想直接在一个屏幕上直接敲代码,然后机器给我反馈,但是总是被所谓 操作系统内核 所阻止,然后我改成了写内核模块,然而又被各种烦人的panic,oops等困扰,接下来重启一遍机器恢复一次环境需要一根烟的工夫。为了吃顿牛肉,养了一头牛,这简直糟糕透顶。
大多数时候我就想看看在一个指令序列执行后机器发生了什么变化,却需要安装并学习大量的工具,然后望而却步,空留了一个想法。说实话,我憎恨复杂的工具。
现如今的工具并不像它们声称的那般简单,相反,它们在故弄玄虚,显得复杂就是高端。
Freedos让事情变好了很多。它更纯粹!
Freedos自带的经典的debug程序,基本就是瑞士军刀了。我先用它写个hello程序,看看效果:
不用单独的汇编器,也不像往常那般先写一个文本的代码,然后编译器汇编器将它转成可执行的二进制文件,debug可以逐行汇编你的指令,然后直接执行。当然了,你也可以将它保存到文件里:
就是这么简单,而且你可以随时改二进制代码,先用 -u $段内地址 查看反汇编,然后想改哪个地址了,直接 -a $段内地址 修改即可了,非常方便。
我准备用debug将这个dos系统带入到保护模式,搞不好能将它变成Linux呢。
【dos把vmlinuz读入内存,准备好rootfs,jmp过去…】
这种想法并不奇怪,LinuxBIOS可以,Grub可以,Lilo可以,为什么dos不可以,无非就是一个控制权转交的问题而已。但不同的是,LinuxBIOS,Grub这些引导程序一开始在上电启动的第一步就已经进保护模式了,因为它们的目标非常明确,就是引导真正的系统,在保护模式下更安全编程更灵活,比如可以访问大内存,所以它们在启动时,无一例外都会一步跨过千年的历史,直接进入保护模式,就连Linux本身也是如此,进保护模式这件事基本都是刻录在512字节的引导扇区的,这是第一步要做的事。
dos与此不同,它本身就是一个实模式下的完整操作系统(Freedos可以使用保护模式,但我还是进实模式更好玩些),它在实模式下提供了一个操作系统应该提供的几乎所有功能,在特定的历史条件下,它没有进保护模式的动机,除非有人带它进。
这就需要 运行时进入保护模式 ,而不是引导时进入保护模式。
看看我写的最初的代码:
显然是错的,为什么呢?有我自己的原因也有debug程序的原因。
我自己显然是不怎么会编程的,我没有系统学过任何语言的语法规则,都是需要解决一个问题时慢慢死磕的,所以写出 jmp -1,jmp 8:b13e 这种代码也就不足为奇了。
此外,debug程序不支持标号,也可能是它支持,但我不知道怎么用,就当不支持吧,所以我必须把标号化作硬编码的地址,这也无可厚非,如果写对了,代码是可以运行产生预期结果的,但是很难写对,比如我上面程序的:
lgdt [b138]
这个就写错了。
在实模式下,程序里的地址都是逻辑地址,代码里是看不见段寄存器的,所以lgdt的操作数就应该是GDTR的相对地址,也就是 0018,而不是 B120<<4+0018=B138 这个地址,B138这个地址是机器负责转换的,就像分页保护模式下程序看到的都是虚拟地址,然后机器通过MMU机制负责转化为物理地址一样。
那么修改了上述错误呢?还是报错,机器直接崩溃。
是的,谭浩强式的崩溃!
【上大学时,谭浩强的C语言教程里说要是指针搞错了,就会系统崩溃,可是我怎么也不会把系统搞崩溃,顶多是程序崩溃。后来知道,原来我在保护模式下工作,如果在实模式下,比如原始dos系统,指针错了系统真的就会崩溃。谭浩强老师是对的】
所以说,我准备先用nasm汇编一个 正儿八经的汇编程序 。
所有的工具都在Linux下,我需要将Linux汇编而成的.com放进Freedos的磁盘中,有万种方法我都想不中,这使我不得开心颜。我不希望在Freedos里折腾网络。
VirtualBox很难操作虚拟磁盘,想从外部放进去一个文件不知道该怎么办,于是我换到了bochs,如果只是为了玩而不是为了用,那么bochs显然要比VBox好太多,这也是听了主音吉他手的建议。
【主音吉他手是一位猛士,就职于阿里巴巴,与就职于腾讯的温州皮鞋厂老板针锋相对!】
Ubuntu里装bochs显然要比Centos里装bochs方便很多,因为Ubuntu是自带桌面的。
【suse也是,但不很。我第一次接触ubuntu是在2007年冬天,姓赵的一位同事展示了ubuntu的立体桌面】
自带桌面当然自带X11,而Centos往往都是纯命令行操作,即便是有什么图形界面,我一般也都不装或者装了之后卸掉。
以下的命令可以初始化一个虚拟磁盘:
root@name-VirtualBox:~/bochs# bximage
========================================================================
bximage
Disk Image Creation / Conversion / Resize and Commit Tool for Bochs
$Id: bximage.cc 13481 2018-03-30 21:04:04Z vruppert $
========================================================================
1. Create new floppy or hard disk image
2. Convert hard disk image to other format (mode)
3. Resize hard disk image
4. Commit 'undoable' redolog to base image
5. Disk image info
0. Quit
Please choose one [0] 1
Create image
Do you want to create a floppy disk image or a hard disk image?
Please type hd or fd. [hd]
What kind of image should I create?
Please type flat, sparse, growing, vpc or vmware4. [flat]
Choose the size of hard disk sectors.
Please type 512, 1024 or 4096. [512]
Enter the hard disk size in megabytes, between 10 and 8257535
[10] 200
What should be the name of the image?
[c.img] freedos1.img
Creating hard disk image 'freedos1.img' with CHS=406/16/63 (sector size = 512)
The following line should appear in your bochsrc:
ata0-master: type=disk, path="freedos.img", mode=flat
root@name-VirtualBox:~/bochs#
然后将freedos.iso挂在cdrom上,将系统装入这个freedos.img虚拟磁盘:
# 编辑bochs配置文件,添加虚拟磁盘和虚拟光驱
ata0-master: type=disk, mode=flat, path=/home/name/bochs/freedos.img
# 一旦安装完成,注释掉虚拟光驱以表示断开
#ata0-slave: type=cdrom, path=/home/name/bochs/freedos.iso, status=inserted
接下来就可以通过以下命令挂载虚拟磁盘到Linux本地目录了:
mount -t msdos -o loop,offset=32256 freedos.img /mnt/dos
然后就可以随便拷贝文件进出了。
现在需要写一个汇编程序,实现进入保护模式的功能,我想从最最最简单的做起,先仅仅完成段式保护模式,先不分页。在进入保护模式后,执行死循环。我的汇编程序代码如下:
; COM程序在dos中的加载位置,这个必须写100h,不写的话会报错,我也是第一次知道
org 100h
;第一行指令跳转到GDT之下
jmp main
;GDT
gdt_start:
dd 0
dd 0
; 仅仅一个段,平坦模式
gdt_code:
dw 0xFFFF
dw 0
db 0
db 0b10011010 ;9a
db 0b11001111 ;cf
db 0
gdt_end:
;GDTR
gdtr:
dw gdt_end-gdt_start-1
dd gdt_start
;保护模式下的死循环代码
_loop_:
jmp $
;主程序
main:
; 必须手工自己计算地址:段<<4+offset
mov eax,cs
shl eax,4
add [gdtr+2],eax
add eax, _loop_
; 这里有个技巧,采用堆栈实现跨段长跳。这需要对jmp指令比较熟悉,进入保护模式的长跳需要重新加载段寄存器,所以需要的jmp指令为:
; 0x66 0xea $4字节的段内偏移 $2字节的段选择子
; 所以在jmp指令的操作数中要构造6个字节的数据,这里采用了堆栈
push dword 0x08
push eax
mov bx, sp
; 进入保护模式的核心3步骤:
; 1. 加载准备好的GDTR
; 2. 开启A20
; 3. 开启cr0保护模式
; 暂时不准备分页。
lgdt [gdtr]
cli
in al, 0x92
or al, 0x02
out 0x92, al
mov eax,cr0
or eax,1
mov cr0,eax
; 按照准备好的6字节地址和段选择子,执行66 ea长跳!
jmp dword far [bx]
用nasm编译之:
nasm -f bin rtime.asm -o pro.com
将这个pro.com放入freedos的虚拟磁盘,umount虚拟磁盘后开启bochs虚拟机,进入实模式dos系统,执行 pro.com,死循环!
然后,我尝试着将这个rtime.asm代码逐行手敲到debug的命令行,结果debug无法识别最后一步的 jmp dword far [bx]
于是准备将其等价转换一下,决定手工写指令,即写入:
db 0x66
db 0xea
dd $4字节偏移
dw 8
问题就在于这4字节偏移,这个偏移显然是在运行时才能算出来的,所以不能事先dd,需要在运行时填充,于是标号注之:
org 100h
jmp main
gdt_start:
dd 0
dd 0
gdt_code:
dw 0xFFFF
dw 0
db 0
db 0b10011010 ;9a
db 0b11001111 ;cf
db 0
gdt_end:
gdtr:
dw gdt_end-gdt_start-1
dd gdt_start
_loop_:
jmp $
main:
mov eax,cs
shl eax,4
mov ecx,eax
add [gdtr+2],eax
add eax, _loop_
; 填充4字节的偏移地址
add [addr], eax
lgdt [gdtr]
cli
in al, 0x92
or al, 0x02
out 0x92, al
mov eax,cr0
or eax,1
mov cr0,eax
; 直接插入指令的方式
db 0x66
db 0xea
addr:dd 0x00000000
dw 0x08
非常不错,这下可以手敲进debug了。但是由于debug程序里逐行汇编无法使用标号,所以需要把所有的标号转化为固定的地址偏移,于是如下:
org 100h
;jmp main
jmp 150
; 110:GDT载入110
gdt_start:
dd 0
dd 0
gdt_code:
dw 0xFFFF
dw 0
db 0
db 0b10011010 ;9a
db 0b11001111 ;cf
db 0
gdt_end:
; 130:GDTR载入130
gdtr:
dw gdt_end-gdt_start-1
dd gdt_start
; 140:32位程序载入140
_loop_:
jmp $
; 150:main载入150
main:
mov eax,cs
shl eax,4
mov ecx,eax
add [gdtr+2],eax
add eax, _loop_
; 填充4字节的偏移地址
add [addr], eax
lgdt [gdtr]
cli
in al, 0x92
or al, 0x02
out 0x92, al
mov eax,cr0
or eax,1
mov cr0,eax
jmp 190
; 190:为了确定addr的地址,这里将jmp far指令单独载入到固定的地址,短跳到达
db 0x66
db 0xea
; 192:addr自然就是192咯
addr:dd 0x00000000
dw 0x08
接下来就要把这些逐行敲入debug了:
每写完一段,可以用 -u $段内偏移 看一下对不对:
如果哪个地址的指令不对,可以用 -a $地址 来修改,比如你写指令的时候,加不加dword修饰符可能结果是不一样,如果真的不行就需要手工写入指令了,比如用db,dd,dw这种,或者直接用debug的 -e $地址 来写指令。
此外,还要注意的是,我们把标号化作了地址偏移,但是我们自己规定的130,140,150这种可能只是一厢情愿,由于有偏移问题,必须用 -u 指令反汇编确认,如果有问题,则需要用 -a 指令来修正。
一切结束后,用 -n -rcx 指令存文件,执行,死循环。这意味着成功了。
以上这些其实是要被主音吉他手耻笑鄙视的,因为原教旨主义者不使用nasm工具,只能用debug,然而我还是用了,其实我是先用nasm把程序调通,然后照着调通之后的程序的反汇编抄到debug命令行逐行汇编的,然后把抄的过程隐藏,就留下一个手工敲进debug命令行的指令的截图。
然后显得我好像是直接逐行汇编的,但其实把截图多截宽一点,旁边还有呢
哈哈。
本文只是做到了进入保护模式,没有做任何事情,这没有意义。
但其实,进入保护模式后,世界就是你的了,打印个字符串那是教科书式的,更没有意义,而且代码还非常冗长,所以我什么也不打印,只是执行 jmp $ 。
你可以再准备一个页表,然后开启分页,打印一些东西,然后呢…厄… 再准备一个页表,执行打印一些别的东西,厄…准备一个时钟中断处理程序,每一次时钟中断到来,CR3在两个页表间切换,同时save/restore寄存器上下文…厄…这就是个多任务现代操作系统了。
以为我会写下去吗?写上面这段话的内容?看上去很有吸引力,也很有成就感,但我不会去写的。
我面试过会写这些的人,也和很多会写这种的人纯粹的技术交流无关利益,貌似很懂操作系统的样子,能说明白段式,页式,段页式,平坦内存等等X86系列兼容处理器一系列复杂且恶心的特性,也确实有作品,所谓的《自己动手写操作系统》这种,从一个MBR引导进保护模式,交叉打印不同的字符,也许很多人都觉得,太厉害了…
然而他不懂如何评价进程调度算法,不知道LRU的实质,无法写出一个高效的并发链表,更不懂网络。操作系统的精髓不是X86处理如何引导进保护模式,操作系统的精髓是进程,内存,IO等一系列时间空间的分配和调度问题,这是个及其复杂的运筹学博弈问题,背后有复杂的博弈论,运筹学,哲学,算法等等看上去很虚但又很难企及的东西。
就不说那些博士才操心的算法方面的事了,同样是在工程领域,写个引导进保护模式交叉打印字符远不如写个LinuxBIOS,Grub这种引导其它程序的程序(而不是自己在那无聊地打印字符串)更加有意义。
那么本文的意义何在?
本文的意义在于 运行时进入保护模式 。这不同于机器刚上电系统启动就进入保护模式。不同点在哪里呢?系统初启时的内存是空的,这意味着引导程序可以将其他的代码任意设置位置,机器的状态寄存器也是唯一由引导程序决定的,可是运行时却不同,运行时的dos系统已经有一个内核在内存里了,替换它需要格外小心,幸亏dos是单任务的,否则便更加麻烦,不信你把Linux变身到Windows试试?鸠占鹊巢正在执行期间,万一来了个调度事件,就会全盘皆输。
所以运行时进保护模式会 更麻烦些 。
这就是本文的全部意义。
话说,既然一般的系统在上电初启时都会进入保护模式,为啥要有实模式?这又是Intel在搞事情。
先不回答这个问题,先看看 “既然有简单的分页并且工作的还那么好,为什么还要有分段?” 而且,大部分的操作系统,比如Linux,Windows都是例行公事用平坦模式绕开分段的,这么看来分段显得毫无意义。
这全是Intel为了兼容老旧处理器而搞的。也就是说,如果说最初的8086有一个机制或者特性,8088上就要支持,随后的286,386都要支持,P4,Core i5/i7大概率都要支持,即前一代的处理支持什么,后面的处理器大概率都要支持,最终的效果就是Intel处理器越发臃肿。
后果是什么?我就不说为操作系统的实现增加复杂性了,另一个重要的毒副作用就是为很多人炫耀技巧提供了及其广阔的空间,不然也就没人觉得能背下段寄存器格式的人很厉害了,毕竟本来无一物,何处惹尘埃。
Intel作为个人计算机独钟的微处理器提供商,推进了近40年个人计算机产业的发展,我们有目共睹的是,个人计算机从零到100的发展速度远远快于大型机,工作站以及服务器,这是因为个人计算机的需求是繁杂的,特别是游戏,互联网等等客户端App需要处理器在各个方面不断进化。与此相对,工作站,服务器则只需要保持高并发高吞吐即可。
个人计算机的不断进化不断复杂意味着向后兼容的压力非常大,但又不得不做,因此Intel处理器就成了现在的样子。
很多别的处理器一开始就是保护模式的,根本就没有实模式一说,这种专门迎合Multics/UNIX的设计,因为UNIX一开始就是进程保护隔离的,UNIX并非从CP/M这种及其简单的系统进化而来,它一开始就是设计出来的。
既然操作系统提供进程间内存的隔离保护,那么处理器做到即可,不多也不少,这种简洁的设计和Intel的风格是相对的。
写完了,2019年8月10日 5:40. 超强台风利奇马已经登陆,杭州风雨大作,但是雨量没有达到预期。迷茫一会儿。
现在7:40,独坐窗前看风起云涌,一家人都醒了,疯子在做早饭,小小在刷抖音,安安在学步车里欢呼,嘟嘟乱跑,我依然在作文。
超强台风即将登陆前的八小时,800019年8月9日 周五,下午六点,皮鞋厂工人都在等着下班。
温州皮鞋厂wu老板发了邮件:
由于此次台风非常强烈,登陆地点离本厂非常近,对本厂影响巨大,或对本厂业务产生严重影响!
于此,特公告,所有车间,所有工人,务必值守岗位,按照日常制度执行,若有急事需请假,电话务必保持24小时畅通,请随时处理紧急事务。
货单照常发货,值班邮件请查收,如遇突发事件按流程请报工伤。
经理与你同在!
– 皮鞋二厂/三厂,行政处
– 钦此!
【800019年8月10日,皮鞋厂厂房在台风登陆时轰然崩摧,wu老板发文,经理穿着湿皮鞋逃跑,若干工人death has no place】
杭州看不见了,因为在台风云下面…
浙江温州皮鞋湿,下雨进水不会胖!
皮鞋湿了,是祸,然而皮鞋不胖,是福报!
浙江温州皮鞋湿,下雨进水不会胖!