动手操作系统(6)- print功能 前文坐标:动手操作系统(5)- 载入内核 1. 回顾 前文讲到终于开始脱离手动写汇编的折磨,运行了编译器生成的elf可执行文件。但很不幸我们目前没有任何库文件,也就是甚至不能printf(“Hello world”),所以我们需要自己汇编手写一个print函数,随后把它和C语言文件一同编译。 本章作者给出的源代码如下:代码 2. 待开发功能 首先我们打印方式仍然是使用显卡提供的内存段,要求调用接口后,将对应的字符、字符串、数字打印到屏幕上。 所以我们创建头文件print.h 给出了三个接口,对应字符、字符串、数字三种打印 其中#include”stdint.h”是引用了我们自定义的一个专门用于统一类型名称的头文件,内容如下: 内容很简单 3. print函数 接下来是本篇文章主要新增的代码实现: 其中TI_GDT和RPL0如之前定义一样,表示选择子的段表选择和权限等级。SELECTOR_VIDEO定义了一个视频段的选择子,用于后续打印工作。
随后在section .data中定义一个8字节的0,用于后续数字到字符转换。 section .text后跟着代码实现 3.1 put_char 首先是put_char函数的实现: 开头对工作环境进行存档,并执行基本初始化,将gs存入视频段的选择子。 这个函数被调用时,call指令会先隐式的将返回地址压入栈中,然后跳转到函数开头,pushad会将所有32位的寄存器进行压栈保存,在函数结束时再恢复。 在打印前,我们需要定位打印字符应该现实的位置,也就是光标位置,因此需要I/O调用光标位置。 用到一对io端口 — 0x03d4和0x03d5用法是向0x03d4中写入想要读取的显卡寄存器索引值,然后在0x03d5端口中读取该寄存器的值,这里寄存器索引是0x0e表示的是待查询的是光标高8位(0x0f表示光标低8位),以此取出完整的光标位置值。 以上部分将完整的16位光标位置值存入了ax中,理论上接下来向光标对应位置写入传入的字符即可,但需要注意的是传入字符可能是可显示字符外的如退格换行等,因此在真正打印前,先做入参检测。 以上取出了栈中压入的参数到cx中(注意这里选择栈顶后的第36字节使用的是esp+36,因为栈的增长方向是从高地址到低地址,栈顶的地址反而是整个栈中值最小的,要寻找压入栈的参数就需要加法。再然后这里加上36的原因如上注释,esp指向的是栈中数据的最顶一个字节,pushad+返回地址占用了36字节,esp+36就是传入的字符) 然后我们拆解这几个入参检测的特殊情况处理函数,首先是对退格的特殊处理,很明显光标要退回一个,并且退回格内显示的字符也要“删掉”(这里我们传入空格字符覆盖掉),实现如下: 处理函数中,首先对我们刚才的光标当前位置减1(但是改的只是我们保存的备份),然后将该位置传入空格进行和默认的字符属性黑屏白字。在操作两端的“shl”和“shr”起到乘2和除2的作用,因为实际上一个字符位对应两个字节(字符字节和属性字节),所以在具体处理时bx*2是字符位,bx*2+1是属性位。 最后跳转.set_cursor设置当前光标值。(大致就是将修改后的bx值作为新的光标值,传入光标位置寄存器。) 接着是回车和换行处理,我们将回车和换行当作相同的符号来处理,需要将光标指向下一行。 以上代码中div指令当除数是16位时(此时位si),默认被除数存放在ax,结果商存放在ax,余数存放在dx。通过该操作使bx指向了当前行的行首,再加80即下一行的行首,判断是否超过了当前屏幕限制的2000个字符。若未超过就正常刷新光标即可。若超过了2000限制就继续后续的代码,进行屏幕滚动。 以上将2-25行的内容整体移动到1-24行,并将25行使用空格进行清除。 结束了入参检测特殊情况处理,以下是正常字符的打印: 这一步同样先bx乘2方便后续对对应字节的操作,将传入的字符写到当前光标位,字符属性0x07表示浅灰色。写入完成后bx除以2恢复成表示字符的位置,然后bx加1表示新的光标值。 当以上执行完后,我们多个分支汇聚到了刷新光标值: 还是通过0x03d4和0x03d5这一对端口,不过这次是写入更新光标值。最后popad将函数调用前的工作环境恢复。 3.2 put_str 有了基础的put_char后我们可以打印连续的字符串了。 这个函数中只需要用到ebx和ecx所以只用备份这两个寄存器。 其中ecx用作循环计数,ebx用作存储待打印字符的地址。 初始化结束后,进入逐个字符的打印环节。 将ebx指向位置的字符取出到cl中,判定是否为0(即‘\0’,字符串结尾的截止符号),若不是截止符号就传入put_char进行打印,传入方式是压栈,所以当执行完后需要手动清理栈空间。然后ebx+1循环直到打印完所有的字符。 最后完成调用恢复用到的ebx和ecx返回调用。 3.3 put_int 和打印字符不同,put_int需要把数字转换成字符,然后再打印到显示器上。 初始化环节中,还是会pushad将所有32位寄存器进行压栈备份,将原来的栈顶作为新的栈底,防止原栈中数据被误改。其中”ebp+4*9″是跳过push压入栈的8个32位寄存器和32位的call返回地址,待打印的4个字节到eax,并拷贝一份到edx中,当作后续操作的对象。因为4个字节打印成16进制显示时最多有8个字符(如0x1234ABCD,是一个32位的16进制显示的数),所以我们需要依次打印出8个字符。赋值edi为7表示当前待打印字符在最终的字符串中的位置(7即表示首先打印0x1234ABCD中的D);赋值ecx为8表示循环控制变量,循环依次打印字符。 这里用到了先前定义的put_int_buffer,回顾以下它的定义是:
定义了一个8字节的缓冲区(写入8个字符)。
(首先约定,接下来所写的不带引号1表示数字,带引号’1’表示字符) 我们可以看到,对于一个字符’0’,它的码值是48,并且字符1-9紧随其后,所以实际上字符’0′-‘9’码值与其对应的数值差值均为48,这意味着0-9以内的数,我们加上48(即’0’的码值),直接打印就可以打印出对应的字符。 另外,我们知道十六进制表示中不只有0-9,还有A-F(表示10-15),所以处理方式如上,’A’-A(使用字符’A’的码值-10)得到其差值是87(或’A’的码值-10)。 根据以上两种情况,我们取出待打印的字符(最低的4bit数) 上述操作如下图:
通过以4bit为单位,逐步转换为字符存入put_int_buffer中,得到了其应有的打印字符。这里已经可以直接调用put_str进行打印了。后续代码是为了消除待打印字符串中高位连续的0(比如000123变成123)。 以上部分从put_int_buffer首字节开始(例如存放的是“1234ABCD”则从‘1’),逐个字节判断是否为’0’,直到判定全部为’0’或找到首个不为0的字符,跳转到打印环节。 .full0专门处理全为0的情况,打印一个0即可,后续代码执行时edi自增会超过8,跳出打印循环。 .put_each_num处理通常情况,edi指向首个不为0的字符,初始时cl中存放着edi指向的那个buffer中字符,作为参数压栈调用put_char,并在随后的循环中edi自增,将其指向的字符存入cl重复以上步骤直到打印完buffer中所有字符,popad恢复现场,返回调用。 4. 调用测试 经过print的开发,我们已经完成了主要工作,后续在内核中使用C语言调用即可,调用时程序会将传入的参数压栈,再将返回地址压栈,最后跳转到我们使用汇编语言写的函数开头处。 结果如下
关于用到的端口号,我找到一份bochs提供的端口号列表,可以参考对用到的端口号做初步了解。 下篇坐标:动手操作系统(7)- 系统中断
2024最新激活全家桶教程,稳定运行到2099年,请移步至置顶文章:https://sigusoft.com/99576.html
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。 文章由激活谷谷主-小谷整理,转载请注明出处:https://sigusoft.com/76209.html