描述:
我做了一些研究和NASM实验,发现可以使用c库中的printf函数打印整数。我通过使用GCC编译器编译目标文件来实现这个目的,一切都还好。
然而,我想实现的是以十进制形式打印任何寄存器中存储的值。
我做了一些研究,并发现DOS命令行的中断向量021h可以在ah寄存器中为2或9时显示字符串和字符,而数据则在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的程序,它将EAX中的值取出,并在EDX中产生商,在EAX中产生余数。如何实现这样的程序,我把它留给你来练习。
然后,一个正确思路的简单程序是为值可能具有的所有数字产生一个数字。32位寄存器可存储4亿个值,因此您可以打印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
留给读者的练习:消除前导零。另外,由于我们要将数字字符写入内存,而不是写入堆栈,因此我们可以将它们写入缓冲区,然后打印缓冲区内容。同样留给读者的练习。
call _printf
真的更快吗? - Beyondo你需要手动将二进制整数转换为ASCII十进制数字的字符串/数组。 ASCII数字由范围在'0'
(0x30)到'9'
(0x39)之间的1字节整数表示。http://www.asciitable.com/
对于像十六进制这样的2的幂基数,请参见如何将二进制整数转换为十六进制字符串? 在二进制和2的幂基数之间转换可以进行更多的优化和简化,因为每组位独立地映射到一个十六进制/八进制数字。
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
}
div
以实现高效的除以10操作。(可使用Godbolt编译器浏览器查看汇编输出)。在无符号绝对值上使用此算法(if(val<0) val=-val;
)。如果原始输入为负数,则在完成后在最前面添加一个'-'
。例如,-10
使用10
运行此算法,生成2个ASCII字节。然后将'-'
存储为字符串的第三个字节。
div
(慢但代码更短)适用于32位无符号整数和Linux write
系统调用。只需通过将寄存器从rcx
改为ecx
就可以轻松将其移植到32位模式代码。但是add rsp,24
将变成add esp,20
,因为push ecx
仅为4个字节,而不是8个字节。(除非你将其制作成宏或仅供内部使用的函数,否则应保存/恢复esi
以遵循通常的32位调用约定。)int 0x80
进行低效的系统调用。请参见Unix/Linux上32位和64位系统调用的调用约定。或者查看另一个问题的rkhb回答,其中包含一个32位int 0x80
版本,其工作方式相同。rsi
会指向离开循环后的第一个数字。你可以将它从tmp缓冲区复制到实际需要的位置的开头。或者,如果你直接将其生成到最终目标中(例如传递指针参数),你可以在达到留给它的空间前面时用前导零填充。除非你总是用零填充到固定宽度,否则没有简单的方法在开始之前找出它将有多少位数。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
。)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标签维基底部的gdb /调试提示以及其他链接。)
相关:
{{link1:使用int 0x80
调用write
系统调用的32位版本,基本上是相同的循环。
使用printf
- 如何在汇编语言NASM中打印数字?有x86-64和i386的答案。
NASM Assembly将输入转换为整数?是另一个方向,字符串->整数。
使用AT&T语法将整数作为字符串打印,使用Linux系统调用而不是printf - AT&T版本的相同内容(但针对64位整数)。请参阅有关性能的更多注释以及使用mul
的编译器生成代码与div
的基准测试。
使用Assembly x86添加2个数字并打印结果非常类似于此的32位版本。
此代码审查Q&A使用乘法逆元,并将字符串累加到8字节寄存器中而不是内存中,准备好将其存储在您想要的字符串起始位置,无需额外复制。
如何将二进制整数转换为十六进制字符串? - 2的幂基数是特殊的。答案包括标量循环(分支和查找表)和SIMD(SSE2、SSSE3、AVX2和AVX512,这对于此非常惊人)。
如何快速打印整数在C中比较一些策略的博客文章。例如x%100
以创建更多的ILP(指令级并行性),并且可以使用查找表或更简单的乘法逆元(仅需要在有限范围内工作,如此答案中所示)将0..99余数分解为2个十进制数字。
例如,使用(x * 103) >> 10
使用一个imul r,r,imm8
/ shr r,10
,如另一个答案所示。可能要将其折叠到余数计算本身中。
https://tia.mat.br/posts/2014/06/23/integer_to_string_conversion.html一篇类似的文章。
无法评论,所以我用回复的方式发表。 @Ira Baxter,完美的答案。我只想补充一点,您不需要像您发布的那样将寄存器cx设置为值10并进行10次除法。只需将ax中的数字除以2,直到"ax==0"即可。
loop1: call dividebyten
...
cmp ax,0
jnz loop1
你还需要存储原始数字中有多少位数字。
mov cx,0
loop1: call dividebyten
inc cx
无论如何,Ira Baxter帮了我,有几种方法可以优化代码 :)
这不仅涉及优化,还涉及格式。当您想打印数字54时,您想要打印54而不是0000000054 :)
我猜你想要将值打印到stdout吧?如果是这种情况,那么你需要使用系统调用来实现。系统调用是依赖于操作系统的。
例如,在Linux下: Linux系统调用列表
本教程中的Hello World程序可能会帮助你获得一些见解。
1-9是1-9。之后,必须进行某些我也不知道的转换。假设你在AX(EAX)中有一个41H,并且你想打印一个65,而不是 'A',而不需要执行一些服务调用。我认为你需要打印一个字符表示6和5,无论那是什么。一定有一个常数可以添加到那里。你需要一个模运算符(无论你在汇编中如何执行),并循环所有数字。
不确定,但这是我的猜测。