操作系统——打印函数(十二)

操作系统——打印函数(十二)

2020-09-25 20:32:00 hawk


概述

  这章主要完善一下内核的相关功能,实现一个简单的打印功能


函数调用

  实际上,函数调用约定,即调用函数时的一套约定,是被调用函数的接口,体现在

1.  参数的传递方式

2.  参数的传递顺序

3.  保存寄存器环境的方式

  为了确保调用者和被调用者可以正常的进行调用,双方需要提前就以上问题达成一致。我们采用的是cdecl(C declaration)调用方式,即参数在栈中进行传递;函数参数从右到左进行传递;EAX、ECX和EDX需要调用者自己进行保存,其余的寄存器由被调用者进行保存,函数的返回值存储在EAX寄存器中,并且由调用者清理栈空间。


打印函数

显卡控制

  前面可以看到,我们是直接向显存中写入数据,基本没有与显卡的IO接口——端口进行通信。这里按照惯例,我们列出VGA上主要的端口,如下所示

寄存器分组 寄存器子类 读端口 写端口 说明
CRT Controller Registers

Address Register

3x4h 这里x的值取决下面Externel Registers组Miscellaneous Output Reigster的Input/Output Address Select字段
Data Register 3x5h
External(General) Registers Miscellaneous Output Register 3cch 3c2h  
Feature Control Register 3cah 3xah 写端口为3BAh(mono模式)或3DAh(color模式)
Input Status #0 Register 3c2h 不可用  
Input Status #1 Register 3xah 不可用 读端口为3BAh(mono模式)或3DAh(color模式)

 

  首先,整个端口实际上被分为了两大类——Address Register和Data Register,这个原因是硬件上的。因为整个计算机端口独立编址范围就0-65535,如果直接每一个端口都进行分配,那么肯定地址是不够用的。因此,计算机工程师通过Address Register和Data Register进行拓展,有点类似于计算机网络中的NAT:虽然进行独立编址的只有有限个端口,但是如果我在有限个独立地址的段端口中拿出两个,其中一个用来指定提前给定的寄存器数组的下标,另一个寄存器用来获取该指定下标的寄存器的值,从而可以访问到更多的端口。所以对于这类端口来说,首先在Address Register中指定相关的索引,然后再在Data Register对所索引的寄存器进行读写即可。

  上面我们提到了,实际上CRT Controller Registers寄存器的地址根据下面的Externel Registers组Miscellaneous Output Reigster的Input/Output Address Select字段影响的。这里我们不需要了解那么多,一般情况下(也就是我们后面的实验情况),CRT Controller Registers寄存器的Address Register的端口地址为0x3d4,Data Register的端口地址为0x3d5。当然,正如前面介绍的,显卡的IO接口对应的端口自然不仅仅这么几个,其指向了寄存器数组,重要的寄存器下标和功能如下所示

索引 寄存器
0eh Cursor Location High Register
0fh Cursor Location Low Register

 


 

实验

  下面我们将在实验中简单描述一下这个打印函数的实现。仓库链接点击进入

数据类型

  类似于Linux,为了开发方便,我们需要提前定义一些数据类型,方便直接进行使用,其主要内容和linux下的/usr/include/stdint.h十分相似,如下所示

#ifndef __LIB_STDINT_H
#define __LIB_STDINT_H

    typedef signed char int8_t;
    typedef signed short int int16_t;
    typedef signed int int32_t;
    typedef signed long long int int64_t;
    typedef unsigned char uint8_t;
    typedef unsigned short int uint16_t;
    typedef unsigned int uint32_t;
    typedef unsigned long long int uint64_t;


#endif

显卡通信

  实际上,我们一直习惯了光标是跟随着字符串的位置,也就是字符始终是在当前光标的位置处进行显示,并且光标的位置会一直更新。那么我们实现的打印函数,自然也需要实现这个功能。首先,我们需要获取当前的光标位置,通过与显卡端口进行通信完成。前面介绍的CRT Controller Registers寄存器组中下标为0xe和0xf,分别表示光标地址的低8位和高8位,从而最终获取了相关的光标地址值,其代表着下一个待打印的字符位置。当然,我们还需要实现一下不可打印字符的光标位置,比如’\n'字符等。这里不进行赘述了。这里需要额外提醒一下,光标位置指的是字符的索引,但是实际内存是需要 * 2的,因为一个字符2字节大小

滚屏

  这里说明一下,如果我们新的光标超出了屏幕右下角最后一个字符的位置;或者最后一行中任意位置有回车或换行符,则我们都需要让屏幕之外的内容显示在屏幕上,这也就是滚屏。我们实现换行的思路也很简单,VGA模式下屏幕显示80 * 25,也就是25行,每行80个字符。则我们只需要将第1~24行的数据搬移到第0~23行;然后将最后一行的字符使用空格覆盖;并且将光标移动到第24行行首即可。

打印字符

  下面就是简单的打印字符函数的源代码,如下所示

;        这里实现打印函数
;-----------------------------------------------------------------------------------------------------------------------------


;        这是前面实现的include/boot.inc中的定义,这里直接复制后使用
GDT_SECT equ 0000000000000_0_00b                                    ;选择子格式
GDT_SECT_TI_GDT equ GDT_SECT    
GDT_SECT_RPL_0 equ GDT_SECT
GDT_SECT_VIDEO equ ((0x0003 << 3) | GDT_SECT_TI_GDT | GDT_SECT_RPL_0)        ;描述符索引值为0x3;在GDT中索引;请求权限为0特权级


[bits 32]

SECTION    .text


;--------------------------------------实现put_char函数--------------------------------------------------------------------------
;    将当前栈中的字符打印到当前的光标处,字符默认为黑底白字
;    可打印字符直接进行打印
;    对于'\n',其功能是回车换行
;    对于'\b‘,其功能是删除前一个字符,并且将光标移动到前一个字符处
;--------------------------------------------------------------------------------------------------------------------------------
global    put_char

put_char:
    pushad                                        ;这里是将所有寄存器双字寄存器入栈
                                            ;先后入栈顺序为eax、ecx、edx、ebx、原始esp、ebp、esi和edi

    
    mov    ax, GDT_SECT_VIDEO
    mov    gs, ax                                    ;将显存段选择子赋值给gs,之后通过gs进行段基址:段偏移进行访问


;    首先获取当前的光标位置,将其值保存到ax中
    mov    dx, 0x3d4
    mov    al, 0xe                    
    out    dx, al                                    ;向CRT Controller Registers的Address Register写入0xe索引,即读写Cursor Location High Register寄存器


    mov    dx, 0x3d5                
    in    al, dx                                    ;从CRT Controller Registers的Data Register读入数据,也就是读取Cursor Location High Register寄存器的值到al中
    mov    ah, al                


    mov    dx, 0x3d4
    mov    al, 0xf                    
    out    dx, al                                    ;向CRT Controller Registers的Address Register写入0xf索引,即读写Cursor Location High Register寄存器


    mov    dx, 0x3d5                
    in    al, dx                                    ;从CRT Controller Registers的Data Register读入数据,也就是读取Cursor Location Low Register寄存器的值到al中,则此时ax中保存着光标的完整位置坐标


    mov    bx, ax                                    ;将光标位置保存到bx寄存器中


    mov    ecx, [esp + 36]                                ;读取栈上传入的字符
                                            ;压入了8个寄存器,还压入了返回地址,则传入字符偏移为4 * (8 + 1) = 36


;-------------------------这里需要判断一下字符类型,对于特殊字符,进行特殊处理
;    回车符CR,即carriage_return,'\r'    ascii:0xd    功能:其将光标移动到行首
;    换行符LF,即line_feed,'\n'        ascii:0xa    功能:将光标跳转到下一行
;    这里类似于c语言,直接将回车符或者换行符都执行回车换行功能
;    退格符b,即back_space,'\b'                ascii:0x8    功能:将上一个字符置为空格,并且光标回退一个字符


    cmp    cl, 0xd
    jz    .IS_CARRIAGE_RETURN
    cmp    cl, 0xa
    jz    .IS_LINE_FEED


    cmp    cl, 0x8
    jz    .IS_BACK_SPACE

    jmp    .IS_OTHER



;----------------------------处理输出退格符-------------------------------------
.IS_BACK_SPACE:
                            
    dec    bx                            ;自减1,获取上一个光标的位置
    shl    bx, 1                            ;将bx内容左移1,也就是bx * 2,获取显存中偏移

    mov byte    [gs:bx], 0x20                    ;置为空格
    inc    bx
    mov byte    [gs:bx], 0x07                    ;即设置为黑底白字
    shr    bx, 1                            ;重新回复光标的位置
    jmp    .SET_CURSOR                        ;将光标的位置重新写入显卡IO接口中

.IS_OTHER:
    shl    bx, 1                            ;将bx内容左移1,也就是bx * 2,获取显存中偏移

    mov byte    [gs:bx], cl                    ;置为传递的字符值
    inc    bx
    mov byte    [gs:bx], 0x07                    ;即设置为黑底白字
    shr    bx, 1
    inc    bx                            ;将光标移动到下一个位置
    
    cmp    bx, 2000
    jl    .SET_CURSOR                        ;如果当前字符已经超过了80 * 25,则进行滚屏,向上滚动1行                




;    这里主要获取当前字符所在的行数
.IS_CARRIAGE_RETURN:
.IS_LINE_FEED:
    xor    dx, dx                            ;dx是被除数的高16位
    mov    ax, bx                            ;ax是被除数的低16位
    
    mov    si, 80
    div    si                            ;32位被除数 / 16位除数,商保存在ax中,余数保存在dx中

    sub    bx, dx                            ;此时获取到当前行首
.IS_CARRIAGE_RETURN_END:
    add    bx, 80                            ;光标移动到下一行处

.IS_LINE_FEED_END:
    cmp    bx, 2000
    jl    .SET_CURSOR                        ;如果当前字符已经超过了80 * 25,则进行滚屏,向上滚动1行



.ROLL_SCREEN:
    cld                                ;设置eflags寄存器,确保遍历地址向高地址增长
    mov    ecx, 960                        ;后面一次复制4字节指令,从而需要复制(24 - 1 + 1) * 80 * 2 / 4 = 960次
    mov    esi, 0xc0000000 + 0xB8000 + 160                ;我们在分页机制中将低端1MB内存映射到了0xc00000000上,这里是内核空间中内存地址的第1行地址
    mov    edi, 0xc0000000 + 0xB8000                ;我们在分页机制中将低端1MB内存映射到了0xc00000000上,这里是内核空间中内存地址的第0行地址
    rep    movsd                            ;每次复制4字节,计数器为ecx



;    下面将最后一行清为空格
    mov    edx, 3840                        ;最后一行在显存中的偏移,即24 * 80 * 2 = 3840
    mov    ax, 0x0720                        ;黑底白字的空格

    mov    ecx, 80
.CLEAR_LAST_LINE:
    mov word    [gs:edx], ax
    add    edx, 2
    loop    .CLEAR_LAST_LINE


    mov    bx, 1920                        ;将光标重新设置为最后一行的行首,也就是2000 - 80



;    这里重新移动光标,也就是将光标地址重新写入显卡的IO接口中
.SET_CURSOR:


    mov    dx, 0x3d4
    mov    al, 0xe                    
    out    dx, al                                    ;向CRT Controller Registers的Address Register写入0xe索引,即读写Cursor Location High Register寄存器


    mov    dx, 0x3d5
    mov    al, bh            
    out    dx, al                                    ;向CRT Controller Registers的Data Register写入数据,也就是将移动后的光标的高8位写入Cursor Location High Register寄存器中


    mov    dx, 0x3d4
    mov    al, 0xf                    
    out    dx, al                                    ;向CRT Controller Registers的Address Register写入0xf索引,即读写Cursor Location High Register寄存器


    mov    dx, 0x3d5    
    mov    al, bl            
    out    dx, al                                    ;向CRT Controller Registers的Data Register写入数据,也就是将移动后的光标的低8位写入Cursor Location Low Register寄存器中

    
.put_char_done:
    popad
    ret

 

  实际上整体思路还是比较清晰的——如果是退格符,就将光标向前移动;如果是回车/换行,就将光标移动到下一行的行首;如果是其他字符,就简单的打印出来。如果此时光标位置已经超出了屏幕范围(即光标值>= 2000,索引从0开始),则将1-24行覆盖到0-23行,并且将光标重新移动到第24行行首即可。这里说明一下,好像书上代码有错误(其覆盖的时候显存段第0行和第1行在内核地址中的偏移不正确,从而导致覆盖不正确,这里注意一下)

打印字符串

  既然我们已经实现了打印字符的函数,那么打印字符串函数就很简单了——对于字符串中每一个字符,调用打印字符进行打印即可,直到碰到'\x00'字符,终止打印即可。源代码如下所示

;--------------------------------------实现put_str函数--------------------------------------------------------------------------
;    将当前栈指向的地址处的字符串输出到当前光标,并且当前光标跟随字符串,字符默认为黑底白字
;    字符串以'\x00'结尾
;--------------------------------------------------------------------------------------------------------------------------------
global    put_str

put_str:
    push    ebx
    push    ecx                                    ;这里由于仅仅使用到了ebx和ecx寄存器,将其进行保存

    xor    ecx, ecx                                
    mov    ebx, [esp + 12]                                ;获取栈上的字符串地址
                                            ;压入了2个寄存器,还压入了返回地址,则传入字符偏移为4 * (2 + 1) = 12

.PUT_STR_IS_END:
    mov byte    cl, [ebx]
    cmp    cl, 0                                    ;判断是否到字符串结果

    jz    .PUT_STR_END                                ;如果当前字符为'\x00',则表示已经到字符串结尾

    push    ecx
    call    put_char                                ;将当前字符入栈,并且调用put_char函数,将字符输出到终端上

    add    esp, 4                                    ;恢复栈平衡
    inc    ebx
    jmp    .PUT_STR_IS_END


.PUT_STR_END:
    pop    ecx
    pop    ebx
    ret

 

  可以看到,就是简单的一个遍历和判断即可。

打印整形(hex)

  将输入的整数以16进制的形式进行打印。也就是整数转换为字符串的代码,然后在调用put_str将该字符串进行输出即可。这里就不在赘述了,直接放出源代码,如下所示

;--------------------------------------实现put_uHex函数--------------------------------------------------------------------------
;    将当前栈中的无符号整形打印到光标所在的位置上,光标跟随字符,字符默认为黑底白字
;    整形就是当前默认的4字节
;--------------------------------------------------------------------------------------------------------------------------------
global put_uHex
put_uHex:

    pushad                                        ;这里是将所有寄存器双字寄存器入栈
                                            ;先后入栈顺序为eax、ecx、edx、ebx、原始esp、ebp、esi和edi


    mov    eax, [esp + 36]                                ;读取栈上传入的整形
                                            ;压入了8个寄存器,还压入了返回地址,则传入字符偏移为4 * (8 + 1) = 36

    mov    ebp, esp    
    sub    esp, 0x18                                
    mov byte    [ebp], 0                
                                            ;实际上,这里[esp, ebp - 1]就是对应的缓冲区地址。对于4字节的整形,其16进制最多8位,不妨直接设置缓冲区大小为0x18 - 2
    


.PUT_UHEX_IS_ZERO:
    cmp    eax, 0
    jz    .PUT_UHEX_ZERO                                ;判断栈上整型是否为0

.PUT_UHEX_LOOP:
    cmp    eax, 0
    jz    .PUT_UHEX_END                                ;判断当前处理的整形是否为0

    mov    bl, al
    shr    eax, 4
    and    bl, 0xf                                    ;获取当前低4位

    dec    ebp                                    ;调整带输入的缓冲区地址

    cmp    bl, 0xa                                
    jl    .PUT_UHEX_029                                ;如果当前最后4位属于0-9,则单独进行处理

    add    bl, 55                                    ;即'A' - 10的值,确保a对应的字符就是'A'
    mov    [ebp], bl                                ;将处理过的数据直接放入缓冲区中

    jmp    .PUT_UHEX_LOOP

.PUT_UHEX_029:
    add    bl, '0'
    mov    [ebp], bl

    jmp    .PUT_UHEX_LOOP


.PUT_UHEX_ZERO:
    dec    ebp
    mov byte    [ebp], '0'

.PUT_UHEX_END:
    
    push    ebp
    call    put_str
    add    esp, 0x18 + 4                                ;调整栈平衡

    popad
    ret

 

  这里我的思路稍稍和书上的不太一样。这里我将缓冲区放置在栈上——也就是首先[原始esp - 0x18, 原始esp]当做缓冲区,然后ebp始终指向当前字符串的头,这样实现起来我个人认为比较简单。

打印整形(Dec)

  这里顺带着在实现以下以十进制输出对应的4字节整形数据,实际上大体思路是一样的,只不过带输出的字符是通过div命令中的余数获取。其源代码如下所示

;--------------------------------------实现put_uDec函数--------------------------------------------------------------------------
;    将当前栈中的无符号整形以10进制打印到光标所在的位置上,光标跟随字符,字符默认为黑底白字
;    整形就是当前默认的4字节
;--------------------------------------------------------------------------------------------------------------------------------
global put_uDec
put_uDec:

    pushad                                        ;这里是将所有寄存器双字寄存器入栈
                                            ;先后入栈顺序为eax、ecx、edx、ebx、原始esp、ebp、esi和edi


    mov    eax, [esp + 36]                                ;读取栈上传入的整形
                                            ;压入了8个寄存器,还压入了返回地址,则传入字符偏移为4 * (8 + 1) = 36


    mov    ebp, esp    
    sub    esp, 0x18                                
    mov byte    [ebp], 0                
                                            ;实际上,这里[esp, ebp - 1]就是对应的缓冲区地址。对于4字节的整形,其16进制最多8位,不妨直接设置缓冲区大小为0x18 - 2
    


.PUT_UDEC_IS_ZERO:
    cmp    eax, 0
    jz    .PUT_UDEC_ZERO                                ;判断栈上整型是否为0

    mov    esi, 10        
                            
.PUT_UDEC_LOOP:
    cmp    eax, 0
    jz    .PUT_UDEC_END                                ;判断当前处理的整形是否为0

    mov    edx, 0                                    ;确保eax保存当前值的低32位,edx保存当前值的高32位
    div    esi                                    ;商在eax中,余数在edx中        
                

    add    dl, '0'
    dec    ebp
    mov    [ebp], dl


    jmp    .PUT_UDEC_LOOP


.PUT_UDEC_ZERO:
    dec    ebp
    mov byte    [ebp], '0'

.PUT_UDEC_END:
    
    push    ebp
    call    put_str
    add    esp, 0x18 + 4                                ;调整栈平衡

    popad
    ret

 

  好了,到这里我们就基本上完成了打印函数的试验了。下面我们要进行测试,这里就直接在内核程序中调用数据即可,内核源代码作如下修改

#include "print.h"

int main(void) {

    put_str("Now is put_uHex\n");
    put_uHex(0xf);
    put_char('\n');

    put_str("Now is put_uDec\n");
    put_uDec(0xf);
    while(1);

    return 0;
}

 

  然后进行编译和链接,首先进行编译,指令如下所示

nasm -f elf -o kernel/lib/kernel/print.o kernel/lib/kernel/print.S

gcc kernel/main.c -m32 -I kernel/lib/kernel/ -c -o kernel/main.o

ld -melf_i386 -Ttext 0xc0002000 -e main -o kernel/kernel.bin kernel/main.o kernel/lib/kernel/print.o

dd if=kernel/kernel.bin of=../hawk.img bs=512 seek=10 count=200 conv=notrunc

 

  结果如图所示

 

 

  然后我们在虚拟机上进行运行,指令如下所示

~/bochs/bin/bochs -f bochsrc.disk

 

  最后结果如图所示

 

posted @ 2020-09-26 14:43  hawkJW  阅读(623)  评论(0编辑  收藏  举报