ASCII调整指令和十进制调整指令是如何工作的?

7

我一直在努力理解x86汇编语言中的ASCII调整指令。

我在互联网上看到了很多不同的信息,但我想这只是不同形式的相同内容,仍然让我难以理解。

有人可以解释一下为什么在AAAAAS的伪代码中,我们需要向AL低位半字节中添加、减去6吗?

还有人可以解释Intel指令集手册中的AAMAAD和十进制调整指令伪代码,它们为什么是这样的,背后的逻辑是什么?

最后,有人能举例说明这些指令何时会有用,或者至少在过去的哪些应用中有用过吗?

我知道现在这些指令已经不再使用,但我仍然想知道这些指令如何工作,了解这些对我也有好处。


http://en.wikipedia.org/wiki/Intel_BCD_opcode - phuclv
2个回答

11

为什么在AAA、AAS的伪代码中,我们需要对AL的低位四位进行加减6操作?

因为十六进制中每个字符有16个不同的值,而BCD只有10个。当你进行十进制数学运算时,如果一个数字大于10,你需要对10取模并进位到下一行。类似地,在BCD数学中,当加法的结果大于9时,你会添加6来跳过剩余的6个“无效”值,并向下一位进位;相反,在减法中会减去6。

例如:27 + 36

  27: 0010 0111
+ 36: 0011 0110
───────────────
5_13: 0101 1101 (13 >= 10)
+  6:      0110
───────────────
  63: 0110 0011 (13 + 6 = 19 = 0x13, where 0x3 is the units digit and 0x10 is the carry)

做未打包加法的步骤相同,只是直接从个位数进位到十位数,丢弃每个字节的高四位。

欲了解更多信息,请阅读:


还有人能解释一下Intel指令集手册中的AAM,AAD和十进制调整指令伪代码吗?它们为什么这样,背后的逻辑是什么?

AAM只是从二进制到BCD的转换。在二进制中正常进行乘法,然后调用AAM将结果除以10,并将商余配对存储在两个未打包的BCD字符中。

例如:

13*6 = 78 = 0100 1110
78/10 = 7 remains 8 => result = 0x78

AAD是反向的:在进行除法运算之前,你需要调用AAD将BCD转换为二进制,然后像其他二进制除法一样进行除法计算。

例如:87/5

0x8*10 + 0x7 = 0x57
0x57/5 = 0x11 remains 0x7

这些指令的原因是因为在过去,内存很昂贵,必须尽可能地减少内存使用。因此,在那个时代,CISC CPU非常普遍。它们使用许多复杂的指令来最小化执行任务所需的指令数。现在内存便宜得多,现代架构几乎都是RISC型,以CPU复杂度和代码密度的折衷为代价。


完美的解释!谢谢,我现在明白了它是多么直观,以及我没有真正阅读aad和aam的功能,我以为它们会做更复杂的事情..... - emilxp
2
鉴于AAD和AAM接受一个立即字节参数并将累加器乘以或除以该值,我想知道为什么英特尔没有用这样的术语来指定它们? - supercat
@supercat在阅读AAD和AAM函数的功能后,我也感到好奇。 - phuclv
1
我发现更有趣的事情是,AAM和AAD实际上可以使用任何字节立即数,这使它们成为真正的基数转换器。 AAD类似于mul imm8,而AAM是实现div imm8的类似方式。http://www.hugi.scene.org/online/coding/hugi%2017%20-%20coaax.htm http://www.rcollins.org/secrets/opcodes/AAD.html https://code.google.com/p/corkami/wiki/x86oddities#aad - phuclv
英特尔的ISA手册目前记录 AAM的任意除数形式,而且只是汇编器语法问题,无参数版本使用0xa。这在2014年没有得到很好的记录或广泛知晓吗?无论如何,供将来参考,这里有一些实际可用的代码,用于数字-> 2位字符串,一次使用DIV,再次使用AAM(不太方便,因为AX中的字节不按打印顺序排列)。在汇编中显示时间 - Peter Cordes

0

我正在编写一个程序,它将帮助理解AAA加法后的结果。

.model small
.data
a db '1234'
len1 db $-a
b db '9876'
len2 db $-b
result db 05 dup(?)
len3 db $-result  

.code
main proc near
mov ax,@data
mov ds,ax
                    
lea bx,a
add bl,len1
mov si,bx

lea bx,b
add bl,len2
mov di,bx

dec si
dec di
dec len3
           
lea bx,result
add bl,len3
             
mov cl,len1  
mov ax,0h

l1:                            
mov al,[si]
mov dl,[di]
cmp ah,00h
je skip 
mov ah,0h
inc al                
skip:
    add al,dl
    aaa    
    or al,30h
    mov [bx],al
    dec bx 
    dec si
    dec di
    loop l1 
cmp ah,00h
je over
mov [bx],31h
jmp finish
over:
mov [bx],30h

finish:
        
mov ax,04ch
int 21h
endp 
end

现在,正如您在程序中看到的,在“add”指令之后,我们使用“aaa”将数字转换为ASCII码(30-39对应于0-9)。因此,为了编写实际输出,我们实际上需要将其转换回十六进制数,为此我们取答案的“or”。现在,我们使用“si”和“di”逐个加载数字,并检查是否存在进位,因为当我们执行“aaa”时,我们会知道,因为当数字大于9时,它将生成数字ah,所以我们将通过“inc” al增加一个。请参见下面的“aaa”工作方式。

  AAA (ASCII Adjust after Addition)
  if low nibble of AL > 9 or AF = 1 then:
  AL = AL + 6  
  AH = AH + 1  
  AF = 1  
  CF = 1  
  else 
  AF = 0  
  CF = 0  
  in both cases: 
  clear the high nibble of AL. 

如需更多与ASCII加法、减法、乘法和除法相关的编程,请查看此链接。GitHub


如果您想要一个指向数字字符串 b 结尾的指针,只需在那里放置一个标签并使用 mov bx, OFFSET b_end。或者至少将 len2 设为汇编时常量(len2 equ $-b),这样您就可以执行 mov bx, OFFSET b + len2。(如果你想浪费一个字节的代码大小,则使用 lea。)对于 a 也是一样,这样您就可以执行 mov si, OFFSET a_end - 1 或其他类似的操作,而不需要进行这些运行时计算或汇编时常量的操作。或者在循环中之前放置指针“减”指令以避免内存访问。 - Peter Cordes
输出不是十六进制,而是十进制。这就是为什么 or al, '0' 起作用,不需要处理 a..f 的情况,因为 aaa 将总和分解成 十进制 数字。(不是 ASCII,你在 AAA 之后手动执行拆包 BCD) - Peter Cordes
此外,您的进位处理看起来效率低下,而且不太容易理解。您可以尝试使用add al, ah / mov ah, 0代替将ah与零进行比较的方法。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接