如何在没有来自 c 库的 printf 的情况下在汇编级编程中打印整数?
Posted
技术标签:
【中文标题】如何在没有来自 c 库的 printf 的情况下在汇编级编程中打印整数?【英文标题】:How do I print an integer in Assembly Level Programming without printf from the c library? 【发布时间】:2021-07-21 03:52:37 【问题描述】:谁能告诉我以十进制格式在寄存器中显示值的纯汇编代码?请不要建议使用 printf hack,然后使用 gcc 编译。
说明:
好吧,我对 NASM 进行了一些研究和实验,并认为我可以使用 c 库中的 printf 函数来打印一个整数。我通过使用 GCC 编译器编译目标文件来做到这一点,并且一切正常。
但是,我想要实现的是以十进制形式打印存储在任何寄存器中的值。
我做了一些研究,发现 DOS 命令行的中断向量 021h 可以显示字符串和字符,而 2 或 9 在 ah 寄存器中,数据在 dx 中。
结论:
我找到的示例都没有显示如何在不使用 C 库的 printf 的情况下以十进制形式显示寄存器的内容值。有谁知道如何在汇编中做到这一点?
【问题讨论】:
是什么号码?浮点数? 为简单起见,我们假设它是一个无符号整数。假设我在 dh 中有 00000101h 我如何才能显示 5?假设我在 dh 中有 00000111h 我如何才能显示 7? 我在 Windows 7 (x86) 上使用 NASM,我使用的是默认的“com”输出格式! 一个 DOS 16 位版本:***.com/questions/4244624/… Outputting integers in assembly on Linux的可能重复 【参考方案1】:我想您想将值打印到标准输出?如果是这样 你必须使用system call 来这样做。系统调用取决于操作系统。
例如Linux: Linux System Call Table
Tutorial 中的 hello world 程序可能会给您一些见解。
【讨论】:
感谢您的建议!我目前在 Windows 7 (x86) 上工作!必须通过 ALP 考试,并且必须在实验室的 Win 环境中组装代码!不过会看教程!非常感激! :)【参考方案2】:你需要编写一个二进制到十进制的转换例程,然后用十进制数字产生“数字字符”来打印。
您必须假设某处某处会在您选择的输出设备上打印一个字符。调用这个子程序“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
留给读者作为练习:抑制前导零。此外,由于我们将数字字符写入内存,而不是将它们写入堆栈,我们可以将它们写入缓冲区,然后打印缓冲区内容。也留给读者作为练习。
【讨论】:
这真的比call _printf
快吗?
@XStylish:可能:如果 printf 你的意思是一个接受格式字符串并格式化十进制数字的方法,当然,因为 printf 例程必须解释格式字符串并产生数字,这只会产生数字。如果您打算为屏幕生成输出,速度可能并不重要,因为人们阅读速度很慢。如果您正在将字符串写入文件,您可能希望乘以“.1”并取分形 aprase - 而不是除以 10。
... 应该是“乘以 0.1 的定点值并取小数部分而不是除以 10,以提高转换速度。”【参考方案3】:
无法发表评论,所以我以这种方式发布回复。 @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 :)
【讨论】:
【参考方案4】:1 -9 是 1 -9。在那之后,一定有一些我也不知道的转换。假设您在 AX (EAX) 中有一个 41H,并且您想在不进行一些服务调用的情况下打印 65,而不是“A”。我认为你需要打印一个 6 和 5 的字符表示,不管它是什么。必须有一个可以添加的常数才能到达那里。您需要一个模数运算符(但是您在汇编中这样做)并循环所有数字。
不确定,但这是我的猜测。
【讨论】:
是的,没错。在ASCII中,'0'
到'9'
的字符编码是连续的,所以你可以计算6 + '0'
得到'6'
。即使用div
或其他任何方法来获得余数,然后使用add edx, '0'
并将该字节存储到缓冲区中。 '0'
= 0x30
,但大多数汇编程序都接受字符常量,因此以这种方式编写代码更清晰。 (OR
或 AND
代替 ADD
/ SUB
可能很有用,因为 0x30
没有设置任何低 4 位。)【参考方案5】:
您需要手动将二进制整数转换为 ASCII 十进制数字的字符串/数组。 ASCII 数字由 '0'
(0x30) 到 '9'
范围内的 1 字节整数表示(0x39)。 http://www.asciitable.com/
对于像十六进制这样的 2 次方基数,请参阅How to convert a binary integer number to a hex string? 在二进制和 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 做得很好,using a magic constant multiplier 而不是 div
有效地除以 10。 (Godbolt compiler explorer 用于 asm 输出)。
这个code-review Q&A 有一个非常高效的 NASM 版本,它将字符串累积到一个 8 字节的寄存器而不是内存中,准备好存储您希望字符串开始的位置而无需额外复制。
处理有符号整数:
对无符号绝对值使用此算法。 (if(val<0) val=-val;
)。如果原始输入是否定的,请在完成后将'-'
粘贴在最后。例如,-10
使用10
运行它,产生 2 个 ASCII 字节。然后在前面存储一个'-'
,作为字符串的第三个字节。
这是一个简单的注释 NASM 版本,使用 div
(慢但较短的代码)用于 32 位无符号整数和 Linux write
系统调用。 应该很容易将其移植到 32 位模式代码,只需将寄存器更改为 ecx
而不是 rcx
。但是add rsp,24
将变为add esp, 20
,因为push ecx
只有4 个字节,而不是8 个字节。(对于通常的32 位调用约定,您还应该保存/恢复esi
,除非您将其制作成宏或仅供内部使用的功能。)
系统调用部分特定于 64 位 Linux。将其替换为适合您系统的任何内容,例如在 32 位 Linux 上调用 VDSO 页面进行高效的系统调用,或者直接使用int 0x80
进行低效的系统调用。见calling conventions for 32 and 64-bit system calls on Unix/Linux。或者在另一个问题上查看rkhb's answer,了解以相同方式工作的 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 / 调试提示,以及那里的其他链接。)
相关:
printf
- How to print a number in assembly NASM? 有 x86-64 和 i386 答案。
NASM Assembly convert input to integer? 是另一个方向,string->int。
Printing an integer as a string with AT&T syntax, with Linux system calls instead of printf - AT&T 版本 相同的东西(但对于 64 位整数)。请参阅有关性能的更多 cmets,以及 div
与使用 mul
的编译器生成的代码的基准。
Add 2 numbers and print the result using Assembly x86 32 位版本,与此非常相似。
此code-review Q&A 使用乘法逆运算,并将字符串累积到一个 8 字节寄存器而不是内存中,准备好存储您希望字符串开始的位置而无需额外复制。
How to convert a binary integer number to a hex string? - 2 次方基数是特殊的。答案包括标量循环(分支和查表)和 SIMD(SSE2、SSSE3、AVX2 和 AVX512,这非常棒。)
How to print integers really fast 博客文章比较了 C 中的一些策略。
例如x % 100
来创建更多 ILP(指令级并行性),以及查找表或更简单的乘法逆运算(只需要在有限的范围内工作,例如在 this answer 中)来分解 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类似的文章。
【讨论】:
以上是关于如何在没有来自 c 库的 printf 的情况下在汇编级编程中打印整数?的主要内容,如果未能解决你的问题,请参考以下文章
如何在没有任何 vue 库的情况下在 vue 回调中获取 http 响应标头(axios)
如何在没有外部库的情况下在 React 中实现 Google Maps JS API?
在没有 JS 库的情况下在 <canvas> 上为 spritesheet 设置动画 [重复]