使用 GNU C 内联汇编在 VGA 内存中绘制一个字符
Drawing a character in VGA memory with GNU C inline assembly
我正在学习使用 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 ,%al\n\t"
"mov ,%ah\n\t"
"mul %ah\n\t"
"lea $characters,%si\n\t"
"add %ax,%si\n\t"
"mov ,%cl\n\t"
"0:\n\t"
"segCS %lodsb\n\t"
"mov ,%ch\n\t"
"1:\n\t"
"shl ,%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 0-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 ,%%al;"
"mov ,%%ah;"
"mul %%ah;"
"lea _characters,%%si;"
"add %%ax,%%si;" //SI point to bitmap
"mov ,%%cl;"
"0:;"
"lodsb %%cs:(%%si);" //load next byte of bitmap
"mov ,%%ch;"
"1:;"
"shl ,%%al;"
"jnc 2f;"
"movb %%dl,(%%edi);" //plot the pixel
"2:\n\t"
"incl %%edi;"
"dec %%ch;"
"jnz 1b;"
"addl 0-6,%%edi;"
"dec %%cl;"
"jnz 0b;"
: "=D" (offset)
: "d" (current_color)
);
}
如果您看到上图,我正在尝试写信 "S"。结果是您在屏幕左上角看到的绿色像素。无论我给函数什么 x 和 y,它总是在同一点上绘制像素。
谁能帮我更正我的代码?
请参阅下文,了解对您的 put_char
函数的一些具体错误的分析,以及可能有效的版本。 (我不确定 %cs
段覆盖,但除此之外它应该按照您的意图进行)。
学习 DOS 和 16 位 asm 并不是学习 asm 的最佳方式
首先,DOS 和 16 位 x86 已经完全过时了,并且 不 比普通的 64 位 x86 更容易学习。即使 32 位 x86 已经过时,但仍在 Windows 世界中广泛使用。
32 位和 64 位代码不必关心很多 16 位限制/复杂性,例如段或寻址模式中有限的寄存器选择。一些现代系统确实使用段覆盖 thread-local 存储,但学习如何在 16 位代码中使用段几乎与此无关。
了解 asm 的主要好处之一是调试/分析/优化实际程序。如果你想了解如何编写 C 或其他 high-level 代码 , you'll probably be . This will be 64-bit (or 32-bit). (e.g. see Matt Godbolt's CppCon2017 talk: “What Has My Compiler Done for Me Lately? Unbolting the Compiler's Lid”,它对 total 初学者阅读 x86 asm 有很好的介绍,并查看编译器输出)。
Asm 知识在查看 performance-counter 注释二进制反汇编结果时很有用(perf stat ./a.out
&& perf report -Mintel
:参见 Chandler Carruth's CppCon2015 talk: "Tuning C++: Benchmarks, and CPUs, and Compilers! Oh My!")。积极的编译器优化意味着查看每个源代码行的周期/cache-miss/停顿计数比每条指令提供的信息少得多。
此外,要让您的程序真正做任何事情,它必须直接与硬件对话,或者进行系统调用。学习文件访问和用户输入的 DOS 系统调用完全是浪费时间(除了回答源源不断的关于如何在 16 位代码中读取和打印 multi-digit 数字的 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 个整数寄存器 + 标志和指令指针,并且每条指令都会对机器的当前架构状态进行 well-defined 修改。 (有关每条指令的完整描述,请参阅英特尔 insn 参考手册(x86 wiki 中的 links,以及 更多好东西 )。
您可能想从简单的事情开始,例如在 asm 中编写单个函数,作为更大程序的一部分。了解进行系统调用所需的 asm 类型很有用,但在实际程序中,通常只对不涉及任何系统调用的内部循环的 hand-write asm 有用。编写 asm 来读取输入和打印结果是 time-consuming,所以我建议在 C 中完成该部分。确保您阅读了编译器输出并了解发生了什么,以及整数和字符串之间的区别,以及 strtol
和 printf
的作用,即使它们不是您自己编写的。
一旦您认为自己对基础知识有了足够的了解,就可以在一些您熟悉的程序中找到一个函数 and/or 感兴趣,看看您是否可以击败编译器并保存指令(或使用更快的指令).或者自己实现它 而不是 使用编译器输出作为起点,以你觉得更有趣的为准。 可能很有趣,尽管那里的重点是找到让编译器生成最佳 ASM 的 C 源代码。
如何尝试解决自己的问题(在提出 SO 问题之前)
人们问“我如何在 asm 中做 X”有很多 SO 问题,答案通常是“与在 C 中做的一样”。不要因为不熟悉 asm 而忘记了如何编程。弄清楚函数操作的数据需要发生什么,然后弄清楚如何在 asm 中做到这一点。如果您遇到困难并且不得不提出问题,您应该拥有大部分有效的实施,只有一部分帽子你不知道一步使用什么指令。
您应该使用 32 位或 64 位 x86 执行此操作。我建议使用 64 位,因为 ABI 更好,但是 32 位函数将迫使您更多地使用堆栈。因此,这可能会帮助您理解 call
指令如何将 return 地址放入堆栈,以及调用者实际推送的 args 所在的位置。 (这似乎是您试图通过使用内联 asm 来避免处理的问题)。
直接对硬件进行编程很巧妙,但不是一般有用的技能
通过直接修改视频 RAM 来学习如何做图形没有用,除了满足对计算机过去如何工作的好奇心。你不能将这些知识用于任何事情。现代图形 API 的存在是为了让多个程序在他们自己的屏幕区域中绘制,并允许间接(例如,在纹理上绘制而不是直接在屏幕上绘制,所以 3D window-flipping alt-tab可以看起来很花哨)。不直接在视频 RAM 上绘图的原因太多,无法在此处列出。
可以在像素图缓冲区上绘图,然后使用图形 API 将其复制到屏幕上。尽管如此,做位图图形或多或少已经过时了,除非你正在为 PNG 或 JPEG 或其他东西生成图像(例如,在 Web 服务的 back-end 代码中优化将直方图箱转换为散点图)。现代图形 API 抽象了分辨率,因此无论每个像素有多大,您的应用程序都可以以合理的尺寸绘制内容。 (小但分辨率极高的屏幕与低分辨率下的大电视)。
写入内存并看到一些变化是很酷的 on-screen。或者更好的是,将 LED(带有小电阻)连接到并行端口上的数据位,然后 运行 一个 outb
指令将它们 on/off。我很久以前就在我的 Linux 系统上这样做过。我做了一个小包装程序,它使用 iopl(2)
和内联 asm,运行 它作为根。您可能可以在 Windows 上做类似的事情。您不需要 DOS 或 16 位代码来与硬件对话。
in
/out
指令,以及正常的 loads/stores 到 memory-mapped IO 和 DMA,是真正的驱动程序与硬件通信的方式,包括比并行端口。了解您的硬件“真正”如何工作很有趣,但只有在您真正感兴趣或想编写驱动程序时才花时间在上面。 Linux 源代码树包括用于大量硬件的驱动程序,并且通常有很好的注释,所以如果你喜欢阅读代码就像编写代码一样,这是另一种方式来感受读取驱动程序在与硬件对话时所做的事情.
了解引擎盖下的工作原理通常是件好事。如果您想要 了解很久以前图形是如何工作的(使用 VGA 文本模式和颜色/属性字节),那么当然,请发疯。请注意,现代操作系统不使用 VGA 文本模式,因此您甚至没有了解现代计算机的幕后情况。
许多人喜欢 https://retrocomputing.stackexchange.com/,重温计算机不那么复杂且无法支持那么多抽象层的简单时代。请注意,这就是您正在做的事情。如果您确定那就是您想要了解 asm / 硬件的原因,我可能是学习为现代硬件编写驱动程序的良好垫脚石。
内联汇编
您使用内联 ASM 的方法完全不正确。你似乎想用 asm 编写整个函数,所以你应该只做 that。例如把你的代码放在 asmfuncs.S
之类的地方。如果您想继续使用 GNU / AT&T 语法,请使用 .S
;或使用 .asm
如果你想使用 Intel / NASM / YASM 语法(我会推荐,因为官方手册都使用 Intel 语法。请参阅 x86 wiki 获取指南和手册。 )
GNU 内联汇编是 最难 的学习 ASM 的方法。您必须了解您的 asm 所做的一切,以及编译器需要了解的内容。真的很难把一切都做好。例如,在您的编辑中,该内联 asm 块修改了许多您未列为破坏的寄存器,包括 %ebx
这是一个 call-preserved 寄存器(因此即使该函数不是' t内联)。至少你去掉了 ret
,这样当编译器将这个函数内联到调用它的循环中时,事情就不会那么糟糕了。如果这听起来真的很复杂,那是因为它确实很复杂,这也是为什么 你不应该使用内联 asm 来学习 asm.
的部分原因
有更多关于内联 asm 以及如何使用它的 link。
让这个烂摊子工作,也许
这部分可以单独回答,但我会放在一起。
除了你的整个方法从根本上说是一个坏主意之外,你的 put_char
函数至少有一个 特定问题 :你使用 offset
作为output-only 运行运行d。 gcc 很高兴 compi将您的整个函数转换为单个 ret
指令,因为 asm 语句不是 volatile
,并且未使用其输出。 (假定没有输出的内联 asm 语句为 volatile
。)
I put your function on godbolt,所以我可以查看编译器围绕它生成的程序集。 link 是固定的 maybe-working 版本,具有 correctly-declared 破坏、评论、清理和优化。如果外部 link 曾经中断,请参阅下面的相同代码。
我使用带有 -m16
选项的 gcc 5.3,这与使用真正的 16 位编译器不同。它仍然以 32 位方式执行所有操作(在堆栈上使用 32 位地址、32 位 int
s 和 32 位函数参数),但告诉汇编器 CPU 将处于 16 位模式,因此它会知道何时发出 operand-size 和 address-size 前缀。
即使您 compile your original version with -O0
,编译器也会计算 offset = (y<<8) + (y<<6) + x;
,但不会将其放入 %edi
,因为您没有要求它这样做。将其指定为另一个输入 ope运行d 会起作用。在内联 asm 之后,它将 %edi
存储到 -12(%ebp)
中,其中 offset
存在。
put_char
其他错误:
你通过全局变量而不是函数参数将 2 个东西(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 或其他语言编写它吗?
您对 characters
使用 16 位地址,但对 VGA 内存使用 32 位寻址模式。我认为这是故意的,但我不知道它是否正确。另外,您确定应该对来自 characters
的负载使用 CS:
覆盖吗? .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
的 immediate-operand 形式让我们首先在 %si
中产生结果:
"movzbw _ascii_char,%%si\n\t"
//"sub ,%%ax\n\t" // AX = ascii_char - 32
"imul , %%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, which is why only imul
(not mul
) has those extra forms. For larger operand-size multiplies, 2 and 3 operand imul
is faster,因为它不必将高半存储在[=76中=].
您可以稍微简化内循环,但会使外循环稍微复杂化:您可以在 shl , %al
设置的零标志上 b运行ch,而不是使用 a柜台。这将使它也变得不可预测,例如 non-foreground 像素的跳过存储,因此增加的 b运行ch 错误预测可能比额外的 do-nothing 循环更糟糕。这也意味着您每次都需要在外循环中重新计算 %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 ,%%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 0,%%edi\n\t" // WITHOUT the -6
"dec %%cl\n\t"
"jnz .Lbyte_loop\n\t"
请注意,您的字符位图中的位将映射到 VGA 内存中的字节,如 {7 6 5 4 3 2 1 0}
,因为您正在测试 left 移出的位] 转移。所以它从 MSB 开始。寄存器中的位总是“大端”。左移乘以 2,即使在像 x86 这样的 little-endian 机器上也是如此。 Little-endian 仅影响内存中 字节 的排序,而不影响字节中的位,甚至寄存器中的字节。
您的函数的一个版本可能会达到您的预期。
这个和神马一样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 ,%%ax\n\t" // AX = ascii_char - 32
"imul , %%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 ,%%cl\n"
".Lbyte_loop:\n\t"
"lodsb %%cs:(%%si)\n\t" //load next byte of bitmap
"mov ,%%ch\n"
".Lbit_loop:\n\t" // map the 1bpp bitmap to 8bpp VGA memory
"shl ,%%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 0-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"
破坏,参见
我没有使用虚拟输出 ope运行ds 将寄存器分配留给编译器自行决定,但这是减少为内联 asm 在正确位置获取数据的开销的好主意。 (额外的 mov
说明)。例如,这里没有必要强制编译器将 offset
放在 %edi
中。它可能是我们尚未使用的任何寄存器。
我正在学习使用 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 ,%al\n\t"
"mov ,%ah\n\t"
"mul %ah\n\t"
"lea $characters,%si\n\t"
"add %ax,%si\n\t"
"mov ,%cl\n\t"
"0:\n\t"
"segCS %lodsb\n\t"
"mov ,%ch\n\t"
"1:\n\t"
"shl ,%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 0-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 ,%%al;"
"mov ,%%ah;"
"mul %%ah;"
"lea _characters,%%si;"
"add %%ax,%%si;" //SI point to bitmap
"mov ,%%cl;"
"0:;"
"lodsb %%cs:(%%si);" //load next byte of bitmap
"mov ,%%ch;"
"1:;"
"shl ,%%al;"
"jnc 2f;"
"movb %%dl,(%%edi);" //plot the pixel
"2:\n\t"
"incl %%edi;"
"dec %%ch;"
"jnz 1b;"
"addl 0-6,%%edi;"
"dec %%cl;"
"jnz 0b;"
: "=D" (offset)
: "d" (current_color)
);
}
如果您看到上图,我正在尝试写信 "S"。结果是您在屏幕左上角看到的绿色像素。无论我给函数什么 x 和 y,它总是在同一点上绘制像素。
谁能帮我更正我的代码?
请参阅下文,了解对您的 put_char
函数的一些具体错误的分析,以及可能有效的版本。 (我不确定 %cs
段覆盖,但除此之外它应该按照您的意图进行)。
学习 DOS 和 16 位 asm 并不是学习 asm 的最佳方式
首先,DOS 和 16 位 x86 已经完全过时了,并且 不 比普通的 64 位 x86 更容易学习。即使 32 位 x86 已经过时,但仍在 Windows 世界中广泛使用。
32 位和 64 位代码不必关心很多 16 位限制/复杂性,例如段或寻址模式中有限的寄存器选择。一些现代系统确实使用段覆盖 thread-local 存储,但学习如何在 16 位代码中使用段几乎与此无关。
了解 asm 的主要好处之一是调试/分析/优化实际程序。如果你想了解如何编写 C 或其他 high-level 代码
Asm 知识在查看 performance-counter 注释二进制反汇编结果时很有用(perf stat ./a.out
&& perf report -Mintel
:参见 Chandler Carruth's CppCon2015 talk: "Tuning C++: Benchmarks, and CPUs, and Compilers! Oh My!")。积极的编译器优化意味着查看每个源代码行的周期/cache-miss/停顿计数比每条指令提供的信息少得多。
此外,要让您的程序真正做任何事情,它必须直接与硬件对话,或者进行系统调用。学习文件访问和用户输入的 DOS 系统调用完全是浪费时间(除了回答源源不断的关于如何在 16 位代码中读取和打印 multi-digit 数字的 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 个整数寄存器 + 标志和指令指针,并且每条指令都会对机器的当前架构状态进行 well-defined 修改。 (有关每条指令的完整描述,请参阅英特尔 insn 参考手册(x86 wiki 中的 links,以及 更多好东西 )。
您可能想从简单的事情开始,例如在 asm 中编写单个函数,作为更大程序的一部分。了解进行系统调用所需的 asm 类型很有用,但在实际程序中,通常只对不涉及任何系统调用的内部循环的 hand-write asm 有用。编写 asm 来读取输入和打印结果是 time-consuming,所以我建议在 C 中完成该部分。确保您阅读了编译器输出并了解发生了什么,以及整数和字符串之间的区别,以及 strtol
和 printf
的作用,即使它们不是您自己编写的。
一旦您认为自己对基础知识有了足够的了解,就可以在一些您熟悉的程序中找到一个函数 and/or 感兴趣,看看您是否可以击败编译器并保存指令(或使用更快的指令).或者自己实现它 而不是 使用编译器输出作为起点,以你觉得更有趣的为准。
如何尝试解决自己的问题(在提出 SO 问题之前)
人们问“我如何在 asm 中做 X”有很多 SO 问题,答案通常是“与在 C 中做的一样”。不要因为不熟悉 asm 而忘记了如何编程。弄清楚函数操作的数据需要发生什么,然后弄清楚如何在 asm 中做到这一点。如果您遇到困难并且不得不提出问题,您应该拥有大部分有效的实施,只有一部分帽子你不知道一步使用什么指令。
您应该使用 32 位或 64 位 x86 执行此操作。我建议使用 64 位,因为 ABI 更好,但是 32 位函数将迫使您更多地使用堆栈。因此,这可能会帮助您理解 call
指令如何将 return 地址放入堆栈,以及调用者实际推送的 args 所在的位置。 (这似乎是您试图通过使用内联 asm 来避免处理的问题)。
直接对硬件进行编程很巧妙,但不是一般有用的技能
通过直接修改视频 RAM 来学习如何做图形没有用,除了满足对计算机过去如何工作的好奇心。你不能将这些知识用于任何事情。现代图形 API 的存在是为了让多个程序在他们自己的屏幕区域中绘制,并允许间接(例如,在纹理上绘制而不是直接在屏幕上绘制,所以 3D window-flipping alt-tab可以看起来很花哨)。不直接在视频 RAM 上绘图的原因太多,无法在此处列出。
可以在像素图缓冲区上绘图,然后使用图形 API 将其复制到屏幕上。尽管如此,做位图图形或多或少已经过时了,除非你正在为 PNG 或 JPEG 或其他东西生成图像(例如,在 Web 服务的 back-end 代码中优化将直方图箱转换为散点图)。现代图形 API 抽象了分辨率,因此无论每个像素有多大,您的应用程序都可以以合理的尺寸绘制内容。 (小但分辨率极高的屏幕与低分辨率下的大电视)。
写入内存并看到一些变化是很酷的 on-screen。或者更好的是,将 LED(带有小电阻)连接到并行端口上的数据位,然后 运行 一个 outb
指令将它们 on/off。我很久以前就在我的 Linux 系统上这样做过。我做了一个小包装程序,它使用 iopl(2)
和内联 asm,运行 它作为根。您可能可以在 Windows 上做类似的事情。您不需要 DOS 或 16 位代码来与硬件对话。
in
/out
指令,以及正常的 loads/stores 到 memory-mapped IO 和 DMA,是真正的驱动程序与硬件通信的方式,包括比并行端口。了解您的硬件“真正”如何工作很有趣,但只有在您真正感兴趣或想编写驱动程序时才花时间在上面。 Linux 源代码树包括用于大量硬件的驱动程序,并且通常有很好的注释,所以如果你喜欢阅读代码就像编写代码一样,这是另一种方式来感受读取驱动程序在与硬件对话时所做的事情.
了解引擎盖下的工作原理通常是件好事。如果您想要 了解很久以前图形是如何工作的(使用 VGA 文本模式和颜色/属性字节),那么当然,请发疯。请注意,现代操作系统不使用 VGA 文本模式,因此您甚至没有了解现代计算机的幕后情况。
许多人喜欢 https://retrocomputing.stackexchange.com/,重温计算机不那么复杂且无法支持那么多抽象层的简单时代。请注意,这就是您正在做的事情。如果您确定那就是您想要了解 asm / 硬件的原因,我可能是学习为现代硬件编写驱动程序的良好垫脚石。
内联汇编
您使用内联 ASM 的方法完全不正确。你似乎想用 asm 编写整个函数,所以你应该只做 that。例如把你的代码放在 asmfuncs.S
之类的地方。如果您想继续使用 GNU / AT&T 语法,请使用 .S
;或使用 .asm
如果你想使用 Intel / NASM / YASM 语法(我会推荐,因为官方手册都使用 Intel 语法。请参阅 x86 wiki 获取指南和手册。 )
GNU 内联汇编是 最难 的学习 ASM 的方法。您必须了解您的 asm 所做的一切,以及编译器需要了解的内容。真的很难把一切都做好。例如,在您的编辑中,该内联 asm 块修改了许多您未列为破坏的寄存器,包括 %ebx
这是一个 call-preserved 寄存器(因此即使该函数不是' t内联)。至少你去掉了 ret
,这样当编译器将这个函数内联到调用它的循环中时,事情就不会那么糟糕了。如果这听起来真的很复杂,那是因为它确实很复杂,这也是为什么 你不应该使用内联 asm 来学习 asm.
让这个烂摊子工作,也许
这部分可以单独回答,但我会放在一起。
除了你的整个方法从根本上说是一个坏主意之外,你的 put_char
函数至少有一个 特定问题 :你使用 offset
作为output-only 运行运行d。 gcc 很高兴 compi将您的整个函数转换为单个 ret
指令,因为 asm 语句不是 volatile
,并且未使用其输出。 (假定没有输出的内联 asm 语句为 volatile
。)
I put your function on godbolt,所以我可以查看编译器围绕它生成的程序集。 link 是固定的 maybe-working 版本,具有 correctly-declared 破坏、评论、清理和优化。如果外部 link 曾经中断,请参阅下面的相同代码。
我使用带有 -m16
选项的 gcc 5.3,这与使用真正的 16 位编译器不同。它仍然以 32 位方式执行所有操作(在堆栈上使用 32 位地址、32 位 int
s 和 32 位函数参数),但告诉汇编器 CPU 将处于 16 位模式,因此它会知道何时发出 operand-size 和 address-size 前缀。
即使您 compile your original version with -O0
,编译器也会计算 offset = (y<<8) + (y<<6) + x;
,但不会将其放入 %edi
,因为您没有要求它这样做。将其指定为另一个输入 ope运行d 会起作用。在内联 asm 之后,它将 %edi
存储到 -12(%ebp)
中,其中 offset
存在。
put_char
其他错误:
你通过全局变量而不是函数参数将 2 个东西(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 或其他语言编写它吗?
您对 characters
使用 16 位地址,但对 VGA 内存使用 32 位寻址模式。我认为这是故意的,但我不知道它是否正确。另外,您确定应该对来自 characters
的负载使用 CS:
覆盖吗? .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
的 immediate-operand 形式让我们首先在 %si
中产生结果:
"movzbw _ascii_char,%%si\n\t"
//"sub ,%%ax\n\t" // AX = ascii_char - 32
"imul , %%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, which is why only imul
(not mul
) has those extra forms. For larger operand-size multiplies, 2 and 3 operand imul
is faster,因为它不必将高半存储在[=76中=].
您可以稍微简化内循环,但会使外循环稍微复杂化:您可以在 shl , %al
设置的零标志上 b运行ch,而不是使用 a柜台。这将使它也变得不可预测,例如 non-foreground 像素的跳过存储,因此增加的 b运行ch 错误预测可能比额外的 do-nothing 循环更糟糕。这也意味着您每次都需要在外循环中重新计算 %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 ,%%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 0,%%edi\n\t" // WITHOUT the -6
"dec %%cl\n\t"
"jnz .Lbyte_loop\n\t"
请注意,您的字符位图中的位将映射到 VGA 内存中的字节,如 {7 6 5 4 3 2 1 0}
,因为您正在测试 left 移出的位] 转移。所以它从 MSB 开始。寄存器中的位总是“大端”。左移乘以 2,即使在像 x86 这样的 little-endian 机器上也是如此。 Little-endian 仅影响内存中 字节 的排序,而不影响字节中的位,甚至寄存器中的字节。
您的函数的一个版本可能会达到您的预期。
这个和神马一样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 ,%%ax\n\t" // AX = ascii_char - 32
"imul , %%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 ,%%cl\n"
".Lbyte_loop:\n\t"
"lodsb %%cs:(%%si)\n\t" //load next byte of bitmap
"mov ,%%ch\n"
".Lbit_loop:\n\t" // map the 1bpp bitmap to 8bpp VGA memory
"shl ,%%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 0-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"
破坏,参见
我没有使用虚拟输出 ope运行ds 将寄存器分配留给编译器自行决定,但这是减少为内联 asm 在正确位置获取数据的开销的好主意。 (额外的 mov
说明)。例如,这里没有必要强制编译器将 offset
放在 %edi
中。它可能是我们尚未使用的任何寄存器。