用AT&T语法打印一个整数作为string,用Linux系统调用代替printf

我写了一个汇编程序来显示AT&T语法后的一个数字的阶乘。但它不工作。我的代码

.text .globl _start _start: movq $5,%rcx movq $5,%rax Repeat: #function to calculate factorial decq %rcx cmp $0,%rcx je print imul %rcx,%rax cmp $1,%rcx jne Repeat # Now result of factorial stored in rax print: xorq %rsi, %rsi # function to print integer result digit by digit by pushing in #stack loop: movq $0, %rdx movq $10, %rbx divq %rbx addq $48, %rdx pushq %rdx incq %rsi cmpq $0, %rax jz next jmp loop next: cmpq $0, %rsi jz bye popq %rcx decq %rsi movq $4, %rax movq $1, %rbx movq $1, %rdx int $0x80 addq $4, %rsp jmp next bye: movq $1,%rax movq $0, %rbx int $0x80 .data num : .byte 5 

这个程序没有打印任何东西,我也用gdb来直观地看到它工作正常,直到循环函数,但是当它接下来的一些随机值开始进入各种寄存器。帮我debugging,以便它可以打印阶乘。

Solutions Collecting From Web of "用AT&T语法打印一个整数作为string,用Linux系统调用代替printf"

几件事情:

0)我想这是64b的linux环境,但你应该说明如此(如果不是,我的一些观点将是无效的)

1) int 0x80是32b调用,但是你使用64b寄存器,所以你应该使用syscall (和不同的参数)

2) int 0x80, eax=4要求ecx包含存储内容的地址,而在ecx =非法内存访问中给出它的ASCII字符(第一个调用应返回错误,即eax为负值) 。 或者使用strace <your binary>应该显示错误的参数+错误返回。

3)为什么addq $4, %rsp ? 对我来说没有任何意义,你正在破坏pop rcx ,所以下一个pop rcx会弹出错误的值,最后你会跑到堆栈里。

…也许还有一些,我没有调试它,这个列表只是通过阅读源代码(所以我可能甚至错误的东西,虽然这将是罕见的)。

顺便说一句你的代码正在工作 。 它只是没有做你所期望的。 但工作得很好,正如CPU的设计,正是你在代码中写的。 无论是达到你想要的还是有意义的,这是不同的话题,但不要责怪硬件或汇编。

…我可以做一个快速猜测如何修复例程(只是部分修复,仍然需要在64b linux下重新编写syscall ):

  next: cmpq $0, %rsi jz bye movq %rsp,%rcx ; make ecx to point to stack memory (with stored char) ; this will work if you are lucky enough that rsp fits into 32b ; if it is beyond 4GiB logical address, then you have bad luck (syscall needed) decq %rsi movq $4, %rax movq $1, %rbx movq $1, %rdx int $0x80 addq $8, %rsp ; now rsp += 8; is needed, because there's no POP jmp next 

再次没有尝试自己,只是从头开始写,所以让我知道它是如何改变的情况。

正如@ ped7g所指出的那样,你在做一些错误的事情:在64位代码中使用int 0x80 32位ABI,并传递字符值而不是指向write()系统调用的指针。

以下是如何在64位Linux中打印一个整数,这是一种简单而有效的方法。 看看为什么GCC在执行整数除法时使用奇数乘法? 因为这很慢( 在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 0x80 ABI完全不同的寄存器。)

 #include <asm/unistd_64.h> // 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:在我的极端Fibonacci代码 – 高尔夫答案 。 它针对代码大小进行了优化(即使以速度为代价),但是经过了充分的评论。

它像你一样使用div ,因为这比使用快速乘法反转要小)。 它使用loop的外部循环(在多个整数的扩展精度),再次代码大小的代价是速度 。

它使用32位int 0x80 ABI,并打印到保存“旧”的斐波那契数值的缓冲区中,而不是当前的。


另一种获得高效的asm的方法是从C编译器。 对于数字循环,看看这个C源代码(这基本上就是asm所做的)是由gcc还是clang产生的。 Godbolt编译器浏览器可以轻松地尝试不同的选项和不同的编译器版本。

请参阅gcc7.2 -O3 asm输出 ,这几乎是print_uint64循环的一个print_uint64 (因为我选择参数在相同的寄存器中):

 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); } 

我在Skylake i7-6700k上通过注释掉syscall指令并在函数调用周围重复循环来测试性能。 mul %rcx / shr $3, %rdx的版本比用于存储长数字串( 10120123425329922 )的div %rcx的版本快大约5倍。 div版本每时钟运行0.25次,而mul版本每时钟运行2.65次(尽管需要更多的指令)。

可能值得用2来展开,然后用100来除数,然后把余数分成2位数。 如果更简单的版本瓶颈在mul + shr等待时间上,那么这将提供更好的指令级并行性。 使val变为零的乘法/移位操作的链将是一半长,在每个短的独立依赖链中处理剩余的0-99的工作量更多。