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

问题描述 投票:15回答:5

任何人都可以告诉我纯粹的汇编代码以十进制格式显示寄存器中的值吗?请不要建议使用printf hack,然后使用gcc进行编译。

描述:

好吧,我做了一些研究和NASM的一些实验,并认为我可以使用c库中的printf函数来打印整数。我是通过使用GCC编译器编译目标文件来完成的,所有工作都很公平。

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

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

结论:

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

assembly x86 nasm cpu-registers
5个回答
13
投票

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

你必须假设某个地方的东西会在你选择的输出设备上打印一个字符。调用这个子程序“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

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


3
投票

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

对于像hex这样的2次幂基数,请参阅How to convert a number to hex?二进制和2次幂之间的转换允许更多的优化和简化,因为每组位分别映射到十六进制/八进制数字。


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

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

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

通常的算法产生数字LSD优先(最低有效数字优先)。由于打印顺序是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做得很好,using a magic constant multiplier而不是div有效地除以10。 (Godbolt compiler explorer为asm输出)。


To handle signed integers:

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


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

系统调用部分特定于64位Linux。将其替换为适合您系统的任何内容,例如:调用VDSO页面以在32位Linux上进行高效的系统调用,或者直接使用int 0x80进行低效的系统调用。见calling conventions for 32 and 64-bit system calls on Unix/Linux

如果你只是需要字符串而不打印它,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, 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

公共区域。随意将其复制/粘贴到您正在处理的任何内容中。如果它破了,你可以保留两件。

这里的代码在循环中调用它,倒数为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 /调试提示,以及其他链接。)


我发布了64位整数的AT&T语法版本作为Printing an integer as a string with AT&T syntax, with Linux system calls instead of printf的答案。有关性能的更多评论,以及div与使用mul编译器生成的代码的基准测试,请参阅此内容。


相关:NASM Assembly convert input to integer?是另一个方向。


1
投票

无法发表评论,所以我这样发帖回复。 @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
投票

我想你想把价值打印到stdout?如果是这样的话 你必须使用system call这样做。系统调用取决于操作系统。

例如Linux:Linux System Call Table

这个Tutorial的hello world程序可能会给你一些见解。


-1
投票

1-9是1-9。在那之后,必定会有一些我不知道的转换。假设您在AX(EAX)中有41H,并且您想要打印65,而不是在没有进行某些服务呼叫的情况下打印'A'。我认为您需要打印6和5的字符表示形式。必须有一个常数才能到达那里。您需要一个模数运算符(但是您在汇编中执行此操作)并循环所有数字。

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

© www.soinside.com 2019 - 2024. All rights reserved.