25

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

描述:

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

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

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

结论:

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

4

5 回答 5

19

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

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

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

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

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

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

从根本上说,挑选数字需要使用 10 的幂。最容易使用 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

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

于 2012-10-31T19:53:19.213 回答
11

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

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


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

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

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

通常的算法产生数字 LSD-first(Least Significant Digit 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;)。如果原始输入是否定'-'的,请在完成后将 a 放在最后。例如,-10使用 运行它10,产生 2 个 ASCII 字节。然后将 a 存储'-'在前面,作为字符串的第三个字节。


这是一个简单的注释 NASM 版本,div对 32 位无符号整数和 Linuxwrite系统调用使用(慢但更短的代码)。 只需将寄存器更改为ecx不是rcx. 但是add rsp,24会变成add esp, 20因为push ecx只有 4 个字节,而不是 8 个。(您还应该esi为通常的 32 位调用约定保存/恢复,除非您将其变成宏或仅供内部使用的函数。)

系统调用部分特定于 64 位 Linux。将其替换为适合您系统的任何内容,例如调用 VDSO 页面以在 32 位 Linux 上进行高效的系统调用,或int 0x80直接用于低效的系统调用。请参阅Unix/Linux 上 32 位和 64 位系统调用的调用约定。或者查看rkhb 对另一个问题的回答int 0x80,了解以相同方式工作的32 位版本。

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

ALIGN 16
; void print_uint32(uint32_t edi)
; x86-64 System V calling convention.  Clobbers RSI, RCX, RDX, RAX.
; optimized for simplicity and compactness, not speed (DIV is slow)
global print_uint32
print_uint32:
    mov    eax, edi              ; function arg

    mov    ecx, 0xa              ; base 10
    push   rcx                   ; ASCII newline '\n' = 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
    ; pointer already in RSI    ; buf = last digit stored = most significant
    lea    edx, [rsp+16 + 1]    ; yes, it's safe to truncate pointers before subtracting to find length.
    sub    edx, esi             ; RDX = length = end-start, including the \n
    syscall                     ; write(1, string /*RSI*/,  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 / 调试提示,以及那里的其他链接。)


相关

于 2017-09-19T13:33:42.460 回答
0

我想您想将值打印到标准输出?如果是这种情况
,您必须使用系统调用来执行此操作。系统调用取决于操作系统。

例如 Linux: Linux 系统调用表

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

于 2012-10-31T19:49:35.577 回答
0

无法发表评论,所以我以这种方式发布回复。@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 :)

于 2015-03-11T13:31:05.880 回答
0

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

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

于 2017-09-19T03:22:44.420 回答