C语言中无法写入屏幕内存

45

我对C语言非常陌生,它是我学习Java之后接触的第二种高级编程语言。我已经掌握了大部分基础知识,但由于某种原因,我无法将单个字符写入屏幕内存。

这个程序使用Turbo C编译,在一个Am486-DX4-100处理器上运行,主频为120mhz。图形卡是一张标准的VLB Diamond Multimedia Stealth SE,使用Trio32芯片。

我的操作系统是PC-DOS 2000,并加载了ISO代码页。我在标准的MDA/CGA/EGA/VGA样式80列文本模式下运行,支持彩色。

以下是我编写的程序:

#include <stdio.h>

int main(void) {
    unsigned short int *Video = (unsigned short int *)0xB8000;
    *Video = 0x0402;
    getchar();
    return 0;
}

正如我所说,我对C语言非常陌生,所以如果我的错误看起来很明显,我很抱歉,我无法找到一个可靠的来源来让我能够理解如何做这个操作。

据我所知,在x86平台上的实模式下,文本模式下的屏幕内存从0xB8000开始。每个字符都存储在两个字节中,一个用于字符本身,另一个用于背景/前景色。想法是将值0x0402(应该是一个红色微笑的脸)写入0xB8000。这应该将其放置在屏幕左上角。

我已经考虑到了屏幕可能正在滚动的可能性,因此在执行时立即将我的字符移除的两种方法。为了解决这个问题,我尝试过:

  • 使用循环重复写入此值
  • 将它写得稍微向下一点。

我可以读取并打印我写入内存的值,所以它显然仍然在内存中的某个位置,但出现在屏幕上的展示效果不正确。很显然我在做错了什么,但我不知道可能会出现什么问题。如果需要任何其他细节,请告诉我。感谢您能提供的任何可能的帮助。


13
您的C语言实现如何将整数映射到指针?您需要使用某种“far”指针关键字吗?或者它在大/超级非实模式下运行,并使用32位指针? 如果C指针仅有16位宽度,则它只是段内偏移量(默认情况下大多数时候为ds),将0xB8000转换为指针将截断为16位,并相对于ds给出0x8000的偏移量。 简而言之,实模式分段与C指针之间的映射不太容易。请注意,这不会很容易,特别是如果您不了解C和x86-16汇编语言。 - Peter Cordes
1
0xB8000 是20位宽的。(每个十六进制数字编码四位,共有五个数字。) unsigned short int 的宽度仅为16位。在大多数bcc内存模型中,接近指针(我认为不同于huge,但已经过去几十年了。)也是16位。因此,您导致了溢出和环绕。我怀疑你最终写入了地址DS:8000 - Davislor
9
在印度有没有一位教授会分配那些运行在16位操作系统上的90年代中期编译器给初学者学习计算机科学,或者其他原因吗?现代工具如Linux、clang和gcc是完全免费的。请问是否有相应的原因? - Davislor
9
欢迎查看https://retrocomputing.stackexchange.com/。 - user11153
2
@Davislor:了解一些汇编语言对于从错误症状反推可能在C中导致它的原因非常有用,因此不应低估理解如何工作的部分。这是一个有趣的例子:未对齐的uint16_t*在x86上可能会导致段错误,但x86可以进行未对齐的加载?自动向量化以有趣的方式打破了这个“偶然有效”的代码。 - Peter Cordes
显示剩余18条评论
3个回答

56

在实模式下,为了寻址前1MiB的内存,使用一种叫做20位段偏移寻址的机制。0xb8000是一个物理内存地址。你需要使用一个叫做far指针,它允许你使用实模式分段来寻址内存。不同类型的指针在这个Stackoverflow答案中有描述。

0xb8000可以表示为一个段为0xb800和一个偏移为0x0000的组合。获取物理地址的计算公式是:segment*16+offset。0xb800*16+0x0000=0xb8000。有了这个知识,你可以包含dos.h并使用CMK_FP来给定段和偏移初始化一个far指针到这样的地址。

文档中得知,MK_FP的定义如下:

MK_FP() Make a Far Pointer

#include   <dos.h>

void       far *MK_FP(seg,off);
unsigned   seg;                         Segment
unsigned   off;                         Offset

MK_FP() 是一个宏,从它的组成部分:段 'seg' 和偏移量 'off' 中创建一个远指针。

返回值:一个远指针。

您的代码可以像这样编写:

#include <stdio.h>
#include <dos.h>
int main(void) {
    unsigned short int far *Video = (unsigned short int far *)MK_FP(0xB800,0x0000);
    *Video = 0x0402;
    getchar();
    return 0;
}

从32位整数转换为far *无法工作吗?就像Stacker的答案一样,使用char far *Video = (char far *)0xb8000000; - Peter Cordes
1
@Ampera 我在关于段:偏移寻址的回答中提到了Starman页面的链接。您需要对16位实模式下的段:偏移寻址工作原理有良好的基础。当您清醒时,我建议阅读Starman链接。另外一个SO答案中还有关于16位Turbo-C支持的指针类型的更多信息。 - Michael Petch
1
@PeterCordes 这是一个简单的复制操作(far<=>uint32_t)。uint32_t(以及far指针)并不代表线性地址。它是一个32位的值,其中高16位是段,低16位是偏移量。不进行任何转换。只要不进入高16位,你可以在这样的转换指针上进行指针算术运算。 - Michael Petch
2
@PeterCordes:TurboC和许多早期编译器没有stdint.h。你可以使用第三方头文件。话虽如此,指针的大小取决于内存模型(Tiny、Small、Medium是16位指针)。在这些模型中,你不能随意将far或huge指针(始终为32位)强制转换为16位指针。在紧凑和大型模型中,默认指针类型为far(uintptr_t的大小为4)。Huge的大小为4。你可以将huge指针转换为far指针,但除非先标准化far指针,否则不能反过来。 - Michael Petch
哇,我本以为这个问题更适合我们的RetroComputing姐妹站点 :-) - paxdiablo
显示剩余9条评论

5

内存段地址取决于所使用的视频模式:

0xA0000 for EGA/VGA graphics modes (64 KB)
0xB0000 for monochrome text mode (32 KB)
0xB8000 for color text mode and CGA-compatible graphics modes (32 KB)

要直接访问VRAM,您需要一个32位指针来保存段地址和偏移地址,否则会破坏您的堆栈。这通常会导致未定义的行为。

char far *Video = (char far *)0xb8000000;

另请参阅:什么是近指针、远指针和巨型指针?


涉及IT技术,具体内容涉及指针类型。

如果您解释一下0xb8000000是OP的0xB8000线性地址的seg:off表示,那么这将是一个更好的答案。即,高16位为0xB8000 >> 4,低16位全部为零(0&15)。您的第一句话应该说“线性地址”,而不是“段地址”,因为它们不是段寄存器值,而是您需要使用分段才能访问的内容。(此外,VGA文本基指针可能应该声明为short或结构体,而不是char)。 - Peter Cordes

2
正如@stacker所指出的,在16位环境中,您需要仔细分配指针。据我所知,您需要放置关键字(我的天啊,多么怀旧)。
还要确保不要在所谓的“Huge”内存模型中编译。它与远程寻址不兼容,因为每个32位指针都会自动“归一化”为20位。尝试选择“Large”内存模型。

1
Huge是否正是OP所需的,以使问题中的代码无需修改即可工作?因此,所有指针都可以被视为线性地址进入实模式物理内存,并且编译器可以通过在需要时重新加载段寄存器来“解决”分段问题。(哎呀,不好意思,事实并非如此。它们不是线性地址。https://dev59.com/N1YN5IYBdhLWcg3wVm-n#47588779?noredirect=1#comment82135205_47588779) - Peter Cordes
2
是的,我找到了这个参考文献:“在DOS/4GW下,物理内存的前1MB被映射为共享线性地址空间。这使得您的应用程序可以使用接近指针来访问视频RAM,设置为屏幕的线性地址。”但是,这不再是x86 16位编程。 - jjmontes
2
@MichaelKjörling:二十世纪的编译器编写者认识到,拥有一系列针对不同平台进行调整但共享公共基础的语言是有价值的。我认为C89标准的作者仅定义了两种C实现(托管和独立),并不是因为他们认为两种就足够了,而是因为他们认为自己无法定义所有使C适用于所有用途所需的内容。8086显然有一种自然的方式将32位值读取为指针... - supercat
1
@PeterCordes:C语言支持奇怪平台的真正价值从来不是它能够使这些平台与各种程序兼容,而是它能够使这些平台与各种程序员兼容。例如,如果一个平台有12位char,那么为基于八位字节存储的代码编写的程序不能指望在其上运行,但告诉程序员“这个平台很正常,只是它有12位char、24位大端int和指针以及48位大端long”应该足够让他们为其编写代码... - supercat
1
@peter - 这让我很烦,我甚至还问了一个相关问题 - BeeOnRope
显示剩余16条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接