繁体   English   中英

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

[英]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位asm不是学习asm的最佳方式

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

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

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

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

人们问“如何在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。 你似乎想要写全功能的ASM,所以你只要做到这一点 例如,将您的代码放入asmfuncs.S或其他东西。 如果你想继续使用GNU / AT&T语法,请使用.S ; 或者如果你想使用英特尔/ NASM / YASM语法(我会推荐,因为官方手册都使用英特尔语法,请使用.asm 。有关指南和手册,请参阅 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的立即操作数形式让我们首先在%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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM