[英]Drawing a character in VGA memory with GNU C inline assembly
我正在学习用DOS和内联汇编在DOS下做一些低级VGA编程。 现在我正在尝试创建一个在屏幕上打印出一个角色的功能。
这是我的代码:
//This is the characters BITMAPS
uint8_t characters[464] = {
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x20,0x20,0x20,0x20,0x00,0x20,0x00,0x50,
0x50,0x00,0x00,0x00,0x00,0x00,0x50,0xf8,0x50,0x50,0xf8,0x50,0x00,0x20,0xf8,0xa0,
0xf8,0x28,0xf8,0x00,0xc8,0xd0,0x20,0x20,0x58,0x98,0x00,0x40,0xa0,0x40,0xa8,0x90,
0x68,0x00,0x20,0x40,0x00,0x00,0x00,0x00,0x00,0x20,0x40,0x40,0x40,0x40,0x20,0x00,
0x20,0x10,0x10,0x10,0x10,0x20,0x00,0x50,0x20,0xf8,0x20,0x50,0x00,0x00,0x20,0x20,
0xf8,0x20,0x20,0x00,0x00,0x00,0x00,0x00,0x60,0x20,0x40,0x00,0x00,0x00,0xf8,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x60,0x60,0x00,0x00,0x08,0x10,0x20,0x40,0x80,
0x00,0x70,0x88,0x98,0xa8,0xc8,0x70,0x00,0x20,0x60,0x20,0x20,0x20,0x70,0x00,0x70,
0x88,0x08,0x70,0x80,0xf8,0x00,0xf8,0x10,0x30,0x08,0x88,0x70,0x00,0x20,0x40,0x90,
0x90,0xf8,0x10,0x00,0xf8,0x80,0xf0,0x08,0x88,0x70,0x00,0x70,0x80,0xf0,0x88,0x88,
0x70,0x00,0xf8,0x08,0x10,0x20,0x20,0x20,0x00,0x70,0x88,0x70,0x88,0x88,0x70,0x00,
0x70,0x88,0x88,0x78,0x08,0x70,0x00,0x30,0x30,0x00,0x00,0x30,0x30,0x00,0x30,0x30,
0x00,0x30,0x10,0x20,0x00,0x00,0x10,0x20,0x40,0x20,0x10,0x00,0x00,0xf8,0x00,0xf8,
0x00,0x00,0x00,0x00,0x20,0x10,0x08,0x10,0x20,0x00,0x70,0x88,0x10,0x20,0x00,0x20,
0x00,0x70,0x90,0xa8,0xb8,0x80,0x70,0x00,0x70,0x88,0x88,0xf8,0x88,0x88,0x00,0xf0,
0x88,0xf0,0x88,0x88,0xf0,0x00,0x70,0x88,0x80,0x80,0x88,0x70,0x00,0xe0,0x90,0x88,
0x88,0x90,0xe0,0x00,0xf8,0x80,0xf0,0x80,0x80,0xf8,0x00,0xf8,0x80,0xf0,0x80,0x80,
0x80,0x00,0x70,0x88,0x80,0x98,0x88,0x70,0x00,0x88,0x88,0xf8,0x88,0x88,0x88,0x00,
0x70,0x20,0x20,0x20,0x20,0x70,0x00,0x10,0x10,0x10,0x10,0x90,0x60,0x00,0x90,0xa0,
0xc0,0xa0,0x90,0x88,0x00,0x80,0x80,0x80,0x80,0x80,0xf8,0x00,0x88,0xd8,0xa8,0x88,
0x88,0x88,0x00,0x88,0xc8,0xa8,0x98,0x88,0x88,0x00,0x70,0x88,0x88,0x88,0x88,0x70,
0x00,0xf0,0x88,0x88,0xf0,0x80,0x80,0x00,0x70,0x88,0x88,0xa8,0x98,0x70,0x00,0xf0,
0x88,0x88,0xf0,0x90,0x88,0x00,0x70,0x80,0x70,0x08,0x88,0x70,0x00,0xf8,0x20,0x20,
0x20,0x20,0x20,0x00,0x88,0x88,0x88,0x88,0x88,0x70,0x00,0x88,0x88,0x88,0x88,0x50,
0x20,0x00,0x88,0x88,0x88,0xa8,0xa8,0x50,0x00,0x88,0x50,0x20,0x20,0x50,0x88,0x00,
0x88,0x50,0x20,0x20,0x20,0x20,0x00,0xf8,0x10,0x20,0x40,0x80,0xf8,0x00,0x60,0x40,
0x40,0x40,0x40,0x60,0x00,0x00,0x80,0x40,0x20,0x10,0x08,0x00,0x30,0x10,0x10,0x10,
0x10,0x30,0x00,0x20,0x50,0x88,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xf8,
0x00,0xf8,0xf8,0xf8,0xf8,0xf8,0xf8};
/**************************************************************************
* put_char *
* Print char *
**************************************************************************/
void put_char(int x ,int y,int ascii_char ,byte color){
__asm__(
"push %si\n\t"
"push %di\n\t"
"push %cx\n\t"
"mov color,%dl\n\t" //test color
"mov ascii_char,%al\n\t" //test char
"sub $32,%al\n\t"
"mov $7,%ah\n\t"
"mul %ah\n\t"
"lea $characters,%si\n\t"
"add %ax,%si\n\t"
"mov $7,%cl\n\t"
"0:\n\t"
"segCS %lodsb\n\t"
"mov $6,%ch\n\t"
"1:\n\t"
"shl $1,%al\n\t"
"jnc 2f\n\t"
"mov %dl,%ES:(%di)\n\t"
"2:\n\t"
"inc %di\n\t"
"dec %ch\n\t"
"jnz 1b\n\t"
"add $320-6,%di\n\t"
"dec %cl\n\t"
"jnz 0b\n\t"
"pop %cx\n\t"
"pop %di\n\t"
"pop %si\n\t"
"retn"
);
}
我从PASCAL编写的这一系列教程中引导自己: http : //www.joco.homeserver.hu/vgalessons/lesson8.html 。
我根据gcc编译器更改了汇编语法,但我仍然收到此错误:
Operand mismatch type for 'lea'
No such instruction 'segcs lodsb'
No such instruction 'retn'
我一直在努力改进我的代码,至少现在我在屏幕上看到了什么。 这是我更新的代码:
/**************************************************************************
* put_char *
* Print char *
**************************************************************************/
void put_char(int x,int y){
int char_offset;
int l,i,j,h,offset;
j,h,l,i=0;
offset = (y<<8) + (y<<6) + x;
__asm__(
"movl _VGA, %%ebx;" // VGA memory pointer
"addl %%ebx,%%edi;" //%di points to screen
"mov _ascii_char,%%al;"
"sub $32,%%al;"
"mov $7,%%ah;"
"mul %%ah;"
"lea _characters,%%si;"
"add %%ax,%%si;" //SI point to bitmap
"mov $7,%%cl;"
"0:;"
"lodsb %%cs:(%%si);" //load next byte of bitmap
"mov $6,%%ch;"
"1:;"
"shl $1,%%al;"
"jnc 2f;"
"movb %%dl,(%%edi);" //plot the pixel
"2:\n\t"
"incl %%edi;"
"dec %%ch;"
"jnz 1b;"
"addl $320-6,%%edi;"
"dec %%cl;"
"jnz 0b;"
: "=D" (offset)
: "d" (current_color)
);
}
如果你看到上面的图像,我试着写下字母“S”。 结果是您在屏幕左上角看到的绿色像素。 无论x和y我给出了功能,它总是在同一点上绘制像素。
任何人都可以帮我纠正我的代码吗?
请参阅下文,了解对put_char
函数特别错误的一些内容以及可能有效的版本。 (我不确定%cs
段覆盖,但%cs
它应该做你想要的)。
首先,DOS和16位x86是彻底过时, 并不比正常的64位x86更容易学习。 即使是32位x86也已经过时,但在Windows世界中仍然广泛使用。
32位和64位代码不必关心许多16位限制/复杂性,例如段或寻址模式中有限的寄存器选择。 一些现代系统确实使用段覆盖来进行线程本地存储,但是学习如何在16位代码中使用段几乎与之无关。
了解asm的主要好处之一是调试/分析/优化实际程序。 如果您想了解如何编写C或其他高级代码可以(实际上也 )编译为高效的ASM ,你可能会看着编译器输出 。 这将是64位(或32位)。 (例如,见马特Godbolt的CppCon2017谈话: “什么?有我编译器完成适合我最近Unbolting编译器的盖子” ,其具有优良的前奏阅读总初学者的x86汇编,并看着编译器输出)。
当查看性能计数器结果注释二进制文件的反汇编时,Asm知识非常有用( perf stat ./a.out
&& perf report -Mintel
:请参阅Chandler Carruth的CppCon2015演讲:“调优C ++:基准测试,CPU和编译器!哦我的!“ )。 积极的编译器优化意味着查看每个源代码行的周期/缓存未命中/停顿计数的信息量远远低于每条指令。
此外,为了让您的程序实际执行任何操作,它必须直接与硬件通信,或进行系统调用。 学习DOS系统调用文件访问和用户输入是完全浪费时间(除了回答关于如何在16位代码中读取和打印多位数字的SO问题的稳定流程)。 它们与当前主流操作系统中的API完全不同。 开发新的DOS应用程序是没用的,所以当你进入使用asm知识做某事的阶段时,你必须学习另一个API(以及ABI)。
在8086模拟器上学习asm更具限制性: imul ecx, 15
和386增加了很多方便的指令,比如imul ecx, 15
,使得ax
不那么“特殊”。 仅限于使用8086上的指令意味着您将找出“糟糕”的做事方式。 其他大的是movzx
/ movsx
,按立即计数(除1之外)移动,并push immediate
。 除了性能之外,当代码可用时编写代码也更容易,因为您不必编写一个循环来移位超过1位。
我主要通过阅读编译器输出来学习asm,然后进行小的更改。 当我没有真正了解事情的时候,我没有尝试在asm中写东西,但如果你要快速学习(而不是在调试/分析C时发展理解),你可能需要测试你的理解编写自己的代码。 您需要了解基础知识,即有8个或16个整数寄存器+标志和指令指针,并且每个指令都对机器的当前架构状态进行明确定义的修改。 (有关每条指令的完整说明,请参阅英特尔insn参考手册( x86 wiki中的链接,以及更多好东西 )。
您可能希望从简单的事情开始,比如在asm中编写单个函数,作为更大程序的一部分。 了解进行系统调用所需的asm类型很有用,但在实际程序中,通常只对不涉及任何系统调用的内部循环手动编写asm很有用。 编写asm来读取输入和打印结果非常耗时,所以我建议你在C中做这个部分。确保你读取编译器输出并了解发生了什么,以及整数和字符串之间的区别,以及strtol
和printf
,即使你自己不写。
一旦你认为你已经理解了足够的基础知识,在你熟悉和/或感兴趣的某个程序中找到一个函数,看看你是否可以击败编译器并保存指令(或使用更快的指令)。 或者自己实现它而不使用编译器输出作为起点,无论你发现哪个更有趣。 这个答案可能很有意思,虽然重点是找到C源,让编译器产生最佳的ASM。
人们问“如何在asm中做X”时,有许多SO问题,答案通常是“与C中的相同”。 不要因为不熟悉而忘记如何编程而陷入困境。 弄清楚函数操作的数据需要发生什么,然后弄清楚如何在asm中做到这一点。 如果你遇到困难并且不得不提出问题,那么你应该拥有大部分可行的实现,只有一部分你不知道在一步中使用什么指令。
你应该使用32或64位x86。 我建议64位,因为ABI更好,但32位功能将迫使你更多地使用堆栈。 这样可以帮助您理解call
指令如何将返回地址放在堆栈上,以及调用者实际推送的args在此之后的位置。 (这似乎是您试图通过使用内联asm来避免处理的内容)。
通过直接修改视频RAM来学习如何进行图形处理是没有用的,除了满足计算机如何工作的好奇心。 你无法将这些知识用于任何事情。 现代图形API的存在是为了让多个程序在它们自己的屏幕区域中绘制,并允许间接(例如直接在纹理而不是屏幕上绘制,因此3D窗口翻转alt-tab看起来很花哨)。 这里列出的原因太多,没有直接在视频RAM上绘图。
可以使用pixmap缓冲区绘图,然后使用图形API将其复制到屏幕上。 尽管如此,做位图图形或多或少都是过时的,除非您为PNG或JPEG或其他东西生成图像(例如,优化将直方图分箱转换为Web服务的后端代码中的散点图)。 现代图形API抽象出分辨率,因此无论每个像素有多大,您的应用都可以以合理的大小绘制内容。 (小而极高的rez屏幕与低电视的大电视)。
写入内存并在屏幕上看到一些变化是很酷的。 或者甚至更好,将LED(带有小电阻)连接到并行端口上的数据位,并运行outb
指令来打开/关闭它们。 我很久以前在Linux系统上做过这个。 我制作了一个使用iopl(2)
和inline asm的小包装器程序,并以root身份运行它。 您可以在Windows上执行类似操作。 你不需要DOS或16位代码来与硬件交谈。
in
/ out
指令,以及内存映射IO和DMA的正常加载/存储,是真正的驱动程序与硬件通信的方式,包括比并行端口复杂得多的东西。 了解您的硬件“真正”如何工作很有趣,但如果您真正感兴趣或想要编写驱动程序,则只花时间在其上。 Linux源代码树包含了大量硬件的驱动程序,并且经常被很好地评论,因此如果您喜欢像编写代码一样阅读代码,那么这是另一种了解读取驱动程序与硬件通信时所做的事情的方法。
一般来说,了解一切是如何运作的。 如果你想了解图形用于很久以前的工作(使用VGA文本模式和颜色/属性字节),那么请确定。 请注意,现代操作系统不使用VGA文本模式,因此您甚至不了解在现代计算机上发生的事情。
许多人喜欢https://retrocomputing.stackexchange.com/ ,在计算机不那么复杂且无法支持多层抽象的情况下,重温一个更简单的时间。 请注意,这就是你正在做的事情。 如果你确定这就是你想要了解asm / hardware的原因,那么我可能是学习为现代硬件编写驱动程序的良好踏脚石。
您采用完全不正确的方法来使用内联ASM。 你似乎想要写全功能的ASM,所以你只要做到这一点 。 例如,将您的代码放入asmfuncs.S
或其他东西。 如果你想继续使用GNU / AT&T语法,请使用.S
; 或者如果你想使用英特尔/ NASM / YASM语法(我会推荐,因为官方手册都使用英特尔语法,请使用.asm
。有关指南和手册,请参阅x86 wiki。)
GNU inline asm是学习ASM 最难的方法 。 您必须了解您的asm所做的一切,以及编译器需要了解的内容。 把事情弄好都很难。 例如,在您的编辑中,内联asm块会修改许多未列为已修改的寄存器,包括%ebx
,这是一个调用保留的寄存器(因此即使该函数未内联也会被破坏)。 至少你拿出了ret
,所以当编译器将这个函数内联到调用它的循环中时,事情不会突然发生。 如果这听起来很复杂,那是因为它是,并且为什么你不应该使用内联asm来学习asm 。
在尝试首先学习asm时误用内联asm的类似问题的答案有更多关于内联asm以及如何使用它的链接。
这部分可能是一个单独的答案,但我会把它放在一起。
除了你的整个方法基本上是一个坏主意之外,你的put_char
函数至少存在一个特定的问题 :你使用offset
作为仅输出操作数。 gcc非常高兴地将您的整个函数编译为单个ret
指令,因为asm语句不是volatile
,并且不使用它的输出。 (没有输出的内联asm语句被认为是volatile
。)
我把你的函数放在godbolt上 ,所以我可以看一下编译器围绕它生成的汇编。 该链接是固定的可能工作版本,具有正确声明的clobbers,注释,清理和优化。 如果外部链接中断,请参阅下面的相同代码。
我使用gcc 5.3和-m16
选项,这与使用真正的16位编译器不同。 它仍然以32位方式执行所有操作(使用32位地址,32位int
和堆栈上的32位函数args),但告诉汇编器CPU将处于16位模式,因此它将知道何时发出操作数大小和地址-size前缀。
即使用-O0
编译原始版本 ,编译器也会计算offset = (y<<8) + (y<<6) + x;
,但不会把它放在%edi
,因为你没有要求它。 将其指定为另一个输入操作数将起作用。 %edi
联asm之后,它将%edi
存储到-12(%ebp)
,其中offset
存在。
put_char
其他错误:
你通过全局变量将两个东西( ascii_char
和current_color
)传递给你的函数,而不是函数参数。 哎呀,这太恶心了。 VGA
和characters
是常量,因此从全局变量加载它们并不是那么糟糕。 在asm中编写意味着只有当它以合理的数量帮助执行时,才应忽略良好的编码实践。 由于调用者可能不得不将这些值存储到全局变量中,因此与将函数存储在堆栈中的调用者相比,您不会保存任何内容。 对于x86-64,你会丢失perf,因为调用者只能将它们传递给寄存器。
也:
j,h,l,i=0; // sets i=0, does nothing to j, h, or l.
// gcc warns: left-hand operand of comma expression has no effect
j;h;l;i=0; // equivalent to this
j=h=l=i=0; // This is probably what you meant
除了offset
之外,所有局部变量都是未使用的。 你要用C或其他东西写吗?
对characters
使用16位地址,对VGA存储器使用32位寻址模式。 我认为这是故意的,但我不知道它是否正确。 另外,你确定你应该使用CS:
覆盖来自characters
的负载吗? .rodata
部分是否进入代码段? 虽然你没有将uint8_t characters[464]
声明为const
,所以它可能只是在.data
部分。 我认为自己很幸运,我实际上并没有为分段内存模型编写代码,但这仍然看起来很可疑。
如果您真的使用djgpp,那么根据Michael Petch的评论, 您的代码将以32位模式运行 。 因此使用16位地址是个坏主意。
您可以完全避免使用%ebx
,而不是加载到ebx,然后将%ebx
添加到%edi
。
"add _VGA, %%edi\n\t" // load from _VGA, add to edi.
你不需要lea
来获取寄存器中的地址。 你可以使用
"mov %%ax, %%si\n\t"
"add $_characters, %%si\n\t"
$_characters
表示地址为立即数。 我们可以通过将此与先前计算的偏移量计算到位图的characters
数组中来节省大量指令。 imul
的立即操作数形式让我们首先在%si
中生成结果:
"movzbw _ascii_char,%%si\n\t"
//"sub $32,%%ax\n\t" // AX = ascii_char - 32
"imul $7, %%si, %%si\n\t"
"add $(_characters - 32*7), %%si\n\t" // Do the -32 at the same time as adding the table address, after multiplying
// SI points to characters[(ascii_char-32)*7]
// i.e. the start of the bitmap for the current ascii character.
由于这种形式的imul
仅保持16 * 16 - > 32b乘法的低16b, 因此2和3操作数形式imul
可用于有符号或无符号乘法 ,这就是为什么只有imul
(而不是mul
)具有这些额外形式。 对于较大的操作数大小乘法,2和3操作数imul
更快 ,因为它不必将高半部分存储在%[er]dx
。
您可以稍微简化内部循环,但它会使外部循环稍微复杂化:您可以在零标志上进行分支,由shl $1, %al
,而不是使用计数器。 这将使它也变得不可预测,就像非前景像素的跳过存储一样,因此增加的分支误预测可能比额外的无操作循环更糟糕。 这也意味着你每次都需要在外循环中重新计算%edi
,因为内循环不会运行常数次。 但它看起来像:
... same first part of the loop as before
// re-initialize %edi to first_pixel-1, based on outer-loop counter
"lea -1(%%edi), %%ebx\n"
".Lbit_loop:\n\t" // map the 1bpp bitmap to 8bpp VGA memory
"incl %%ebx\n\t" // inc before shift, to preserve flags
"shl $1,%%al\n\t"
"jnc .Lskip_store\n\t" // transparency: only store on foreground pixels
"movb %%dl,(%%ebx)\n" //plot the pixel
".Lskip_store:\n\t"
"jnz .Lbit_loop\n\t" // flags still set from shl
"addl $320,%%edi\n\t" // WITHOUT the -6
"dec %%cl\n\t"
"jnz .Lbyte_loop\n\t"
请注意,字符位图中的位将映射到VGA存储器中的字节,如{7 6 5 4 3 2 1 0}
,因为您正在测试通过左移位移出的位。 所以它从MSB开始。 寄存器中的位总是“大端”。 左移乘以2,即使在像x86这样的小端机器上也是如此。 Little-endian只影响内存中字节的排序,而不影响字节中的位,而不影响寄存器内的字节。
这与godbolt链接相同。
void put_char(int x,int y){
int offset = (y<<8) + (y<<6) + x;
__asm__ volatile ( // volatile is implicit for asm statements with no outputs, but better safe than sorry.
"add _VGA, %%edi\n\t" // edi points to VGA + offset.
"movzbw _ascii_char,%%si\n\t" // Better: use an input operand
//"sub $32,%%ax\n\t" // AX = ascii_char - 32
"imul $7, %%si, %%si\n\t" // can't fold the load into this because it's not zero-padded
"add $(_characters - 32*7), %%si\n\t" // Do the -32 at the same time as adding the table address, after multiplying
// SI points to characters[(ascii_char-32)*7]
// i.e. the start of the bitmap for the current ascii character.
"mov $7,%%cl\n"
".Lbyte_loop:\n\t"
"lodsb %%cs:(%%si)\n\t" //load next byte of bitmap
"mov $6,%%ch\n"
".Lbit_loop:\n\t" // map the 1bpp bitmap to 8bpp VGA memory
"shl $1,%%al\n\t"
"jnc .Lskip_store\n\t" // transparency: only store on foreground pixels
"movb %%dl,(%%edi)\n" //plot the pixel
".Lskip_store:\n\t"
"incl %%edi\n\t"
"dec %%ch\n\t"
"jnz .Lbit_loop\n\t"
"addl $320-6,%%edi\n\t"
"dec %%cl\n\t"
"jnz .Lbyte_loop\n\t"
:
: "D" (offset), "d" (current_color)
: "%eax", "%ecx", "%esi", "memory"
// omit the memory clobber if your C never touches VGA memory, and your asm never loads/stores anywhere else.
// but that's not the case here: the asm loads from memory written by C
// without listing it as a memory operand (even a pointer in a register isn't sufficient)
// so gcc might optimize away "dead" stores to it, or reorder the asm with loads/stores to it.
);
}
我没有使用虚拟输出操作数来保留寄存器分配,这取决于编译器的判断,但是减少在内联asm的正确位置获取数据的开销是个好主意。 (额外的mov
指令)。 例如,这里没有必要强制编译器在%edi
放置offset
。 它可能是我们尚未使用的任何注册表。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.