繁体   English   中英

如何在没有 c 库中的 printf 的情况下在汇编级编程中打印整数?

[英]How do I print an integer in Assembly Level Programming without printf from the c library?

谁能告诉我以十进制格式在寄存器中显示值的纯汇编代码? 请不要建议使用 printf hack 然后用 gcc 编译。

描述:

好吧,我用 NASM 做了一些研究和一些实验,并认为我可以使用 c 库中的 printf 函数来打印一个整数。 我通过使用 GCC 编译器编译目标文件来做到这一点,并且一切正常。

但是,我想要实现的是以十进制形式打印存储在任何寄存器中的值。

我做了一些研究,发现 DOS 命令行的中断向量 021h 可以显示字符串和字符,而 2 或 9 在 ah 寄存器中,数据在 dx 中。

结论:

我发现的所有示例都没有展示如何在不使用 C 库的 printf 的情况下以十进制形式显示寄存器的内容值。 有谁知道如何在组装中做到这一点?

您需要编写一个二进制到十进制转换例程,然后使用十进制数字生成“数字字符”进行打印。

您必须假设某处某处会在您选择的输出设备上打印一个字符。 调用这个子程序“print_character”; 假设它在 EAX 中使用字符代码并保留所有寄存器..(如果您没有这样的子例程,则您有一个额外的问题,应该是另一个问题的基础)。

如果您在寄存器(例如 EAX)中有一个数字的二进制代码(例如,0-9 的值),您可以通过添加“零”字符的 ASCII 代码将该值转换为该数字的字符到登记册。 这很简单:

       add     eax, 0x30    ; convert digit in EAX to corresponding character digit

然后您可以调用 print_character 来打印数字字符代码。

要输出任意值,您需要挑选数字并打印它们。

挑选数字从根本上需要使用十的幂。 最容易使用 10 的幂,例如 10 本身。 想象一下,我们有一个除以 10 的例程,它在 EAX 中取一个值,并在 EDX 中产生一个商,在 EAX 中产生一个余数。 我把它作为一个练习,让你弄清楚如何实现这样的例程。

然后一个带有正确想法的简单例程是为该值可能具有的所有数字生成一个数字。 32 位寄存器可存储 40 亿个值,因此您可能会打印 10 位数字。 所以:

         mov    eax, valuetoprint
         mov    ecx, 10        ;  digit count to produce
loop:    call   dividebyten
         add    eax, 0x30
         call   printcharacter
         mov    eax, edx
         dec    ecx
         jne    loop

这有效......但以相反的顺序打印数字。 哎呀! 好吧,我们可以利用下推堆栈来存储产生的数字,然后以相反的顺序弹出它们:

         mov    eax, valuetoprint
         mov    ecx, 10        ;  digit count to generate
loop1:   call   dividebyten
         add    eax, 0x30
         push   eax
         mov    eax, edx
         dec    ecx
         jne    loop1
         mov    ecx, 10        ;  digit count to print
loop2:   pop    eax
         call   printcharacter
         dec    ecx
         jne    loop2

作为练习留给读者:抑制前导零。 此外,由于我们将数字字符写入内存,因此我们可以将它们写入缓冲区,然后打印缓冲区内容,而不是将它们写入堆栈。 也留给读者作为练习。

您需要手动将二进制整数转换为 ASCII 十进制数字的字符串/数组。 ASCII 数字由 1 字节整数表示,范围为'0' (0x30)到'9' (0x39)。 http://www.asciitable.com/

对于像十六进制这样的 2 的幂,请参阅如何将二进制整数转换为十六进制字符串? 在二进制和 2 的幂数之间进行转换允许进行更多的优化和简化,因为每组位分别映射到一个十六进制/八进制数字。


大多数操作系统/环境没有接受整数并将它们转换为十进制的系统调用。 在将字节发送到操作系统之前,您必须自己执行此操作,或者自己将它们复制到显存中,或者在显存中绘制相应的字体字形...

到目前为止,最有效的方法是进行一次系统调用,一次执行整个字符串,因为写入 8 个字节的系统调用与写入 1 个字节的成本基本相同。

这意味着我们需要一个缓冲区,但这根本不会增加我们的复杂性。 2^32-1 只是 4294967295,只有 10 位十进制数字。 我们的缓冲区不需要很大,所以我们可以使用堆栈。

通常的算法产生数字 LSD-first(最低有效数字在前)。 由于打印顺序是 MSD 优先,我们可以从缓冲区的末尾开始,然后向后工作 对于其他地方的打印或复制,只需跟踪它的开始位置,而不必费心将其置于固定缓冲区的开始处。 无需使用 push/pop 来反转任何东西,只需首先向后生成它。

char *itoa_end(unsigned long val, char *p_end) {
  const unsigned base = 10;
  char *p = p_end;
  do {
    *--p = (val % base) + '0';
    val /= base;
  } while(val);                  // runs at least once to print '0' for val=0.

  // write(1, p,  p_end-p);
  return p;  // let the caller know where the leading digit is
}

gcc/clang 做得非常好, 使用魔术常数乘法器而不是div地除以 10。 (用于 asm 输出的Godbolt 编译器资源管理器)。

这个代码审查问答有一个很好的高效 NASM 版本,它将字符串累积到一个 8 字节的寄存器而不是内存中,准备好存储你希望字符串开始而不需要额外复制的地方。


处理有符号整数:

在无符号绝对值上使用此算法。 ( if(val<0) val=-val; )。 如果原始输入为负,则在完成后在末尾添加一个'-' 例如, -10使用10运行它,产生 2 个 ASCII 字节。 然后在前面存储一个'-' ,作为字符串的第三个字节。


这是一个简单的注释 NASM 版本,对 32 位无符号整数和 Linux write系统调用使用div (慢但较短的代码)。 只需将寄存器更改为ecx而不是rcx可以轻松将其移植到 32 位模式代码 但是add rsp,24将变成add esp, 20因为push ecx只有 4 个字节,而不是 8 个。(您还应该保存/恢复esi以用于通常的 32 位调用约定,除非您将其设置为宏或内部- 仅使用功能。)

系统调用部分特定于 64 位 Linux。 将其替换为适合您系统的任何内容,例如调用 VDSO 页面以在 32 位 Linux 上进行高效系统调用,或者直接使用int 0x80进行低效系统调用。 请参阅Unix/Linux 上 32 位和 64 位系统调用的调用约定

如果您只需要字符串而不打印它rsi指向离开循环后的第一个数字。 您可以将它从 tmp 缓冲区复制到您真正需要它的任何地方的开头。 或者,如果您直接将其生成到最终目的地(例如,传递一个指针 arg),您可以填充前导零,直到到达您为它留下的空间的前面。 没有简单的方法可以在开始之前找出它将是多少位数字,除非您总是用零填充到固定宽度。

ALIGN 16
; void print_uint32(uint32_t edi)
; x86-64 System V calling convention.  Clobbers RSI, RCX, RDX, RAX.
global print_uint32
print_uint32:
    mov    eax, edi              ; function arg

    mov    ecx, 0xa              ; base 10
    push   rcx                   ; newline = 0xa = base
    mov    rsi, rsp
    sub    rsp, 16               ; not needed on 64-bit Linux, the red-zone is big enough.  Change the LEA below if you remove this.

;;; rsi is pointing at '\n' on the stack, with 16B of "allocated" space below that.
.toascii_digit:                ; do {
    xor    edx, edx
    div    ecx                   ; edx=remainder = low digit = 0..9.  eax/=10
                                 ;; DIV IS SLOW.  use a multiplicative inverse if performance is relevant.
    add    edx, '0'
    dec    rsi                 ; store digits in MSD-first printing order, working backwards from the end of the string
    mov    [rsi], dl

    test   eax,eax             ; } while(x);
    jnz  .toascii_digit
;;; rsi points to the first digit


    mov    eax, 1               ; __NR_write from /usr/include/asm/unistd_64.h
    mov    edi, 1               ; fd = STDOUT_FILENO
    lea    edx, [rsp+16 + 1]    ; yes, it's safe to truncate pointers before subtracting to find length.
    sub    edx, esi             ; length=end-start, including the \n
    syscall                     ; write(1, string,  digits + 1)

    add  rsp, 24                ; (in 32-bit: add esp,20) undo the push and the buffer reservation
    ret

公共区域。 随意将其复制/粘贴到您正在处理的任何内容中。 如果它坏了,你可以保留两块。 (如果性能很重要,请参阅下面的链接;您需要乘法逆而不是div 。)

这是在循环中调用它的代码,它倒计时到 0(包括 0)。 把它放在同一个文件中很方便。

ALIGN 16
global _start
_start:
    mov    ebx, 100
.repeat:
    lea    edi, [rbx + 0]      ; put +whatever constant you want here.
    call   print_uint32
    dec    ebx
    jge   .repeat


    xor    edi, edi
    mov    eax, 231
    syscall                             ; sys_exit_group(0)

组装和链接

yasm -felf64 -Worphan-labels -gdwarf2 print-integer.asm &&
ld -o print-integer print-integer.o

./print_integer
100
99
...
1
0

使用strace查看该程序进行的唯一系统调用是write()exit() (另请参阅标签 wiki 底部的 gdb / 调试提示,以及那里的其他链接。)


相关

无法发表评论,所以我以这种方式回复。 @Ira Baxter,完美的答案我只想补充一点,当您发布将寄存器 cx 设置为值 10 时,您不需要除以 10 次。只需将数字除以 ax 直到“ax==0”

loop1: call dividebyten
       ...
       cmp ax,0
       jnz loop1

您还必须存储原始数字中有多少位数字。

       mov cx,0
loop1: call dividebyten
       inc cx

无论如何,您 Ira Baxter 帮助了我,只有几种方法可以优化代码:)

这不仅与优化有关,而且与格式有关。 当你想打印数字 54 时,你想打印 54 而不是 0000000054 :)

1 -9 是 1 -9。 在那之后,一定有一些我也不知道的转换。 假设您在 AX (EAX) 中有一个 41H,并且您想打印 65,而不是 'A' 而不进行一些服务呼叫。 我认为您需要打印 6 和 5 的字符表示,无论可能是什么。 必须有一个可以添加到那里的常数。 您需要一个模数运算符(但是您在汇编中这样做)并循环所有数字。

不确定,但这是我的猜测。

我想你想把值打印到标准输出? 如果是这种情况
你必须使用系统调用来做到这一点。 系统调用依赖于操作系统。

例如 Linux: Linux 系统调用表

教程中的 hello world 程序可能会给您一些见解。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM