汇编语言中的结构体与C流I/O函数使用
1. 结构体的使用
在汇编语言中,使用与C兼容的结构体是比较简单的。结构体是一种复合对象,可以包含不同类型的数据项。例如,我们有一个C结构体
Customer
:
struct Customer {
int id;
char name[64];
char address[64];
int balance;
};
假设我们知道结构体中每个项的偏移量,就可以使用汇编代码来访问客户数据。以下是一段示例代码:
mov rdi, 136
call malloc
mov [c], rax
mov [rax], dword 7
lea rdi, [rax+4]
lea rsi, [name]
call strcpy
mov rax, [c]
lea rdi, [rax+68]
lea rsi, [address]
call strcpy
mov rax, [c]
mov edx, [balance]
mov [rax+132], edx
上述代码中,首先分配了内存,然后设置了
id
,接着复制了
name
和
address
,最后设置了
balance
。
1.1 偏移量的符号名
使用具体的数字来表示结构体中的偏移量并不是理想的做法。因为对结构体的任何更改都需要修改代码,而且在计算偏移量时可能会出错。更好的方法是让yasm协助进行结构体定义。yasm中定义结构体的关键字是
struc
,结构体组件定义在
struc
和
ends true
之间。以下是
Customer
结构体的定义:
struc Customer
id resd 1
name resb 64
address resb 64
balance resd 1
endstruc
使用这种定义方式与使用
equ
来设置偏移量的符号名具有相同的效果。这些名字是全局可用的,因此不允许在多个结构体中使用相同的名字。可以在每个名字前加上一个点,例如:
struc Customer
.id resd 1
.name resb 64
.address resb 64
.balance resd 1
ends true
现在必须使用
Customer.id
来引用
id
字段的偏移量。一个好的折衷方案是在字段名前加上结构体名称的缩写。除了为偏移量提供符号名外,yasm还会将
Customer_size
定义为结构体的字节数,这使得为结构体分配内存变得容易。以下是一个从单独变量初始化结构体的程序:
segment .data
name db "Calvin", 0
address db " 12 Mockingbird Lane", 0
balance dd 12500
struc Customer
c_id resd 1
c_name resb 64
c_address resb 64
c_balance resd 1
endstruc
c dq 0
main:
segment .text
global main
extern malloc, strcpy
push rbp
mov rbp, rsp
sub rsp, 32
mov rdi, Customer_size
call malloc
mov [c], rax ; save the pointer
mov [rax+c_id], dword 7
lea rdi, [rax+c_name]
lea rsi, [name]
call strcpy
mov rax, [c] ; restore the pointer
lea rdi, [rax+c_address]
lea rsi, [address]
call strcpy
mov rax, [c] ; restore the pointer
mov edx, [balance]
mov [rax+c_balance], edx
xor eax, eax
leave
ret
需要注意的是,如果将
address
字段增大1个字节,可能会出现与C语言的对齐问题。在C语言中,
balance
的偏移量会从132增加到136,而在yasm中会从132增加到133。虽然仍然可以工作,但结构体定义与C语言的对齐方式不匹配。为了解决这个问题,必须在
c_balance
的定义之前加上
align 4
。
另一种方法是创建一个
Customer
类型的静态变量。如果要使用默认数据,可以这样做:
c
istruc Customer
iend
如果要定义字段,则按顺序定义它们。可以缩短字符串的数据:
c
istruc Customer
at c_id, dd 7
at c_name, db "Calvin", 0
at c_address, db " 12 Mockingbird Lane", 0
at c_balance, dd 12500
iend
1.2 分配和使用结构体数组
如果要分配结构体数组,需要将结构体的大小乘以元素的数量来分配足够的空间。但
Customer_size
给出的大小可能与C语言中
sizeof(struct Customer)
的值不匹配。C语言会将每个数据项对齐到适当的边界,并报告一个大小,使得数组的每个元素都有对齐的字段。可以通过添加
align X
来帮助yasm,其中
X
表示结构体中最大数据项的大小。如果结构体有任何四字(quad word)字段,则需要
align 8
来强制
_size
值是8的倍数;如果结构体没有四字字节字段但有一些双字(double word)字段,则需要
align 4
;如果有任何字(word)字段,则可能需要
align 2
。以下是声明结构体并分配数组的代码:
segment .data
struc Customer
c_id resd 1 ; 4 bytes
c_name resb 65 ; 69 bytes
c_address resb 65 ; 134 bytes
align 4 ; aligns to 136
c_balance resd 1 ; 140 bytes
c_rank resb 1 ; 141 bytes
align 4 ; aligns to 144
endstruc
customers dq 0
segment .text
mov edi, 100 ; for 100 structs
mul edi, Customer_size
call malloc
mov [customers], rax
要处理数组的每个元素,可以从一个保存
customers
值的寄存器开始,处理完每个客户后,将
Customer_size
加到该寄存器上。以下是一个示例:
segment .data
format db "%s %s %d", 0x0a, 0
segment .text
push r15
push r14
mov r15, 100 ; counter saved through calls
mov r14, [customers] ; pointer saved through calls
more:
lea edi, [format]
lea esi, [r14+c_name]
lea edx, [r14+c_address]
mov rcx, [r14+c_balance]
call printf
add r14, Customer_size
sub r15, 1
jnz more
pop r14
pop r15
ret
2. 结构体相关练习
- 练习1 :设计一个结构体来表示一个集合。该结构体将保存集合的最大大小和一个指向数组的指针,数组中每个可能的元素用1位表示。集合的成员将是从0到集合大小减1的整数。编写一个测试程序来读取对集合进行操作的命令,命令包括 “add”、”remove” 和 “test”,每个命令都有一个整数参数。程序应能够向集合中添加元素、从集合中移除元素以及测试数字是否属于集合。
- 练习2 :使用练习1中集合的设计,编写一个程序来操作多个集合。实现命令 “add”、”union”、”print” 和 “intersect”。创建10个大小为10000的集合。”add s k” 将把k添加到集合s中;”union s t” 将用s和t的并集替换集合s;”intersect s t” 将用s和t的交集替换集合s;”print s” 将打印集合s的元素。
- 练习3 :设计一个结构体来表示大整数。为了简单起见,使用四字数组作为大整数的数据。每个四字将表示数字的18位。因此,1个四字可以存储高达999,999,999,999,999,999的数字,2个四字可以存储高达999,999,999,999,999,999,999,999,999,999,999,999的数字。仅实现正数,实现加法和乘法(基于加法),计算50!。可以用C或C++编写一个主程序,使用汇编代码来表示所有长算术运算。
3. C流I/O函数的使用
从C语言中可调用的函数包括许多领域的各种函数,如进程管理、文件处理、网络通信、字符串处理和图形编程等。流输入和输出设施是一个高级库的示例,在许多程序中非常有用。
在之前关于系统调用的讨论中,我们关注的是
open
、
read
、
write
和
close
,它们只是系统调用的包装函数。在这部分内容中,我们将关注一组类似的执行缓冲I/O的函数。
3.1 缓冲I/O的优势
使用缓冲I/O系统进行读取可能更高效。假设要求缓冲I/O系统读取1字节,它会尝试从已读取数据的缓冲区中读取1字节。如果必须进行读取,它会读取足够的字节来填充其缓冲区 - 通常是8192字节。这意味着8192次1字节的读取可以通过1次实际的系统调用满足。从缓冲区读取字节非常快,实际上,使用C流的
getchar
函数一次读取1字节来读取大文件比使用
read
函数一次读取1字节快20倍以上。
需要注意的是,操作系统也会为打开的文件使用缓冲区。当调用
read
读取1字节时,操作系统会被磁盘驱动器强制读取完整的扇区,因此至少必须读取1个扇区(可能是512字节)。最有可能的是,操作系统读取4096字节并保存已读取的数据以便使用。如果操作系统不使用缓冲区,一次读取1字节将需要为每个字节与磁盘进行交互,这可能比使用缓冲区慢10到20倍。
综上所述,如果程序需要读写少量数据,使用流I/O设施会比使用系统调用更快。通常可以使用系统调用并进行自己的缓冲,以满足特定需求,从而节省时间,但这样做需要付出更多的努力。必须权衡提高性能的重要性与增加的工作量。
3.2 打开文件
使用流I/O函数打开文件的函数是
fopen
。与其他流I/O函数一样,它以字母 “f” 开头,以使其名称与它类似的系统调用包装函数区分开来。
fopen
的原型是:
FILE *fopen(char *pathname, char *mode);
第一个参数指定要打开的文件的名称,第二个参数指定打开模式。模式可以是以下值之一:
| 模式 | 描述 |
| ---- | ---- |
| r | 只读模式 |
| r+ | 读写模式 |
| w | 只写模式,截断或创建文件 |
| w+ | 读写模式,截断或创建文件 |
| a | 只写模式,追加或创建文件 |
| a+ | 读写模式,追加或创建文件 |
返回值是一个指向
FILE
对象的指针。这是一个不透明的指针,意味着不需要知道
FILE
对象的组件。最有可能的是,
FILE
对象是一个结构体,包含指向文件缓冲区的指针和关于文件的各种“内务管理”数据项。这个指针用于其他流I/O函数。在汇编语言中,只需将指针存储在一个四字中,并在需要进行函数调用时使用该四字即可。以下是一段打开文件的代码:
segment .data
name db "customers.dat", 0
mode db "w+", 0
fp dq 0
segment .text
global fopen
lea rdi, [name]
lea rsi, [mode]
call fopen
mov [fp], rax
下面是一个简单的mermaid流程图,展示了打开文件的基本流程:
graph TD;
A[开始] --> B[设置文件名和模式];
B --> C[调用fopen函数];
C --> D{是否成功打开};
D -- 是 --> E[保存文件指针];
D -- 否 --> F[处理错误];
E --> G[结束];
F --> G;
3.3 fscanf和fprintf
之前的代码中已经遇到过
scanf
和
printf
。
scanf
是一个函数,它将名为
stdin
的
FILE
指针作为第一个参数调用
fscanf
,而
printf
是一个函数,它将
stdout
作为第一个参数调用
fprintf
。这两对函数的唯一区别是
fscanf
和
fprintf
可以处理任何
FILE
指针。它们的原型是:
int fscanf(FILE *fp, char *format, ...);
int fprintf(FILE *fp, char *format, ...);
对于简单的使用,可以参考相关文档中关于
scanf
和
printf
的讨论。如需更多信息,可以使用
man fscanf
或
man fprintf
命令查看,或者查阅C语言书籍。
3.4 fgetc和fputc
如果需要逐字符处理数据,使用
fgetc
读取字符和
fputc
写入字符会很方便。它们的原型是:
int fgetc(FILE *fp);
int fputc(int c, FILE *fp);
fgetc
的返回值是已读取的字符,除非遇到文件结束或错误,此时它返回符号值
EOF
(即 -1)。
fputc
将字符
c
写入文件,除非发生错误,否则返回它所写入的相同字符,发生错误时返回
EOF
。
通常,获取一个字符并根据读取的字符执行某些操作是很方便的。对于某些字符,可能需要将控制权交给另一个函数。可以使用
ungetc
将字符放回文件流中,这样可以简化操作。
ungetc
保证只能放回1个字符,但有1个字符的前瞻功能可能非常有用。
ungetc
的原型是:
int ungetc(int c, FILE *fp);
以下是一个使用
fgetc
和
fputc
将文件从一个流复制到另一个流的循环:
more:
mov rdi, [ifp] ; input file pointer
call fgetc
cmp eax, -1
je done
mov rdi, rax
mov rsi, [ofp] ; output file pointer
call fputc
jmp more
done:
下面是这个文件复制过程的mermaid流程图:
graph TD;
A[开始] --> B[设置输入和输出文件指针];
B --> C[调用fgetc读取字符];
C --> D{是否到达文件末尾};
D -- 是 --> E[结束];
D -- 否 --> F[调用fputc写入字符];
F --> C;
3.5 fgets和fputs
另一个常见的需求是逐行读取输入并逐行处理。
fgets
函数读取1行文本(如果数组太小则读取更少),
fputs
函数写入1行文本。它们的原型是:
char *fgets(char *S, int size, FILE *fp);
int fputs(char *s, FILE *fp);
fgets
的第一个参数是一个字符数组,用于接收数据行,第二个参数是数组的大小。传递大小参数是为了防止缓冲区溢出。
fgets
将最多读取
size - 1
个字符到数组中,当遇到换行符或文件结束时停止读取。如果读取到换行符,它会将换行符存储在缓冲区中。无论是否读取到完整的行,
fgets
总是在读取的数据末尾放置一个0字节。成功时返回
s
,发生错误或文件结束时返回
NULL
指针。
fputs
将字符串
s
写入文件,但不包括字符串末尾的0字节。需要自己在数组中放置任何所需的换行符,并在末尾添加0字节。成功时返回一个非负数,发生错误时返回
EOF
。
在
fgets
之后使用
sscanf
从数组中读取数据可能非常有用。
sscanf
类似于
scanf
,只是第一个参数是一个字符数组,它会尝试以与
scanf
相同的方式转换数据。使用这种模式可以有机会用
sscanf
读取数据,确定数据不是预期的,然后用不同的格式字符串再次使用
sscanf
读取。
以下是一段代码,用于将文本行从一个流复制到另一个流,跳过以 “;” 开头的行:
more:
lea rdi, [s]
mov esi, 200
mov rdx, [ifp]
call fgets
cmp rax, 0
je done
mov al, [s]
cmp al, ';'
je more
lea rdi, [s]
mov rsi, [ofp]
call fputs
jmp more
done:
3.6 fread和fwrite
fread
和
fwrite
函数用于读写数据数组。它们的原型是:
int fread(void *p, int size, int nelts, FILE *fp);
int fwrite(void *p, int size, int nelts, FILE *fp);
这两个函数的第一个参数是任何类型的数组,第二个参数是数组中每个元素的大小,第三个参数是要读写的数组元素的数量。它们返回实际读写的数组元素的数量。在发生错误或文件结束时,返回值可能小于
nelts
或为0。
以下是一段代码,用于将
customers
数组的所有100个元素写入磁盘文件:
mov rdi, [customers] ; allocated array
mov esi, Customer_size
mov edx, 100
mov rcx, [fp]
call fwrite
3.7 fseek和ftell
使用
fseek
函数可以定位流的位置,而
ftell
函数用于确定当前位置。它们的原型是:
int fseek(FILE *fp, long offset, int whence);
long ftell(FILE *fp);
fseek
的第二个参数
offset
是一个字节位置值,其含义取决于第三个参数
whence
。
whence
的含义与
lseek
中的完全相同。如果
whence
为0,则
offset
是字节位置;如果
whence
为1,则
offset
是相对于当前位置的偏移量;如果
whence
为2,则
offset
是相对于文件末尾的偏移量。
fseek
成功时返回0,发生错误时返回 -1。如果发生错误,
errno
变量会被适当设置。
ftell
返回文件中的当前字节位置,除非发生错误,发生错误时返回 -1。
以下是一个将
Customer
记录写入文件的函数:
void write_customer(FILE *fp, struct Customer *C, int record_number);
segment .text
global write_customer
write_customer:
.fp equ 0
.c equ 8
.rec equ 16
push rbp
mov rbp, rsp
sub rsp, 32
mov [rsp+.fp], rdi
mov [rsp+.c], rsi
mov [rsp+.rec], rdx
mov rdx, Customer_size
mul rdx
mov rsi, rdx
mov rdx, 0 ; 2nd parameter to ftell; whence
call ftell
mov rdi, [rsp+.c]
mov rsi, Customer_size
mov rdx, 1
mov rcx, [rsp+.fp]
call fwrite
leave
ret
3.8 fclose
fclose
用于关闭流。这很重要,因为流的缓冲区中可能有需要写入的数据。调用
fclose
时会写入这些数据,如果不调用它,这些数据将被遗忘。
4. C流I/O函数相关练习
-
练习1
:编写一个汇编程序,使用本章中的结构体定义创建一个新的
Customer。程序应提示并读取文件名、客户姓名、地址、余额和等级字段。然后代码应扫描文件中的数据,查找空位置。空位置是id字段为0的记录。一般来说,记录的id值比记录号大1。如果没有空记录,则在文件末尾添加一条新记录。报告客户的id。 -
练习2
:编写一个汇编程序,更新客户的余额。程序应从命令行接受数据文件的名称、客户
id和要添加到该客户余额的金额。客户的id比记录号大1。如果客户记录未使用(id = 0),则报告错误。 -
练习3
:编写一个汇编程序,读取文件中的客户数据,按余额排序并按余额递增顺序打印数据。应打开文件并使用
fseek定位到文件末尾,使用ftell确定文件中的记录数。应分配一个足够大的数组来容纳整个文件,一次读取一条记录,跳过未使用的记录(id = 0)。然后使用qsort进行排序。可以使用以下方式调用qsort:
qsort(struct Customer *C, int count, int size, compare);
count
参数是要排序的结构体数量,
size
是每个结构体的字节大小。
compare
参数是一个函数的地址,该函数接受2个参数,每个参数都是指向
struct Customer
的指针。该函数将比较两个结构体的
balance
字段,并根据两个余额的顺序返回一个负数、0或正数。
汇编语言中的结构体与C流I/O函数使用(续)
5. 结构体与C流I/O函数的综合应用思路
在实际编程中,结构体和C流I/O函数常常结合使用。例如,我们可以将结构体数组存储到文件中,之后再从文件中读取这些数据进行处理。下面我们以
Customer
结构体为例,详细阐述综合应用的步骤。
5.1 存储结构体数组到文件
要将
Customer
结构体数组存储到文件中,可按以下步骤操作:
1.
分配内存
:为结构体数组分配足够的内存。
2.
初始化结构体数组
:给结构体数组的每个元素赋值。
3.
打开文件
:使用
fopen
函数以写入模式打开文件。
4.
写入数据
:使用
fwrite
函数将结构体数组写入文件。
5.
关闭文件
:使用
fclose
函数关闭文件。
以下是示例代码:
segment .data
struc Customer
c_id resd 1
c_name resb 64
c_address resb 64
c_balance resd 1
endstruc
customers dq 0
filename db "customers.dat", 0
mode db "w+", 0
fp dq 0
segment .text
global main
extern malloc, fopen, fwrite, fclose
main:
; 分配内存
mov edi, 10 ; 假设10个结构体
mul edi, Customer_size
call malloc
mov [customers], rax
; 初始化结构体数组(这里简单示例,可根据需求修改)
mov rcx, 10
mov rsi, [customers]
init_loop:
mov [rsi+c_id], dword rcx
lea rdi, [rsi+c_name]
mov rdx, 64
mov al, 'A'
rep stosb ; 简单填充名字
lea rdi, [rsi+c_address]
mov rdx, 64
mov al, 'B'
rep stosb ; 简单填充地址
mov [rsi+c_balance], dword rcx * 100
add rsi, Customer_size
loop init_loop
; 打开文件
lea rdi, [filename]
lea rsi, [mode]
call fopen
mov [fp], rax
; 写入数据
mov rdi, [customers]
mov esi, Customer_size
mov edx, 10
mov rcx, [fp]
call fwrite
; 关闭文件
mov rdi, [fp]
call fclose
; 释放内存
mov rdi, [customers]
call free
ret
5.2 从文件读取结构体数组
从文件读取
Customer
结构体数组的步骤如下:
1.
打开文件
:使用
fopen
函数以读取模式打开文件。
2.
获取文件大小
:使用
fseek
和
ftell
函数确定文件中的记录数。
3.
分配内存
:为结构体数组分配足够的内存。
4.
读取数据
:使用
fread
函数从文件中读取结构体数组。
5.
关闭文件
:使用
fclose
函数关闭文件。
以下是示例代码:
segment .data
struc Customer
c_id resd 1
c_name resb 64
c_address resb 64
c_balance resd 1
endstruc
customers dq 0
filename db "customers.dat", 0
mode db "r", 0
fp dq 0
segment .text
global main
extern malloc, fopen, fseek, ftell, fread, fclose
main:
; 打开文件
lea rdi, [filename]
lea rsi, [mode]
call fopen
mov [fp], rax
; 获取文件大小
mov rdi, [fp]
mov rsi, 0
mov rdx, 2 ; SEEK_END
call fseek
mov rdi, [fp]
call ftell
mov rcx, rax
mov rdx, Customer_size
div rdx ; 计算记录数
mov r12, rax ; 保存记录数
; 分配内存
mov rdi, r12
mul rdi, Customer_size
call malloc
mov [customers], rax
; 读取数据
mov rdi, [customers]
mov esi, Customer_size
mov edx, r12
mov rcx, [fp]
call fread
; 关闭文件
mov rdi, [fp]
call fclose
; 处理读取的数据(这里简单示例,可根据需求修改)
mov rcx, r12
mov rsi, [customers]
process_loop:
mov eax, [rsi+c_id]
; 可在此处添加更多处理逻辑
add rsi, Customer_size
loop process_loop
; 释放内存
mov rdi, [customers]
call free
ret
6. 性能优化建议
在使用结构体和C流I/O函数时,为了提高性能,可以考虑以下几点:
6.1 结构体对齐
确保结构体的对齐方式与C语言一致,避免因对齐问题导致的性能损失。可以使用
align
指令来实现对齐。例如:
struc Customer
c_id resd 1 ; 4 bytes
c_name resb 64 ; 69 bytes
c_address resb 64 ; 134 bytes
align 4 ; aligns to 136
c_balance resd 1 ; 140 bytes
c_rank resb 1 ; 141 bytes
align 4 ; aligns to 144
endstruc
6.2 缓冲I/O的合理使用
尽量使用缓冲I/O函数,如
fread
和
fwrite
,而不是系统调用的
read
和
write
。缓冲I/O可以减少与磁盘的交互次数,提高读写效率。例如,在读取大文件时,使用
fread
一次性读取多个元素,而不是逐个字节读取。
6.3 内存管理
合理分配和释放内存,避免内存泄漏。在使用
malloc
分配内存后,确保在不再使用时使用
free
释放内存。例如:
mov rdi, [customers]
call free
7. 总结
通过本文的介绍,我们了解了汇编语言中结构体和C流I/O函数的使用方法。结构体可以方便地组织不同类型的数据,而C流I/O函数提供了高效的文件读写功能。在实际编程中,我们可以将两者结合使用,实现数据的存储和处理。
同时,我们还介绍了一些性能优化的建议,如结构体对齐、缓冲I/O的合理使用和内存管理等。遵循这些建议可以提高程序的性能和稳定性。
希望本文能帮助你更好地掌握汇编语言中结构体和C流I/O函数的使用,在实际项目中发挥更大的作用。
以下是一个简单的mermaid流程图,展示了结构体数组存储和读取的综合流程:
graph LR;
A[开始] --> B{选择操作};
B -- 存储 --> C[分配内存];
B -- 读取 --> D[打开文件];
C --> E[初始化结构体数组];
E --> F[打开文件];
F --> G[写入数据];
G --> H[关闭文件];
H --> I[释放内存];
D --> J[获取文件大小];
J --> K[分配内存];
K --> L[读取数据];
L --> M[关闭文件];
M --> N[处理数据];
N --> O[释放内存];
I --> P[结束];
O --> P;
相关练习回顾
-
结构体相关练习
:
- 设计集合结构体并编写操作集合的程序,实现添加、移除和测试元素的功能。
- 使用集合结构体操作多个集合,实现并集、交集和打印元素等功能。
- 设计大整数结构体,实现加法和乘法运算,并计算50!。
-
C流I/O函数相关练习
:
-
编写程序创建新的
Customer,扫描文件查找空位置并添加记录。 - 编写程序更新客户的余额,处理客户记录未使用的情况。
- 编写程序读取客户数据,按余额排序并打印。
-
编写程序创建新的
通过完成这些练习,可以进一步巩固对结构体和C流I/O函数的理解和掌握。
超级会员免费看
15万+

被折叠的 条评论
为什么被折叠?



