9

我正在学习使用 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 我给出什么函数,它总是在同一个点上绘制像素。

在此处输入图像描述

谁能帮我更正我的代码?

4

1 回答 1

29

请参阅下面对您的功能特别错误的一些事情的分析put_char,以及可能有效的版本。(我不确定%cs段覆盖,但除此之外它应该做你想要的)。


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

首先,DOS 和 16 位 x86 已经彻底过时,并且比普通的 64 位 x86 学习起来并不容易。甚至 32 位 x86 也已过时,但仍在 Windows 世界中广泛使用。

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

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

在查看注释二进制反汇编的性能计数器结果时,Asm 知识很有用(perf stat ./a.out&& perf report -Mintel:请参阅Chandler Carruth 的 CppCon2015 演讲:“Tuning C++: Benchmarks, and CPUs, and Compilers! Oh My!”)。积极的编译器优化意味着查看每条源代码行的周期/缓存未命中/停顿计数比每条指令提供的信息要少得多。

此外,为了让您的程序真正任何事情,它必须要么直接与硬件对话,要么进行系统调用。学习文件访问和用户输入的 DOS 系统调用完全是浪费时间(除了回答关于如何以 16 位代码读取和打印多位数字的源源不断的 SO 问题)。它们与当前主要操作系统中的 API 完全不同。开发新的 DOS 应用程序是没有用的,所以当你到了用你的 asm 知识做某事的阶段时,你必须学习另一个 API(以及 ABI)。

在 8086 模拟器上学习 asm 的限制更大:186、286 和 386 添加了许多方便的指令,例如imul ecx, 15ax减少“特殊”。将自己限制在仅适用于 8086 的指令上意味着您将找出“坏”的做事方式。其他大的是movzx/ movsx,立即数(1除外)和push immediate. 除了性能之外,当这些可用时编写代码也更容易,因为您不必编写循环来移位超过 1 位。


关于更好的自学 asm 方法的建议

我主要通过阅读编译器输出来学习 asm,然后进行一些小改动。当我不太了解事物时,我没有尝试用 asm 编写东西,但是如果您要快速学习(而不是在调试/分析 C 时发展理解),您可能需要通过以下方式测试您的理解编写自己的代码。您确实需要了解基础知识,即有 8 或 16 个整数寄存器 + 标志和指令指针,并且每条指令都对机器的当前架构状态进行了明确定义的修改。(有关每条指令的完整描述,请参阅 Intel insn 参考手册( wiki 中的链接,以及更多好东西)。

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

一旦您认为您对基础知识了解足够多,请在您熟悉和/或感兴趣的某个程序中找到一个函数,然后看看您是否可以击败编译器并保存指令(或使用更快的指令)。或者在使用编译器输出作为起点的情况下自己实现它,无论您觉得哪个更有趣。 这个答案可能很有趣,尽管重点是寻找 C 源代码让编译器生成最佳 ASM。

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

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

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


直接编程硬件很整洁,但不是一个普遍有用的技能

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

在像素图缓冲区上绘图,然后使用图形 API 将其复制到屏幕上是可能的。尽管如此,制作位图图形或多或少已经过时,除非您正在为 PNG 或 JPEG 或其他东西生成图像(例如,优化将直方图箱转换为 Web 服务后端代码中的散点图)。现代图形 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或什么的。.S如果您想继续使用 GNU / AT&T 语法,请使用;或者.asm如果您想使用 Intel / NASM / YASM 语法,请使用(我会推荐,因为官方手册都使用 Intel 语法。有关指南和手册,请参阅

GNU inline asm 是学习 ASM最难的方法。您必须了解 asm 所做的一切,以及编译器需要了解的内容。把所有事情都做好真的很难。例如,在您的编辑中,该内联 asm 块修改了许多您未列为已破坏的寄存器,包括%ebx哪些是保留调用的寄存器(因此即使该函数未内联,这也会被破坏)。至少你取出了ret, 所以当编译器将此函数内联到调用它的循环中时,事情不会那么严重。如果这听起来真的很复杂,那是因为它确实是,这也是你不应该使用内联 asm 来学习 asm的部分原因。

This answer to an similar question from misusing inline asm while trying to learn asm 首先有更多关于 inline asm 以及如何很好地使用它的链接。


让这个烂摊子工作,也许

这部分可能是一个单独的答案,但我会把它放在一起。

除了您的整个方法基本上是一个坏主意之外,您的函数至少存在一个特定问题put_char:您将offset其用作仅输出操作数。gcc 很高兴地将您的整个函数编译为一条ret指令,因为 asm 语句不是volatile,并且它的输出未被使用。(假定没有输出的内联 asm 语句是volatile。)

把你的函数放在 godbolt 上,所以我可以看看编译器围绕它生成的程序集。该链接指向固定的可能工作版本,具有正确声明的破坏、评论、清理和优化。如果该外部链接中断,请参见下面的相同代码。

我使用带有选项的 gcc 5.3 -m16,这与使用真正的 16 位编译器不同。它仍然以 32 位方式执行所有操作(使用int堆栈上的 32 位地址、32 位和 32 位函数参数),但告诉汇编器 CPU 将处于 16 位模式,因此它将知道何时发出操作数大小和地址-大小前缀。

即使您使用编译原始版本-O0,编译器也会计算offset = (y<<8) + (y<<6) + x;,但不会将其放入%edi,因为您没有要求它这样做。将其指定为另一个输入操作数会起作用。在 inline asm 之后,它存储%edi-12(%ebp), whereoffset中。


其他错误put_char

您通过全局变量而不是函数参数将两件事(ascii_char和)传递给您的函数。current_color呸,真恶心。 VGA并且characters是常量,因此从全局变量加载它们看起来并不那么糟糕。用 asm 编写意味着只有在对性能有一定帮助时才应该忽略良好的编码实践。由于调用者可能必须将这些值存储到全局变量中,因此与调用者将它们作为函数参数存储在堆栈中相比,您不会保存任何内容。而对于 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

无论如何,所有局部变量都未使用,除了offset. 你打算用C还是什么的?

您使用 16 位地址characters,但 VGA 内存使用 32 位寻址模式。我认为这是故意的,但我不知道它是否正确。另外,您确定应该CS:对来自的负载使用覆盖characters吗?该部分是否.rodata进入代码段?虽然您没有声明uint8_t characters[464]as const,但它可能只是在该.data部分中。我认为自己很幸运,我实际上并没有为分段内存模型编写代码,但这看起来仍然很可疑。

如果您真的在使用 djgpp,那么根据 Michael Petch 的评论,您的代码将以 32bit 模式运行。因此,使用 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(not 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 开始。寄存器中的位总是“大端”。即使在 x86 这样的小端机器上,左移也会乘以 2。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)        // EDI modified by the asm, compiler needs to know that, so use a read-write "+" input.  Early-clobber "&" because we read the other input after modifying this.
        : "d" (current_color)  // used read-only
        : "%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.    
    );
}

回复:"memory"clobber,请参阅如何指示可以使用内联 ASM 参数*指向*的内存?

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

于 2016-01-21T08:22:08.950 回答