NASM汇编如何将输入转换为整数?

8

好的,我对汇编还比较陌生,事实上,我对汇编非常陌生。我写了一段代码,简单地接收用户输入的数字,将其乘以10,并通过程序退出状态将结果表示给用户(在终端中键入echo $?)。

问题是,它没有给出正确的结果,4x10显示为144。然后我想,输入可能是字符,而不是整数。我的问题是,如何将字符输入转换为整数,以便可以用于算术计算?

如果有人能考虑到我是个初学者来回答,那就太好了 :) 另外,如何将该整数转换回字符?

section .data

section .bss
input resb 4

section .text

global _start
_start:

mov eax, 3
mov ebx, 0
mov ecx, input
mov edx, 4
int 0x80

mov ebx, 10
imul ebx, ecx

mov eax, 1
int 0x80

我成功地将用户输入与数字进行了比较: mov ecx, dword[input]这是否实际上将ecx中的值更改为整数? 如何将其转换回字符串? - user2862492
2个回答

15

这里有几个字符串和整数之间相互转换的函数:

; 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

以下是如何使用它们:

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

请注意,这些例程中没有进行太多的错误检查(比如检查是否存在超出范围'0' - '9'的字符)。同时,这些例程也不处理有符号数。因此,如果您需要这些功能,则需要自行添加。

2
请不要推荐使用缓慢的“loop”指令(https://dev59.com/XFsV5IYBdhLWcg3w0xsK)!另外,两个LEA指令比“imul”+“add”明显更好:`lea ebx,[4ebx + ebx]`(ebx=5)/ lea ebx,[eax + 2*ebx]。或者你是在优化代码大小吗?sub al,'0'sub eax, '0' 节省1字节,但会导致Nehalem / Core2甚至PIII上的部分寄存器停顿更糟糕。(在Sandybridge上很好;这是对AL的RMW操作,因此它不会将部分寄存器与EAX分别重命名。) - Peter Cordes
1
如果您不需要检查循环条件是否为有效的十进制数字,您可以使用 lea ebx,[eax + 2 * ebx -'0'] 来避免完全使用 sub,但这会增加Intel CPU上LEA延迟到3。 - Peter Cordes
通常情况下,EBX是被调用保留的。不需要销毁它,您可以使用EAX进行总和计算,并将其movzx加载到EDX中。 - Peter Cordes

7
基本的字符串转数字算法是:total = total*10 + digit,从最高位开始计算 (例如对于一个由数字组成的ASCII字符串,digit = *p++ - '0')。因此,最左侧/最高位/第一个数字(在内存中和读取顺序中)乘以10的N次方,其中N是其后的总数字数。

这种方法通常比每个数字乘以正确的10的幂之后再相加更有效率。后者需要2次乘法;一次增长为10的幂,另一次将其应用于数字。(或者使用递增的10的幂查找表)。
当然,为了效率,您可以使用SSSE3 pmaddubsw和SSE2 pmaddwd并行地将数字与它们的位值相乘:请参见有没有快速将8个ASCII十进制数字的字符串转换为二进制数字的方法?以及任意长度的 如何使用SIMD实现atoi?。但是当数字通常较短时,后者可能不会获胜。当大多数数字仅有几个数字时,一个标量循环是有效的。

在 @Michael 的回答基础上,可能有用的是将 int->string 函数停止于第一个非数字字符,而不是固定长度。这将捕捉到像用户按下回车键时字符串包含换行符之类的问题,以及避免将 12xy34 转换为一个非常大的数字。(将其视为 12就像 C 中的 atoi 函数)。停止字符也可以是 C 隐式长度字符串中的终止符 0

我还进行了一些改进:

  • 如果你不是为了优化代码大小,就不要使用慢的loop指令。在需要倒数计数至零的情况下,只需忘记它的存在,使用dec/jnz代替,而不是比较指针或其他内容。

  • 两个LEA指令比imul+add更好:延迟更低。

  • 将结果累加到EAX中,因为我们想要返回它。(如果你内联这个函数而不是调用它,请使用任何你想要结果的寄存器。)

我更改了寄存器,使其遵循x86-64 System V ABI(第一个参数在RDI中,返回值在EAX中)。

迁移至32位:这与64位无关;只需使用32位寄存器即可将其迁移到32位。 (例如,用edi替换rdi,用ecx替换rax,用eax替换rax)。请注意,在32位和64位之间存在C调用约定的差异,例如,EDI是调用保留的,并且参数通常在堆栈上传递。但是,如果您的调用者是汇编语言,则可以在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

这会在第一个非数字字符处停止转换。通常,这将是终止隐式长度字符串的0字节。如果您想检测尾随垃圾,则可以在循环后通过检查 ecx == -'0'来检查它是否为字符串末尾,而不是其他非数字字符(其仍然保持超出范围的 str [i] - '0'整数“数字”值)。
如果您的输入是显式长度的字符串,则需要使用循环计数器而不是检查终止符(如@Michael的答案),因为内存中的下一个字节可能是另一个数字,或者可能在未映射的页中。
将第一次迭代特殊处理并在进入循环的主要部分之前处理它被称为循环剥离。剥离第一次迭代允许我们特别优化它,因为我们知道total = 0,所以没有必要将任何东西乘以10。这就像从sum = array [0];i = 1开始,而不是sum = 0,i = 0;
为了获得良好的循环结构(带有底部条件分支),我使用了跳转到循环中间进行第一次迭代的技巧。这甚至不需要额外的jmp,因为我已经在剥离的第一次迭代中进行了分支。重新排列循环,使中间的if()break成为底部的循环分支,称为循环旋转,并且可能涉及剥离第一次迭代的前半部分和最后一次迭代的第二部分。
解决非数字退出循环的简单方法是在循环体中加入一个jcc,就像在total = total*10 + digit之前的C语言if() break;语句一样。但是这样我需要一个jmp,并且在循环中有2个总分支指令,意味着更多的开销。
如果我不需要 sub ecx, '0' 的结果作为循环条件,我也可以使用 lea eax, [rax*2 + rcx - '0'] 在 LEA 中完成。但这会使 Sandybridge 系列 CPU 上的 LEA 延迟增加到 3 个时钟周期,而不是 1 个(3 组成部分的 LEA 而不是 2 或更少)。两个 LEA 在 eaxtotal)上形成了一个循环传递的依赖链,因此在 Intel 上(特别是对于大数),这样做并不值得。在 base + scaled-indexbase + scaled-index + disp8 速度相同的 CPU 上(Bulldozer-family / Ryzen),如果您有一个明确的长度作为循环条件,并且根本不想检查数字,那么当然可以这样做。
我一开始使用了movzx进行零扩展加载,而不是在将数字从ASCII转换为整数后再进行。 (必须在某个时候执行此操作以添加到32位EAX中)。经常操作ASCII数字的代码使用字节操作数大小,例如mov cl,[rdi]。但这会在大多数CPU上创建对RCX旧值的错误依赖。 sub al,'0'sub eax,'0'节省1个字节,但在Nehalem / Core2甚至PIII上会导致部分寄存器停顿。在其他所有CPU系列上都很好,即使是Sandybridge:它是AL的RMW,因此它不会单独重命名部分寄存器和EAX。但是,cmp al,9没有问题,因为读取字节寄存器总是fine。它可以节省一个字节(特殊编码,没有ModRM字节),所以我在函数顶部使用了它。

如果想了解更多优化方面的内容,请查看http://agner.org/optimize,以及 标签维基中的其他链接。

标签维基还有初学者链接,包括一个FAQ部分,其中包含指向整数转换为字符串函数和其他常见初学者问题的链接。

相关:


通常这将是隐式长度字符串的终止符0字节,但如果您想检测其他非数字字符的结束,可以在循环后检查ecx == -'0'。我花了一会儿才明白你的意思,如果数字字符串以零字节结尾,那么ecx将保持值为零减去30h(数字'0'的代码点)。 - ecm
@ecm:感谢您对意义不够清晰的反馈。0 - '0'是否更加清晰,或者已经足够清晰了?也许句子的其他部分可以改进。哦,可能修复“with -> will”拼写错误会有所帮助。 - Peter Cordes
@ecm:使用新措辞进行了更新。在详细介绍之前,最好先谈论一下想法的方向,这样读者就会为他们所看到的做好准备。虽然篇幅更长,但也许更好理解。 - Peter Cordes
更好了,但是你有一个没有相应关闭括号的开放括号。 - ecm
@ecm:谢谢,已经修复了 :P - Peter Cordes

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