打印一个整数(或将整数转换为字符串)

7

我正在寻找一种在汇编语言中打印整数的方法(我使用的编译器是Linux上的NASM),但是在进行了一些研究后,我并没有找到一个真正可行的解决方案。我能够找到一个基本算法的描述来实现这个目的,并且根据这个算法,我开发了以下代码:

global _start

section .bss
digit: resb 16
count: resb 16
i: resb 16

section .data

section .text

_start:
mov             dword[i], 108eh         ; i = 4238
mov             dword[count], 1
L01:
mov             eax, dword[i]
cdq
mov             ecx, 0Ah
div             ecx  
mov             dword[digit], edx

add             dword[digit], 30h       ; add 48 to digit to make it an ASCII char
call            write_digit

inc             dword[count]

mov             eax, dword[i]
cdq
mov             ecx, 0Ah
div             ecx  
mov             dword[i], eax 
cmp             dword[i], 0Ah  
jg              L01

add             dword[i], 48            ; add 48 to i to make it an ASCII char
mov             eax, 4                  ; system call #4 = sys_write
mov             ebx, 1                  ; file descriptor 1 = stdout
mov             ecx, i                  ; store *address* of i into ecx
mov             edx, 16                 ; byte size of 16
int             80h

jmp             exit

exit:
mov             eax, 01h                ; exit()
xor             ebx, ebx                ; errno
int             80h

write_digit:
mov             eax, 4                  ; system call #4 = sys_write
mov             ebx, 1                  ; file descriptor 1 = stdout
mov             ecx, digit              ; store *address* of digit into ecx
mov             edx, 16                 ; byte size of 16
int             80h
ret

我想要实现的C#版本(为了更清晰):

static string int2string(int i)
{
    Stack<char> stack = new Stack<char>();
    string s = "";

    do
    {
        stack.Push((char)((i % 10) + 48));
        i = i / 10;
    } while (i > 10);

    stack.Push((char)(i + 48));

    foreach (char c in stack)
    {
        s += c;
    }

    return s;
}

问题在于它以相反的顺序输出字符,因此对于4238,输出为8324。起初,我认为我可以使用x86堆栈来解决这个问题,将数字推入堆栈中,在最后弹出并打印它们,然而当我尝试实现该功能时,它失败了,我无法得到输出。
因此,我有点困惑如何将堆栈实现到该算法中,以完成我的目标,即打印一个整数。如果有更简单/更好的解决方案可用,我也会感兴趣(因为这是我的第一个汇编程序之一)。

1
这段C#代码太糟糕了。通常(对于所有高级语言),有一些很好易用的抽象化方法(例如stack.push())来防止人们意识到生成的代码实际上有多烂。注意:我敢你去反汇编那个C#生成的代码。。;-) - Brendan
我同意,我只是花了大约5分钟的时间把它拼凑在一起,以展示我希望使用汇编语言实现的目标。 - jszaday
4个回答

7

一种方法是使用递归。在这种情况下,您将数字除以10(得到商和余数),然后使用商作为要显示的数字调用自己; 然后显示对应于余数的数字。

这样做的一个例子是:

;Input
; eax = number to display

    section .data
const10:    dd 10
    section .text

printNumber:
    push eax
    push edx
    xor edx,edx          ;edx:eax = number
    div dword [const10]  ;eax = quotient, edx = remainder
    test eax,eax         ;Is quotient zero?
    je .l1               ; yes, don't display it
    call printNumber     ;Display the quotient
.l1:
    lea eax,[edx+'0']
    call printCharacter  ;Display the remainder
    pop edx
    pop eax
    ret

另一种方法是通过更改除数来避免递归。一个例子如下所示:
;Input
; eax = number to display

    section .data
divisorTable:
    dd 1000000000
    dd 100000000
    dd 10000000
    dd 1000000
    dd 100000
    dd 10000
    dd 1000
    dd 100
    dd 10
    dd 1
    dd 0
    section .text

printNumber:
    push eax
    push ebx
    push edx
    mov ebx,divisorTable
.nextDigit:
    xor edx,edx          ;edx:eax = number
    div dword [ebx]      ;eax = quotient, edx = remainder
    add eax,'0'
    call printCharacter  ;Display the quotient
    mov eax,edx          ;eax = remainder
    add ebx,4            ;ebx = address of next divisor
    cmp dword [ebx],0    ;Have all divisors been done?
    jne .nextDigit
    pop edx
    pop ebx
    pop eax
    ret

这个例子没有压制前导零,但是添加起来很容易。


谢谢您的回复,我对您第一个示例中的printNumber函数如何工作有点困惑。首先,在除法之前为什么要进行“xor”操作?此外,“test eax,eax je .l1”不应该是jz(测试是否为零)吗?另外,要打印的字符是否存储在eax中? - jszaday
xor edx,edx 只是将 EDX 设置为零(这对于除法运算是必要的)。je 指令(跳转如果相等)和 jz 指令(跳转如果为零)是同义词(它们具有完全相同的操作码/指令)。要打印的字符将在 EAX 中。 - Brendan
另一种按照 MSD(Most Significant Digit) 顺序打印的标准方法是将数字存储到缓冲区中(在堆栈上),例如 How do I print an integer in Assembly Level Programming without printf from the c library?。特别是当打印整个字符串与打印一个字符的成本相当时(系统调用甚至只是 stdio 函数调用开销)。 - Peter Cordes

1
; Input
; EAX = pointer to the int to convert
; EDI = address of the result
; Output:
; None
int_to_string:
    xor   ebx, ebx        ; clear the ebx, I will use as counter for stack pushes
.push_chars:
    xor edx, edx          ; clear edx
    mov ecx, 10           ; ecx is divisor, devide by 10
    div ecx               ; devide edx by ecx, result in eax remainder in edx
    add edx, 0x30         ; add 0x30 to edx convert int => ascii
    push edx              ; push result to stack
    inc ebx               ; increment my stack push counter
    test eax, eax         ; is eax 0?
    jnz .push_chars       ; if eax not 0 repeat

.pop_chars:
    pop eax               ; pop result from stack into eax
    stosb                 ; store contents of eax in at the address of num which is in EDI
    dec ebx               ; decrement my stack push counter
    cmp ebx, 0            ; check if stack push counter is 0
    jg .pop_chars         ; not 0 repeat
    mov eax, 0x0a
    stosb                 ; add line feed
    ret                   ; return to main

1

我认为实现一个栈可能不是最好的方法(我真的认为你可以想出如何做到这一点,因为 pop 只是一个 movsp 的减量,所以你可以通过分配内存并将其中一个寄存器设置为新的“堆栈指针”来随意设置堆栈)。

如果你为 C 风格的空终止字符串分配内存,然后创建一个函数将 int 转换为字符串,使用与你现在使用的算法相同,然后将结果传递给另一个能够打印这些字符串的函数,那么代码会更清晰、更模块化。这将避免你正在遭受的一些意大利面条式代码综合症,并解决你的问题。如果你想让我演示一下,只要问我就行了,但如果你写了上面的东西,我认为你可以通过更加分散的过程来解决问题。


谢谢您的建议,非常有价值!我会尝试编写一个像您描述的函数,如果需要更多帮助,我会再问的! - jszaday
我已经在这里得到了一个可用的版本,并且我认为我现在知道如何使用字符串来完成这个任务,我创建了这个程序以供学习目的。然而,我的问题是如何反转一个字符串? - jszaday
虽然有几种方法可以反转字符串,但我建议使用以下算法作为示例:1)计算字符串K中的字符数(称为n); 2)令i = 0,j = n-1; 3)当j-i> 0时; 3.1)交换K [i]和K [j]; 3.2)增加i并减少j; 4)返回K; - deftfyodor
你也可以使用堆栈,这需要你做以下几步:1)为堆栈分配内存;2)将字符串推入堆栈;3)再次弹出字符。这会使用额外的内存,算法本身并不是非常好,但基本上可以用与处理整数打印程序的工作版本相同的方式轻松完成。 - deftfyodor

0
; eax = number to stringify/output
; edi = location of buffer

intToString:
    push  edx
    push  ecx
    push  edi
    push  ebp
    mov   ebp, esp
    mov   ecx, 10

 .pushDigits:
    xor   edx, edx        ; zero-extend eax
    div   ecx             ; divide by 10; now edx = next digit
    add   edx, 30h        ; decimal value + 30h => ascii digit
    push  edx             ; push the whole dword, cause that's how x86 rolls
    test  eax, eax        ; leading zeros suck
    jnz   .pushDigits

 .popDigits:
    pop   eax
    stosb                 ; don't write the whole dword, just the low byte
    cmp   esp, ebp        ; if esp==ebp, we've popped all the digits
    jne   .popDigits

    xor   eax, eax        ; add trailing nul
    stosb

    mov   eax, edi
    pop   ebp
    pop   edi
    pop   ecx
    pop   edx
    sub   eax, edi        ; return number of bytes written
    ret

1
除非字符串恰好是UTF-32,否则此代码将无法正常工作。对于32位代码,“push”存储的是32位值而不是8位值,因此值123最终会变成类似于“1\0\0\02\0\0\03\0\0\0”的形式。当以ASCII或UTF-8字符串显示时,这些不需要的零被视为字符串终止符,意味着只有一个数字被显示出来。 - Brendan
我对这个问题一无所知,我是否应该在调用方法之前写“mov eax, number”和“mov edi, [buffer]”? - İsmet Alkan
1
@IsmetAlkan:这取决于buffer是什么。如果它是实际的内存块,那么你应该说mov edi, buffer,这样EDI包含地址buffer而不是它的第一个dword。 - cHao
1
同样地,如果“number”是包含数字的某个内存的标签,那么您会说mov eax,[buffer],以便EAX包含该内存的内容。另一方面,如果它只是一个“equ”,那么您所拥有的就很好。 - cHao

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