assembly 如何在汇编级编程中打印整数而不从C库中打印?(itoa,整数到十进制ASCII字符串)

rnmwe5a2  于 2022-12-13  发布在  其他
关注(0)|答案(5)|浏览(120)

有人能告诉我用十进制格式显示寄存器中的值的 * 纯汇编 * 代码吗?请不要建议使用printf hack,然后用gcc编译。
说明:
我对NASM做了一些研究和实验,发现我可以使用c库中的printf函数打印一个整数。我用愚者编译器编译了目标文件,一切都很正常。
但是,我想要实现的是以十进制形式打印存储在任何寄存器中的值。
我做了一些研究,发现DOS命令行的中断向量021h可以显示字符串和字符,而2或9在ah寄存器中,数据在dx中。
结论:
我找到的例子中没有一个展示了如何在不使用C库的printf的情况下以十进制形式显示寄存器的内容值。有人知道如何在汇编中做到这一点吗?

nnsrf1az

nnsrf1az1#

你需要编写一个二进制到十进制的转换例程,然后用十进制的位数产生“数字字符”来打印。
你必须假设某个东西,某个地方,将在你选择的输出设备上打印一个字符。假设它采用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

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

l7wslrjt

l7wslrjt2#

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

对于2的幂基数(如十六进制),请参见How to convert a binary integer number to a hex string?在二进制和2的幂基数之间转换允许更多优化和简化,因为每组位分别Map到一个十六进制/八进制数字。
大多数操作系统/环境都没有接受整数并将其转换为十进制的系统调用。在将字节发送到操作系统之前,或者自己将其复制到显存,或者在显存中绘制相应的字体字形之前,您必须自己完成这些操作。
到目前为止,最有效的方法是进行一次处理整个字符串的单个系统调用,因为写入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编译器资源管理器用于asm输出)。
这个code-review Q&A有一个很好的高效的NASM版本,它将字符串累积到一个8字节的寄存器中,而不是存储到内存中,准备存储在您希望字符串开始的位置,而无需额外的复制。

要处理有符号整数:

对无符号绝对值(if(val<0) val=-val;)使用此算法。如果原始输入为负数,则在完成后在末尾添加一个'-'。例如,-1010运行此算法,生成2个ASCII字节。然后在前面存储一个'-',作为字符串的第三个字节。
这里有一个简单的注解版NASM,使用div(速度慢但代码较短)。只需将寄存器更改为ecx而不是rcx,就可以轻松地将其移植到32位模式代码中。但是add rsp,24将变成add esp, 20,因为push ecx只有4个字节,而不是8个字节。(您还应该保存/恢复esi,以符合通常的32位调用约定,除非您要将其制作成宏或仅供内部使用的函数。)
system-call部分是64位Linux特有的。用任何适合您系统的内容替换它,例如,调用VDSO页面以在32位Linux上进行有效的系统调用,或者直接使用int 0x80以进行低效的系统调用。请参见calling conventions for 32 and 64-bit system calls on Unix/Linux。或者,请参见rkhb对另一个问题的回答,以了解以同样方式工作的32位int 0x80版本。

如果您只需要字符串而不打印它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()。(另请参见x86标记wiki底部的gdb / debugging提示,以及那里的其他链接。)

相关
*这个的32比特版本,使用int 0x80做为结尾的write系统呼叫。几乎是相同的循环。
*使用printf-如何在组件NASM中打印数字?有x86-64和i386答案。

  • NASM汇编将输入转换为整数?是另一个方向,string-〉int

  • 使用AT&T语法将整数打印为字符串,使用Linux系统调用而不是printf -AT&T版本(但针对64位整数)。请参阅该文章以了解更多关于性能的评论,以及div与使用mul的编译器生成代码的基准测试。

  • 将2个数字相加,并使用与此非常相似的汇编x86 32位版本打印结果。

  • 这个code-review Q&A使用乘法逆运算,将字符串累加到一个8字节的寄存器中,而不是存储到内存中,准备存储在您希望字符串开始的位置,而不需要额外的复制。

  • How to convert a binary integer number to a hex string?-2的幂基是特殊的。答案包括标量循环(分支和表查找)和SIMD(SSE 2、SSSE 3、AVX 2和AVX 512,这是令人惊讶的。)

  • How to print integers really fast博客文章比较了C语言中的一些策略。例如,x % 100可以创建更多的ILP(指令级并行),或者使用查找表或更简单的乘法逆运算(只需在有限范围内工作,如本答案所示)将0..99余数分解为2个十进制数字。

例如,使用imul r,r,imm8/shr r,10(x * 103) >> 10进行运算,如另一个答案所示。

  • 类似的文章。
rjee0c15

rjee0c153#

我想你想把这个值输出到stdout
必须使用system call来执行此操作。系统调用依赖于操作系统。
例如Linux:Linux System Call Table
这个Tutorial中的hello world程序可能会给予你一些见解。

p8ekf7hl

p8ekf7hl4#

无法发表评论,所以我以这种方式发布回复。@伊拉巴克斯特,完美的答案,我只想补充一点,你不需要除以10倍,因为你发布了你将寄存器cx设置为值10。只需除以数字,直到“ax==0”

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

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

mov cx,0
loop1: call dividebyten
       inc cx

无论如何,你伊拉巴克斯特帮助了我,只是有几种方法如何优化代码:)
这不仅是关于优化,而且是格式化。当你想打印数字54时,你想打印54而不是0000000054:)

juud5qan

juud5qan5#

1 - 9是1 - 9,在这之后,一定有一些我也不知道的转换,假设你有一个AX中的41 H(EAX),并且您希望打印65,而不是“A”,不做一些服务调用。我认为你需要打印一个6和5的字符表示。必须有一个常数,可以添加到那里。您需要一个模数运算符(无论在汇编中如何操作),并对所有数字执行循环。
不确定,但这是我的猜测。

相关问题