NASM Assembly convert input to integer?

后端 未结 2 2060
灰色年华
灰色年华 2020-11-22 04:16

Ok, so I\'m fairly new to assembly, infact, I\'m very new to assembly. I wrote a piece of code which is simply meant to take numerical input from the user, multiply it by 1

相关标签:
2条回答
  • 2020-11-22 04:43

    The basic algorith for string->digit is: total = total*10 + digit, starting from the MSD. (e.g. with digit = *p++ - '0' for an ASCII string of digits). So the left-most / Most-Significant / first digit (in memory, and in reading order) gets multiplied by 10 N times, where N is the total number of digits after it.

    Doing it this way is generally more efficient than multiplying each digit by the right power of 10 before adding. That would need 2 multiplies; one to grow a power of 10, and another to apply it to the digit. (Or a table look-up with ascending powers of 10).

    Of course, for efficiency you might use SSSE3 pmaddubsw and SSE2 pmaddwd to multiply digits by their place-value in parallel: see How to implement atoi using SIMD?. That probably isn't a win when numbers are typically short, though. A scalar loop is efficient when most numbers are only a couple digits long.


    Adding on to @Michael's answer, it may be useful to have the int->string function stop at the first non-digit, instead of at a fixed length. This will catch problems like your string including a newline from when the user pressed return, as well as not turning 12xy34 into a very large number. (Treat it as 12, like C's atoi function). The stop character can also be the terminating 0 in a C implicit-length string.

    I've also made some improvements:

    • Don't use the slow loop instruction unless you're optimizing for code-size. Just forget it exists and use dec / jnz in cases where counting down to zero is still what you want to do, instead of comparing a pointer or something else.

    • 2 LEA instructions are significantly better than imul + add: lower latency.

    • accumulate the result in EAX where we want to return it anyway. (If you inline this instead of calling it, use whatever register you want the result in.)

    I changed the registers so it follows the x86-64 System V ABI (First arg in RDI, return in EAX).

    Porting to 32-bit: This doesn't depend on 64-bitness at all; it can be ported to 32-bit by just using 32-bit registers. (i.e. replace rdi with edi, rax with ecx, and rax with eax). Beware of C calling-convention differences between 32 and 64-bit, e.g. EDI is call-preserved and args are usually passed on the stack. But if your caller is asm, you can pass an arg in EDI.

        ; args: pointer in RDI to ASCII decimal digits, terminated by a non-digit
        ; clobbers: ECX
        ; returns: EAX = atoi(RDI)  (base 10 unsigned)
        ;          RDI = pointer to first non-digit
    global base10string_to_int
    base10string_to_int:
    
         movzx   eax, byte [rdi]    ; start with the first digit
         sub     eax, '0'           ; convert from ASCII to number
         cmp     al, 9              ; check that it's a decimal digit [0..9]
         jbe     .loop_entry        ; too low -> wraps to high value, fails unsigned compare check
    
         ; else: bad first digit: return 0
         xor     eax,eax
         ret
    
         ; rotate the loop so we can put the JCC at the bottom where it belongs
         ; but still check the digit before messing up our total
      .next_digit:                  ; do {
         lea     eax, [rax*4 + rax]    ; total *= 5
         lea     eax, [rax*2 + rcx]    ; total = (total*5)*2 + digit
           ; imul eax, 10  / add eax, ecx
      .loop_entry:
         inc     rdi
         movzx   ecx, byte [rdi]
         sub     ecx, '0'
         cmp     ecx, 9
         jbe     .next_digit        ; } while( digit <= 9 )
    
         ret                ; return with total in eax
    

    This stops converting on the first non-digit character. Often this will be the 0 byte that terminates an implicit-length string. You could check after the loop that it was a string-end, not some other non-digit character, by checking ecx == -'0' (which still holds the str[i] - '0' integer "digit" value that was out of range), if you want to detect trailing garbage.

    If your input is an explicit-length string, you'd need to use a loop counter instead of checking a terminator (like @Michael's answer), because the next byte in memory might be another digit. Or it might be in an unmapped page.


    Making the first iteration special and handling it before jumping into the main part of the loop is called loop peeling. Peeling the first iteration allows us to optimize it specially, because we know total=0 so there's no need to multiply anything by 10. It's like starting with sum = array[0]; i=1 instead of sum=0, i=0;.

    To get nice loop structure (with the conditional branch at the bottom), I used the trick of jumping into the middle of the loop for the first iteration. This didn't even take an extra jmp because I was already branching in the peeled first iteration. Reordering a loop so an if()break in the middle becomes a loop branch at the bottom is called loop rotation, and can involve peeling the first part of the first iteration and the 2nd part of the last iteration.

    The simple way to solve the problem of exiting the loop on a non-digit would be to have a jcc in the loop body, like an if() break; statement in C before the total = total*10 + digit. But then I'd need a jmp and have 2 total branch instructions in the loop, meaning more overhead.


    If I didn't need the sub ecx, '0' result for the loop condition, I could have used lea eax, [rax*2 + rcx - '0'] to do it as part of the LEA as well. But that would have made the LEA latency 3 cycles instead of 1, on Sandybridge-family CPUs. (3-component LEA vs. 2 or less.) The two LEAs form a loop-carried dependency chain on eax (total), so (especially for large numbers) it would not be worth it on Intel. On CPUs where base + scaled-index is no faster than base + scaled-index + disp8 (Bulldozer-family / Ryzen), then sure, if you have an explicit length as your loop condition and don't want to check the digits at all.

    I used movzx to load with zero extension in the first place, instead of doing that after converting the digit from ASCII to integer. (It has to be done at some point to add into 32-bit EAX). Often code that manipulates ASCII digits uses byte operand-size, like mov cl, [rdi]. But that would create a false dependency on the old value of RCX on most CPUs.

    sub al,'0' saves 1 byte over sub eax,'0', but causes a partial-register stall on Nehalem/Core2 and even worse on PIII. Fine on all other CPU families, even Sandybridge: it's a RMW of AL, so it doesn't rename the partial reg separately from EAX. But cmp al, 9 doesn't cause a problem, because reading a byte register is always fine. It saves a byte (special encoding with no ModRM byte), so I used that at the top of the function.


    For more optimization stuff, see http://agner.org/optimize, and other links in the x86 tag wiki.

    The tag wiki also has beginner links, including an FAQ section with links to integer->string functions, and other common beginner questions.

    Related: How do I print an integer in Assembly Level Programming without printf from the c library? is the reverse of this question, integer -> base10string.

    0 讨论(0)
  • 2020-11-22 05:07

    Here's a couple of functions for converting strings to integers, and vice versa:

    ; Input:
    ; ESI = pointer to the string to convert
    ; ECX = number of digits in the string (must be > 0)
    ; Output:
    ; EAX = integer value
    string_to_int:
      xor ebx,ebx    ; clear ebx
    .next_digit:
      movzx eax,byte[esi]
      inc esi
      sub al,'0'    ; convert from ASCII to number
      imul ebx,10
      add ebx,eax   ; ebx = ebx*10 + eax
      loop .next_digit  ; while (--ecx)
      mov eax,ebx
      ret
    
    
    ; Input:
    ; EAX = integer value to convert
    ; ESI = pointer to buffer to store the string in (must have room for at least 10 bytes)
    ; Output:
    ; EAX = pointer to the first character of the generated string
    int_to_string:
      add esi,9
      mov byte [esi],STRING_TERMINATOR
    
      mov ebx,10         
    .next_digit:
      xor edx,edx         ; Clear edx prior to dividing edx:eax by ebx
      div ebx             ; eax /= 10
      add dl,'0'          ; Convert the remainder to ASCII 
      dec esi             ; store characters in reverse order
      mov [esi],dl
      test eax,eax            
      jnz .next_digit     ; Repeat until eax==0
      mov eax,esi
      ret
    

    And this is how you'd use them:

    STRING_TERMINATOR equ 0
    
    lea esi,[thestring]
    mov ecx,4
    call string_to_int
    ; EAX now contains 1234
    
    ; Convert it back to a string
    lea esi,[buffer]
    call int_to_string
    ; You now have a string pointer in EAX, which
    ; you can use with the sys_write system call
    
    thestring: db "1234",0
    buffer: resb 10
    

    Note that I don't do much error checking in these routines (like checking if there are characters outside of the range '0' - '9'). Nor do the routines handle signed numbers. So if you need those things you'll have to add them yourself.

    0 讨论(0)
提交回复
热议问题