正如@ped7g 指出的那样,您做错了几件事:在 64 位代码中使用 int 0x80 32 位 ABI,并传递字符值而不是指向 write() 系统调用的指针。
以下是在 x8-64 Linux 中打印整数的方法,这是一种简单且效率较高的1方式,使用相同的重复除法/模除 10。 p>
系统调用代价高昂(write(1, buf, 1) 可能需要数千个周期),并且在循环内执行 syscall 会占用寄存器,因此既不方便又笨重且效率低下。我们应该按照打印顺序(最低地址的最高有效数字)将字符写入一个小缓冲区,然后对其进行一次write() 系统调用。
但是我们需要一个缓冲区。一个 64 位整数的最大长度只有 20 个十进制数字,所以我们可以只使用一些堆栈空间。在 x86-64 Linux 中,我们可以使用低于 RSP(最高 128B)的堆栈空间,而无需通过修改 RSP 来“保留”它。这称为red-zone。如果您想将缓冲区传递给另一个函数而不是系统调用,则必须使用 sub $24, %rsp 或其他方式保留空间。
使用 GAS 可以轻松使用在 .h 文件中定义的常量,而不是硬编码系统调用号。请注意函数末尾附近的 mov $__NR_write, %eax。 The x86-64 SystemV ABI passes system-call arguments in similar registers to the function-calling convention。 (所以它与 32 位 int 0x80 ABI 完全不同,你在 64 位代码中 shouldn't use。)
// building with gcc foo.S will use CPP before GAS so we can use headers
#include <asm/unistd.h> // This is a standard Linux / glibc header file
// includes unistd_64.h or unistd_32.h depending on current mode
// Contains only #define constants (no C prototypes) 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 stored the newline with movb %cl, (%rsi) 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 # call number from asm/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 # 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 -static -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
脚注 1: 请参阅 Why does GCC use multiplication by a strange number in implementing integer division? 避免使用 div r64 除以 10,因为这非常慢 (21 to 83 cycles on Intel Skylake)。乘法逆运算将使该函数实际上有效,而不仅仅是“有点”。 (当然还有优化的空间……)
相关:Linux x86-32 扩展精度循环,从每个 32 位“肢体”打印 9 个十进制数字:请参阅 .toascii_digit: in my Extreme Fibonacci code-golf answer。它针对代码大小进行了优化(即使以牺牲速度为代价),但受到好评。
它像你一样使用div,因为它比使用快速乘法逆要小)。它将loop 用于外循环(超过多个整数以获得扩展精度),再次用于code-size at the cost of speed。
它使用 32 位 int 0x80 ABI,并打印到保存“旧”斐波那契值而不是当前值的缓冲区中。
另一种获得高效 asm 的方法是使用 C 编译器。 对于数字循环,看看 gcc 或 clang 为这个 C 源代码生成了什么(这基本上是 asm 正在做的事情) . Godbolt 编译器资源管理器让您可以轻松尝试不同的选项和不同的编译器版本。
请参阅gcc7.2 -O3 asm output,它几乎可以替代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 的版本快大约 5 倍,用于将长数字字符串 (10120123425329922) 存储到缓冲区中。 div 版本以每时钟 0.25 条指令运行,而 mul 版本以每时钟 2.65 条指令运行(尽管需要更多指令)。
可能值得展开 2,然后除以 100 并将余数分成 2 位数。这将提供更好的指令级并行性,以防mul + shr 延迟上的更简单版本瓶颈。将val 归零的乘法/移位操作链将减半,每个短的独立依赖链需要更多的工作来处理 0-99 余数。
相关: