【问题标题】:Printing an integer as a string with AT&T syntax, with Linux system calls instead of printf使用 AT&T 语法将整数打印为字符串,使用 Linux 系统调用而不是 printf
【发布时间】:2018-01-31 19:00:33
【问题描述】:

我编写了一个汇编程序来显示遵循 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 来可视化它工作正常,直到循环函数,但是当它进入下一个随机值时,开始进入各种寄存器。帮我调试一下,让它可以打印阶乘。

【问题讨论】:

标签: linux assembly x86-64 att


【解决方案1】:

正如@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 来“保留”它。这称为。如果您想将缓冲区传递给另一个函数而不是系统调用,则必须使用 sub $24, %rsp 或其他方式保留空间。

使用 GAS 可以轻松使用在 .h 文件中定义的常量,而不是硬编码系统调用号。请注意函数末尾附近的 mov $__NR_write, %eaxThe 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 余数。


相关:

【讨论】:

    【解决方案2】:

    几件事:

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

    1) int 0x80 是 32b 调用,但您使用的是 64b 寄存器,因此您应该使用 syscall(以及不同的参数)

    2) int 0x80, eax=4 要求ecx 包含存储内容的内存地址,而您在ecx 中给它ASCII 字符= 非法内存访问(第一次调用应该返回错误,即@ 987654327@ 是负值)。或者使用strace &lt;your binary&gt; 应该会显示错误的参数+返回的错误。

    3) 为什么是addq $4, %rsp?对我来说毫无意义,你正在破坏rsp,所以下一个pop rcx 会弹出错误的值,最后你会“向上”运行到堆栈中。

    ...也许还有更多,我没有调试它,这个列表只是通过阅读源代码(所以我什至可能对某些事情有误,尽管这种情况很少见)。

    顺便说一句,您的代码正在运行。它只是没有达到你的预期。但是工作正常,正如 CPU 的设计和您在代码中所写的一样。这是否确实实现了您想要的,或者是否有意义,这是不同的话题,但不要责怪硬件或汇编程序。

    ...我可以快速猜测一下如何修复例程(只是部分 hack-fix,在 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
    

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

    【讨论】:

    • @AyushMishra 当然,我看到您通过addq $48, %rdx 将余数的单个数字转换为ASCII ...顺便说一句,gcc/as 不能编译addq $'0', %rdx 吗?我更喜欢这种风格,以使其“明显”它是快速的单位数字到 ASCII 的东西。但我认为next: 之前的堆栈包含所有正确的 ASCII 数字,只有打印输出不起作用?您可以检查 gdb 也堆栈内存,以查看自己。您仍然应该验证 rsp 是否适合 32b,因此请在 gdb 中运行它。
    • 它不可能是“没有工作”,它一定是在做某事。正如我所说,从 look 解释它在做什么在堆栈内存中。你能至少验证一下,以确保只有打印是错误的吗?然后,您可以代替堆栈将这些数字存储到某个缓冲区中。但是你可能会再次遇到int 0x80 的 32b 与 64b 地址问题。
    • 这不是垃圾,那是栈内存的地址,如果你要在那个内存中打印内容,应该有三个数字存储在那里(5!= 120,所以在栈顶值0x31 == '1'是在地址0x7fffffffddb8: 31 00 00 00 00 00 00 00 处存储为八个字节......继续其他两位数字......32 00 00 00 00 00 00 00 30 00 00 00 00 00 00 00(这就是你在loop: 中产生的push rdx)......问题是,那个地址不是32b值,所以int 0x80 可能无法到达那个0x31
    • 使用正确的 API(syscall 用于 64b 模式)。或者重写代码以仅使用 32b 寄存器,并将其编译为 32b ELF 二进制文件(我认为是-m32)。 Google 搜索的第一个热门是 blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64(我觉得很实用)
    • @Ped7g:我添加了一个带有一个漂亮而干净的函数的答案,该函数通过一个 write() 调用打印整个数字字符串,作为打印整数问题的规范答案的尝试。用小的调整来修复 OP 的代码是可能的,但它很难看。 (顺便说一句,对于常规 64 位二进制文​​件,Linux 总是将 %rsp 置于 4G 以上,而将整个低于 4G 用于静态代码/数据。x32 ABI 在长模式下使用 32 位指针,因此使用 -mx32 构建将让 Linux 在低 4G 中使用 %rsp 启动您的二进制文件。一些系统调用具有特定于 x32 的版本。)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-02-11
    • 1970-01-01
    • 1970-01-01
    • 2013-12-14
    相关资源
    最近更新 更多