扫描一个整数并打印NASM中的区间(1,整数)

Scan an integer and print the interval (1, integer) in NASM

我正在尝试从 Linux Ubuntu 16.04 x64 学习汇编语言。 现在我有以下问题: - 扫描一个整数 n 并打印从 1 到 n 的数字。

对于 n = 5 我应该有 1 2 3 4 5。 我试着用 scanf 和 printf 来做,但在我输入数字后,它退出了。

密码是:

;nasm -felf64 code.asm && gcc code.o && ./a.out

SECTION .data
    message1: db "Enter the number: ",0
    message1Len: equ $-message1
    message2: db "The numbers are:", 0
    formatin: db "%d",0
    formatout: db "%d",10,0 ; newline, nul
    integer: times 4 db 0 ; 32-bits integer = 4 bytes

SECTION .text
    global main
    extern scanf
    extern printf

main:

    mov eax, 4
    mov ebx, 1
    mov ecx, message1
    mov edx, message1Len
    int 80h

    mov rdi, formatin
    mov rsi, integer
    mov al, 0
    call scanf
    int 80h

    mov rax, integer
    loop:
        push rax
        push formatout
        call printf
        add esp, 8
        dec rax
    jnz loop

    mov rax,0

ret

我知道在这个循环中我会得到反向输出 (5 4 3 2 1 0),但我不知道如何设置条件。

我使用的命令如下:

nasm -felf64 code.asm && gcc code.o && ./a.out

你能帮我找出我哪里出错了吗?

存在几个问题:
1. printf 的参数,如评论中所述。在x86-64中,前几个参数是在寄存器中传递的。
2. printf不保留eax的值。
3. 堆栈错位。
4.使用rbx不保存调用者的值
5. 正在加载 integer 的地址而不是它的值。
6. 由于printf是可变参数函数,调用前需要将eax设为0
7. 调用 scanf.

后虚假 int 80h

我将重复整个函数以显示上下文中的必要更改。

main:
    push rbx           ; This fixes problems 3 and 4.

    mov eax, 4
    mov ebx, 1
    mov ecx, message1
    mov edx, message1Len
    int 80h

    mov rdi, formatin
    mov rsi, integer
    mov al, 0
    call scanf

    mov ebx, [integer] ; fix problems 2 and 5
    loop:
        mov rdi, formatout   ; fix problem 1
        mov esi, ebx
        xor eax, eax   ; fix problem 6
        call printf
        dec ebx
    jnz loop

    pop rbx            ; restore caller's value
    mov rax,0

ret

P.S。要使其向上计数而不是向下计数,请像这样更改循环:

    mov ebx, 1
    loop:
        <call printf>
        inc ebx
        cmp ebx, [integer]
    jle loop

您正在使用 x86-64 System V 调用约定正确调用 scanf。它在 eax 中保留其 return 值。成功转换一个操作数 (%d) 后,它 returns 与 eax = 1.

... correct setup for scanf, including zeroing AL.

call scanf    ; correct
int 80h       ; insane: system call with eax = scanf return value

然后你 运行 int 80h,它使用 eax=1 作为代码进行 32 位 legacy-ABI 系统调用来确定 哪个 系统调用。 (参见 What happens if you use the 32-bit int 0x80 Linux ABI in 64-bit code?)。

eax=1 / int 80h 在 Linux 上是 sys_exit。 (unistd_32.h__NR_exit = 1)。 使用调试器;那会告诉你是哪条指令让你的程序退出。

您的标题(在我更正之前)说您遇到了分段错误,但我在我的 x86-64 桌面上进行了测试,事实并非如此。它使用 int 80h 退出系统调用干净地退出。 (但在发生段错误的代码中,使用调试器找出是哪条指令。)strace decodes int 0x80 system calls incorrectly in 64-bit processes,使用来自 unistd_64.h 的 64 位 syscall 调用编号,而不是 32 位 unistd_32.h 电话号码。


您的代码接近工作:您为 sys_write 正确使用 int 0x80 32 位 ABI,并且只传递 32 位参数。 (指针参数适合 32 位,因为在 x86-64 上的默认代码模型中,static code/data 始终位于虚拟地址 space 的低 2GiB 中。正是出于这个原因,您可以使用 compact mov edi, formatin 之类的指令将地址放入寄存器,或将它们用作立即数或 rel32 有符号位移。)

OTOH 我认为你这样做是出于错误的原因。正如@prl 指出的那样,您忘记保持 16 字节堆栈对齐。

此外,将系统调用与 C stdio 函数混合通常不是一个好主意。 Stdio 使用内部缓冲区而不是总是对每个函数调用进行系统调用,因此事情可能会出现乱序,或者当 [=32= 的 stdio 缓冲区中已经有数据时,read 可以等待用户输入].


你的循环在几个方面也被打破了。您似乎正在尝试使用 32 位调用约定(堆栈上的参数)调用 printf

即使在 32 位代码中,这也是错误的,因为 printf 的 return 值在 eax 中。所以你的循环是无限的,因为 printf returns 打印的字符数。这至少是 %d\n 格式字符串中的两个,因此 dec rax / jnz 将始终跳转。

在 x86-64 SysV ABI 中,如果您没有在 XMM 寄存器中传递任何 FP 参数,则在调用 printf(使用 xor eax,eax)之前需要将 al 清零.您还必须在 rdirsi、... 中传递参数,例如 scanf.

在压入两个 8 字节的值后,您还 add rsp, 8,因此堆栈会永远增长。 (但是你永远不会 return,所以最终的段错误将在堆栈溢出时发生,而不是在尝试 return 时 rsp 不指向 return 地址。)


决定您是在制作 32 位还是 64 位代码,并且仅 copy/paste 来自模式示例和您的目标 OS. (不过请注意,64 位代码可以而且经常使用大部分 32 位寄存器。)

另请参阅 Assembling 32-bit binaries on a 64-bit system (GNU toolchain)(其中包含一个 NASM 部分,其中包含一个方便的 asm-link 脚本,可将 link 和 link 汇编成静态二进制文件)。但是因为你正在写 main 而不是 _start 并且正在使用 libc 函数,你应该只 link 和 gcc -m32 (如果你决定使用 32 位代码而不是用 64 位 function-calling 和 system-call 约定替换程序的 32 位部分。

参见 What are the calling conventions for UNIX & Linux system calls on i386 and x86-64