我正在寻找一种在汇编器中打印整数的方法(我使用的编译器是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 堆栈来解决这个问题,将数字压入,然后弹出并在最后打印它们,但是当我尝试实现该功能时,它失败了,我无法再获得输出。
因此,我对如何在该算法中实现堆栈以实现我的目标(即打印整数)感到有点困惑。如果有的话,我也会对更简单/更好的解决方案感兴趣(因为它是我的第一个汇编程序之一)。
一种方法是使用递归。在本例中,您将数字除以 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
此示例不会抑制前导零,但这很容易添加。
我认为也许实现一个堆栈并不是最好的方法(我真的认为你可以弄清楚如何做到这一点,比如
pop
只是一个 mov
和 sp
的减量,因此,您实际上可以在任何您喜欢的地方设置堆栈,只需为其分配内存并将寄存器之一设置为新的“堆栈指针”)。
我认为如果您实际上为 c 风格的 null 分隔字符串分配了内存,然后创建一个函数通过您使用的相同算法将 int 转换为字符串,然后将结果传递给另一个函数,那么这段代码可以变得更清晰、更模块化能够打印这些字符串。它将避免您所遭受的一些意大利面条代码综合症,并解决您的问题。如果你想让我演示,尽管问,但如果你写了上面的内容,我想你可以弄清楚如何通过更多的拆分过程来实现。
; 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
; 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
cdq mov ecx, 0Ah div ecx
在无符号除法
div
之前,必须将 EDX 寄存器清零。您的测试数据是正数 (4238),因此 cdq
产生 EDX=0,但请考虑如果您的 32 位整数是负数会发生什么。 EDX 将被设置为 -1 并且会发生除法溢出!我下面的解决方案考虑了负数。
cmp dword[i], 0Ah jg L01 add dword[i], 48 ; add 48 to i to make it an ASCII char
这是错误的,因为你太早离开循环了!如果 EAX 中的商正好是 10,则仅通过添加 48 就无法生成有效的 ASCII 字符。只要商对应的十进制数字多于 1 位,您就必须继续循环。写下
jge L01
。 } while (i > 10);
)。
mov edx, 16 ; byte size of 16 int 80h
对于每个数字,只需 1 个字节即可传递到 Linux。指定 16 的大小是没有意义的。
问题是它反向输出字符,因此对于
,输出是4238
。8324
您选择的算法从最低有效数字到最高有效数字进行工作。由于您立即输出每个数字,因此输出相反是正常的。最好不要在循环仍在运行时输出,并将数字存储在缓冲区中。然后一次打印所有数字。在下一个代码中,我将缓冲区放在堆栈上:
; IN (eax) OUT () MOD (eax,ecx,edx)
PrintInteger32:
push ebx
mov ebx, 10 ; CONST divider
mov ecx, esp
sub esp, 16 ; Small local buffer
test eax, eax
pushf ; (1)
jns .next ; Is positive [0,2GB-1]
neg eax ; Make positive [1,2GB]
.next:
dec ecx
xor edx, edx
div ebx ; EDX:EAX / EBX
add edx, '0' ; Remainder [0,9] -> ["0","9"]
mov [ecx], dl
test eax, eax
jnz .next
popf ; (1)
jns .print
dec ecx
mov byte [ecx], '-'
.print: ; ECX is address of the MSD or the minus char
lea edx, [esp + 16]
sub edx, ecx ; -> EDX is number of digits to print
mov ebx, 1 ; file descriptor 1 = stdout
mov eax, 4 ; system call #4 = sys_write
int 80h
add esp, 16
pop ebx
ret
如果有的话,我也会对更简单/更好的解决方案感兴趣(因为它是我的第一个汇编程序之一)。
用倒数乘法代替除法运算的解决方案不一定更好,也不一定更简单,但绝对更快。 请参阅为什么 GCC 在实现整数除法时使用乘以奇怪的数字?