使用GNU C内联汇编在VGA内存中绘制字符

我正在学习用DOS和内联汇编在DOS下进行一些低级VGA编程。 现在我正在尝试创建一个在屏幕上打印出一个角色的function。

这是我的代码:

//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我给出了function,它总是在同一点上绘制像素。

在此处输入图像描述

任何人都可以帮我纠正我的代码吗?

请参阅下文,了解put_char函数特别错误的一些内容,以及可能有效的版本。 (我不确定%cs段覆盖,但%cs它应该做你想要的)。


学习DOS和16位asm不是学习asm的最佳方式

首先,DOS和16位x86完全过时,并且比普通的64位x86更容易学习。 即使是32位x86也已经过时,但在Windows世界中仍然广泛使用。

32位和64位代码不必关心许多16位限制/复杂性,例如段或寻址模式中有限的寄存器选择。 一些现代系统确实使用段覆盖来进行线程本地存储,但是学习如何在16位代码中使用段几乎与之无关。

了解asm的主要好处之一是调试/分析/优化实际程序。 如果您想了解如何编写C或其他可以(实际上)编译为高效asm的高级代码,您可能会看到编译器输出 。 这将是64位(或32位)。 (例如,请参阅Matt Godbolt的CppCon2017演讲: “我的编译器最近为我做了什么?解开编译器的Lid” ,它有一个很好的介绍,可以为初学者阅读x86 asm,并查看编译器输出)。

当查看性能计数器结果来注释二进制的反汇编时,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,然后进行小的更改。 当我不理解事情的时候,我没有尝试在asm中写东西,但是如果你要快速学习(而不是仅仅在调试/分析C时发展理解),你可能需要通过以下方式测试你的理解。编写自己的代码。 您需要了解基础知识,即有8个或16个整数寄存器+标志和指令指针,并且每个指令都对机器的当前架构状态进行明确定义的修改。 (有关每条指令的完整说明,请参阅英特尔insn参考手册( x86 wiki中的链接以及更多好东西 )。

您可能希望从简单的事情开始,比如在asm中编写单个函数,作为更大程序的一部分。 了解进行系统调用所需的asm类型很有用,但在实际程序中,通常只对不涉及任何系统调用的内部循环手动编写asm很有用。 编写asm来读取输入和打印结果非常耗时,所以我建议你在C中做这个部分。确保你读取编译器输出并了解发生了什么,以及整数和字符串之间的区别,以及strtolprintf ,即使你自己不写。

一旦你认为你已经理解了足够的基础知识,就可以在你熟悉和/或感兴趣的某个程序中找到一个函数,看看你是否可以击败编译器并保存指令(或使用更快的指令)。 或者自己实现它而不使用编译器输出作为起点,无论你发现哪个更有趣。 这个答案可能很有意思,虽然重点是找到C源,让编译器产生最佳ASM。

如何尝试解决自己的问题(在提出SO问题之前)

人们问“我如何在asm中做X”时有很多SO问题,答案通常是“与你在C中相同”。 不要因为不熟悉而忘记如何编程而陷入困境。 弄清楚函数操作的数据需要发生什么,然后弄清楚如何在asm中执行此操作。 如果你遇到困难并且不得不提出问题,那么你应该拥有大部分可行的实现,只有一部分你不知道用于一步的指令。

你应该使用32或64位x86。 我建议使用64位,因为ABI更好,但是32位function会迫使你更多地使用堆栈。 这样可以帮助您理解call指令如何将返回地址放在堆栈上,以及调用者实际推送的args在此之后的位置。 (这似乎是您试图通过使用内联asm来避免处理的内容)。


直接编程硬件很整洁,但不是一般的有用技巧

学习如何通过直接修改videoRAM来做图形是没有用的,除了满足计算机如何工作的好奇心。 你无法将这些知识用于任何事情。 现代图形API的存在是为了让多个程序在它们自己的屏幕区域中绘制,并允许间接(例如直接在纹理而不是屏幕上绘制,因此3D窗口翻转alt-tab看起来很花哨)。 这里列出的原因太多,没有直接在videoRAM上绘图。

可以使用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。 你似乎想在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_charcurrent_color )传递给你的函数,而不是函数参数。 哎呀,这太恶心了。 VGAcharacters是常量,因此从全局变量加载它们并不是那么糟糕。 在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的立即操作数forms让我们首先在%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] // ie the start of the bitmap for the current ascii character. 

由于这种forms的imul仅保留16 * 16 - > 32b乘法的低16b, 因此2和3操作数formsimul可用于有符号或无符号乘法 ,这就是为什么只有imul (而不是mul )具有这些额外forms。 对于较大的操作数大小乘法,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] // ie 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 。 它可能是我们尚未使用的任何注册表。