返回

x86-64汇编: printf与scanf详解

windows

x86-64 汇编中 printfscanf 的使用

在 Windows x86-64 汇编中,与标准 C 库进行交互是完成输入输出的关键, 其中printfscanf 函数非常常用。 使用这两个函数处理格式化输出和用户输入是基础的。 此文了在汇编语言中调用这两个函数的方式。

printf 的使用

printf 用于将格式化的文本输出到控制台。 在 Windows x86-64 环境中,使用 System V AMD64 调用约定传递参数。 这意味着前六个整数或指针类型的参数依次通过 RCXRDXR8R9、栈上的[RSP+08][RSP+16] 进行传递。 浮点类型通过 XMM 寄存器传递,此处不进行讨论。

对于打印字符串字面量,首先需要准备好要输出的字符串数据。 字符串通常放在 .data 段中,并以 NULL 字节 ( 0 ) 结尾。之后,需要把这个字符串的地址加载到 RCX 寄存器。 如果有其它参数,也需要把他们按序传入相应的寄存器或者入栈。

示例代码:

includelib legacy_stdio_definitions.lib
extrn printf:near

.data
    prompt db 'Hello, world!', 0 ; 字符串字面量, NULL 结尾。

.code
    main proc
    
        lea rcx, prompt     ;  加载字符串地址到 rcx
        sub rsp, 32       ; 分配空间(shadow space),因为 printf 会使用到 stack 的部分
        call printf        ; 调用 printf
        add rsp, 32       ; 回收栈空间
        ret  
        main endp
    end

操作步骤:

  1. 使用汇编器 (ml64) 编译汇编代码。
  2. 使用链接器 (link) 链接生成可执行文件。
  3. 运行生成的可执行文件,会在控制台上打印 “Hello, world!” 。

scanf 的使用

scanf 用于从标准输入(通常是控制台)读取格式化的输入。 它的参数传递方式与printf类似, 格式字符串的地址加载到 RCX 寄存器, 接受数据的内存地址需要从RDX 寄存器传递。 注意为接受用户输入分配足够的内存。 确保在scanf 使用前后平衡好堆栈指针,保持数据正常入栈。

示例代码:

includelib legacy_stdio_definitions.lib
extrn printf:near, scanf:near

.data
    prompt db 'Please enter an integer: ', 0
    format_str db '%lld', 0  ; format specifier for scanf and printf
    result_str db 'You entered: %lld', 0AH, 0 ;  换行符 0AH.
    inp_int dq ?         ; 用于存放输入的值, 为一个64位数据预留空间
    
.code
    main proc
        
        ; prompt the user
        lea rcx, prompt     
        sub rsp, 32
        call printf         ; 调用 printf, 打印提示符.
        add rsp, 32

        ; call scanf
        lea rcx, format_str
        lea rdx, inp_int     ; 读取数据的地址
        sub rsp, 32         ;  分配影子空间
        call scanf         ; 从标准输入读取整数。
        add rsp, 32
     
        ; print input to stdout
        mov rcx, result_str   ; 第一个参数格式化字符串地址.
        mov rdx, QWORD PTR [inp_int]   ; 获取存储在`inp_int`的数据. 放到RDX中
        sub rsp, 32        
        call printf          ; 将输入打印到控制台
        add rsp, 32

        ret
    main endp
    end

操作步骤:

  1. 使用汇编器 (ml64) 编译汇编代码。
  2. 使用链接器 (link) 链接生成可执行文件。
  3. 运行程序。 程序会首先提示 “Please enter an integer: ”, 待输入整数后,再输出 “You entered: [用户输入的整数]”。

安全建议:

  • 检查scanf的返回值,确保它成功读取了预期数量的输入。 尽管在汇编中处理返回值有点复杂,但应当认识到潜在的风险。
  • 使用格式化字符串时, 要确保提供的类型匹配输入数据类型, 类型不匹配可能会导致意外行为或者安全问题。
  • scanf分配足够的内存,避免缓冲区溢出。

printfscanf 在原代码中的应用:
原始代码主要问题在于如何把scanf 正确调用以读取用户输入, 以及在printf的格式化字符串中使用多个参数, 这里提供解决示例.

includelib legacy_stdio_definitions.lib
extrn printf:near, scanf:near

.data                           ; Data section

istr db 'Please enter an integer: ', 0 ; 提示语
stri byte '%lld', 0AH, 00       ; scanf 的 format 字符串
ostr byte 'The sum of proc. and user inputs (%lld, %lld, %lld, %lld): %lld', 0AH, 00; 输出format
inp_int dq ?           ; 存放用户输入整数

.code                           ; Code section

public use_scanf                ; int use_scanf(long long a, long long b, long long c)
use_scanf:                      ; { 
    push rbp           ; 保存栈帧基址,建立新栈帧
    mov rbp, rsp
    sub rsp, 32       ; 为局部变量分配 32 bytes 的栈空间


    mov rax, 0                  ; sum = 0;
    mov rax, rcx         ; sum = a
    add rax, rdx                ; sum += b;
    add rax, r8                 ; sum += c;


   ; printf('Please enter an integer');
   ; scanf();
   ; printf('%lld', &inp_int);

     ; print out prompt for user input
    lea rcx, istr            ; load prompt to rcx, args in rcx first
    call printf             ; 

    ; call scanf
    lea rcx, stri           ;format string arg, format strings always come first.
    lea rdx, inp_int    ;input result into inp_int variable
    call scanf             ; call scanf with address

   
    mov r9, QWORD PTR [inp_int]
    add rax, r9         ;sum = sum + input value;
   
    mov QWORD PTR [rbp - 8], rax  ; save sum to local memory (rbp-8), 


    ; args must come in reverse order.
    ; put them into reverse registers and then call the functions with registers
    mov r9, QWORD PTR [inp_int] ; usr input
    push r9;  arg push last into stack to form FIFO for push.

    mov r9, r8       ;
    mov r8, rdx     ; b 
    mov rdx, rcx    ; a

    mov rcx, ostr           ; set the fmt string, this arg has to be last so stack and registers do not overwrite the format string 
    sub rsp, 32    ; allocate space on stack
    call printf             ; call to print function.  


    add rsp, 40    ; deallocate memory
    mov rax, QWORD PTR [rbp -8]; recover final sum 
    mov rsp, rbp     ; 清理栈帧
    pop rbp          ; 还原旧栈帧基址
    ret                         ; return retVal}

上面这段代码修改后的汇编,展示了如何正确的使用scanfprintf 。 使用正确的寄存器传递参数是关键。 同时要保证栈的正确使用。注意在使用 call 指令前先分配好 32 个字节的 shadow space (调用惯例的一部分)

正确执行以上汇编程序会返回期望的结果。 注意,为了获取编译汇编代码的完整上下文,应该参考全文。 这些修改不仅修复了输入和输出问题,还体现了在汇编程序中使用标准库函数的关键细节。 深入理解这些概念对于编写有效的汇编程序至关重要。

通过本文, 你可以更好地了解如何在x86-64汇编中实现简单的控制台输入和输出操作。