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

2024-10-09 09:10:00
admin
原创
212
摘要:问题描述:我编写了一个汇编程序,按照 AT&T 语法显示数字的阶乘。但它不起作用。这是我的代码.text .globl _start _start: movq $5,%rcx movq $5,%rax Repeat: #function to calc...

问题描述:

我编写了一个汇编程序,按照 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 来可视化它,直到循环函数运行正常,但当它进入下一个时,一些随机值开始进入各种寄存器。帮我调试一下,这样它就可以打印阶乘了。


解决方案 1:

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

以下是在 x8-64 Linux 中打印整数的方法,这是一种简单且高效的方法,使用相同的重复除法/以 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 位代码中使用 32 位 ABI。)

// 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  $'
', (%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 '
'.  Or  push $'
'

    mov    $10, %ecx            # same as  mov $10, %rcx  but 2 bytes shorter
    # note that newline (
) 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
" 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
", 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:请参阅为什么 GCC 在实现整数除法时使用奇数乘法?以避免div r64除以 10,因为这非常慢(在 Intel Skylake 上为 21 到 83 个周期)。乘法逆元将使该函数实际上高效,而不仅仅是“某种程度上”。(但当然仍有优化的空间……)



相关:Linux x86-32 扩展精度循环,从每个 32 位“肢体”打印 9 位十进制数字:请参阅我的 Extreme Fibonacci 代码高尔夫答案中的 .toascii_digit:。它针对代码大小进行了优化(即使以速度为代价),但注释得很好。

div它像您一样使用,因为这比使用快速乘法逆元要小)。它loop用于外循环(超过多个整数以扩展精度),同样以速度为代价来减小代码大小。

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


获得高效 asm 的另一种方法是使用 C 编译器。 对于数字循环,查看 gcc 或 clang 为该 C 源生成的内容(这基本上就是 asm 正在做的事情)。Godbolt 编译器资源管理器可以轻松尝试不同的选项和不同的编译器版本。

查看gcc7.2 -O3 asm 输出,它几乎是循环的直接替代品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);
}

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

可能值得将其展开为 2,然后除以 100,并将余数拆分为 2 位数字。如果更简单的版本在mul+shr延迟方面出现瓶颈,那么这将提供更好的指令级并行性。归零的乘法/移位运算链val将只有一半长,并且每个短的独立依赖链中的工作量会更大,以处理 0-99 的余数。


有关的:

  • 这个答案的 NASM 版本,适用于 x86-64 或 i386 Linux,如何在没有来自 c 库的 printf 的情况下在汇编级编程中打印整数?

  • 如何将二进制整数转换为十六进制字符串? - 基数 162 的幂,转换更简单并且不需要div

解决方案 2:

有几件事:

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 的设计一样,也正如你在代码中写的那样。这是否能达到你想要的效果,或者是否有意义,那是另一个话题,但不要责怪硬件或汇编程序。

...我可以快速猜测一下该例程是如何修复的(只是部分破解修复,仍然需要针对syscall64b 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

再次,我没有亲自尝试,只是从头脑中写下来,所以让我知道它是如何改变情况的。

相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   1565  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1354  
  信创国产芯片作为信息技术创新的核心领域,对于推动国家自主可控生态建设具有至关重要的意义。在全球科技竞争日益激烈的背景下,实现信息技术的自主可控,摆脱对国外技术的依赖,已成为保障国家信息安全和产业可持续发展的关键。国产芯片作为信创产业的基石,其发展水平直接影响着整个信创生态的构建与完善。通过不断提升国产芯片的技术实力、产...
国产信创系统   21  
  信创生态建设旨在实现信息技术领域的自主创新和安全可控,涵盖了从硬件到软件的全产业链。随着数字化转型的加速,信创生态建设的重要性日益凸显,它不仅关乎国家的信息安全,更是推动产业升级和经济高质量发展的关键力量。然而,在推进信创生态建设的过程中,面临着诸多复杂且严峻的挑战,需要深入剖析并寻找切实可行的解决方案。技术创新难题技...
信创操作系统   27  
  信创产业作为国家信息技术创新发展的重要领域,对于保障国家信息安全、推动产业升级具有关键意义。而国产芯片作为信创产业的核心基石,其研发进展备受关注。在信创国产芯片的研发征程中,面临着诸多复杂且艰巨的难点,这些难点犹如一道道关卡,阻碍着国产芯片的快速发展。然而,科研人员和相关企业并未退缩,积极探索并提出了一系列切实可行的解...
国产化替代产品目录   28  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用