Printing To Screen

来自osdev
跳到导航 跳到搜索

基础知识

假设您处于 保护模式 并且不使用 BIOS 将文本写入屏幕,则您将直接写入 “视频” 存储器。

这很容易。 彩色显示器的文本屏幕视频存储器位于0xB8000,单色显示器的文本屏幕视频存储器位于地址0xB0000 (有关更多信息,请参见 检测彩色和单色显示器)。

文本模式内存对屏幕上的每个“字符”占用两个字节。 一个是“ASCII代码”字节,另一个是“属性”字节。因此,文本“Hello”将存储为:

0x000b8000: 'H', colour_for_H
0x000b8002: 'e', colour_for_e
0x000b8004: 'L', colour_for_L
0x000b8006: 'l', colour_for_l
0x000b8008: 'o', colour_for_o

“属性” 字节以其最低的4位带有 “前景颜色”,以其最高的3位带有 “背景颜色”。 位#7的解释取决于您(或BIOS)如何配置硬件(有关更多信息,请参阅VGA参考资料)。

例如,使用0x00作为属性byte意味着黑底黑字(您将什么也看不到)。 0x07 为黑色浅灰色 (DOS默认),0x1F 为蓝色白色 (Win9x的死亡蓝屏),0x2a 用于绿色单色怀旧。

对于彩色视频卡,您可以使用32KB的文本视频内存。 由于80x25模式不会使用全部32KB(80x25x2=4,000字节/屏幕),因此您有8个显示页面可用。

当您打印到0以外的任何其他页面时,它将 “不” 出现在屏幕上,直到该页面 “启用” 或 “复制” 到第0页内存空间中。

颜色表

颜色编号 颜色名称 RGB值 Hex值
0 Black 0 0 0 00 00 00
1 Blue 0 0 170 00 00 AA
2 Green 0 170 0 00 AA 00
3 Cyan 0 170 170 00 AA AA
4 Red 170 0 0 AA 00 00
5 Purple 170 0 170 AA 00 AA
6 Brown 170 85 0 AA 55 00
7 Gray 170 170 170 AA AA AA
8 Dark Gray 85 85 85 55 55 55
9 Light Blue 85 85 255 55 55 FF
10 Light Green 85 255 85 55 FF 55
11 Light Cyan 85 255 255 55 FF FF
12 Light Red 255 85 85 FF 55 55
13 Light Purple 255 85 255 FF 55 FF
14 Yellow 255 255 85 FF FF 55
15 White 255 255 255 FF FF FF

打印字符串

如果你有一个指向视频内存的指针,想写一个字符串,下面是你可以做的;

//注意此示例将始终写入顶部
// 屏幕一行
void write_string( int colour, const char *string )
{
    volatile char *video = (volatile char*)0xB8000;
    while( *string != 0 )
    {
        *video++ = *string++;
        *video++ = colour;
    }
}

这只是循环遍历字符串中的每个字符,并将其复制到视频内存中的适当位置。

对于更高级的打印功能,您需要存储x和y的变量,因为显示控制器不会打印换行符。 这涉及一个switch语句或类似的构造。 您还必须测试x>80或y>25,如果x>80将x设置为0并递增y,或者在y>25滚动的情况下。

打印整数

就像在任何其它环境中一样,需求您反复将值除以进制基数(Base,译者注:这里说明了把整数转成字符的算法),除法的其余部分为您提供该值的最低有效数字。

例如,由于1234=4+3*10+2*100+1*1000,如果重复将“1234”除以10,并使用除法的剩余部分,则得到以下数字:

1234 = 123*10 + 4
123 = 12*10 + 3
12 = 1*10 + 2
1 = 1

由于此算法以“错误”的顺序(从最后到第一)检索数字,因此您必须使用递归,或者通过随后工作反转数字序列。 如果您知道 number % 10 的数值,则只需将其添加到字符char '0' 即可具有正确的字符 (例如“0”+4==“4”)

以下是itoa()函数的示例实现(该函数不是标准的,但由许多库提供):

char * itoa( int value, char * str, int base )
{
    char * rc;
    char * ptr;
    char * low;
    // 检查支持的进制基础。
    if ( base < 2 || base > 36 )
    {
        *str = '\0';
        return str;
    }
    rc = ptr = str;
    // 为负数设置“-”负号。
    if ( value < 0 && base == 10 )
    {
        *ptr++ = '-';
    }
    // 记住数字从哪里开始。
    low = ptr;
    // 实际转换。
    do
    {
        // 对于负值,Modulo为负值。此技巧使abs()变得不必要。
        *ptr++ = "zyxwvutsrqponmlkjihgfedcba9876543210123456789abcdefghijklmnopqrstuvwxyz"[35 + value % base];
        value /= base;
    } while ( value );
    // 终止字符串。
    *ptr-- = '\0';
    // 把数字倒过来。
    while ( low < ptr )
    {
        char tmp = *low;
        *low++ = *ptr;
        *ptr-- = tmp;
    }
    return rc;
}

参考[1]

故障排除

不显示任何内容

请记住,只有当屏幕正确设置为80x25视频模式(即模式03)时,这种写入视频内存的方式才会起作用。 您可以通过手动初始化每个VGA寄存器来执行此操作,也可以在仍处于实模式 (例如,在引导扇区中) 时调用BIOS Int10h的 “设置视频模式” 服务。 大多数BIOS都会为您进行初始化,但其他一些(主要是在笔记本电脑上)不会。 有关详细信息,请查看Ralf Brown‘s Interrupt List。 还请注意,某些模式被模式列表报告为 “both text & graphic” 的模式实际上是具有BIOS功能的图形模式,当您通过Int10h调用char/message输出时,这些功能会绘制字体 (这意味着一旦进入保护模式,您将结束当前模式,进入纯图形模式)。

(GRUB为您执行此设置。)

另一个常见的错误,例如在遍布网络的众多教程中,是链接内核/操作系统的.text节到错误的内存地址。 如果还没有完成内存管理功能,请确保在链接器脚本中使用物理内存位置。

打印字符

在保护模式下,尝试一个简单的命令,如:

// C
*((int*)0xb8000)=0x07690748;

// NASM
mov [0xb8000], 0x07690748

// GAS
movl $0x07690748,0xb8000

它应该在你的屏幕上方以灰色和黑色显示“Hi”。 如果这不起作用,请检查您的分页/分段设置是否将假定的视频内存地址正确映射到0xB8000(或0xB0000)。

缺少字符串

有时打印单个字符是可行的,但打印字符串失败。 这通常是因为链接器脚本中缺少.rodata部分。

以前,GCC有一个选项 -fwritable-strings,可以用作解决此问题的方法,但它在3.0版中被弃用,并在4.0版和更高版本中删除,2005年发布。 即使在有选择的情况下,这也是一个难题;真正的解决方案是,现在仍然是,添加.rodata到脚本中。

另见