x86-64汇编: printf与scanf详解
2025-01-10 19:21:09
x86-64 汇编中 printf
和 scanf
的使用
在 Windows x86-64 汇编中,与标准 C 库进行交互是完成输入输出的关键, 其中printf
和 scanf
函数非常常用。 使用这两个函数处理格式化输出和用户输入是基础的。 此文了在汇编语言中调用这两个函数的方式。
printf
的使用
printf
用于将格式化的文本输出到控制台。 在 Windows x86-64 环境中,使用 System V AMD64 调用约定传递参数。 这意味着前六个整数或指针类型的参数依次通过 RCX
、RDX
、R8
、R9
、栈上的[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
操作步骤:
- 使用汇编器 (
ml64
) 编译汇编代码。 - 使用链接器 (
link
) 链接生成可执行文件。 - 运行生成的可执行文件,会在控制台上打印 “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
操作步骤:
- 使用汇编器 (
ml64
) 编译汇编代码。 - 使用链接器 (
link
) 链接生成可执行文件。 - 运行程序。 程序会首先提示 “Please enter an integer: ”, 待输入整数后,再输出 “You entered: [用户输入的整数]”。
安全建议:
- 检查
scanf
的返回值,确保它成功读取了预期数量的输入。 尽管在汇编中处理返回值有点复杂,但应当认识到潜在的风险。 - 使用格式化字符串时, 要确保提供的类型匹配输入数据类型, 类型不匹配可能会导致意外行为或者安全问题。
- 为
scanf
分配足够的内存,避免缓冲区溢出。
printf
和 scanf
在原代码中的应用:
原始代码主要问题在于如何把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}
上面这段代码修改后的汇编,展示了如何正确的使用scanf
和 printf
。 使用正确的寄存器传递参数是关键。 同时要保证栈的正确使用。注意在使用 call 指令前先分配好 32 个字节的 shadow space (调用惯例的一部分)
正确执行以上汇编程序会返回期望的结果。 注意,为了获取编译汇编代码的完整上下文,应该参考全文。 这些修改不仅修复了输入和输出问题,还体现了在汇编程序中使用标准库函数的关键细节。 深入理解这些概念对于编写有效的汇编程序至关重要。
通过本文, 你可以更好地了解如何在x86-64汇编中实现简单的控制台输入和输出操作。