7

I've been struggling with understanding the ASCII adjust instructions from x86 assembly language.

I see all over the internet information telling me different things, but I guess it's just the same thing explained in a different form that I still don't get.

Can anyone explain why in the pseudo-code of AAA, AAS we have to add, subtract 6 from the low-order nibble in AL?

And can someone explain AAM, AAD and the Decimal adjust instructions pseudo-code in the Intel instruction set manuals too, why are they like that, what's the logic behind them?

And at last, can someone give examples when these instructions can be useful, or at least in what applications they have been useful in the past.

I know that nowadays these instructions aren't used, but I still want to know how these instructions work, it's good to know.

phuclv
  • 37,963
  • 15
  • 156
  • 475
emilxp
  • 91
  • 1
  • 1
  • 3

2 Answers2

11

why in the pseudo-code of AAA, AAS we have to add, subtract 6 from the low-order nibble in AL

Because in hexadecimal each character has 16 distinct values and BCD has only 10. When you do math in decimal, if a number is larger than 10 you need to take the modulus of 10 and carry to the next row. Similarly, in BCD math, when the result of the addition is larger than 9 you add 6 to skip the 6 remaining "invalid" values and carry to the next digit. Conversely you subtract 6 in subtractions.

For example: 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)

Doing unpacked addition is the same except that you carry directly from the units digit to the tens digit, discarding the top nibbles of each byte

For more information you can read


and can someone explain AAM, AAD and the Decimal adjust instructions pseudo-code in the Intel instruction set manuals too, why are they like that, what's the logic behind them?

AAM is just a conversion from binary to BCD. You do the multiplication normally in binary, then calling AAM divides the result by 10 and store the quotient-remainder pair in two unpacked BCD characters

For example:

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

AAD is the reverse: before the division, you call AAD to convert it from BCD to binary and do the division just like other binary divisions

For example: 87/5

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

The reason for those instruction is because in the past, memories are expensive and you must reduce the memory usage as much as possible. Hence in that era CISC CPUs are very common. They use lots of complex instructions to minimize the instructions used to do a task. Nowadays memory is much cheaper and modern architectures are almost RISCy, with the trade off of CPU complexity and code density

phuclv
  • 37,963
  • 15
  • 156
  • 475
  • Perfect explanation! Thank you, I now see how intuitive that is, and how I didn't really read what aad and aam do, I thought they do even more complicated stuff..... – emilxp Jun 07 '14 at 09:41
  • 2
    Given that AAD and AAM accept an immediate byte argument and either divide or multiply the accumulator by that value, I wonder why Intel didn't specify them in such terms? – supercat Jun 09 '14 at 17:19
  • @supercat after reading the function of AAD and AAM I wondered that too – phuclv Jun 10 '14 at 00:43
  • 1
    an even more interesting thing that I've found is that AAM and AAD actually work with any byte immediates, which make them a real base converter. AAD is like `mul imm8` and AAM is a similar way to achieve `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 Jun 11 '14 at 02:51
  • Intel's ISA manuals [currently document](http://felixcloutier.com/x86/AAM.html) the arrbitrary-divisor form of AAM, and it's only an assembler syntax thing that the no-args version uses 0xa. Was this not well-documented in 2014, or well known? Anyway, for future reference, here's some actual working code for number -> 2-digit string, once using DIV and again using AAM (less convenient because bytes in AX aren't in printing order). [Displaying Time in Assembly](https://stackoverflow.com/a/37131263) – Peter Cordes Jul 05 '18 at 07:41
0

I writing one program which will help to understand AAA After Addition.

.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

Now As you can see in program after "add" instruction we are using "aaa" which convert number into ASCII (30-39 correspond to 0-9). So writing real output we actually need to it back hexadecimal for that we are taking "or" of the answer. Now with "si" and "di" we are loading digits one by one and checking if there is carry as when we do "aaa" we will know because when digit is greater than 9 it will generate digit ah and so we will "inc" al by one.See how "aaa" works below.

  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. 

For More Programs related ASCII addition,subtraction,multiplication and division Check this link. GitHub

  • If you want a pointer to the end of the digit-string `b`, just put a label there and `mov bx, OFFSET b_end`. Or at least make `len2` an assemble-time constant (`len2 equ $-b`) so you can do `mov bx, OFFSET b + len2`. (Or lea if you want to waste a byte of code-size.) Same for `a` so you can just do `mov si, OFFSET a_end - 1` or something, instead of all this runtime calculation of assemble-time constant stuff. Or put the pointer-`dec` instructions in the loop *before* the memory accesses. – Peter Cordes Oct 09 '20 at 02:51
  • The output isn't *hexa*decimal, it's just decimal. That's why `or al, '0'` works, not needing to handle the `a..f` case, because `aaa` breaks up the sum into *decimal* digits. (Not ASCII, you're doing that manually after AAA gives you unpacked BCD) – Peter Cordes Oct 09 '20 at 02:56
  • Also, your carry handling looks inefficient and isn't very easy to follow. Instead of comparing `ah` against zero, you could just do `add al, ah` / `mov ah, 0` – Peter Cordes Oct 09 '20 at 03:01