Han*_*iel 2 assembly tasm x86-16
我有一项任务,要求我使形状移动并用颜色改变形状。我一开始就没有成功地画出圆的八分圆。假设使用 Intel 8086 汇编语言,在 DMA 模式下使用 TASM。(模式 19)我在想如果我能完成一个圆圈,我可以给它设置动画并改变形状。我无法弄清楚是算法错误还是代码错误。
.model small
.stack 256
.code
startaddr dw 0a000h ;start of video memory
color db 3
xc dw 160
yc dw 100
r dw 50 ; radius
x dw 0
y dw 50 ;radius
pk dw 1
temp dw 1
plot macro r1, r2, r3 ;x,y,color
mov ax, r2
mov bx, 320
mul bx
add ax, r1
mov di, ax
mov al, r3
mov es:[di], al
endm
start:
mov ax, yc
add y, ax
mov ah,00
mov al, 13h
int 10h ;switch to 320x200 mode
mov es, startaddr
mov dx, y
mov ax, xc
add x, ax
plot x, dx, color
mov bx, r
mov pk, bx
sub pk, 1
neg pk
cmp pk, 0
jge pk1
drawc:
mov bx, x
sub bx, xc
mov ax, y
sub ax, yc
mov temp, ax
cmp bx, temp
jge keypress
mov dx, y
plot x, dx, color
peekay:
cmp pk, 0
jg pk1
mov ax, x
mov bx, 2
mul bx
add ax, pk
mov pk, ax
inc x ;x+1
jmp drawc
pk1:
dec y
mov ax, x
sub ax, y
mov bx, 2
mul bx
add ax, pk
mov pk, ax
inc x
jmp drawc
keypress:
mov ah,00
int 16h ;await keypress
mov ah,00
mov al,03
int 10h
mov ah,4ch
mov al,00 ;terminate program
int 21h
end start
Run Code Online (Sandbox Code Playgroud)
到目前为止,您的代码很难理解,因为它的注释很少。当您编写汇编语言时,重要的是要自由地进行注释,解释代码应该做什么,因为语言本身的表达能力不是很强。当然,我知道每条指令的含义,但作为一个人,我很难跟踪所有注册的值及其整个流程。我也不知道你的代码应该在高层做什么。
更糟糕的是,当我尝试阅读代码时,你的标签名称对我来说也毫无意义。什么是peekay?pk1?我猜drawc是这样,DrawCircle但为什么不这么称呼它呢?那么你甚至不需要在那里发表评论,因为从名字上就可以明显看出。
至于您的实际问题,从输出看来您已经成功绘制了一条线。但这并不是您真正想要画的。您想使用中点算法来绘制圆。您很幸运,因为维基百科文章有用C 实现此算法的示例代码。如果您是汇编新手,并且正在努力解决这个问题,我的建议是首先用 C 编写代码,并确保您的算法可以正常工作。然后,您可以将工作的 C 代码翻译为汇编语言。一旦您对汇编更加熟悉,您就可以开始跳过步骤并直接用汇编编写,将 C 风格的算法翻译成您头脑中的汇编指令。至少,我就是这样做的,每当遇到困难时我仍然会回到 C。
那么让我们从 Wikipedia 窃取一些 C 代码:
void DrawCircle(int x0, int y0, int radius)
{
int x = radius;
int y = 0;
int err = 0;
while (x >= y)
{
PlotPixel(x0 + x, y0 + y);
PlotPixel(x0 + y, y0 + x);
PlotPixel(x0 - y, y0 + x);
PlotPixel(x0 - x, y0 + y);
PlotPixel(x0 - x, y0 - y);
PlotPixel(x0 - y, y0 - x);
PlotPixel(x0 + y, y0 - x);
PlotPixel(x0 + x, y0 - y);
if (err <= 0)
{
y += 1;
err += 2*y + 1;
}
if (err > 0)
{
x -= 1;
err -= 2*x + 1;
}
}
}
Run Code Online (Sandbox Code Playgroud)
通常,发明、编写和测试算法是最困难的部分,但我们只是通过窃取他人的工作成果来绕过这一点。现在,我们所需要的只是PlotPixel函数。不过,这很简单——您的汇编代码已经包含了该部分,因为您已经成功地画了一条线!
因此,我们都在同一页面上,最简单的方法是调用 BIOS 中断 10h、函数 0Ch,该函数在图形模式下绘制像素。DX包含点的 x 坐标、CX包含 y 坐标、AL包含颜色属性、BH包含视频页面。为简单起见,我们假设视频页面为 0(默认值)。这可以包含在一个简单的宏中,如下所示:
PlotPixel MACRO x, y, color, videoPage
mov cx, x
mov dx, y
mov bh, videoPage
mov ax, (color | (0Ch << 4)) ; AL == color, AH == function 0Ch
int 10h
ENDM
Run Code Online (Sandbox Code Playgroud)
第二阶段是将 C 函数翻译成汇编。由于对PlotPixel函数的重复调用以及循环结构,此转换将不是一个简单的练习。我们最终将得到一长串代码。我们还有另一个问题:没有足够的寄存器来保存所有临时值!当然,这在通用寄存器数量非常有限的 x86 上很常见,因此我们将做我们一直必须做的事情:使用堆栈。它速度较慢,但有效。(无论如何,这段代码不会很快。)这是我想到的:
; Draws a circle of the specified radius at the specified location
; using the midpoint algorithm.
;
; Parameters: DX == center, x
; CX == center, y
; BX == radius
; AL == color
; Clobbers: <none>
; Returns: <none>
DrawCircle:
push bp
mov bp, sp
push dx ; xCenter [bp - 2]
push cx ; yCenter [bp - 4]
push bx ; x [bp - 6]
push 0 ; y [bp - 8]
push 0 ; err [bp - 10]
; Prepare to plot pixels:
mov ah, 0Ch ; AH == function 0Ch (plot pixel in graphics mode)
xor bx, bx ; BH == video page 0
DrawLoop:
mov dx, WORD [bp - 6]
cmp dx, WORD [bp - 8]
jl Finished ; (x < y) ? we're finished drawing : keep drawing
; Plot pixels:
mov cx, WORD [bp - 2]
mov dx, WORD [bp - 4]
add cx, WORD [bp - 6] ; CX = xCenter + x
add dx, WORD [bp - 8] ; DX = yCenter + y
int 10h
mov cx, WORD [bp - 2]
sub cx, WORD [bp - 6] ; CX = xCenter - x
int 10h
mov dx, WORD [bp - 4]
sub dx, WORD [bp - 8] ; DX = yCenter - y
int 10h
mov cx, WORD [bp - 2]
add cx, WORD [bp - 6] ; CX = xCenter + x
int 10h
mov cx, WORD [bp - 2]
mov dx, WORD [bp - 4]
add cx, WORD [bp - 8] ; CX = xCenter + y
add dx, WORD [bp - 6] ; DX = yCenter + x
int 10h
mov cx, WORD [bp - 2]
sub cx, WORD [bp - 8] ; CX = xCenter - y
int 10h
mov dx, WORD [bp - 4]
sub dx, WORD [bp - 6] ; DX = yCenter - x
int 10h
mov cx, WORD [bp - 2]
add cx, WORD [bp - 8] ; CX = xCenter + y
int 10h
; Update state values and check error:
mov dx, WORD [bp - 8]
inc dx
mov WORD [bp - 8], dx
add dx, dx ; DX *= 2
inc dx
add dx, WORD [bp - 10]
mov WORD [bp - 10], dx
sub dx, WORD [bp - 6]
add dx, dx ; DX *= 2
js DrawLoop ; DX < 0 ? keep looping : fall through and check error
inc dx
dec cx
mov WORD [bp - 6], cx
add cx, cx ; CX *= 2
neg cx
inc cx ; CX = (1 - CX)
add WORD [bp - 10], cx
jmp DrawLoop ; keep looping
Finished:
pop bx ; (clean up the stack; no need to save this value)
pop bx ; (clean up the stack; no need to save this value)
pop bx
pop cx
pop dx
pop bp
ret
END DrawCircle
Run Code Online (Sandbox Code Playgroud)
您将看到,在函数的顶部,我在堆栈上为临时值分配了空间。我们将使用寄存器的偏移量来访问它们中的每一个BP,如内联注释中所述。*
该函数的大部分由主绘图循环 组成DrawLoop。在顶部,我们进行比较,看看是否应该继续循环。然后我们开始认真绘制像素,进行必要的操作,就像维基百科的 C 代码中所示的那样。绘图后,我们进行更多操作,将结果存储回堆栈上的临时值中,然后再运行几次比较,看看是否应该继续循环(同样,大致类似于if原始 C 代码中的块)。最后,一旦我们完成,我就会在返回之前清理堆栈。
请注意,我从宏中“内联”了代码PlotPixel。这使我可以在顶部设置AH和寄存器,并为所有调用重用它们。BH这会稍微缩短代码,并加快速度。并行结构使其具有足够的可读性(至少在我看来)。
除了我的一些算术运算之外,这里没有什么特别棘手的事情。显而易见,向其自身添加一个寄存器与将其乘以 2 相同。我通过对原始值求反,然后将其递增 1,从 1 中减去一个寄存器。这些在代码中进行了注释。我认为唯一值得指出的另一件事是我用于test reg, reg简单比较,而不是cmp reg, 0. 前者更短、更快。
只需设置您的视频模式,将参数放入适当的寄存器中,然后调用它!

当然有一些方法可以加速这个功能,但它们是以严重牺牲可读性和可理解性为代价的。在继续阅读之前,请确保您首先了解这里发生的事情!
这段代码的主要瓶颈有两个:
使用堆栈。
可能有一种更有创意的方法来编写代码,以更优化地利用寄存器,根据需要对值进行改组,以避免尽可能多地访问内存。但这对于我脆弱的头脑来说实在是太难以理解了。它不应该太慢——毕竟所有这些值都适合缓存。
使用 BIOS 像素绘图功能。
即使在现代机器上,这个速度也非常慢。它可以很好地在屏幕上绘制一个简单的圆圈,尤其是在虚拟化硬件上,但对于多个圆圈的复杂输出来说还不够快。要解决这个问题,您将不得不诉诸视频内存的原始位调整,这是不可移植的。我想这就是“DMA 模式”的意思。如果这样做,您将限制您的代码只能在具有符合标准规范的 VGA 硬件的系统上运行。您还会失去分辨率/模式独立性。
进行更改非常简单。我刚刚添加了一个PlotPixel执行繁重工作的函数,并更改了其中的代码DrawCircle以调用该函数而不是 BIOS 中断(并删除了前导mov ah, 0Ch和xor bx, bx行):
; Plots a pixel by writing a BYTE-sized color value directly to memory,
; based on the formula: 0A000h + (Y * 320) + X
;
; Parameters: DX == x-coordinate
; CX == y-coordinate
; AL == color
; Clobbers: CX
; Returns: <none>
PlotPixel:
push di
mov di, 0A000h
mov es, di ; ES == video memory offset
mov di, cx
add cx, cx
add cx, cx
add di, cx
shl di, 6 ; Y *= 320
add di, dx ; Y += X
mov BYTE es:[di], al ; write the color byte to memory at (X, Y)
pop di
ret
END PlotPixel
Run Code Online (Sandbox Code Playgroud)
我想你已经理解了这一部分,但它起作用的原因是因为在模式 19 (13h) 下,有 320×200 像素和 256 种颜色。因为 256 == 2 8,所以每个像素的颜色值恰好存储在 1 个字节(8 位)中。因此,如果从视频存储器的开头(地址A000h)开始,像素将被线性存储,并且可以直接写入它们的颜色值。写入像素 (x, y) 的公式为:A000h + (y * 320) + x。
正如您在原始代码中所做的那样,您可以通过将 out 的设置提升ES到调用者中并为函数设置ES == 0A000h前提条件来进一步改进这一点PlotPixel。但我确实对您的原始代码进行了重大更改,MUL用左移和几个加法替换了缓慢的乘法 ( )。您最初的基于乘法的代码也有一个溢出错误,我的重写修复了该错误。
您可以通过一次写入多个字节来进一步加快速度——在 8086 上,这将写入一个 WORD(两个字节)。这需要直接在函数中“内联”像素绘制代码DrawCircle,并进行一些创造性的寄存器分配,以确保您想要绘制的第一个像素位于,例如,,AL第二个像素位于AH。我将把这个作为练习。
*我喜欢使用宏将这些偏移量转换为常量,然后在整个函数中使用该符号名称,而不是对数值进行硬编码。这不仅可以使代码更具可读性,而且如果您决定更改推送参数的顺序或推送参数的数量,也可以更轻松地进行更改。我没有在这里这样做,因为我不知道 TASM 的正确语法是什么,并且在调试这段代码时它让我感到困惑。
说到这里,我用 NASM 编写了代码,并尝试将其即时转换为 TASM,这是一种我不太熟悉的语法。很抱歉,如果您在组装之前需要解决任何语法问题!
| 归档时间: |
|
| 查看次数: |
1335 次 |
| 最近记录: |