如何在汇编语言NASM中打印数字?

28
假设我在一个寄存器中有一个整数,如何打印它?你能展示一个简单的代码示例吗?
我已经知道如何打印字符串,比如“hello, world”。
我正在Linux上开发。

请指定程序将运行的操作系统。 - Alexey Frunze
3
相关:在堆栈上将整数转换为ASCII十进制字符串,并使用Linux write系统调用打印,不使用printf或任何其他函数,包含注释和说明。(译文):本篇讨论如何在堆栈上将整数转换为ASCII十进制字符串,并使用Linux write系统调用打印,而不使用printf或其他函数。其中还包括注释和说明。 - Peter Cordes
6个回答

24
如果你已经使用Linux操作系统,那么就不需要自己进行转换了。只需使用printf即可:
;
; assemble and link with:
; nasm -f elf printf-test.asm && gcc -m32 -o printf-test printf-test.o
;
section .text
global main
extern printf

main:

  mov eax, 0xDEADBEEF
  push eax
  push message
  call printf
  add esp, 8
  ret

message db "Register = %08X", 10, 0

请注意,printf 使用 cdecl 调用约定,因此我们需要在函数调用后还原堆栈指针,即每传递一个参数需要添加 4 字节。


谢谢,这似乎是我要找的东西。您知道它是否也适用于Mac OS X吗? - AR89
如何在64位编译它? - Figen Güngör
1
2021年更新:如果要使用call printf而不是call printf@plt,并且使用绝对地址作为立即数,而不是位置无关的,则可能需要gcc -m32 -no-pie,或者至少这是一个好主意。但在实践中,对于32位代码,通常可以轻松解决。 - Peter Cordes

17

您需要将它转换为字符串;如果您要处理十六进制数字,那非常容易。任何数字都可以用这种方式表示:

0xa31f = 0xf * 16^0 + 0x1 * 16^1 + 3 * 16^2 + 0xa * 16^3

所以当你有这个数字时,你需要像我展示的那样将其分割,然后将每个“部分”转换为其ASCII等效项。
通过一些位运算很容易得到四个部分,特别是通过右移来将我们感兴趣的部分移动到前四位,然后将结果与0xf进行AND运算以将其与其他部分隔离开来。这就是我的意思(假设我们想要取第3部分):

0xa31f -> shift right by 8 = 0x00a3 -> AND with 0xf = 0x0003

现在我们有一个数字,需要将其转换为ASCII值。如果该数字小于或等于9,则可以直接添加0的ASCII值(0x30),如果大于9,则必须使用a的ASCII值(0x61)。 下面是代码实现:
    mov si, ???         ; si points to the target buffer
    mov ax, 0a31fh      ; ax contains the number we want to convert
    mov bx, ax          ; store a copy in bx
    xor dx, dx          ; dx will contain the result
    mov cx, 3           ; cx's our counter

convert_loop:
    mov ax, bx          ; load the number into ax
    and ax, 0fh         ; we want the first 4 bits
    cmp ax, 9h          ; check what we should add
    ja  greater_than_9
    add ax, 30h         ; 0x30 ('0')
    jmp converted

greater_than_9:
    add ax, 61h         ; or 0x61 ('a')

converted:
    xchg    al, ah      ; put a null terminator after it
    mov [si], ax        ; (will be overwritten unless this
    inc si              ; is the last one)

    shr bx, 4           ; get the next part
    dec cx              ; one less to do
    jnz convert_loop

    sub di, 4           ; di still points to the target buffer

PS: 我知道这是16位代码,但我仍然使用旧的TASM :P

PPS: 这是Intel语法,转换为AT&T语法并不难,可以看看这里


你不需要使用AT&T语法在Linux上运行此程序。 - Andrei Bârsan
@AndreiBârsan:你说得对,已经修复了。这个回答太老了 :) - BlackBear
1
在我看来,这个答案更好,因为你不需要C运行时库(调用printf(...)需要)。 - Andrei Bârsan
1
@AndreiBârsan 是的,使用C运行时库来编写汇编代码有点毫无意义。 - BlackBear
32位代码:如何将二进制整数转换为十六进制字符串?。32位/64位转换为十进制:如何在汇编级别编程中打印整数而不使用c库中的printf?,使用64位Linux syscall写入到标准输出。 - Peter Cordes

14

使用printf的Linux x86-64

main.asm

default rel            ; make [rel format] the default, you always want this.
extern printf, exit    ; NASM requires declarations of external symbols, unlike GAS
section .rodata
    format db "%#x", 10, 0   ; C 0-terminated string: "%#x\n" 
section .text
global main
main:
    sub   rsp, 8             ; re-align the stack to 16 before calling another function

    ; Call printf.
    mov   esi, 0x12345678    ; "%x" takes a 32-bit unsigned int
    lea   rdi, [rel format]
    xor   eax, eax           ; AL=0  no FP args in XMM regs
    call  printf

    ; Return from main.
    xor   eax, eax
    add   rsp, 8
    ret

GitHub 上游。

然后:

nasm -f elf64 -o main.o main.asm
gcc -no-pie -o main.out main.o
./main.out

输出:

0x12345678

注意:

  • sub rsp, 8:如何使用printf编写64位Mac OS X汇编语言hello world程序?
  • xor eax,eax:调用printf之前为什么要将%eax清零?
  • -no-pie:在PIE可执行文件(-pie)中,普通的call printf不起作用,链接器仅自动为旧式可执行文件生成PLT存根。你有两个选择:

    • call printf wrt ..plt像传统的call printf一样通过PLT调用

    • call [rel printf wrt ..got]完全不使用PLT,就像gcc -fno-plt一样。

    像GAS语法call *printf@GOTPCREL(%rip)

    这些选项在非PIE可执行文件中也可以使用,除非您正在静态链接libc。在这种情况下,call printf可以直接解析为call rel32到libc,因为从您的代码到libc函数的偏移量会在静态链接时已知。

    另请参见:无法从汇编(yasm)代码中调用64位Linux上的C标准库函数

如果要使用C库以外的十六进制数:使用汇编打印十六进制数

在Ubuntu 18.10,NASM 2.13.03上测试通过。


请不要在64位模式下推荐使用mov将静态地址放入寄存器中。除非您正在优化位置相关代码,可以使用mov r32,imm32,否则请使用RIP相对LEA。 - Peter Cordes
嗨@PeterCordes,感谢您的编辑。是的,我想当时我不知道PIE是什么+许多其他细节:-)如果您觉得也可以使用“-pie”使其工作,那就太酷了;-)我现在懒得研究。 - Ciro Santilli OurBigBook.com
我已经在我的第一次编辑中包含了 call printf wrt ..plt。现在我把它放回到一个更合适的位置,因为你为它列了一个项目符号。我不得不查找NASM等效于GAS call *printf@GOTPCREL(%rip)的代码,用于没有PLT风格的代码,通过动态符号的早期绑定而不是通过PLT进行惰性链接。(但是有一个好处,就是只需要间接调用,而不是使用PLT进行惰性动态链接的调用+jmp) - Peter Cordes
@PeterCordes 哦,好的,我以为那只是伪符号,这个语法好奇怪! - Ciro Santilli OurBigBook.com
同意。.plt是节名称,我猜里面可能有一个额外的.与缩写一起使用? - Peter Cordes
从汇编(yasm)代码中无法在64位Linux上调用C标准库函数。Can't call C standard library function on 64-bit Linux from assembly (yasm) code有更多关于call [rel printf wrt ..got]call printf wrt ..plt的详细信息。我刚刚更新了它,使其适用于NASM和YASM。 - Peter Cordes

1

这取决于您使用的架构/环境。

例如,如果我想在Linux上显示一个数字,汇编代码将与我在Windows上使用的代码不同。

编辑:

您可以参考此处了解转换示例。


一个Linux的例子就可以了。 - AR89
@AR89 这是一个糟糕的工作.. 你必须先将数字转换为ASCII。看一下编辑后的问题。 - moongoal

0

因为您没有提到数字表示,所以我编写了以下代码,用于任意基数(当然不要太大)的无符号数字,因此您可以使用它:

BITS 32
global _start

section .text
_start:

mov eax, 762002099 ; unsigned number to print
mov ebx, 36        ; base to represent the number, do not set it too big
call print

;exit
mov eax, 1
xor ebx, ebx
int 0x80

print:
mov ecx, esp
sub esp, 36   ; reserve space for the number string, for base-2 it takes 33 bytes with new line, aligned by 4 bytes it takes 36 bytes.

mov edi, 1
dec ecx
mov [ecx], byte 10

print_loop:

xor edx, edx
div ebx
cmp dl, 9     ; if reminder>9 go to use_letter
jg use_letter

add dl, '0'
jmp after_use_letter

use_letter:
add dl, 'W'   ; letters from 'a' to ... in ascii code

after_use_letter:
dec ecx
inc edi
mov [ecx],dl
test eax, eax
jnz print_loop

; system call to print, ecx is a pointer on the string
mov eax, 4    ; system call number (sys_write)
mov ebx, 1    ; file descriptor (stdout)
mov edx, edi  ; length of the string
int 0x80

add esp, 36   ; release space for the number string

ret

这段代码没有针对以2的幂次方为基数的数字进行优化,也没有使用libc中的printf函数。

print函数会在输出数字后加上一个换行符。数字字符串是在堆栈上创建的。请使用nasm编译代码。

输出:

clockz

https://github.com/tigertv/stackoverflow-answers/tree/master/8194141-how-to-print-a-number-in-assembly-nasm


您正在使用ESP下方的空间。这只有在您知道没有安装信号处理程序的情况下才是安全的,并且不应在可能在其他上下文中调用的函数中使用。32位Linux不保证有一个红区。此外,请使用xor edx,edx / divcdq / idiv,以便被除数的零扩展或符号扩展与除法的有符号性匹配。在这种情况下,您需要xor/div,以便始终具有正余数。如果您想将输入视为有符号的,则需要测试/js并打印无符号绝对值(如果需要,带有前导“-”)。 - Peter Cordes
@PeterCordes,你好,Peter!你说得对,安全性是一个部分解决方案,我没有考虑到有符号数。 - TigerTV.ru
你应该将 idiv 改为 div,这样它才能适用于完整的无符号数范围。嗯,实际上这可能是安全的,因为 2^32-1 / 10 不会溢出 EAX。通过零扩展到 edx:eax,可以得到一个来自 0..2^32-1 的有符号非负被除数。 - Peter Cordes
@PeterCordes,idiv 已经被替换了。我还为数字添加了一个基数。您对此有什么看法?另外,我在堆栈上预留了一个大小为 32 的数字字符串缓冲区。 - TigerTV.ru
add esp, 32 应该改为 sub 来保留空间。你正在占用调用者的堆栈空间。mov byte [ecx], 10 比先设置一个寄存器更有效率。甚至可以使用 push 10 / mov ecx, esp / sub esp, 32。 (对于当前版本,基数为2的大数字将使用32个数字,但你会用一个换行符占用其中的一个。) - Peter Cordes
当然,我错了。 :) 在这种情况下,它甚至没有破坏堆栈。已修复。 - TigerTV.ru

0

我对汇编语言相对较新,这显然不是最好的解决方案,但它能够工作。主要函数是_iprint,它首先检查eax中的数字是否为负数,如果是,则打印一个减号,然后通过调用每个数字的函数_dprint来打印单个数字。思路如下:如果我们有512,则等于:512 =(5 * 10 + 1)* 10 + 2 = Q * 10 + R,因此我们可以通过将其除以10并获取余数R来找到数字的最后一位,但如果我们在循环中执行此操作,则数字将以相反的顺序排列,因此我们使用堆栈将它们推入,并在将它们写入stdout时以正确的顺序弹出。

; Build        : nasm -f elf -o baz.o baz.asm
;                ld -m elf_i386 -o baz baz.o
section .bss
c: resb 1 ; character buffer
section .data
section .text
; writes an ascii character from eax to stdout
_cprint:
    pushad        ; push registers
    mov [c], eax  ; store ascii value at c
    mov eax, 0x04 ; sys_write
    mov ebx, 1    ; stdout
    mov ecx, c    ; copy c to ecx
    mov edx, 1    ; one character
    int 0x80      ; syscall
    popad         ; pop registers
    ret           ; bye
; writes a digit stored in eax to stdout 
_dprint:
    pushad        ; push registers
    add eax, '0'  ; get digit's ascii code
    mov [c], eax  ; store it at c
    mov eax, 0x04 ; sys_write
    mov ebx, 1    ; stdout
    mov ecx, c    ; pass the address of c to ecx
    mov edx, 1    ; one character
    int 0x80      ; syscall
    popad         ; pop registers
    ret           ; bye
; now lets try to write a function which will write an integer
; number stored in eax in decimal at stdout
_iprint:
    pushad       ; push registers
    cmp eax, 0   ; check if eax is negative
    jge Pos      ; if not proceed in the usual manner
    push eax     ; store eax
    mov eax, '-' ; print minus sign
    call _cprint ; call character printing function 
    pop eax      ; restore eax
    neg eax      ; make eax positive
Pos:
    mov ebx, 10 ; base
    mov ecx, 1  ; number of digits counter
Cycle1:
    mov edx, 0  ; set edx to zero before dividing otherwise the
    ; program gives an error: SIGFPE arithmetic exception
    div ebx     ; divide eax with ebx now eax holds the
    ; quotent and edx the reminder
    push edx    ; digits we have to write are in reverse order
    cmp eax, 0  ; exit loop condition
    jz EndLoop1 ; we are done
    inc ecx     ; increment number of digits counter
    jmp Cycle1  ; loop back
EndLoop1:
; write the integer digits by poping them out from the stack
Cycle2:
    pop eax      ; pop up the digits we have stored
    call _dprint ; and print them to stdout
    dec ecx      ; decrement number of digits counter
    jz EndLoop2  ; if it's zero we are done
    jmp Cycle2   ; loop back
EndLoop2:   
    popad ; pop registers
    ret   ; bye
global _start
_start:
    nop           ; gdb break point
    mov eax, -345 ;
    call _iprint  ; 
    mov eax, 0x01 ; sys_exit
    mov ebx, 0    ; error code
    int 0x80      ; край

你可以在生成数字时使用“add '0'”将它们存储在缓冲区中。使用“dec”向下移动指针。完成后,您将拥有指向最后一个存储的数字的指针,因此可以将其传递给sys_write()(以及数字计数)。这比为每个字节进行单独的系统调用要高效得多,并且实际上不需要更多的代码。很容易分配足够长的缓冲区来容纳可能的最长数字字符串,并从末尾开始,因为您知道2^32有多少十进制位数。 - Peter Cordes
相关:我在这个扩展精度斐波那契代码高尔夫答案中编写了一个整数->字符串循环。请参见.toascii_digit:循环。当然,这是为了优化大小,因此它使用缓慢的div而不是乘法技巧。 - Peter Cordes
1
谢谢,这绝对比为每个数字调用sys_write要好:) - baz
我在另一个问题上发布了我的int->string + sys_write代码作为独立的函数,并带有注释。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接