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

Kau*_*der 16 x86 assembly nasm cpu-registers

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

描述:

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

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

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

结论:

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

Ira*_*ter 15

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

你必须假设某个地方的东西会在你选择的输出设备上打印一个字符.调用这个子程序"print_character"; 假设它在EAX中需要一个字符代码并保留所有寄存器..(如果你没有这样的子程序,你还有一个额外的问题应该是另一个问题的基础).

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

       add     eax, 0x30    ; convert digit in EAX to corresponding character digit
Run Code Online (Sandbox Code Playgroud)

然后,您可以调用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
Run Code Online (Sandbox Code Playgroud)

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

         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
Run Code Online (Sandbox Code Playgroud)

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


Pet*_*des 9

您需要手动将二进制整数转换为 ASCII 十进制数字的字符串/数组。 ASCII 数字由'0'(0x30) 到'9'(0x39)范围内的 1 字节整数表示。 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
}
Run Code Online (Sandbox Code Playgroud)

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

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


处理有符号整数:

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


这是一个简单的注释 NASM 版本,使用div(缓慢但较短的代码)用于 32 位无符号整数和 Linuxwrite系统调用。 它应该很容易端口这32位模式的代码只是通过改变寄存器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 位系统调用的调用约定

如果您只需要字符串而不打印它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
Run Code Online (Sandbox Code Playgroud)

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

组装和链接

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

./print_integer
100
99
...
1
0
Run Code Online (Sandbox Code Playgroud)

使用strace地看到,只有系统调用这个程序可以是write()exit()。(另请参阅标签 wiki底部的 gdb / 调试提示,以及那里的其他链接。)


相关


Nic*_*tti 0

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

例如Linux: Linux系统调用表

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


归档时间:

查看次数:

33645 次

最近记录:

6 年,1 月 前