为了账号安全,请及时绑定邮箱和手机立即绑定

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

/ 猿问

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

慕仰8121524 2019-12-06 15:24:48

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


查看完整描述

2 回答

?
qq_笑_17

几件事:


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


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


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


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


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


顺便说一句,您的代码正在工作。它只是没有达到您的预期。但是,正如CPU的设计和代码中的编写一样,它可以很好地工作。无论是实现了您想要的目标还是有意义的目标,这都是不同的话题,但是不要怪怪硬件或汇编程序。


...我可以快速猜测一下例程可能是如何修复的(只是部分hack-fix,仍然需要syscall在64b linux下重写):


  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

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


查看完整回答
反对 回复 2019-12-06
?
至尊宝的传说

正如@ 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 <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:在我的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的余数。


查看完整回答
反对 回复 2019-12-06

添加回答

回复

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信