Printing To Screen
基础知识
假设您处于 保护模式 并且不使用 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到脚本中。