【问题标题】:How to use scanf in NASM?如何在 NASM 中使用 scanf?
【发布时间】: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

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

【问题讨论】:

    标签: assembly nasm


    【解决方案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 你需要:

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

      所以,假设您正在处理

      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。学到了很多!我会相应地编辑答案。
      猜你喜欢
      • 2015-06-29
      • 1970-01-01
      • 2015-01-09
      • 1970-01-01
      • 1970-01-01
      • 2010-09-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多