- 写入记录
- 读取记录
- 修改记录
- 彩蛋
- 参考
作者:高玉涵
时间:2021.11.16 09:30
博客:blog.csdn.net/cg_i
环境:Linux 7e142849497c 5.10.47-linuxkit #1 SMP Sat Jul 3 21:51:47 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
如前一章所述,许多应用程序处理持久性数据,即通过将数据存储在文件中,使数据的寿命比程序长。你可以关闭程序,然后再打开,然后回到之前打开的地方。现在,存在两种持久性数据:结构化的和非结构化的。非结构化数据就如同我们在 toupper 程序中所处理的数据,仅处理某人输入的文本文件。文件内容对于程序来说不可用,因为程序无法解读用户试图通过无序文本表达的内容。
结构化数据则正好相反,是计算机擅长处理的数据。结构化数据是拆分为字段和记录的数据,且绝大部分为固定长度的字段和记录。由于数据划分为固定长度的记录和固定格式字段,计算机就能解读数据。结构化数据可包含变长字段,但在此情况下,你最好使用数据库。
本章涉及读写固定长度的简单记录。比如,我们想要存储一些认识的人的基本信息,可以设想关于他们的以下固定长度的示例记录:
- 姓——40 字节
- 名——40 字节
- 地址——240 字节
- 年龄——4 字节
本例中,除了年龄是使用 4 字节的数字字段(我们可以只用单个字节,但使用一个字更便于处理),其他所有字段都是字符数据。
在编程过程中,某些定义是你经常会在一个或多个程序中反复使用的。你最好将这些定义分别独立存放入文件,这些文件仅仅在需要时包含在汇编语言文件中。例如,在下面几个程序中,我们将访问上述记录的不同部分。这意味着为了用基址寻址方式访问各字段,我们需要各字段相对于记录起始处的偏移量。以下常量描述了上述结构的各字段偏移量。将这些常量放入名为 record-def.s 的文件:
.equ RECORD_FIRSTNAME, 0 .equ RECORD_LASTNAME, 40 .equ RECORD_ADDRESS, 80 .equ RECORD_AGE, 320 .equ RECORD_SIZE, 324
此外,有几个常量我们在程序中一再定义,因此将其放入某个文件是很有用的,这样就不必总是重复输入它们。这里将其放入名为 linux.s 的文件 参见 1.2。
# Linux 常量定义 # 系统调用号 .equ SYS_EXIT, 1 .equ SYS_READ, 3 .equ SYS_WRITE, 4 .equ SYS_OPEN, 5 .equ SYS_CLOSE, 6 .equ SYS_BRK, 45 # 系统调用中断号 .equ LINUX_SYSCALL, 0x80 # 标准文件描述符 .equ STDIN, 0 .equ STDOUT, 1 .equ STDERR, 2 # 能用状态码 .equ END_OF_FILE, 0
我们将采用 record-def.s 中定义的结构编写本章的 3 个程序。第一个程序将生成包含几个如上定义记录的文件。第二个程序将显示文件中的记录。第三个程序将每个记录中的年龄增加一岁。
除了将在所有这些程序中使用的标准常量,我们还要在其中几个程序中使用两个函数:一个用用于读记录,一个用于写记录。
这两个函数需要哪些参数才能工作呢?我们大致需要:
- 能将记录读入的缓冲区位置;
- 想写入或从中读取的文件的文件描述符。
先来看读取函数:
.include "record-def.s" .include "linux.s" # 目的:此函数从文件描述符读取一条记录 # # 输入:文件描述符及缓冲区 # # 输出:本函数将数据读取缓冲区并返回状态码 # # 栈局部变量 .equ ST_READ_BUFFER, 8 .equ ST_FILEDES, 12 .section .text .globl read_record .type read_record, @function read_record: pushl %ebp movl %esp, %ebp pushl %ebx movl ST_FILEDES(%ebp), %ebx # 文件描述符放入 %ebx movl ST_READ_BUFFER(%ebp), %ecx # 读取数据存储位置放入 %ecx movl $RECORD_SIZE, %edx # 缓冲区大小 movl $SYS_READ, %eax int $LINUX_SYSCALL # 读取缓冲区大小返回到 %eax 中 # 注意:%eax 中含返回值,我们将该值传回调用程序 popl %ebx movl %ebp, %esp popl %ebp ret
这是个相当简单的函数,只是从给定的文件描述符读取特定结构大小的数据,放入相应大小的缓冲区。而写函数与之类似:
.include "linux.s" .include "record-def.s" # 目的:本函数将一条记录写入给定文件描述符 # # 输入:文件描述符和缓冲区 # # 输出:本函数将数据写入缓冲区并返回状态码 # # 栈局部变量 .equ ST_WRITE_BUFFER, 8 .equ ST_FILEDES, 12 .section .text .globl write_record .type write_record, @function write_record: pushl %ebp movl %esp, %ebp pushl %ebx movl $SYS_WRITE, %eax movl ST_FILEDES(%ebp), %ebx # 写入文件描述符放入 %ebx movl ST_WRITE_BUFFER(%ebp), %ecx # 缓冲区位置 movl $RECORD_SIZE, %edx # 写入的大小 int $LINUX_SYSCALL # 注意:%eax含返回值,我们将之传回调用程序 popl %ebx movl %ebp, %esp popl %ebp ret
现在我们已经有了基本定义,可以开始写程序了。
写入记录这个程序简单地将一些硬编码记录写入磁盘。具体来讲,程序将:
- 打开文件;
- 写 3 条记录;
- 关闭文件。
输入以下代码到文件 write-records.s :
.include "linux.s" .include "record-def.s" .section .data # 我们想写入的常量数据 # 每个数据项以空字节(0)填充到适当的长度 # # .rept 用于填充每一项。 # .rept 告诉汇编程序将 .rept 和 .endr 之间的段重复指定次数 # 在这个程序中,此指令用于将多余的空白字符增加到每个字段未尾以将之填满 # record1: .ascii "Fredrick " .rept 31 # 填充到 40 字节 .byte 0 .endr .ascii "Bartlett " .rept 31 # 填充到 40 字节 .byte 0 .endr .ascii "4242 S PraireitnTulsa, OK 55555 " .rept 209 # 填充到 240 字节 .byte 0 .endr .long 45 record2: .ascii "Marilyn " .rept 32 # 填充到 40 字节 .byte 0 .endr .ascii "Taylor " .rept 33 # 填充到 40 字节 .byte 0 .endr .ascii "2224 S Johannan StnChicago, IL 12345 " .rept 203 # 填充到 240 字节 .byte 0 .endr .long 29 record3: .ascii "Derrick " .rept 32 # 填充到 40 字节 .byte 0 .endr .ascii "McIntire " .rept 31 # 填充到 40 字节 .byte 0 .endr .ascii "500 W OaklandnSan Diego, CA 54321 " .rept 206 # 填充到 240 字节 .byte 0 .endr .long 36 # 这是我们要写入文件的文件名 file_name: .ascii "test.dat " .equ ST_FILE_DEscriptOR, -4 .globl _start _start: # 复制栈指针到 %ebp movl %esp, %ebp # 为文件描述符分配空间 subl $4, %esp # 打开文件 movl $SYS_OPEN, %eax movl $file_name, %ebx movl $0101, %ecx # 本指令表明如文件不存在则创建,并打开文件用于写入 movl $0666, %edx int $LINUX_SYSCALL # 存储文件描述符 movl %eax, ST_FILE_DEscriptOR(%ebp) # 写第一条记录 pushl ST_FILE_DEscriptOR(%ebp) pushl $record1 call write_record addl $8, %esp # 重新指向文件描述符 # 写第二条记录 pushl ST_FILE_DEscriptOR(%ebp) pushl $record2 call write_record addl $8, %esp # 写第三条记录 pushl ST_FILE_DEscriptOR(%ebp) pushl $record3 call write_record addl $8, %esp # 关闭文件描述符 movl $SYS_CLOSE, %eax movl ST_FILE_DEscriptOR(%ebp), %ebx int $LINUX_SYSCALL # 退出程序 movl $SYS_EXIT, %eax movl $0, %ebx int $LINUX_SYSCALL
这是一个相当简单的程序,仅定义要写入 .data 段的数据,以及适当的系统调用和函数调用来完成工具。要复习所有用到过的系统调用,请参见《Linux 下用汇编语言处理文件》。
为了生成应用程序,我们运行以下命令:
as --gstabs --32 write-records.s -o write-records.o as --gstabs --32 write-record.s -o write-record.o ld -m elf_i386 write-record.o write-records.o -o write-records
目前我们分别汇编两个文件,然后用链接器将之合并。要运行程序,请输入命令:
./write-records
这条命令会创建一个包含记录的 test.dat 文件。但是,由于记录包含非打印字符(即空字符),可能无法通过文本编辑器查看。因此,我们需要下一个程序来为我们读取记录。下面是文件部分内容:
[root@7e142849497c:~/html/record# xxd -b test.dat 00000000: 01000110 01110010 01100101 01100100 01110010 01101001 Fredri 00000006: 01100011 01101011 00000000 00000000 00000000 00000000 ck.... 0000000c: 00000000 00000000 00000000 00000000 00000000 00000000 ...... 00000012: 00000000 00000000 00000000 00000000 00000000 00000000 ...... 00000018: 00000000 00000000 00000000 00000000 00000000 00000000 ...... 0000001e: 00000000 00000000 00000000 00000000 00000000 00000000 ...... 00000024: 00000000 00000000 00000000 00000000 01000010 01100001 ....Ba 0000002a: 01110010 01110100 01101100 01100101 01110100 01110100 rtlett 00000030: 00000000 00000000 00000000 00000000 00000000 00000000 ...... 00000036: 00000000 00000000 00000000 00000000 00000000 00000000 ...... 0000003c: 00000000 00000000 00000000 00000000 00000000 00000000 ...... 00000042: 00000000 00000000 00000000 00000000 00000000 00000000 ...... 00000048: 00000000 00000000 00000000 00000000 00000000 00000000 ......读取记录
现在,我们将考虑读取记录的过程。这个程序将读取每个记录,并显示每条记录中的名。
由于每个人的姓名长度不同,我们需要一个函数来计算要写入的字符数。由于我们用空字符填充每个字段,因此只需对空字符之前的字符计数。注意,这意味着每条记录都必须包含至少一个空字符。
# 目的:对字符进行计数,直到遇到空字符 # # 输入:字符串地址 # # 输出:将计数值返回到 %eax # # 过程: # 用到的寄存器: # %ecx - 字符计数 # %al - 当前字符 # %edx - 当前字符地址 # .type count_chars, @function .globl count_chars # 这是我们的一个参数在栈上的位置 .equ ST_STRING_START_ADDRESS, 8 count_chars: pushl %ebp movl %esp, %ebp # 计数是从 0 开始 movl $0, %ecx # 数据的起始地址 movl ST_STRING_START_ADDRESS(%ebp), %edx count_loop_begin: # 获取当前字符 movb (%edx), %al # 是否为空字符? cmpb $0, %al # 若为空字符则结束 je count_loop_end # 否则,递增计数器和指针 incl %ecx incl %edx # 返回循环起始处 jmp count_loop_begin count_loop_end: # 结束循环,将计数值移入 %eax 并返回 movl %ecx, %eax popl %ebp ret
正如你所看到的,这是一个相当简单的函数,只是遍历所有字节并计数,直到遇到空字符。然后,它返回计数值。
我们的记录读取程序也是相当简单的。程序将完成如下步骤:
-
打开文件;
-
尝试读取一条记录;
-
若到达文件结束处则退出,否则计算名的字符数;
-
将名写到 STDOUT;
-
写一个换行符到 STDOUT;
-
返回并读取另一条记录。
为了编写此程序,我们需要另一个简单函数——写一个换行符到 STDOUT 的函数。将下面代码放置到 write-newlines.s 文件中。
.include 'linux.s' .globl write_newline .type write_newline, @function .section .data newline: .ascii 'n' .section .text .equ ST_FILEDS, 8 write_newline: pushl %ebp movl %esp, %ebp movl $SYS_WRITE, %eax movl ST_FILEDS(%ebp), %ebx movl %newline, %ecx movl $1, %edx int $linux_syscall movl %ebp, %esp popl %ebp ret
现在,我们准备编写主程序,read-records.s 的代码如下:
.include 'linux.s' .include 'record-def.s' .section .data file_name: .ascii 'test.dat ' .section .bss .lcomm record_buffer, RECORD_SIZE .section .text # 主程序 .globl _start _start # 这些是我们将存储输入输出描述符的栈位置(仅供参考:也可以用一个 .data 段中的内在地址代替) .equ ST_INPUT_DEscriptOR, -4 .equ ST_OUTPUT_DEscriptOR, -8 # 复制栈指针到 %ebp movl %esp, %ebp # 为保存文件描述符分配空间 subl $8, %esp # 打开文件 movl $SYS_OPEN, %eax movl $file_name, %ebx movl $0, %ecx # 表示只读文件 movl $0666, %edx # 表示所有者、组和所有用户对文件都有读/写权限。 int $LINUX_SYSCALL # 保存文件描述符 movl %eax, ST_INPUT_DEscriptOR(%ebp) # 即使输出文件描述符是常数,我们也将其保存在本地变量,这样 # 如果稍后决定不将其输出到 STDOUT,很容易加以更改 movl $STDOUT, ST_OUTPUT_DEscriptOR(%ebp) record_read_loop: pushl ST_INPUT_DEscriptOR(%ebp) # 把参数压入栈 pushl $record_buffer call read_record addl $8, %esp # 返回读取的字节数 # 如果字节数与我们请求的字节数不同,说明已到达文件结束处或出现错误, # 我们就要退出 cmpl $RECORD_SIZE, %eax jne finished_reading # 否则,打印出名,但我们首先必须知道名的大小 pushl $RECORD_FIRSTNAME + record_buffer call count_chars addl $4, %esp movl %eax, %edx movl ST_OUTPUT_DEscriptOR(%ebp), %ebx movl $SYS_WRITE, %eax movl $RECORD_FIRSTNAME + record_buffer, %ecx int $LINUX_SYSCALL pushl ST_OUTPUT_DEscriptOR(%ebp) call write_newline addl $4, %esp jmp record_read_loop finished_reading: movl $SYS_EXIT, %eax movl $0, %ebx int $LINUX_SYSCALL
要生成这个程序,我们要汇编其所有组成文件并链接它们:
as --gstabs --32 read_record.s -o read-record.o as --gstabs --32 count-chars.s -o count-chars.o as --gstabs --32 write-newline.s -o write-newline.o as --gstabs --32 read-records.s -o read-records.o ld -m elf_i386 read-record.o count-chars.o write-newline.o read-records.o -o read-records
你可以通过命令 ./read-records读取记录。
root@7e142849497c:~/html/record# ./read-records Fredrick Marilyn Derrick root@7e142849497c:~/html/record#
如上所述,这个程序打开该文件,然后运行用于读取的循环,检查文件是否结束,并写入名。也许对于你来说,下面一行代码中存在新结构:
pushl $RECORD_FIRSTNAME + record_buffer
它看起来就像我们把 add 指令和 pushl 结合起来一样,但实际上并非如此。你看, RECORD_FIRSTNAME 和 record_buffer 都是常数,前者是直接常数,通过使用 .equ 指令创建,后者则是由汇编程序自动定义为标签(其值是紧随其后的数据的起始地址)。由于两者都是汇编程序知道的常数,因此汇编程序在实际汇编程序时能将两者相加,这样整个指令就是立即寻址方式的单个常量入栈。
RECORD_FIRSTNAME 常量是一条记录从起始地址到名字段之间的字节数。record_buffer 是用于保存记录缓冲区的名字。将以上两者相加,我们就可获得存储在 record_buffer 中记录的名字段地址。
修改记录本节,我们将编写完成如下步骤的程序:
- 打开一个输入文件和一个输出文件;
- 从输入文件中读取记录;
- 递增年龄;
- 将新记录写入输出文件。
如同我们最近遇到的多数程序一样,这个程序相当直观。
.include "linux.s" .include "record-def.s" .section .data input_file_name: .ascii "test.dat " output_file_name: .ascii "testout.dat " .section .bss .lcomm record_buffer, RECORD_SIZE # 局部亦是的栈偏移量 .equ ST_INPUT_DEscriptOR, -4 .equ ST_OUTPUT_DEscriptOR, -8 .section .text .globl _start _start: # 复制栈指针并为局部亦是分配空间 movl %esp, %ebp subl $8, %esp # 打开用于读取的文件 movl $SYS_OPEN, %eax movl $input_file_name, %ebx movl $0, %ecx movl $0666, %edx int $LINUX_SYSCALL movl %eax, ST_INPUT_DEscriptOR(%ebp) # 打开用于写入的文件 movl $SYS_OPEN, %eax movl $output_file_name, %ebx movl $0101, %ecx movl $0666, %edx int $LINUX_SYSCALL movl %eax, ST_OUTPUT_DEscriptOR(%ebp) loop_begin: pushl ST_INPUT_DEscriptOR(%ebp) pushl $record_buffer call read_record addl $8, %esp # 返回读取的字节数 # 如果字节数与我们请求的字节数不同, # 说明已到达文件结束处或出现错误, # 我们就要退出 cmpl $RECORD_SIZE, %eax jne loop_end # 递增年龄 incl record_buffer + RECORD_AGE # 写入记录 pushl ST_OUTPUT_DEscriptOR(%ebp) pushl $record_buffer call write_record add $8, %esp jmp loop_begin loop_end: movl $SYS_EXIT, %eax movl $0, %ebx int $LINUX_SYSCALL
我们可以将以上代码输入名为 add-year.s 的文件。为生成程序,请输入以下命令:
as --gstabs --32 add-year.s -o add-year.o ld -m elf_i386 add-year.o read-record.o write-record.o -o add-year
要运行此程序,请输入以下命令:
./add-year
本程序将 test.dat 中每一条记录的年龄字段增加一年,并将新记录写到文件 testout.dat 。
正如你所看到的,写固定长度的记录百常简单。你只需要读取缓冲区块的数据,进行处理,然后将它们写回文件。遗憾的是,这个程序并未将新的年龄显示在屏幕上,你无法验证程序是否有效。关于显示数字将来会单独出一篇文章。
彩蛋汇编语言大部分都是对栈进行操作。在先前几篇文章中,我虽给出 GDB 调试程序的栈表,但如果您不亲自用 GDB 边调试边观查表,相信即使是老鸟也不能一时理清头绪(过一段时间后我看也很懵)。鉴于此,这一次我尝试将开始部分的代码,将它们操作栈的步骤以图的形式展现出来,以期能给大家一个直观的感受。涉及代码如下:
_start: # 这些是我们将存储输入输出描述符的栈位置(仅供参考:也可以用一个 .data 段中的内在地址代替) .equ ST_INPUT_DEscriptOR, -4 .equ ST_OUTPUT_DEscriptOR, -8 # 复制栈指针到 %ebp movl %esp, %ebp # 为保存文件描述符分配空间 subl $8, %esp # 打开文件 movl $SYS_OPEN, %eax movl $file_name, %ebx movl $0, %ecx # 表示只读文件 movl $0666, %edx # 表示所有者、组和所有用户对文件都有读/写权限。 int $LINUX_SYSCALL # 保存文件描述符 movl %eax, ST_INPUT_DEscriptOR(%ebp) # 即使输出文件描述符是常数,我们也将其保存在本地变量,这样 # 如果稍后决定不将其输出到 STDOUT,很容易加以更改 movl $STDOUT, ST_OUTPUT_DEscriptOR(%ebp) record_read_loop: pushl ST_INPUT_DEscriptOR(%ebp) # 把参数压入栈 pushl $record_buffer call read_record addl $8, %esp
其中ESP实线箭头表示栈顶,虚线箭头表示先前指向的栈位置。其它位置的实线箭头,多用于标记指令、栈、数据来源和去向。如,图2 指向 图9 的箭头,表示此时它们栈内容一样。图8 指向 read_record,表示接下来执行的是 read_record 函数的内容。图15 指向图9 是函数执行完返回到调用它的位置。
栈中保存的数据也不是程序实际在计算机中运行的样子,这里更多的是用于标识或区分的作用。如,图2 中的 (-4)、(-8) 用于表示栈相对位置(实际栈的位置是一串 16 进制地址值)图 8 中栈顶实际应保存的是 addl $8… 指令的地址,因为我们不知道地址是什么所以用指令代表。
参考-
使用 C 调用约定的汇编语言函数
-
Linux 如何从命令行执行程序
-
Linux 下用汇编语言处理文件



