正如@ ped7g指出的,您做错了几件事:int 0x80在64位代码中使用32位ABI,并传递字符值而不是指向write()系统调用的指针。
这是在64位Linux中以简单且有效的方式打印整数的方法。 请参阅为什么GCC在实现整数除法时为何使用乘以奇数的乘法?避免div r64除以10,因为这非常慢(在Intel Skylake上为21到83个周期)。乘法逆将使该函数实际上有效,而不仅仅是“有点”。(但是,当然还有优化的余地...)
系统调用很昂贵(可能需要上千个周期write(1, buf, 1)),并且syscall在寄存器内执行循环内部步骤,因此不方便,笨拙且效率低下。我们应该将字符按打印顺序(在最低地址中的最高有效数字)写入一个小的缓冲区中,并在此上进行单个write()系统调用。
但是然后我们需要一个缓冲区。64位整数的最大长度只有20个十进制数字,因此我们只能使用一些堆栈空间。在x86-64 Linux中,我们可以使用RSP以下的堆栈空间(最大128B),而无需通过修改RSP来“保留”它。这称为红色区域。
使用GAS无需对系统调用号进行硬编码,因此可以轻松使用.h文件中定义的常量。 注意mov $__NR_write, %eax函数的结尾。 x86-64 SystemV ABI将系统调用参数传递给类似函数调用约定的寄存器。(因此,它与32位int 0x80ABI 完全不同。)
#include // This is a standard glibc header file
// It contains no C code, only only #define constants, so we can include it from asm without syntax errors.
.p2align 4
.globl print_integer #void print_uint64(uint64_t value)
print_uint64:
lea -1(%rsp), %rsi # We use the 128B red-zone as a buffer to hold the string
# a 64-bit integer is at most 20 digits long in base 10, so it fits.
movb $'\n', (%rsi) # store the trailing newline byte. (Right below the return address).
# If you need a null-terminated string, leave an extra byte of room and store '\n\0'. Or push $'\n'
mov $10, %ecx # same as mov $10, %rcx but 2 bytes shorter
# note that newline (\n) has ASCII code 10, so we could actually have used movb %cl to save code size.
mov %rdi, %rax # function arg arrives in RDI; we need it in RAX for div
.Ltoascii_digit: # do{
xor %edx, %edx
div %rcx # rax = rdx:rax / 10. rdx = remainder
# store digits in MSD-first printing order, working backwards from the end of the string
add $'0', %edx # integer to ASCII. %dl would work, too, since we know this is 0-9
dec %rsi
mov %dl, (%rsi) # *--p = (value%10) + '0';
test %rax, %rax
jnz .Ltoascii_digit # } while(value != 0)
# If we used a loop-counter to print a fixed number of digits, we would get leading zeros
# The do{}while() loop structure means the loop runs at least once, so we get "0\n" for input=0
# Then print the whole string with one system call
mov $__NR_write, %eax # SYS_write, from unistd_64.h
mov $1, %edi # fd=1
# %rsi = start of the buffer
mov %rsp, %rdx
sub %rsi, %rdx # length = one_past_end - start
syscall # sys_write(fd=1 /*rdi*/, buf /*rsi*/, length /*rdx*/); 64-bit ABI
# rax = return value (or -errno)
# rcx and r11 = garbage (destroyed by syscall/sysret)
# all other registers = unmodified (saved/restored by the kernel)
# we don't need to restore any registers, and we didn't modify RSP.
ret
为了测试此功能,我将其放在同一文件中以调用它并退出:
.p2align 4
.globl _start
_start:
mov $10120123425329922, %rdi
# mov $0, %edi # Yes, it does work with input = 0
call print_uint64
xor %edi, %edi
mov $__NR_exit, %eax
syscall # sys_exit(0)
我将其内置到静态二进制文件中(没有libc):
$ gcc -Wall -nostdlib print-integer.S && ./a.out
10120123425329922
$ strace ./a.out > /dev/null
execve("./a.out", ["./a.out"], 0x7fffcb097340 /* 51 vars */) = 0
write(1, "10120123425329922\n", 18) = 18
exit(0) = ?
+++ exited with 0 +++
$ file ./a.out
./a.out: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=69b865d1e535d5b174004ce08736e78fade37d84, not stripped
相关:Linux x86-32扩展精度循环,从每个32位“肢体”中打印9个十进制数字:参见.toascii_digit:在我的Extreme Fibonacci code-golf答案中。它已针对代码大小进行了优化(即使以牺牲速度为代价),但得到了很好的评价。
它的用法div与您一样,因为它比使用快速乘法逆函数要小。它loop用于外部循环(在多个整数上用于扩展精度),再次用于代码大小,但代价是speed。
它使用32位int 0x80ABI,并打印到保存“旧”斐波那契值而不是当前值的缓冲区中。
获得高效asm的另一种方法是使用C编译器。对于仅数字循环,请查看此C源产生的gcc或clang(基本上是asm所做的事情)。Godbolt编译器资源管理器使您可以轻松尝试使用不同的选项和不同的编译器版本。
参见gcc7.2 -O3 asm输出,它几乎替代了循环print_uint64(因为我选择了args放入相同的寄存器中):
void itoa_end(unsigned long val, char *p_end) {
const unsigned base = 10;
do {
*--p_end = (val % base) + '0';
val /= base;
} while(val);
// write(1, p_end, orig-current);
}
我通过注释掉syscall指令并在函数调用周围放置重复循环,在Skylake i7-6700k上测试了性能。带mul %rcx/ shr $3, %rdx的版本比将div %rcx长数字字符串(10120123425329922)存储到缓冲区中的版本快约5倍。div版本每时钟运行0.25条指令,而mul版本每时钟运行2.65条指令(尽管需要更多指令)。
可能值得将其展开为2,再除以100,然后将其余部分分成两位数。万一更简单的版本在mul+ shr延迟上出现瓶颈,那将提供更好的指令级并行性。val归零的乘法/移位运算链的长度将是原来的一半,而每个较短的独立依存关系链中还有更多工作要处理0-99的余数。