NASM 程序集将输入转换为整数?

Posted

技术标签:

【中文标题】NASM 程序集将输入转换为整数?【英文标题】:NASM Assembly convert input to integer? 【发布时间】:2013-10-19 00:55:28 【问题描述】:

好的,所以我对组装很陌生,事实上,我对组装很陌生。我写了一段代码,它只是为了从用户那里获取数字输入,将它乘以 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 中的值更改为整数吗?以及如何将其改回字符串? 【参考方案1】:

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

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

【讨论】:

请不要推荐the slow loop instruction!此外,2 个 LEA 指令明显优于 imul + add: lea ebx, [4*ebx + ebx] (ebx*=5) / lea ebx, [eax + 2*ebx]。或者您是否针对代码大小进行了优化? sub al,'0'sub eax,'0' 节省 1 个字节,但会导致 Nehalem/Core2 上的部分寄存器停顿,在 PIII 上更糟。 (在 Sandybridge 上很好;它是 AL 的 RMW,因此它不会将部分 reg 与 EAX 分开重命名。) 如果你不需要检查是否是一个有效的十进制数字作为循环条件,你可以`lea ebx, [eax + 2*ebx - '0']`来避免@完全是 987654332@,但这会将 Intel CPU 上的 LEA 延迟增加到 3。 通常 EBX 是保留呼叫的。无需销毁它,您可以使用 EAX 进行总和 movzx-load 到 EDX。【参考方案2】:

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

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

当然,为了提高效率,您可以使用 SSSE3 pmaddubsw 和 SSE2 pmaddwd 并行地将数字乘以它们的位值:请参阅 How to implement atoi using SIMD?。不过,当数字通常很短时,这可能不是一场胜利。当大多数数字只有几位数时,标量循环是有效的。


除了@Michael 的回答之外,让 int->string 函数停在第一个非数字而不是固定长度可能会很有用。这将捕获诸如您的字符串之类的问题,包括用户按下回车时的换行符,以及不会将12xy34 变成一个非常大的数字。 (将其视为12,like C's atoi function)。停止字符也可以是 C 隐式长度字符串中的终止符 0

我也做了一些改进:

不要使用the slow loop instruction,除非您正在优化代码大小。忘记它的存在并使用 dec / jnz 以防倒数到零仍然是您想要做的事情,而不是比较指针或其他东西。

2 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 是保留调用的,并且参数通常在堆栈上传递。但是如果你的调用者是 asm,你可以在 EDI 中传递一个 arg。

    ; 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 的回答),因为内存中的下一个字节可能是另一个数字。或者它可能位于未映射的页面中。


使第一次迭代特别并在跳转到循环的主要部分之前对其进行处理称为loop peeling。剥离第一次迭代可以让我们特别优化它,因为我们知道 total=0,所以不需要将任何东西乘以 10。这就像从 sum = array[0]; i=1 而不是 sum=0, i=0; 开始。

为了获得nice loop structure (with the conditional branch at the bottom),我在第一次迭代中使用了跳到循环中间的技巧。这甚至不需要额外的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 上具有 made the LEA latency 3 cycles instead of 1。 (3 组件 LEA 与 2 或更少。)这两个 LEA 在 eax (total) 上形成一个循环携带的依赖链,因此(特别是对于大量数据)在 Intel 上是不值得的。在 base + scaled-index 不比 base + scaled-index + disp8 (Bulldozer-family / Ryzen) 快的 CPU 上,如果您有明确的长度作为循环条件并且根本不想检查数字,那么可以确定。

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

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


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

标签 wiki 也有初学者链接,包括一个常见问题解答部分,其中包含指向整数->字符串函数的链接,以及其他常见的初学者问题。

相关:

How do I print an integer in Assembly Level Programming without printf from the c library? 是这个问题的反面,整数 -> base10string。

Conversion of huge decimal numbers (128bit) formatted as ASCII to binary (hex) 处理长字符串,例如一个 128 位整数,需要 4 个 32 位寄存器。 (这不是很有效,并且可能会更好地转换为多个块,然后进行扩展精度乘以 1e9 或其他东西。)

Convert from ascii to integer in AT&T Assembly 效率低下的 AT&T 版本。

【讨论】:

“通常这是终止隐式长度字符串的 0 字节,但如果您想检测其他非数字字符的结尾,您可以在循环后检查 ecx == -'0'。”我花了一点时间才明白你的意思是,如果数字字符串以零字节结尾,ecx 将保持值零减去 30h(数字“0”的代码点)。 @ecm:感谢您对未立即明确含义的反馈。 0 - '0' 会更清晰,还是足够清晰?也许在句子的其余部分可以改进。哦,可能修复 with -> 会错字会有所帮助。 @ecm:更新了新的措辞。更长一点,但最好先谈谈想法的发展方向,然后再详细说明如何做,这样读者就会为他们所看到的做好准备。 更好,但你有一个没有相应的右括号的左括号。 @ecm:谢谢,也修复了:P

以上是关于NASM 程序集将输入转换为整数?的主要内容,如果未能解决你的问题,请参考以下文章

NASM x86,FIST 的意外结果

在R中创建一个函数,在整个数据帧中将字符串转换为整数

将 C 代码转换为 x86-64 位程序集?

将两个32位数相乘并将64位结果打印为十进制NASM程序集

微软工具ILMerge

nasm 程序集 linux 计时器或睡眠