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

Pab*_*ada 8 c x86 gcc dos djgpp

我正在学习用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"

    );


}
Run Code Online (Sandbox Code Playgroud)

我从PASCAL编写的这一系列教程中引导自己:http://www.joco.homeserver.hu/vgalessons/lesson8.html.

我根据gcc编译器更改了汇编语法,但我仍然收到此错误:

Operand mismatch type for 'lea'
No such instruction 'segcs lodsb'
No such instruction 'retn'
Run Code Online (Sandbox Code Playgroud)

编辑:

我一直在努力改进我的代码,至少现在我在屏幕上看到了什么.这是我更新的代码:

/**************************************************************************
 *  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)

    );


}
Run Code Online (Sandbox Code Playgroud)

如果你看到上面的图像,我试着写下字母"S".结果是您在屏幕左上角看到的绿色像素.无论x和y我给出了功能,它总是在同一点上绘制像素.

在此输入图像描述

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

Pet*_*des 23

请参阅下文,分析您的put_char功能特别错误的一些事情,以及可能有效的版本.(我不确定%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更是限制:186,286,和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)和内联asm 的小包装器程序,并以root身份运行它.您可以在Windows上执行类似操作.你不需要DOS或16位代码来与硬件交谈.

in/ out指令,正常加载/存储到内存映射IO和DMA,是真正的驱动程序与硬件通信的方式,包括比并行端口复杂得多的东西.了解您的硬件"真正"如何工作很有趣,但如果您真正感兴趣或想要编写驱动程序,则只花时间在其上.Linux源代码树包含了大量硬件的驱动程序,并且经常被很好地评论,因此如果您喜欢像编写代码一样阅读代码,那么这是另一种了解读取驱动程序与硬件通信时所做的事情的方法.

一般来说,了解一切是如何运作的.如果你了解图形用于很久以前的工作(使用VGA文本模式和颜色/属性字节),那么请确定.请注意,现代操作系统不使用VGA文本模式,因此您甚至不了解现代计算机下发生的事情.

许多人喜欢https://retrocomputing.stackexchange.com/,在计算机不那么复杂且无法支持多层抽象的情况下,重温一个更简单的时间.请注意,这就是你正在做的事情.如果你确定就是你想要了解asm/hardware的原因,那么我可能是学习为现代硬件编写驱动程序的好基石.


内联asm

You are taking a totally incorrect approach to using inline ASM. You seem to want to write whole functions in asm, so you should just do that. e.g. put your code in asmfuncs.S or something. Use .S if you want to keep using GNU/AT&T syntax; or use .asm if you want to use Intel/NASM/YASM syntax (which I would recommend, since the official manuals all use Intel syntax. See the wiki for guides and manuals.)

GNU inline asm is the hardest way to learn ASM. You have to understand everything that your asm does, and what the compiler needs to know about it. It's really hard to get everything right. For example, in your edit, that block of inline asm modifies many registers that you don't list as clobbered, including %ebx which is a call-preserved register (so this is broken even if that function isn't inlined). At least you took out the ret, so things won't break as spectacularly when the compiler inlines this function into the loop that calls it. If that sounds really complicated, that's because it is, and part of why you shouldn't use inline asm to learn asm.

This answer to a similar question from misusing inline asm while trying to learn asm in the first place has more links about inline asm and how to use it well.


Getting this mess working, maybe

This part could be a separate answer, but I'll leave it together.

Besides your whole approach being fundamentally a bad idea, there is at least one specific problem with your put_char function: you use offset as an output-only operand. gcc quite happily compiles your whole function to a single ret instruction, because the asm statement isn't volatile, and its output isn't used. (Inline asm statements without outputs are assumed to be volatile.)

I put your function on godbolt, so I could look at what assembly the compiler generates surrounding it. That link is to the fixed maybe-working version, with correctly-declared clobbers, comments, cleanups, and optimizations. See below for the same code, if that external link ever breaks.

I used gcc 5.3 with the -m16 option, which is different from using a real 16bit compiler. It still does everything the 32bit way (using 32bit addresses, 32bit ints, and 32bit function args on the stack), but tells the assembler that the CPU will be in 16bit mode, so it will know when to emit operand-size and address-size prefixes.

Even if you compile your original version with -O0, the compiler computes offset = (y<<8) + (y<<6) + x;, but doesn't put it in %edi, because you didn't ask it to. Specifying it as another input operand would have worked. After the inline asm, it stores %edi into -12(%ebp), where offset lives.


Other stuff wrong with put_char:

You pass 2 things (ascii_char and current_color) into your function through globals, instead of function arguments. Yuck, that's disgusting. VGA and characters are constants, so loading them from globals doesn't look so bad. Writing in asm means you should ignore good coding practices only when it helps performance by a reasonable amount. Since the caller probably had to store those values into the globals, you're not saving anything compared to the caller storing them on the stack as function args. And for x86-64, you'd be losing perf because the caller could just pass them in registers.

Also:

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
Run Code Online (Sandbox Code Playgroud)

All the local variables are unused anyway, other than offset. Were you going to write it in C or something?

You use 16bit addresses for characters, but 32bit addressing modes for VGA memory. I assume this is intentional, but I have no idea if it's correct. Also, are you sure you should use a CS: override for the loads from characters? Does the .rodata section go into the code segment? Although you didn't declare uint8_t characters[464] as const, so it's probably just in the .data section anyway. I consider myself fortunate that I haven't actually written code for a segmented memory model, but that still looks suspicious.

If you're really using djgpp, then according to Michael Petch's comment, your code will run in 32bit mode. Using 16bit addresses is thus a bad idea.


Optimizations

You can avoid using %ebx entirely by doing this, instead of loading into ebx and then adding %ebx to %edi.

 "add    _VGA, %%edi\n\t"   // load from _VGA, add to edi.
Run Code Online (Sandbox Code Playgroud)

You don't need lea to get an address into a register. You can just use

    "mov    %%ax, %%si\n\t"
    "add    $_characters, %%si\n\t"
Run Code Online (Sandbox Code Playgroud)

$_characters means the address as an immediate constant. We can save a lot of instructions by combining this with the previous calculation of the offset into the characters array of bitmaps. The immediate-operand form of imul lets us produce the result in %si in the first place:

    "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.
Run Code Online (Sandbox Code Playgroud)

Since this form of imul only keeps the low 16b of the 16*16 -> 32b multiply, the 2 and 3 operand forms imul can be used for signed or unsigned multiplies, which is why only imul (not mul) has those extra forms. For larger operand-size multiplies, 2 and 3 operand imul is faster, because it doesn't have to store the high half in %[er]dx.

You could simplify the inner loop a bit, but it would complicate the outer loop slightly: you could branch on the zero flag, as set by shl $1, %al, instead of using a counter. That would make it also unpredictable, like the jump over store for non-foreground pixels, so the increased branch mispredictions might be worse than the extra do-nothing loops. It would also mean you'd need to recalculate %edi in the outer loop each time, because the inner loop wouldn't run a constant number of times. But it could look like:

    ... 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"
Run Code Online (Sandbox Code Playgroud)

Note that the bits in your character bitmaps are going to map to bytes in VGA memory like {7 6 5 4 3 2 1 0}, because you're testing the bit shifted out by a left shift. So it starts with the MSB. Bits in a register are always "big endian". A left shift multiplies by two, even on a little-endian machine like x86. Little-endian only affects ordering of bytes in memory, not bits in a byte, and not even bytes inside registers.


A version of your function that might do what you intended.

This is the same as the godbolt link.

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.    
    );
}
Run Code Online (Sandbox Code Playgroud)

我没有使用虚拟输出操作数来保留寄存器分配,这取决于编译器的判断,但是减少在内联asm的正确位置获取数据的开销是个好主意.(额外mov说明).例如,这里没有必要强制编译器offset输入%edi.它可能是我们尚未使用的任何注册表.