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

标签 c gcc x86 dos djgpp

我正在学习使用C和内联汇编在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是什么,我都始终将像素绘制在同一点上。

enter image description here

谁能帮助我更正我的代码?

最佳答案

请参阅下文,以分析某些put_char函数特别有问题的地方,以及可能起作用的版本。 (我不确定%cs段覆盖是否正确,但除此之外,它应该可以实现您的预​​期)。

学习DOS和16位asm并不是学习asm的最佳方法
首先,DOS和16位x86已经过时,并且不比普通的64位x86更容易学习。即使32位x86也已过时,但在Windows世界中仍被广泛使用。
32位和64位代码不必关心很多16位限制/复杂性,例如段或寻址模式中有限的寄存器选择。一些现代系统确实使用段替代来进行线程本地存储,但是,学习如何在16位代码中使用段几乎没有联系。
了解asm的主要好处之一是调试/分析/优化实际程序。如果您想了解如何编写C或其他高级代码that can (and actually does) compile to efficient asm,您可能会成为looking at compiler output。这将是64位(或32位)。 (例如,请参阅Matt Godbolt的CppCon2017演讲:“What Has My Compiler Done for Me Lately? Unbolting the Compiler's Lid”,该书为入门的初学者阅读x86 asm以及介绍编译器输出提供了很好的介绍)。
在查看性能计数器结果并注释二进制文件的反汇编时,Asm知识非常有用(perf stat ./a.out && perf report -Mintel:参见Chandler Carruth's CppCon2015 talk: "Tuning C++: Benchmarks, and CPUs, and Compilers! Oh My!")。激进的编译器优化意味着,与每条指令相比,查看每条源代码行的周期/缓存丢失/停顿计数所提供的信息少得多。
另外,要使您的程序实际执行任何操作,它必须直接与硬件对话或进行系统调用。学习DOS系统需要文件访问和用户输入,这完全是浪费时间(除了回答有关如何读取和打印16位代码中的多位数数字的源源不断的SO问题)。它们与当前主要操作系统中的API完全不同。开发新的DOS应用程序无济于事,因此,当您要掌握使用asm知识进行操作的阶段时,您必须学习其他API(以及ABI)。
在8086模拟器上学习asm的限制更大:186、286和386添加了许多方便的指令,例如imul ecx, 15,从而使ax的“特殊性”降低了。将自己限制在仅适用于8086的指令上意味着您将找出做事的“不好”方法。其他较大的变量是movzx / movsx,立即移位(除1之外)和push immediate。除了性能之外,在可用时编写代码也更加容易,因为您不必编写循环来移位多于1位。

有关更好地教自己asm的建议
我主要通过阅读编译器输出来学习asm,然后进行一些小的更改。当我不太了解内容时,我并没有尝试在asm中编写东西,但是,如果您要快速学习(而不是仅在调试/分析C时形成理解),则可能需要通过以下方法测试您的理解:编写自己的代码。您确实需要了解基本知识,有8或16个整数寄存器+标志和指令指针,并且每条指令都对计算机的当前体系结构状态进行了明确定义的修改。 (有关每条指令的完整说明,请参阅Intel insn ref手册( Wiki中的链接以及更多有用的内容)。
您可能想从简单的事情开始,例如在更大的程序中用asm编写单个函数。了解进行系统调用所需的asm类型很有用,但是在实际程序中,通常只为不涉及任何系统调用的内部循环手写asm才有用。编写asm来读取输入和打印结果非常耗时,因此我建议在C语言中进行此操作。请确保您阅读了编译器的输出,并了解发生了什么,整数与字符串之间的区别以及strtolprintf可以,即使您自己不写它们。
一旦您认为您对基础知识已经足够了解,就可以在您熟悉和/或感兴趣的某个程序中找到一个函数,并查看是否可以击败编译器并保存指令(或使用更快的指令)。或者您自己实现它,而不以编译器输出为起点,以您发现更有趣的为准。 This answer可能很有趣,尽管那里的重点是找到使编译器产生最佳ASM的C源代码。
如何尝试解决自己的问题(问一个SO问题之前)
人们有很多SO问题,问“我如何在asm中执行X”,答案通常是“与在C中相同”。不要被不熟悉的asm所困扰,而忘记了如何编程。弄清楚该函数所操作的数据需要做什么,然后弄清楚如何在asm中进行操作。如果您陷入困境并不得不提出问题,那么您应该拥有大部分有效的实施方案,只有一部分您不知道要在哪一步中使用什么指令。
您应该使用32或64位x86。我建议使用64位,因为ABI更好,但是32位功能将迫使您更多地使用堆栈。因此,这可能有助于您了解call指令如何将返回地址放置在堆栈上,以及调用方实际推入的args在此之后。 (这似乎是您尝试使用内联asm避免处理的内容)。

直接对硬件进行编程很整洁,但不是一般有用的技能
通过直接修改视频RAM来学习如何做图形,除了满足人们对计算机过去工作方式的好奇心外,是没有用的。您不能将这些知识用于任何事情。现代图形API的存在是为了让多个程序可以在自己的屏幕区域中绘画并允许间接调用(例如,直接在纹理而不是屏幕上绘画,因此3D窗口翻转alt选项卡看起来不错)。这里有太多的原因要列出而不直接在视频RAM上绘制。
可以在像素图缓冲区上绘图,然后使用图形API将其复制到屏幕上。不过,除非要为PNG或JPEG等生成图像(例如,将Web后端代码中的直方图bin转换为散点图进行优化,否则完全不做位图图形)。现代图形API抽象化了分辨率,因此您的应用程序可以以合理的大小绘制图形,而不管每个像素有多大。 (小屏幕但rez屏幕非常高,而rez屏幕大而低)。
写内存并在屏幕上看到某些变化是很酷的。甚至更好的是,将LED(带有小电阻器)连接到并行端口上的数据位,并运行outb指令打开/关闭它们。我是在很久以前的Linux系统上执行此操作的。我编写了一个使用iopl(2)和内联asm的包装程序,并以root身份运行它。您可能可以在Windows上执行类似操作。您不需要DOS或16位代码即可与硬件对话。
in / out指令以及对内存映射的IO和DMA的正常加载/存储是真正的驱动程序与硬件的通信方式,包括比并行端口复杂得多的事情。知道您的硬件“真正”如何工作很有趣,但是只有在您真正感兴趣或想要编写驱动程序时才花时间。 Linux源代码树包含用于加载大量硬件的驱动程序,并且经常被注释,因此,如果您既喜欢阅读代码,又喜欢编写代码,那是另一种感受读驱动程序与硬件对话时的方式的方式。
总体了解事情的幕后工作通常是一件好事。如果您想了解图形在过去如何工作(使用VGA文本模式和颜色/属性字节),那么请放心。请注意,现代操作系统不使用VGA文本模式,因此您甚至都不了解现代计算机的幕后情况。
许多人喜欢https://retrocomputing.stackexchange.com/,这使计算机不那么复杂并且无法支持那么多抽象层,从而使工作时间更短。请注意,这就是您在做什么。如果您确定这就是为什么要了解asm /硬件,那么我可能是学习编写现代硬件驱动程序的一个很好的垫脚石。

内联汇编
您使用完全错误的方法来使用嵌入式ASM。您似乎想在asm中编写整个函数,因此您应该这样做。例如将您的代码放入asmfuncs.S之类的东西中。如果要继续使用GNU / AT&T语法,请使用.S;否则,请使用.asm。或使用%ebx(如果您想使用Intel / NASM / YASM语法)(我建议这样做,因为官方手册都使用Intel语法。有关指南和手册,请参见 Wiki)。
GNU内联汇编是学习ASM的最困难的方法。您必须了解asm所做的一切,以及编译器需要了解的内容。很难正确解决所有问题。例如,在您的编辑中,该内联汇编代码块修改了许多您没有列出的寄存器,包括ret这是一个调用保留的寄存器(因此即使未内联该函数,它也会被破坏)。至少您取出了put_char,所以当编译器将该函数内联到调用它的循环中时,事情不会中断得那么厉害。如果这听起来真的很复杂,那是因为它是,这也是为什么您不应该使用嵌入式asm学习asm的部分原因。
This answer to a similar question from misusing inline asm while trying to learn asm in the first place有更多关于内联汇编以及如何使用它的链接。

使这个混乱工作,也许
这部分可能是一个单独的答案,但我将其保留在一起。
除了整个方法从根本上来说不是一个好主意外,offset函数还存在至少一个特定问题:您将ret用作仅输出操作数。 gcc非常高兴地将整个函数编译为一条volatile指令,因为asm语句不是volatile,并且不使用其输出。 (不带输出的嵌入式asm语句假定为-m16。)
put your function on godbolt,所以我可以看看编译器围绕它生成的程序集。该链接指向固定的可能工作的版本,带有正确声明的Clobber,注释,清理和优化。如果该外部链接中断,请参见下面的相同代码。
我将gcc 5.3与int选项一起使用,这与使用真正的16位编译器不同。它仍然以32位方式(在堆栈上使用32位地址,32位-O0和32位函数args)执行所有操作,但告诉汇编器CPU将处于16位模式,因此它将知道何时发出操作数大小和地址大小前缀。
即使您compile your original version with offset = (y<<8) + (y<<6) + x;,编译器也会计算%edi,但不会将其放在%edi中,因为您没有要求它。将其指定为另一个输入操作数将起作用。内联汇编之后,它将-12(%ebp)存储到offset所在的put_char中。

ascii_char的其他错误:
您通过全局变量而不是函数参数将2个东西(current_colorVGA)传递给函数。 uck,真恶心。 charactersoffset是常量,因此从全局变量加载它们看起来并不那么糟糕。使用asm编写意味着您仅应在良好的性能帮助下才忽略良好的编码习惯。由于调用者可能必须将这些值存储到全局变量中,因此与将它们作为函数args存储在堆栈中的调用者相比,您无需保存任何内容。对于x86-64,您将失去性能,因为调用者可以将它们传递到寄存器中。
也:

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

除了characters以外,所有局部变量都未使用。您要用C或其他语言编写它吗?
您将16位地址用于CS:,但将32位寻址模式用于VGA内存。我认为这是故意的,但我不知道它是否正确。另外,您确定应该对characters的负载使用.rodata替代吗? uint8_t characters[464]部分是否进入代码段?尽管您没有将const声明为.data,但是无论如何它都可能只是在%ebx部分中。我认为自己很幸运,虽然我实际上没有为分段内存模型编写代码,但是看起来仍然很可疑。
如果您真的在使用djgpp,那么根据Michael Petch的评论,您的代码将以32位模式运行。因此,使用16位地址是个坏主意。

最佳化
通过这样做,您可以避免完全使用%ebx,而不是加载到ebx中,然后在%edi中添加lea
 "add    _VGA, %%edi\n\t"   // load from _VGA, add to edi.

您不需要$_characters将地址保存到寄存器中。你可以用
    "mov    %%ax, %%si\n\t"
    "add    $_characters, %%si\n\t"

characters表示地址为立即数。通过将其与以前的偏移量计算组合到位图的imul数组中,我们可以节省很多指令。 %si的立即操作数形式使我们首先在imul中产生结果:
    "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乘以the 2 and 3 operand forms imul can be used for signed or unsigned multiplies,这就是为什么只有mul(不是imul)具有那些额外形式的原因。对于较大的操作数大小乘法,请使用2和3操作数%[er]dx is faster,因为它不必将高半部分存储在shl $1, %al中。
您可以稍微简化内部循环,但是会使外部循环稍微复杂一些:您可以分支到零标志(由%edi设置),而不使用计数器。这也将使其变得不可预测,就像非前景像素的跳转存储一样,因此增加的分支错误预测可能比多余的虚无循环还要糟糕。这也意味着您每次都需要在外循环中重新计算{7 6 5 4 3 2 1 0},因为内循环不会运行恒定的次数。但它看起来像:
    ... 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"

请注意,字符位图中的位将映射到mov之类的VGA内存中的字节,因为您正在测试左移的位。因此,它从MSB开始。寄存器中的位始终是“大端”。即使在像x86这样的低端机器上,左移也乘以2。小尾数仅影响存储器中字节的顺序,不影响字节中的位,甚至不影响寄存器中的字节。

函数的一个版本,可能会实现您想要的功能。
这与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.    
    );
}

我没有使用伪输出操作数来让寄存器分配由编译器自行决定,但这是一个好主意,可减少为内联汇编在正确的位置获取数据的开销。 (额外的offset说明)。例如,这里不需要强制编译器将%edi放在中。可能是我们尚未使用的任何寄存器。

关于c - 使用GNU C内联汇编在VGA内存中绘制字符,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34748733/

相关文章:

C-将txt读入txt文件

c - 如何避免 'C' 中的 TCP 聚合?

c - malloc 使用什么系统调用?

c - 如何从我的程序中运行一些机器代码

c - 从 8 位数据中去除 C 中的奇偶校验位,后跟 1 个奇偶校验位

GCC 编译器中的条件移动 (cmov)

c++ - 忽略 C++ 中断言表达式的副作用

gcc - 使用 Cmake 的混合语言 C++、C 和 Fortran 编译

gcc - 使用外部C代码编译ASM引导加载程序

assembly - 对 10 个数字求和并在 NASM 中打印结果