使用 AT&T 语法将整数打印为字符串,使用 Linux 系统调用而不是 printf

Ayu*_*hra 2 linux assembly x86-64 att

我编写了一个汇编程序来显示遵循 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
Run Code Online (Sandbox Code Playgroud)

这个程序没有打印任何内容,我还使用 gdb 来可视化它在循环函数之前工作正常,但是当它出现时,一些随机值开始进入各种寄存器。帮我调试,以便它可以打印阶乘。

Pet*_*des 5

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

下面是如何打印的整数x8-64 Linux的,简单的,有点高效的1路, 10使用相同的重复师/模数。

系统调用很昂贵(对于 来说可能有数千个周期write(1, buf, 1)),并且syscall在循环内部执行寄存器,因此它既不方便又笨重且效率低下。我们应该将字符写入一个小缓冲区,按打印顺序(最低地址的最高有效数字),并对其进行单个write()系统调用。

但是我们需要一个缓冲区。64 位整数的最大长度只有 20 位十进制数字,因此我们可以使用一些堆栈空间。在 x86-64 Linux 中,我们可以使用低于 RSP(最多 128B)的堆栈空间,而无需通过修改 RSP 来“保留”它。这称为。如果您想将缓冲区传递给另一个函数而不是系统调用,则必须保留空间sub $24, %rsp或其他东西。

与硬编码系统调用号不同,使用 GAS 可以轻松使用.h文件中定义的常量。 注意mov $__NR_write, %eax函数的结尾。 x86-64 SystemV ABI 将类似寄存器中的系统调用参数传递给函数调用约定。(所以它与 32 位int 0x80ABI完全不同,你不应该在 64 位代码中使用它。)

// 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
Run Code Online (Sandbox Code Playgroud)

为了测试这个函数,我把它放在同一个文件中来调用它并退出:

.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)
Run Code Online (Sandbox Code Playgroud)

我将它构建到一个静态二进制文件中(没有 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
Run Code Online (Sandbox Code Playgroud)

脚注 1:请参阅为什么 GCC 在实现整数除法时使用乘以奇怪的数?避免div r64除以 10,因为这非常慢(英特尔 Skylake 上的 21 到 83 个周期)。乘法逆将使这个函数实际上有效,而不仅仅是“有点”。(但当然还有优化的空间......)



相关:Linux x86-32 扩展精度循环,从每个 32 位“肢体”打印 9 个十进制数字:参见.toascii_digit:在我的 Extreme Fibonacci code-golf answer 中。它针对代码大小进行了优化(即使以牺牲速度为代价),但得到了很好的评论。

div像您一样使用,因为这比使用快速乘法逆运算要小)。它loop用于外循环(超过多个整数以获得扩展精度),再次用于以速度为代价的代码大小

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


获得高效 asm 的另一种方法是使用 C 编译器。 对于数字循环,看看 gcc 或 clang 为这个 C 源代码生成了什么(这基本上就是 asm 正在做的事情)。Godbolt Compiler Explorer 可以轻松尝试不同的选项和不同的编译器版本。

参见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);
}
Run Code Online (Sandbox Code Playgroud)

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

可能值得展开 2,然后除以 100 并将其余部分分成 2 位数。这将提供更好的指令级并行性,以防更简单的版本在mul+shr延迟上遇到瓶颈。val归零的乘法/移位运算链将是原来的一半,在每个短的独立依赖链中需要做更多的工作来处理 0-99 的余数。


有关的:


归档时间:

查看次数:

2969 次

最近记录:

4 年,11 月 前