您需要手动将二进制整数转换为 ASCII 十进制数字的字符串/数组。 ASCII 数字由'0'
(0x30) 到'9'
(0x39) 范围内的 1 字节整数表示。 http://www.asciitable.com/
对于像十六进制这样的 2 次方基数,请参阅如何将二进制整数转换为十六进制字符串? 在二进制和 2 次方基数之间进行转换可以进行更多优化和简化,因为每组位分别映射到十六进制/八进制数字。
大多数操作系统/环境没有接受整数并将它们转换为十进制的系统调用。在将字节发送到操作系统之前,您必须自己执行此操作,或者自己将它们复制到视频内存,或者在视频内存中绘制相应的字体字形......
到目前为止,最有效的方法是进行一次执行整个字符串的单个系统调用,因为写入 8 个字节的系统调用与写入 1 个字节的成本基本相同。
这意味着我们需要一个缓冲区,但这根本不会增加我们的复杂性。2^32-1 只有 4294967295,只有 10 位十进制数字。我们的缓冲区不需要很大,所以我们可以使用堆栈。
通常的算法产生数字 LSD-first(Least Significant Digit first)。由于打印顺序是 MSD 优先的,我们可以从缓冲区的末尾开始并向后工作。对于其他地方的打印或复制,只需跟踪它的开始位置,而不必费心将其置于固定缓冲区的开头。无需使用 push/pop 来反转任何内容,只需首先将其向后生成即可。
char *itoa_end(unsigned long val, char *p_end) {
const unsigned base = 10;
char *p = p_end;
do {
*--p = (val % base) + '0';
val /= base;
} while(val); // runs at least once to print '0' for val=0.
// write(1, p, p_end-p);
return p; // let the caller know where the leading digit is
}
gcc/clang 做得很好,使用魔法常数乘数而不是div
有效地除以 10。(用于 asm 输出的Godbolt 编译器资源管理器)。
这个代码审查问答有一个非常高效的 NASM 版本,它将字符串累积到一个 8 字节的寄存器而不是内存中,准备好存储您希望字符串开始的位置而无需额外复制。
处理有符号整数:
对无符号绝对值使用此算法。( if(val<0) val=-val;
)。如果原始输入是否定'-'
的,请在完成后将 a 放在最后。例如,-10
使用 运行它10
,产生 2 个 ASCII 字节。然后将 a 存储'-'
在前面,作为字符串的第三个字节。
这是一个简单的注释 NASM 版本,div
对 32 位无符号整数和 Linuxwrite
系统调用使用(慢但更短的代码)。 只需将寄存器更改为而ecx
不是rcx
. 但是add rsp,24
会变成add esp, 20
因为push ecx
只有 4 个字节,而不是 8 个。(您还应该esi
为通常的 32 位调用约定保存/恢复,除非您将其变成宏或仅供内部使用的函数。)
系统调用部分特定于 64 位 Linux。将其替换为适合您系统的任何内容,例如调用 VDSO 页面以在 32 位 Linux 上进行高效的系统调用,或int 0x80
直接用于低效的系统调用。请参阅Unix/Linux 上 32 位和 64 位系统调用的调用约定。或者查看rkhb 对另一个问题的回答int 0x80
,了解以相同方式工作的32 位版本。
如果您只需要字符串而不打印它,rsi
则在离开循环后指向第一个数字。您可以将它从 tmp 缓冲区复制到您实际需要它的任何位置的开头。或者,如果您直接将它生成到最终目的地(例如,传递一个指针 arg),您可以用前导零填充,直到到达您为它留下的空间的前面。除非您总是用零填充到固定宽度,否则没有简单的方法可以在开始之前找出它将是多少位数。
ALIGN 16
; void print_uint32(uint32_t edi)
; x86-64 System V calling convention. Clobbers RSI, RCX, RDX, RAX.
; optimized for simplicity and compactness, not speed (DIV is slow)
global print_uint32
print_uint32:
mov eax, edi ; function arg
mov ecx, 0xa ; base 10
push rcx ; ASCII newline '\n' = 0xa = base
mov rsi, rsp
sub rsp, 16 ; not needed on 64-bit Linux, the red-zone is big enough. Change the LEA below if you remove this.
;;; rsi is pointing at '\n' on the stack, with 16B of "allocated" space below that.
.toascii_digit: ; do {
xor edx, edx
div ecx ; edx=remainder = low digit = 0..9. eax/=10
;; DIV IS SLOW. use a multiplicative inverse if performance is relevant.
add edx, '0'
dec rsi ; store digits in MSD-first printing order, working backwards from the end of the string
mov [rsi], dl
test eax,eax ; } while(x);
jnz .toascii_digit
;;; rsi points to the first digit
mov eax, 1 ; __NR_write from /usr/include/asm/unistd_64.h
mov edi, 1 ; fd = STDOUT_FILENO
; pointer already in RSI ; buf = last digit stored = most significant
lea edx, [rsp+16 + 1] ; yes, it's safe to truncate pointers before subtracting to find length.
sub edx, esi ; RDX = length = end-start, including the \n
syscall ; write(1, string /*RSI*/, digits + 1)
add rsp, 24 ; (in 32-bit: add esp,20) undo the push and the buffer reservation
ret
公共区域。 随意将其复制/粘贴到您正在处理的任何内容中。如果它坏了,你可以保留两块。(如果性能很重要,请参阅下面的链接;您需要乘法逆而不是div
。)
这是在循环倒数到 0(包括 0)时调用它的代码。把它放在同一个文件中很方便。
ALIGN 16
global _start
_start:
mov ebx, 100
.repeat:
lea edi, [rbx + 0] ; put +whatever constant you want here.
call print_uint32
dec ebx
jge .repeat
xor edi, edi
mov eax, 231
syscall ; sys_exit_group(0)
组装和链接
yasm -felf64 -Worphan-labels -gdwarf2 print-integer.asm &&
ld -o print-integer print-integer.o
./print_integer
100
99
...
1
0
用于strace
查看该程序进行的唯一系统调用是write()
和exit()
。(另请参阅x86标签 wiki底部的 gdb / 调试提示,以及那里的其他链接。)
相关: