50、汇编语言与C库函数的交互使用

汇编语言与C库函数的交互使用

1. 栈对齐

在汇编编程中,栈操作是非常重要的一部分。当我们向栈中压入数据时,栈指针(RSP)会发生移动。例如,将两个QWORD值压入栈中,会使RSP向下移动16字节。为了清理栈并使其重新对齐,我们可以使用 ADD RSP,16 指令将这16字节加回到RSP中。

需要注意的是,有时“弹出你所压入的内容”并不实际。只要将栈指针恢复到压入数据之前的值,程序就能正常运行。如果将值压入栈作为局部存储,要确保将这些值的总大小加回到RSP中,使栈再次“干净”。若压入栈的数据不是16字节的倍数,可通过压入虚拟值进行填充,直到总大小为16字节的倍数。

x86 - 64 System V ABI要求栈按16字节对齐,这是因为始终保持栈按16字节边界对齐能简化代码,例如在使用存储在栈上的SSE向量时。

另外,SASM对这里展示的程序序言和尾声有特殊要求,它需要在开头使用 mov rbp,rsp 指令,而尾声只需最后一个 RET 指令。

2. 使用puts()输出字符

在glibc中, puts() 是一个简单且实用的函数,用于将字符输出到标准输出。从汇编语言调用 puts() 非常简单,只需三行代码。以下是一个示例程序 eatlibc.asm

;  Executable name : eatlibc
;  Version         : 3.0
;  Created date    : 11/12/2022
;  Last update     : 5/24/2023
;  Author          : Jeff Duntemann
;  Description     : Demonstrates calls made into libc, using NASM 
;                    2.14.02 to send a short text string to stdout 
;                    with puts().
;
;  Build using these commands:
;    nasm -f elf64 -g -F dwarf eatlibc.asm
;    gcc eatlibc.o -o eatlibc –no-pie

SECTION .data           ; Section containing initialized data

EatMsg: db "Eat at Joe's!",0

SECTION .bss            ; Section containing uninitialized data

SECTION .text           ; Section containing code

extern puts             ; The simple "put string" routine from libc
global main          ; Required for the linker to find the entry point

main:
    push rbp         ; Prolog sets up stack frame
    mov rbp,rsp

;; Everything before this is boilerplate; use it for all ordinary apps!

    mov rdi,EatMsg   ; Put address of string into rdi
    call puts        ; Call libc function for displaying strings
    xor rax,rax      ; Pass a 0 as the program's return value.

;; Everything after this is boilerplate; use it for all ordinary apps!
    pop rbp          ; Destroy stack frame before returning
    ret              ; Return control to Linux

调用 puts() 的过程是x64调用约定的一个缩影。根据x64调用约定,我们将待显示字符串的地址放入RDI,无需传递字符串长度。 puts() 函数从RDI传递的地址处开始读取字符串,直到遇到0(空字符),并将字符发送到标准输出。

3. 使用printf()进行格式化文本输出

虽然 puts() 很有用,但与一些更复杂的函数相比,它功能有限。 puts() 只能将简单的文本字符串输出到文件(默认是标准输出),且总是在显示内容末尾添加换行符,这使得无法使用多次调用 puts() 在终端的同一行输出多个文本字符串。

printf() 则强大得多,它允许在一次函数调用中完成许多有用的操作:
- 输出带或不带换行符的文本。
- 通过输出格式化代码将数值数据转换为多种格式的文本。
- 将包含多个单独存储字符串的文本输出到文件。

3.1 格式化代码示例

格式化代码以百分号开头,包含与要合并到基础字符串的数据项的类型、大小以及显示方式相关的信息。例如, %d 用于将有符号整数转换为文本并替换基础字符串中的格式化代码。

以下是一个包含一个格式化代码的基础字符串示例:

"The answer is %d, and don't you forget it!"

如果传递的十进制值为42,在控制台将看到:

The answer is 42, and don't you forget it!

3.2 常见格式化代码

格式化代码 说明
%d 打印有符号的十进制整数。
%u 打印无符号的十进制整数。
%x, %X 以十六进制打印无符号整数,%x为小写,%X为大写。
%s 打印以空字符结尾的字符串。
%c 打印单个字符。
%f 打印浮点数。
%% 打印字面“%”字符。

3.3 向printf()传递参数

printf() 传递值遵循x64调用约定。如果要显示嵌入了格式代码的字符串,基础字符串应作为第一个参数,其地址在RDI中。之后,要合并到字符串中的第一个值放在RSI中,第二个放在RDX中,依此类推。

以下是一个简单的 printf() 格式化示例程序 answer.asm

section .data
   answermsg db "The answer is %d ... or is it %d? No! It's 0x%x!",10,0
   answernum dd 42

section .bss

section .text

extern  printf

global  main

main:
    push rbp            ; Prolog
    mov rbp,rsp

    mov rax,0           ; Count of vector regs..here, 0

    mov rdi,answermsg   ; Message/format string goes in RDI
    mov rsi,[answernum] ; 2nd arg in RSI
    mov rdx,43          ; 3rd arg in RDX. You can use a numeric literal
    mov rcx,42          ; 4th arg in RCX. Show this one in hex
    mov rax,0           ; This tells printf no vector params are coming
    call printf         ; Call printf()

    pop rbp             ; Epilog

    ret                 ; Return from main() to shutdown code

运行 answer 程序,将看到:

The answer is 42 … or is it 43? No! It's 0x2a!

3.4 printf()的特殊要求

在使用 printf() 时,几乎在所有情况下(尤其是刚开始学习汇编时),应在调用 printf() 之前使用 MOV RAX,0 指令。RAX中的0告诉 printf() 函数没有通过向量寄存器传递浮点参数。当开始使用向量值时,需要在调用 printf() 之前将这些参数的数量放入RAX。

此外,在使用gcc作为链接器的程序的makefile中,会看到 –no-pie 选项。该选项用于防止gcc将程序链接为PIE(位置无关可执行文件)。PIE是一种防止某些代码攻击的方法,但会使调试变得复杂,所以在示例中不使用PIE。不过,当程序调试完成并正常工作后,应将其重新构建为PIE。

4. 使用fgets()和scanf()获取数据

4.1 使用fgets()读取文本

使用 SYSCALL 指令和 sys_read 内核调用从Linux键盘读取字符简单但不够灵活,标准C库提供了更好的方法。 gets() 函数虽然简单,但存在安全风险,因为它无法限制用户输入的字符数量,可能导致缓冲区溢出。

fgets() gets() 的替代函数,它具有内置的安全机制。使用 fgets() 时,需要传递一个文件句柄,由于在Unix系统中,键盘连接到标准输入文件 stdin ,我们可以将 fgets() 连接到 stdin 来从键盘读取文本。

使用 fgets() 的步骤如下:
1. 在程序的 .text 部分顶部,与其他外部声明一起声明 EXTERN fgets EXTERN stdin
2. 在 .bss 部分使用 RESB 指令声明一个足够大的缓冲区变量,以保存用户输入的字符串数据。
3. 将缓冲区的地址加载到RDI。
4. 将 fgets() 要接受的最大字符数加载到RSI,确保该值不大于在 .bss 中声明的缓冲区变量大小。
5. 使用 [stdin] stdin 的值加载到RDX。
6. 调用 fgets()

以下是一个使用 fgets() 的示例程序 fgetstest.asm

;   Use this makefile, after adding the required tabs:
;
;   fgetstest: fgetstest.o
;       gcc fgetstest.o -o fgetstest -no-pie
;   fgetstest.o: fgetstest.asm
;       nasm -f elf64 -g -F dwarf fgetstest.asm

SECTION .data       ; Section containing initialized data

message: db "You just entered: %s."

SECTION .bss        ; Section containing uninitialized data

testbuf: resb 20 
BUFLEN   equ $-testbuf

SECTION .text       ; Section containing code

extern printf
extern stdin
extern fgets

global main         ; Required so the linker can find the entry point

main:
    push rbp        ; Set up stack frame for debugger
    mov rbp,rsp

;;; Everything before this is boilerplate; use for all ordinary apps!

; Get a number of characters from the user:
    mov rdi,testbuf   ; Put address of buffer into RDI
    mov rsi,BUFLEN    ; Put # of chars to enter into RSI
    mov rdx,[stdin]   ; Put value of stdin into RDX
    call fgets        ; Call libc function for entering data

;Display the entered characters:
    mov rdi,message   ; Base string's address goes in RDI
    mov rsi,testbuf   ; Data entry buffer's address goes in RSI
    mov rax,0         ; Count of vector regs..here, 0
    call printf       ; Call libc function to display entered chars

;;; Everything after this is boilerplate; use for all ordinary apps!
    pop rbp           ; Epilog: Destroy stack frame before returning

    ret                  ; Return to glibc shutdown code

4.2 使用scanf()输入数值

scanf() 函数可以看作是 printf() 的反向操作,它从键盘读取字符数据并将其转换为存储在数值变量中的数值数据。

使用 scanf() 的步骤如下:
1. 在 .TEXT 部分顶部与其他外部声明一起声明 EXTERN scanf
2. 声明一个适当类型的内存变量,以保存 scanf() 读取和转换的数值数据。对于整数数据,可以使用 DQ RESQ 指令创建变量。
3. 调用 scanf() 输入单个值时,首先将指定数据格式的格式字符串的地址复制到RDI,对于整数,通常是字符串 %d
4. 将保存该值的内存变量的地址复制到RSI。
5. 将RAX清零,告诉 scanf() 在函数调用中没有传递向量寄存器参数。
6. 调用 scanf()

以下是一个示例程序 charsin.asm ,展示了如何设置提示信息并接受用户的字符串和数值数据输入:

;  Executable name : charsin
;  Version         : 3.0
;  Created date    : 11/19/2022
;  Last update     : 11/20/2022
;  Author          : Jeff Duntemann
;  Description     : A character input demo for Linux, using 
;                    NASM 2.14.02, incorporating calls to both 
;                    fgets() and scanf().
;
;  Build using these commands:
;    nasm -f elf64 -g -F dwarf charsin.asm
;    gcc charsin.o -o charsin -no-pie
;

[SECTION .data]         ; Section containing initialized data

SPrompt  db 'Enter string data, followed by Enter: ',0
IPrompt  db 'Enter an integer value, followed by Enter: ',0
IFormat  db '%d',0
SShow    db 'The string you entered was: %s',10,0
IShow    db 'The integer value you entered was: %5d',10,0

[SECTION .bss]          ; Section containing uninitialized data

IntVal   resq 1         ; Reserve an uninitialized double word
InString resb 128       ; Reserve 128 bytes for string entry buffer

[SECTION .text]         ; Section containing code

extern stdin            ; Standard file variable for input
extern fgets
extern printf
extern scan

global main             ; Required so linker can find entry point

main:
    push rbp            ; Prolog: Set up stack frame
    mov rbp,rsp

;;; Everything before this is boilerplate; use for all ordinary apps!

总结

通过以上内容,我们了解了汇编语言与C库函数的交互使用,包括栈对齐、使用 puts() 输出字符、使用 printf() 进行格式化文本输出以及使用 fgets() scanf() 获取数据。这些知识为我们在汇编编程中更灵活地处理输入输出提供了有力的支持。在实际应用中,我们可以根据具体需求选择合适的函数,并按照相应的步骤进行操作。

4.2 使用scanf()输入数值(续)

    ; Prompt for string input
    mov rdi,SPrompt
    mov rsi,InString
    mov rdx,[stdin]
    call fgets

    ; Prompt for integer input
    mov rdi,IPrompt
    call printf

    mov rdi,IFormat
    mov rsi,IntVal
    mov rax,0
    call scanf

    ; Display the entered string
    mov rdi,SShow
    mov rsi,InString
    mov rax,0
    call printf

    ; Display the entered integer
    mov rdi,IShow
    mov rsi,IntVal
    mov rax,0
    call printf

;;; Everything after this is boilerplate; use for all ordinary apps!
    pop rbp           ; Epilog: Destroy stack frame before returning
    ret                  ; Return to glibc shutdown code

在这个程序中,首先使用 fgets() 获取用户输入的字符串,然后使用 printf() 显示提示信息,接着使用 scanf() 获取用户输入的整数。最后,使用 printf() 显示用户输入的字符串和整数。

4.3 输入输出流程总结

下面是一个使用mermaid绘制的流程图,展示了使用 fgets() scanf() 进行输入,以及使用 printf() 进行输出的整体流程:

graph TD;
    A[开始] --> B[设置栈帧];
    B --> C[使用fgets()获取字符串];
    C --> D[使用printf()显示字符串提示];
    D --> E[使用scanf()获取整数];
    E --> F[使用printf()显示字符串输入];
    F --> G[使用printf()显示整数输入];
    G --> H[销毁栈帧];
    H --> I[返回];

5. 注意事项和其他补充说明

5.1 栈操作注意事项

在进行栈操作时,一定要确保栈指针的正确恢复。如果栈指针没有正确恢复,可能会导致程序崩溃或出现不可预期的行为。例如,在压入数据后,要记得通过 ADD RSP 指令或者 pop 指令将栈指针恢复到原来的位置。

5.2 格式化代码的灵活性

printf() scanf() 的格式化代码非常灵活,可以根据不同的需求进行组合和使用。例如,可以使用 %5d 来指定显示宽度,使用 %x 来显示十六进制数等。在实际使用中,可以根据具体的输出格式要求选择合适的格式化代码。

5.3 安全问题

在使用 fgets() scanf() 时,要注意输入数据的安全性。例如,在使用 fgets() 时,要确保指定的最大字符数不超过缓冲区的大小,避免缓冲区溢出。在使用 scanf() 时,也要确保输入的数据类型和格式与程序预期的一致,避免出现数据错误。

5.4 调试建议

在编写和调试使用C库函数的汇编程序时,可以使用调试工具(如GDB)来帮助定位问题。在程序中添加适当的调试信息,例如在关键步骤输出一些提示信息,有助于快速发现问题所在。

6. 总结与展望

6.1 总结

本文详细介绍了汇编语言与C库函数的交互使用,涵盖了栈对齐、字符输出( puts() printf() )以及数据输入( fgets() scanf() )等方面的内容。通过具体的代码示例和操作步骤,展示了如何在汇编程序中调用C库函数来实现各种功能。

6.2 展望

掌握汇编语言与C库函数的交互使用,为进一步开发复杂的程序奠定了基础。在未来的学习和实践中,可以尝试将这些知识应用到更大型的项目中,例如开发系统级程序、嵌入式系统等。同时,还可以深入研究C库函数的更多功能和用法,以及汇编语言与其他高级语言的混合编程,以提高编程效率和程序的性能。

以下是一个总结表格,概括了本文介绍的主要函数及其功能:
| 函数名 | 功能 | 使用步骤 |
| — | — | — |
| puts() | 将字符串输出到标准输出 | 1. 将字符串地址放入RDI;2. 调用 puts() |
| printf() | 格式化输出文本 | 1. 将格式字符串地址放入RDI;2. 依次将参数放入相应寄存器;3. 调用 printf() ;4. 调用前 MOV RAX,0 |
| fgets() | 从标准输入读取字符串 | 1. 声明 EXTERN fgets EXTERN stdin ;2. 声明缓冲区;3. 将缓冲区地址放入RDI;4. 将最大字符数放入RSI;5. 将 stdin 值放入RDX;6. 调用 fgets() |
| scanf() | 从标准输入读取数值 | 1. 声明 EXTERN scanf ;2. 声明存储变量;3. 将格式字符串地址放入RDI;4. 将存储变量地址放入RSI;5. 清零RAX;6. 调用 scanf() |

通过对这些函数的灵活运用,可以在汇编语言中实现丰富的输入输出功能,提高程序的实用性和交互性。

本项目构建于RASA开源架构之上,旨在实现一个具备多模态交互能力的智能对话系统。该系统的核心模块涵盖自然语言理解、语音转文本处理以及动态对话流程控制三个主要方面。 在自然语言理解层面,研究重点集中于增强连续对话中的用户目标判定效能,并运用深度神经网络技术提升关键信息提取的精确度。目标判定旨在解析用户话语背后的真实需求,从而生成恰当的反馈;信息提取则专注于从语音输入中析出具有特定意义的要素,例如个体名称、空间位置或时间节点等具体参数。深度神经网络的应用显著优化了这些功能的实现效果,相比经典算法,其能够解析更为复杂的语言结构,展现出更优的识别精度更强的适应性。通过分层特征学习机制,这类模型可深入捕捉语言数据中隐含的语义关联。 语音转文本处理模块承担将音频信号转化为结构化文本的关键任务。该技术的持续演进大幅提高了人机语音交互的自然度流畅性,使语音界面日益成为高效便捷的沟通渠道。 动态对话流程控制系统负责维持交互过程的连贯性逻辑性,包括话轮转换、上下文关联维护以及基于情境的决策生成。该系统需具备处理各类非常规输入的能力,例如用户使用非规范表达或对系统指引产生歧义的情况。 本系统适用于多种实际应用场景,如客户服务支持、个性化事务协助及智能教学辅导等。通过准确识别用户需求并提供对应信息或操作响应,系统能够创造连贯顺畅的交互体验。借助深度学习的自适应特性,系统还可持续优化语言模式理解能力,逐步完善对新兴表达方式用户偏好的适应机制。 在技术实施方面,RASA框架为系统开发提供了基础支撑。该框架专为构建对话式人工智能应用而设计,支持多语言环境并拥有活跃的技术社区。利用其内置工具集,开发者可高效实现复杂的对话逻辑设计部署流程。 配套资料可能包含补充学习文档、实例分析报告或实践指导手册,有助于使用者深入掌握系统原理应用方法。技术文档则详细说明了系统的安装步骤、参数配置及操作流程,确保用户能够顺利完成系统集成工作。项目主体代码及说明文件均存放于指定目录中,构成完整的解决方案体系。 总体而言,本项目整合了自然语言理解、语音信号处理深度学习技术,致力于打造能够进行复杂对话管理、精准需求解析高效信息提取的智能语音交互平台。 资源来源于网络分享,仅用于学习交流使用,请勿用于商业,如有侵权请联系我删除!
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值