如何在 NASM 中使用 scanf?

Posted

技术标签:

【中文标题】如何在 NASM 中使用 scanf?【英文标题】:How to use scanf in NASM? 【发布时间】:2012-06-13 23:27:03 【问题描述】:

我试图弄清楚如何使用scanf 来获取用户输入。我知道使用printf:我所要做的就是将我想在屏幕上写入的数据推送到堆栈中,如下所示:

global _main
extern _printf
extern _scanf

section .data
msg db "Hi", 0

section .text
_main:
  push ebp
  mov ebp, esp  

  push msg
  call _printf

  mov esp, ebp
  pop ebp
ret

但我不知道如何使用scanf。有人可以给我scanf 的最简单的源代码吗?我真的只想输入用户输入的内容。

我不习惯 32 位汇编。我只用过 16 位,而且我知道在 16 位 (DOS) 中你可以这样做:

mov ah, 3fh
mov dx, input
int 21h

input rb 100d

无论你输入什么,都会放在“输入”的地址。

【问题讨论】:

【参考方案1】:

我找到了这个'Programming in NASM.PDF'

; add1.asm
SECTION .data
    message1: db "Enter the first number: ", 0
    message2: db "Enter the second number: ", 0
    formatin: db "%d", 0
    formatout: db "%d", 10, 0 ; newline, nul terminator
    integer1: times 4 db 0 ; 32-bits integer = 4 bytes
    integer2: times 4 db 0 ;
SECTION .text
   global _main 
   extern _scanf 
   extern _printf     

_main:

   push ebx ; save registers
   push ecx
   push message1
   call printf

   add esp, 4 ; remove parameters
   push integer1 ; address of integer1 (second parameter)
   push formatin ; arguments are right to left (first parameter)
   call scanf

   add esp, 8 ; remove parameters
   push message2
   call printf

   add esp, 4 ; remove parameters
   push integer2 ; address of integer2
   push formatin ; arguments are right to left
   call scanf

   add esp, 8 ; remove parameters

   mov ebx, dword [integer1]
   mov ecx, dword [integer2]
   add ebx, ecx ; add the values          ; the addition
   push ebx
   push formatout
   call printf                            ; call printf to display the sum
   add esp, 8                             ; remove parameters
   pop ecx
   pop ebx ; restore registers in reverse order
   mov eax, 0 ; no error
   ret

这个C函数的asm版本是什么:

#include <stdio.h>
int main(int argc, char *argv[])

    int integer1, integer2;
    printf("Enter the first number: ");
    scanf("%d", &integer1);
    printf("Enter the second number: ");
    scanf("%d", &integer2);
    printf("%d\n", integer1+integer2);
    return 0;

【讨论】:

当前版本的 i386 System V ABI(在 Linux 上使用)需要在 call 之前进行 16 字节堆栈对齐。您的 printf 调用是使用正确对齐的堆栈(返回地址 + 3 次推送)进行的,但 scanf 调用未对齐。 glibc scanf 将被允许进行段错误(就像在 64 位模式下一样),但 32 位版本可能碰巧不会。此外,ecx 不是呼叫保留寄存器。保存它是没有意义的; printf / scanf 破坏 ECX 和 EDX,你的调用者也希望你破坏它们。 (使用 EDX 而不是 EBX,您可以避免任何保存/恢复)。 @PeterCordes 我在 2012 年从上述文档中收集了答案。请随时用更合适的例子来纠正答案。【参考方案2】:

谢谢普瑞特。我根据你的代码做了一个简单的例子来说明scanf的使用。

请求一个整数并将其打印到屏幕上的程序:

section .text
  global main
  extern printf
  extern scanf

section .data
  message: db "The result is = %d", 10, 0
  request: db "Enter the number: ", 0
  integer1: times 4 db 0 ; 32-bits integer = 4 bytes
  formatin: db "%d", 0

main:
  ;  Ask for an integer
  push request
  call printf
  add esp, 4    ; remove the parameter

  push integer1 ; address of integer1, where the input is going to be stored (second parameter)
  push formatin ; arguments are right to left (first  parameter)
  call scanf
  add esp, 8    ; remove the parameters

  ; Move the value under the address integer1 to EAX
  mov eax, [integer1]

  ; Print out the content of eax register
  push eax
  push message
  call printf
  add esp, 8

  ;  Linux terminate the app
  MOV AL, 1
  MOV EBX, 0 
  INT 80h 

【讨论】:

【参考方案3】:

假设你想做类似的事情

scanf("%d %d", &var1, &var2);

这需要两个值并将它们存储在变量中。

在汇编中,您将 push 变量的地址放入堆栈(按倒序排列),然后 call scanf。 像你有两个变量

var1 resd 1
var2 resd 1

...然后

push var2
push var1
call scanf

*注意我是按倒序推送的,第一个值会存储在var1中。 执行后,您输入的值将存储在变量中。 如果您只想要一个值,只需推送一个变量。

【讨论】:

scanf 返回后别忘了出栈。 (add esp, 8 用于 32 位代码)。 64 位代码在寄存器中传递前最多 6 个参数,因此您不会有堆栈参数。此外,Linux 上的当前 i386 System V ABI 需要在 call 之前进行 16 字节堆栈对齐,因此您不能只推送任意数量的东西。【参考方案4】:

这是您在汇编中搜索 scanf 时出现的第一个帖子,因此,即使它是 4 年前的帖子,我认为它应该是正确的。 Oukei,所以,在 NASM 程序集中 call scanf 你需要:

    外部扫描 为您准备格式扫描f 准备变量或单个变量以存储预期值 倒序推送参数 调用scanf 恢复堆栈

所以,假设您正在处理

scanf ("%d %d", &val1, &val2);

并遵循列表: ... 1.

section .text
extern scanf

... 2. 这是你传递给你的 C scanf 的第一个参数,它说明你会得到什么。一个整数、两个、一个浮点数、字符串、字符……在这种情况下,两个整数之间用空格隔开(也适用于回车)

section .data
fmt: db "%d %d",0

... 3.

section .bss
val1: resd 1
val2: resd 1

... 4 5 6. 请注意,您推送的是变量的地址,而不是其内容(即 [var])

push val2
push val1
push fmt
call scanf
add esp, 12

还请注意,您必须将 12 添加到堆栈指针,因为您压入了 3 个双字参数。因此,您将 12 字节(3*4 字节)添加到堆栈中以“跳转”参数。 *我为变量声明了 dword,因为 %d 使用 dword,就像 printf 一样。 **格式字符串末尾的,0 是一个标记字符。

【讨论】:

您可以合并您的帐户(搜索 Meta Stack Overflow 了解如何)。或者您可以从您的其他帐户中删除您之前的答案,因为它无论如何都没有任何赞成票。并且请edit这个独立,而不是引用你的其他帖子。 我以 PunditPoe 的名字作为访客回答,不知道我是否可以“进入”这个访客帐户并删除帖子。但是,无论如何,我纠正了这个,让它独立存在。谢谢。 很好的解释。谢谢。【参考方案5】:

对于 64 位 nasm:

要在 nasm 中使用 scanf,首先需要在 .text 部分之前添加语句。

extern scanf

现在您需要首先使用

设置您的堆栈
push rbp

如果您不想出现分段错误,这一点很重要。在进行调用之前,堆栈指针 rsp 必须与 16 字节边界对齐。进行调用的过程会将返回地址(8 个字节)压入堆栈,因此当函数获得控制权时,rsp 未对齐。你必须自己创造额外的空间,通过推动一些东西或从 rsp 中减去 8。你可以阅读更多关于它的信息here。

现在,您的堆栈已准备就绪,您需要首先将输入格式化字符串移入 rdi 寄存器,然后按严格顺序将参数移入 rsi、rdx、rcx、r8、r9 中。

我们以模仿c语句为例

scanf("%d %d", &a, &b);

等效的 nasm 代码是:

section .data

Input_format db "%d %d", 0

section .bss

var1: resd 1 ;reserves one double(4 bytes) for int variable
var2: resd 1

extern scanf
global main
default rel  ; use this by default for efficiency. This is even mandatory if you run your code on macOS.


section .text
main:

push rbp
lea rdi, [Input_format] ;loading format
lea rsi, [var1] 
lea rdx, [var2]
call scanf

pop rbp  ;restores stack

;simulates return 0
mov rax, 0
ret

以下是上述代码的更漂亮版本的代码。它提示用户输入,并打印输入的数字。

        section .data

int_inMsg:    db        "Enter an integer value" , 10, 0 ;10 for new line, 0 for null
real_inMsg:   db        "Enter a real value", 10, 0
bool_inMsg:   db        "Enter a boolean value", 10, 0
arr_inMsg:    db        "Enter %d elements for array range %d to %d", 10, 0
intFormat     db        "%d", 0


        section .bss
var1:         resd      1


global main
extern printf
extern scanf
extern puts
extern exit
default rel

section .text

main: 

        push rbp ;setup stack

        ;printf("Enter blah blah\n");

        lea rdi, [int_inMsg] ;first argument
        xor rax, rax
        call printf


        ;take input from the user
        ;scanf("%d", &var1);

        lea rdi, [intFormat]
        lea rsi, [var1]
        xor rax, rax
        call scanf

        lea rdi, [intFormat]
        mov esi, [var1]  ;notice the [] for sending value of var1 instead of pointer to var1
        xor rax, rax
        call printf

        ; return
        pop rbp ;restore stack
        mov rax, 0 ;normal exit
        ret

感谢 @peter 提供帮助和有见地的 cmets。

【讨论】:

最好指出为什么需要push rbp:进入函数后将堆栈重新对齐16(main)。如果有人不明白这一点,他们也可能sub rsp, 8 为本地变量保留一些空间并再次错位堆栈。实际需要的是sub rsp, 8 + 16*npushes 的等价物。否则,是的,这是一个很好的例子;对“食谱”的那部分缺乏解释是阻止我投票的唯一原因。 请注意,您可以在任何需要 RAX=0 的地方使用xor eax,eax,而不仅仅是在 printf 之前。另外mov r64, imm64 效率低下; lea rdi, [rel intFormat] 是将指针放入 64 位代码中的寄存器的标准方法。或者对于非 PIE 可执行文件中的 Linux,mov edi, intFormat 因为绝对符号地址适合 32 位立即数。但是,对于不完全清楚符号和 64 位与 32 位寄存器如何工作的初学者来说,这可能会分散他们的注意力。 哦,您有一种错误:mov rsi, [var1] 是从您仅保留一个 dword 的位置加载的 qword。使用mov esi, [var1]int 是 32 位的,long 和指针是 64 位的。别忘了default rel;您总是希望 x86-64 使用它,它可能应该是 NASM 的默认设置。否则你必须写mov esi, [rel var1],除非你想要一个低效的32位绝对地址。 非常感谢您富有洞察力的 cmets。学到了很多!我会相应地编辑答案。 很高兴它有帮助。如果您想了解更多信息,请参阅***.com/tags/x86/info 中的链接。 How to load address of function or label into register in GNU Assembler / 32-bit absolute addresses no longer allowed in x86-64 Linux? / glibc scanf Segmentation faults when called from a function that doesn't align RSP

以上是关于如何在 NASM 中使用 scanf?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 ubuntu20.04 中使用最新的 nasm?

$ 究竟是如何在 NASM 中工作的?

NASM 汇编器,如何定义标签两次?

NASM ctypes SIMD - 如何访问返回到ctypes的128位数组?

NASM,如何直接写入硬盘并在实模式下读取

如何使用NASM调用位于后两个扇区的代码?