7

好的,所以我对装配很陌生,事实上,我对装配很陌生。我写了一段代码,它只是为了从用户那里获取数字输入,将其乘以 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
4

2 回答 2

13

这里有几个用于将字符串转换为整数的函数,反之亦然:

; 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')。例程也不处理带符号的数字。所以如果你需要这些东西,你必须自己添加它们。

于 2013-10-11T07:25:29.083 回答
6

string->digit 的基本算法是:total = total*10 + digit,从 MSD 开始。(例如,digit = *p++ - '0'用于 ASCII 数字字符串)。因此,最左边/最重要/第一个数字(在内存中,按阅读顺序)乘以 10 N 次,其中 N 是它之后的总位数。

这样做通常比在添加之前将每个数字乘以 10 的右幂更有效。那将需要2次乘法;一个增长 10 的幂,另一个将其应用于数字。(或 10 次幂的查表)。

当然,为了提高效率,您可以使用 SSSE3pmaddubsw和 SSE2pmaddwd并行地将数字乘以它们的位置值:请参阅如何使用 SIMD 实现 atoi?. 不过,当数字通常很短时,这可能不是一场胜利。当大多数数字只有几位数时,标量循环是有效的。


添加到@Michael 的答案,让 int->string 函数停在第一个 non-digit而不是固定长度可能会很有用。这将捕获诸如您的字符串之类的问题,包括用户按下回车时的换行符,以及不会12xy34变成一个非常大的数字。(把它当作12就像 C 的atoi函数)。停止字符也可以是0C 隐式长度字符串中的终止符。

我也做了一些改进:

  • 除非您针对代码大小进行优化,否则不要使用慢速loop指令。忘记它的存在并使用dec/jnz以防倒数到零仍然是您想要做的,而不是比较指针或其他东西。

  • 2 LEA 指令明显优于imul+ add:延迟更低。

  • 将结果累积到我们想要返回的 EAX 中。(如果您将其内联而不是调用它,请使用您想要结果的任何寄存器。)

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

移植到 32 位: 这完全不依赖于 64 位;只需使用 32 位寄存器即可将其移植到 32 位。(即替换rdiedi、和)。注意 32 位和 64 位之间的 C 调用约定差异,例如,EDI 是保留调用的,而参数通常在堆栈上传递。但是如果你的调用者是 asm,你可以在 EDI 中传递一个 arg。raxecxraxeax

    ; 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在循环体中添加 a ,就像if() break;C 中的 . 之前的语句一样total = total*10 + digit。但是我需要一个jmp并且在循环中有两个分支指令,这意味着更多的开销。


如果我不需要sub ecx, '0'循环条件的结果,我也可以lea eax, [rax*2 + rcx - '0']将其作为 LEA 的一部分。但这会使Sandybridge 系列 CPU的 LEA 延迟 3 个周期而不是 1 个周期。eax(3 组件 LEA 与 2 或更少。)两个 LEA 在( )上形成一个循环携带的依赖链total,因此(尤其是对于大量数据)在 Intel 上不值得。在base + scaled-index不比base + scaled-index + disp8Bulldozer-family / Ryzen)快的 CPU 上,如果您有明确的长度作为循环条件并且根本不想检查数字,那么可以肯定。

我首先使用 movzx 加载零扩展名,而不是在将数字从 ASCII 转换为整数之后这样做。(必须在某个时候添加到 32 位 EAX 中)。处理 ASCII 数字的代码通常使用字节操作数大小,例如mov cl, [rdi]. 但这会在大多数 CPU 上创建对 RCX 旧值的错误依赖。

sub al,'0'节省 1 个字节sub eax,'0',但会导致 Nehalem/Core2 上的部分寄存器停顿,在 PIII 上更糟。在所有其他 CPU 系列上都很好,甚至是 Sandybridge:它是 AL 的 RMW,因此它不会将部分 reg 与 EAX 分开重命名。但cmp al, 9不会造成问题,因为读取字节寄存器总是没问题的。它保存了一个字节(没有 ModRM 字节的特殊编码),所以我在函数的顶部使用了它。


有关更多优化内容,请参阅http://agner.org/optimize以及标签 wiki的其他链接。

标签 wiki 也有初学者链接,包括一个 FAQ 部分,其中包含指向 integer->string 函数的链接,以及其他常见的初学者问题。

有关的:

于 2018-03-29T04:29:30.560 回答