陈莉君老师说:
张孝家同学在上研一,他有一个愿望,在西邮学习的这段时间,能够写出一个简单的操作系统,将学到的内核的理论知识(进程管理,内存管理,中断,文件系统,网络),都能都写进这个操作系统里面。 他说不论成败,能写多少就写多少,万一实现了呢,当我看到他说人没有梦想,和咸鱼有什么区别呢,我就想起每次上课时,他那双执着的求知的眼神。我们知道编译好的程序通常都放在像硬盘这样的载体上,需要加载到内存之后才能执行。这个过程并不简单,首先要读取硬盘,然后决定把它加载到内存的什么位置。最重要的是,程序通常是分段的,载入内存之后,还要重新计算段地址,这叫做段的重定位。
程序可以有千千万万,但加载过程却是固定的。在上篇文章中,我们编写了一个简单的加载器程序(它的功能是加载用户程序,并执行该程序,将处理器的控制权交给该程序),这篇我们写个简单的用户程序(功能是向屏幕显示信息)。然后将加载器的程序写入到主引导扇区,将用户程序也写入到硬盘中。最后,我们看看实验现象。
应用程序的代码如下:
;uesr program
;创建时间:2019/11/24
;=============================================
;功能:向显示器上面显示文本字符串信息
;程序的基本思路:
;算法:
; 1.先写出程序的基本框架
; 2.打印一个字符串的信息。
;============================================
;算法:
;1).首先判断要打印的字符是否为结束字符标志,如果不是继续2),否则,跳转到10)
;2).读取光标的位置信息
;3).判断要打印的字符是否为回车字符,如果不是,继续4),否则,跳转到11)
;4).判断要打印的字符是否为换行字符,如果不是,继续5),否则,跳转到12)
;5).将字符写入到相应的显存的位置(光标位置*2处的地方)
;6).计算下一个光标的具体位置(光标的位置加1)
;7).判断下一个光标位置是否超屏,如果没有,继续8),否则,跳转到13)
;8).设置下一个光标的位置。
;9).读取下一个字符的信息,在跳转到1)
;10).ret,退出调用程序
;11).计算出光标的行的头部的具体位置。跳转到8)
;12).将光标处的位置加上每行的字符的个数,计算下一个光标的具体位置,跳转到7)
;13).显存里面的数据,要搬移,腾出最后一行的位置,重新计算光标的位置,跳转到8)
;=============================================
;=============================================
;头部信息应该包含哪些:
1程序的大小,2程序的入口地址(偏移地址和段地址)
;3段重定位表项的个数,4段重定位表
SECTION header align=16 vstart=0
program_length: dd program_end ;程序的大小
program_entry: ;程序的入口地址
dw start
dd section.code1.start
segement_num: dw (header_end-code1_segement)/4
;段重定位表项的个数
code1_segement: dd section.code1.start ;段重定位表
code2_segement: dd section.code2.start
data1_segement: dd section.data1.start
data2_segement: dd section.data2.start
stack_segement: dd section.stack.start
header_end:
;=============================================
;code1段主要处理什么功能:
;1.在加载器跳转到此程序时,必须将本程序用的各个段寄存器初始化一下,才能够真正的运行本程序
;2.打印一个字符串的信息到显示器上面
;3.最后停留在本程序中,因为本程序执行完,没有其他程序的程序要执行,所以要逗留。(其他程序待加)
SECTION code1 align=16 vstart=0
put_string:
mov cl,[bx]
cmp cl,0 ;0
jz .exit ;10
call put_char
;9).读取下一个字符的信息,在跳转到1)
inc bx
jmp put_string ;9
;10).ret,退出调用程序
.exit:
ret
put_char:
push ax
push bx
push cx
push dx
push ds
push es
;2)获取光标的位置 ;2
mov dx,0x3d4
mov al,0x0e
out dx,al
mov dx,0x3d5
in al,dx
mov ah,al
;上面是高8位的获取
mov dx,0x3d4
mov al,0x0f
out dx,al
mov dx,0x3d5
in al,dx
;这边是低8位的获取
mov bx,ax
;3).判断要打印的字符是否为回车字符,如果不是,继续4),否则,跳转到11)
cmp cl,0x0d ;3
jz .eleven ;11
;4).判断要打印的字符是否为换行字符,如果不是,继续5),否则,跳转到12)
cmp cl,0x0a ;4
jz .twelve ;12
;5).将字符写入到相应的显存的位置(光标位置*2处的地方)
mov ax,0xb800
mov es,ax
shl bx,1
mov [es:bx],cl
;6).计算下一个光标的具体位置(光标的位置加1)
shr bx, 1
add bx,1
;7).判断下一个光标位置是否超出屏幕,如果没有,继续8),否则,跳转到13)
.seven: ;光标超出屏幕?滚屏
cmp bx,2000
jge .thirteen
;8).设置下一个光标的位置。
.eight:
mov dx,0x3d4
mov al,0x0e
out dx,al
mov dx,0x3d5
mov al,bh
out dx,al
mov dx,0x3d4
mov al,0x0f
out dx,al
mov dx,0x3d5
mov al,bl
out dx,al
pop es
pop ds
pop dx
pop cx
pop bx
pop ax
ret
;11 计算出光标的行的头部的具体位置。跳转到8)
.eleven:
mov bl,80
div bl
mul bl
mov bx,ax
jmp .eight
;12 将光标处的位置加上每行的字符的个数,计算下一个光标的具体位置,跳转到7)
.twelve:
add bx,80
jmp .seven
;13 显存里面的数据,要搬移,腾出最后一行的位置,重新计算光标的位置,跳转到8)
.thirteen:
mov ax,0xb800
mov ds,ax
mov es,ax
cld
mov si,0xa0
mov di,0x00
mov cx,1920
rep movsw
mov bx,3840 ;清除屏幕最底一行
mov cx,80
.clear:
mov word[es:bx],0x0720
add bx,2
loop .clear
mov bx,1920
jmp .eight
start:
;初始化栈段寄存器,和栈指针寄存器,其中保留256个字节,作用是用来隔离保护
mov ax,[stack_segement]
mov ss,ax
mov sp,stack_end
;代码段相关的寄存器并没有初始化,因为在加载器跳转的时候,就已经初始化好了
mov ax,ds
mov es,ax
;我不清楚上边实现的功能:ds的值给es,是否有问题,如果有问题的话,可以使用下面的语句代替
; push ds
; pop es
;下面是数据段和附加段的相关的寄存器的初始化
mov ax,[data1_segement]
mov ds,ax
; mov es,ax 我先不将es段寄存器也初始化和数据段寄存器一样的内容
;下面就是向屏幕打印第一个数据段的具体的字符串信息
mov bx,msg1
call put_string
;下面代码段的切换执行,这边我是为了加深理解retf,给自己加的一点难度,其实在code2中什么也没有实现。
push word [es:code2_segement]
mov ax,begin
push ax
retf
;可以看出虽然ret,retf经常和call,call far一起配对使用,但是他们并不是夫妻。
continue:
;注意打印不同的数据段,要将数据段寄存器置为相应的数据段的地址。这样才能够实现段的切换。
mov ax,[es:data2_segement]
mov ds,ax
mov bx,msg2
call put_string
jmp $
;=============================================
SECTION code2 align=16 vstart=0
begin:
push word [es:code1_segement]
mov ax,continue
push ax
retf
;=============================================
SECTION data1 align=16 vstart=0
msg1:
db ' This is NASM - the famous Netwide Assembler. '
db 'Back at SourceForge and in intensive development! '
db 0x0d,0x0a,0x0d,0x0a
db'
Example code for calculate 1+2+...+1000:',0x0d,0x0a,0x0d,0x0a
db ' xor dx,dx',0x0d,0x0a
db ' xor ax,ax',0x0d,0x0a
db ' xor cx,cx',0x0d,0x0a
db ' @@:',0x0d,0x0a
db ' inc cx',0x0d,0x0a
db ' add ax,cx',0x0d,0x0a
db ' adc dx,0',0x0d,0x0a
db ' inc cx',0x0d,0x0a
db ' cmp cx,1000',0x0d,0x0a
db ' jle @@',0x0d,0x0a
db ' ... ...(Some other codes)',0x0d,0x0a,0x0d,0x0a
db 0
;=============================================
SECTION data2 align=16 vstart=0
msg2:
db ' The above contents is written by xiaojia-zhang. '
db '2019-11-24'
db 0
;=============================================
SECTION stack align=16 vstart=0
resb 256
stack_end:
;=============================================
SECTION trail align=16
program_end:
应用程序的功能是:向显示器上面显示文本字符串信息。
编译生成.bin文件
在我代码中是将加载器的代码.bin文件写入到1号逻辑扇区,将应用程序写入到256号逻辑扇区。写入硬盘用到的工具是:
最后的,实验结果演示:
下面的这个是虚拟机virtualbox的实验现象
工具:
NASM编译器、fixvhdwr.exe、bochsdbg调试器、virtualbox虚拟机。
注意事项:
1.bochsdbg调试工具的使用,由于自己的操作不规范,产生了一个非常令人头疼的事情。
问题:怎么也加载不了我的虚拟硬盘,所有配置都是正确的
问题产生的原因是:bochsdbg时如果你是强退的话,而不是通过指令q退出。会在虚拟盘的那个目录下产生一个.lock后缀的文件(原因可能是调试的时候,读虚拟盘都会上锁,产生一个.lock后缀的文件,防止其他机器读取这个盘,可能是为了保护。如果强制退出,调试器不会把这个文件删除。所以下次在调试的时候,就没法读取这个盘了,被上锁了),只要将这个文件删除就好了,只要正常退出,就不会产生这个问题。
以下说明:我学习的是李忠的老师的书《x86汇编语言从实模式到保护模式》,我上面所做的东西,是我在学习的时候,不断还原书中的内容,上面我写的东西,基本上都是来自书中,代码是我自己敲出来的,并不断调试出来的(学习的过程)。东西并不是原创,我只是参考书籍和资料。
在上篇中,有个读者留言提出了一个问题,BIOS-ROM如果是一个I2C协议的EEPROM芯片的话,读取内容的话应该还要按照I2C的协议去处理,这部分I2C的协议处理代码是怎么被CPU读取的?还是说直接硬件实现了I2C的协议完成了串行到并行的译码?
作者是个小白(在读学生),以下是个人想法(纯粹猜测),如果不对,还希望指出共同学习。EEPROM芯片有并行和串行两种方式,(并行方式)EEPROM芯片作为BIOS-ROM
1.从效率上来讲,并行的速度比串行的快。如果说你的一个电子设备开机速度非常慢,给用户的感觉是不是很不好,用户会很不耐烦。厂家一直都追求开机的速度,因而,当然会采用并行的芯片
2.如图所示
cpu和内存地址空间的通信通过内部总线,是并行的。像显卡中的显存、高地址空间的ROM地址空间,都是直接内存映射到内存地址空间中,对它们的操作就如同直接操作主存RAM一样,通过地址线。(在文章中提到,Intel 8086可以访问1MB的内存空间,地址范围为0x00000到0xFFFFF但是设计者并没有将这1MB的空间全部用来访问DRAM(内存条),而是划分了几个部分,只不过大部分用于访问DRAM(掉电丢失),剩余的部分给了只读存储器ROM(掉电不丢失)和外围的板卡)
3.I2C串行的EEPROM一般是作为外部存储设备(例如at24c02),对这类要通过I2C总线来访问,根据时序图来编写代码,复杂程度明显比较高。像这类I/O设备都是独立编址,映射到I/O空间,而不是内存空间。通过端口寄存器来操作设备。
4.如果说用的是I2C协议的EEPROM的话,cpu跳转的物理地址(如文章中说的jmp 0xf000:0xe05b),根本没法实现,因为你要读取这个地址里面的内容,必须要通过I2C协议,然而,这是上电最初的时候,根本没有实现这样的代码(通过端口寄存,操作这个地址空间里面的内容)。
如果说真的有如你说的,那必须在硬件上做文章,必须将串行译码到并行。我想厂家不会为了这么个事情将cpu做的这么复杂,要做也是外部器件上做,而不会集成到cpu内部。cpu中有管脚实现I2C总线、SPI总线的,但是那些都是跟外设通信的(比如你说的I2C方式的EEPROM),cpu总线内部里面是有buffer缓冲器的,用来和外设通信的用的,不知道是不是你想。你应该比我懂,问的题目比我想的深(你应该是做驱动的),以上是学生的回答。