一直以来,我们在往屏幕上输出文本时,要么利用bios中断,要么利用系统调用,这些都是依赖别人的方法。咱们还用过一个稍微有点独立的方法,就是直接写显存,但这貌似又没什么含量。如今我们要写一个打印函数了,似乎,我们马上就要站起来了
之前我们讲述了有关显卡的知识,但当时怕影响兄弟们的学习积极性,我们并没有说把有关显卡的寄存器罗列出来。话说,出来混早晚要还的,躲得过初一躲不过十五。如今我们需要通过端口来控制显卡的行为,有些问题还是要面对的。
之前咱们对显卡的操作和对普通内存操作是一样的,打印字符时,就是往显存中mov一些字符的ascii码和属性,那还是我们在显存默认的文本模式下。您想,我们都爱看视频、电影,话说十年前第一次看到DVD版本的电影时我都被震撼到了,当时看的是《星河战队》,清晰到毛发可见的程度,何况大家现在都偏爱蓝光高清版本,咳咳,说远了,总之能够让我们看到如此炫丽的画面,这都是显卡的功劳,这说明显卡还可以工作在彩色图形模式。对于显卡的操作可不是咱们之前的mov来mov去就行了。不过我们也并不需要那么复杂的功能,咱们还是在80*25的文本模式下转悠,而且还只是简单的操作。
之前我们已经对硬盘有过端口操作了,无非就是用in和out指令加不同的端口号,对显卡也是如此。显卡中的寄存器很多,不,是非常多,这里按照它们在图形管线(位于cpu和video之间)中的位置的顺序给大家介绍下,见下表
如您所见,表中列出的寄存器的数量似乎没我说的那么恐怖,不要高兴的太早,马上就要让大伙儿难过了,其实这些只是寄存器的目录而已,这有没有让大家想起了周星驰主演的电影《鹿鼎记》中,天地会总舵主陈近南让韦小宝练武功时的场景,拿出了一本不算太厚的“武功秘密”,起初小宝还很高兴,但陈近南告诉他这只是个目录,而且是练了之后才九死一生,否则就十死无生^_^。
好了,下节到解释,本节到此,现来玩哦。
接上文,请见“一步步编写操作系统 71 直接操作显卡,编写自己的打印函数1” 下面解释下显卡寄存器的内容。
以上所说的目录其实就是寄存器分组,在这些寄存器中也不全是分组。前四组寄存器属于分组,它们有一个特征,就是被分成了两类寄存器,即Address Register和Data Register。这两个寄存器是干吗的呢?这得先从寄存器为什么要分成组开始说。
端口实际上就是IO接口电路上的寄存器,为了能访问到这些cpu外部的寄存器,计算机系统为这些寄存器统一编址,一个寄存器被赋予一个地址,这些地址可不是我们所说的内存地址,内存地址是用来访问内存用的,其范围取决于地址总线的宽度,而寄存器的地址范围是0~65535(Intel系统)。这些地址就是我们所说的端口号,用专门的IO指令in和out来读写这些寄存器。至于计算机内部访问端口怎么实现的,这是硬件工程师的事,咱们暂且奉行拿来主义,认同这个事实就够了。
IO接口电路上的寄存器数量有多有少,这要看具体的外设了,我这么说您就明白了,这里给寄存器分组的原因是,显卡(显示器的IO接口电路)上的寄存器太多了,如果一个寄存器就要占用一个系统端口的话,这得多浪费硬件资源,万一别的硬件也这么干,这63336个地址可就捉襟见肘了。所以计算机系统说了,我不管你们内部有多少寄存器,给你们的端口地址是有数的,你们自己内部协调吧。
计算机工程师是非常聪明的,把数据结构中数组的知识用到了硬件中。他们把每一个寄存器分组视为一个寄存器数组,提供个寄存器用于指定数组下标,再提供个寄存器用于对索引所指向的数组元素(也就是寄存器)进行输入输出操作。这样用这两个寄存器就能够定位寄存器数组中的任何寄存器啦。
这两个寄存器就是各组中的Address Register和Data Register。Address Register做为数组的索引(下标),Data Register做为寄存器数组中该索引对应的寄存器,它相当于所对应的寄存器的窗口,往此窗口读写的数据都作用在索引所对应的寄存器上。
所以,对这类分组的寄存器操作方法是,先在Address Register中指定寄存器的索引值,用来确定所操作的寄存器是哪个,然后在Data Register寄存器中对所索引的寄存器进行读写操作。
上面CRT Controller Registers寄存器组中的Address Register和Data Register的端口地址有些特殊,它的端口地址并不固定,具体值取决于Miscellaneous Output Register寄存器中的Input/Output Address Select字段,现在咱们看一下这个寄存器。
和大家坦白一点,显卡参数还需要专业人士来解释,由于咱们用不到这么高深的设置,加之我对显卡没有深入学习,所以这里面有好多参数术语,我不敢随意翻译成中文,担心误导大家,所以我直接把此寄存器各字段的英文描述搬过来了,至于中文的意思,大家仁者见仁智者见智吧,请您见谅。
好了,简直了,就这样吧,晚安。
万事开头难,我们先从简单的打印字符开始。这个功能类似c语言中的putchar,每次只打印一个字符,由于此函数咱们是在内核中实现的,暂且将其命名为put_char。
在这之前,为了开发方便,我们定义一些数据类型。主要是参考了linux的/usr/include/stdint.h文件,有环境的同学可以自行看下,没环境的同学,请看图
该文件在我目前的linux版本上是320行,这里只是冰山一角,里面各种宏显得好高大上啊,不过请放心,把这个图贴出来就是为了“吓唬”大家的^_^,咱们不会写这么复杂,不信请看代码:
1 #ifndef __LIB_STDINT_H2 #define __LIB_STDINT_H3 typedef signed char int8_t;4 typedef signed short int int16_t;5 typedef signed int int32_t;6 typedef signed long long int int64_t;7 typedef unsigned char uint8_t;8 typedef unsigned short int uint16_t;9 typedef unsigned int uint32_t;
10 typedef unsigned long long int uint64_t;
11 #endif
怎么样,确实是很简单吧。以后我们采用的任何数据类型就要用这些定义好的啦。估计大家也注意到啦,咱们定义的stdint.h文件位于lib目录下,也就是说我新建了个lib目录做来专门存放各种库文件。不仅如此,在lib目录下还建立了user和kernel两个子目录,以后供内核使用的库文件就放在lib/kernel/下,lib/user/中是用户进程使用的库文件。
我们要实现的字符打印函数叫put_char,它是用汇编语言写的。因为要和显卡打交道啦,里面涉及到端口的读写操作,目前还是用纯汇编文件较方便,以后慢慢发展起来后,咱们会采取内联汇编的方式。
直接上代码啦,我们的打印函数统统在print.S文件中完成,该文件是各种打印函数的核心,重中之重,这里先给大家介绍下它的处理流程:
- 备份寄存器现场。
- 获取光标坐标值,光标坐标值是下一个可打印字符的位置。
- 获取待打印的字符。
- 判断字符是否为控制字符,若是回车符、换行符、退格符三种控制字符之一,则进入相应的处理流程。否则,其余字符都被粗暴地认为是可见字符,进入输出流程处理。
- 判断是否需要滚屏
- 更新光标坐标值,使其指向下一个打印字符的位置。
- 恢复寄存器现场,退出。
该文件相对来说又有点长,故需要将其拆分成3部分,先给大伙儿呈上其第一部分,代码:
1 TI_GDT equ 02 RPL0 equ 03 SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL045 [bits 32]6 section .text7 ;------------------------ put_char -----------------------------8 ;功能描述:把栈中的1个字符写入光标所在处9 ;-------------------------------------------------------------------10 global put_char11 put_char:12 pushad ;备份32位寄存器环境13 ;需要保证gs中为正确的视频段选择子,;为保险起见,每次打印时都为gs赋值14 mov ax, SELECTOR_VIDEO ; 不能直接把立即数送入段寄存器 15 mov gs, ax1617 ;;;;;;;;; 获取当前光标位置 ;;;;;;;;;18 ;先获得高8位19 mov dx, 0x03d4 ;索引寄存器20 mov al, 0x0e ;用于提供光标位置的高8位21 out dx, al22 mov dx, 0x03d5 ;通过读写数据端口0x3d5来获得或设置光标位置23 in al, dx ;得到了光标位置的高8位24 mov ah, al2526 ;再获取低8位27 mov dx, 0x03d428 mov al, 0x0f29 out dx, al30 mov dx, 0x03d531 in al, dx3233 ;将光标存入bx34 mov bx, ax35 ;下面这行是在栈中获取待打印的字符36 mov ecx, [esp + 36] ;pushad压入4×8=32字节,;加上主调函数4字节的返回地址,故esp+36字节37 cmp cl, 0xd ;CR是0x0d,LF是0x0a38 jz .is_carriage_return39 cmp cl, 0xa40 jz .is_line_feed4142 cmp cl, 0x8 ;BS(backspace)的asc码是843 jz .is_backspace44 jmp .put_other45 ;;;;;;;;;;;;;;;;;;
下节我们再解释代码吧,再来玩哦。
接前文,下面把代码解释一下。
1 TI_GDT equ 02 RPL0 equ 03 SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL045 [bits 32]6 section .text7 ;------------------------ put_char -----------------------------8 ;功能描述:把栈中的1个字符写入光标所在处9 ;-------------------------------------------------------------------10 global put_char11 put_char:12 pushad ;备份32位寄存器环境13 ;需要保证gs中为正确的视频段选择子,;为保险起见,每次打印时都为gs赋值14 mov ax, SELECTOR_VIDEO ; 不能直接把立即数送入段寄存器 15 mov gs, ax1617 ;;;;;;;;; 获取当前光标位置 ;;;;;;;;;18 ;先获得高8位19 mov dx, 0x03d4 ;索引寄存器20 mov al, 0x0e ;用于提供光标位置的高8位21 out dx, al22 mov dx, 0x03d5 ;通过读写数据端口0x3d5来获得或设置光标位置23 in al, dx ;得到了光标位置的高8位24 mov ah, al2526 ;再获取低8位27 mov dx, 0x03d428 mov al, 0x0f29 out dx, al30 mov dx, 0x03d531 in al, dx3233 ;将光标存入bx34 mov bx, ax35 ;下面这行是在栈中获取待打印的字符36 mov ecx, [esp + 36] ;pushad压入4×8=32字节,;加上主调函数4字节的返回地址,故esp+36字节37 cmp cl, 0xd ;CR是0x0d,LF是0x0a38 jz .is_carriage_return39 cmp cl, 0xa40 jz .is_line_feed4142 cmp cl, 0x8 ;BS(backspace)的asc码是843 jz .is_backspace44 jmp .put_other45 ;;;;;;;;;;;;;;;;;;
put_char函数中以后我们任何一个打印功能的核心,所以光它的实现就要112行,这似乎是我们目前写过的最长的一个函数了,我保证以后也没有这么长的啦。好啦,长归长,不过也没什么难度,下面咱们开讲啦。
put_char的打印原理是直接写显存,在32位保护模式下对内存的操作是“[段基址(选择子):段内偏移量]”,所以这就涉及到视频段选择子啦。一直以来我们都是用段寄存器gs来存储视频段选择子,以后也是,所以得保证在写显存之前,gs中的值是正确的选择子。第14~15行是我们为GS寄存器赋值的代码,别小看这两行,大有来头,可不亚于摊上大事呢,吼吼,待咱们把put_char函数说完再跟大家好好说道说道吧,大家要做好心理准备。咱们先说别的。
第1~3行是定义了视频段的选择子,由于只需要这三行,专门定义个配置文件有点不值当的,所以直接在这定义了,好的习惯是放在配置文件中,大家在实践中不要学我。
第10行是通过关键字global把函数put_char导出为全局符号,这样对外部文件便可见了,外部文件通过声明便可以调用。
第11行开始定义函数put_char。
第12行是用pushad指令备份32位寄存器的环境,按理说用到哪些寄存器就要备份哪些,我这里是偷懒行为,将8个32位全部备份了。PUSHAD是push all double,该指令压入所有双字长的寄存器,这里的“所有”一共是8个,它们的入栈先后顺序是: EAX->ECX->EDX->EBX->ESP->EBP->ESI->EDI,EAX是最先入栈。
第14~15行是为gs安装正确的选择子,原因如前所述完事再说。
我们在打印字符时,通常都不用指定字符显示的坐标位置,大家也没觉得有什么奇怪,原因是字符是在当前光标的位置处显示的,而且光标的位置会一直更新顺延,我们的字符一直跟着光标走,似乎光标就是字符的导航一样,而我们已经习惯了跟随光标。我想大伙儿已经清楚了光标和字符的关系了,对,它们的关系就是没有任何关系^_^。“光标在哪字符就在哪”,这是我们人为有意设置的,我们是在光标处打印字符。也就是说,我们也可以不在光标处打印字符,让光标和字符的位置分开。这一点在理论上就能证明,我们知道打印字符本质上就是把字符写入在显存中的某个地址处。在文本模式80*25下的显存可以显示80*25=2000个字符,每个字符占2字节,低字节是字符的ascii码,高字节是前景色和背景色属性,所以在4000字节的显存空间中,只要起始地址为偶数的任意2字节我们都可以写入字符,您看,这哪里是光标能限制的。光标只是个亮点,用来吸引用户眼球的,它能够帮助咱们快速找到屏幕上的活跃位置,它本身与字符显示的位置没有关系。
有点长,下节再说吧。