Linux 汇编读写简单记录

Linux 汇编读写简单记录

作者:高玉涵
时间: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\0"
	.rept	31	# 填充到 40 字节
	.byte	0
	.endr

	.ascii	"Bartlett\0"
	.rept	31	# 填充到 40 字节
	.byte	0
	.endr

	.ascii	"4242 S Praireit\nTulsa, OK 55555\0"
	.rept	209	# 填充到 240 字节
	.byte	0
	.endr

	.long	45

record2:
	.ascii	"Marilyn\0"
	.rept	32	# 填充到 40 字节
	.byte	0
	.endr

	.ascii	"Taylor\0"
	.rept	33	# 填充到 40 字节
	.byte	0
	.endr

	.ascii	"2224 S Johannan St\nChicago, IL 12345\0"
	.rept	203	# 填充到 240 字节
	.byte	0
	.endr

	.long	29

record3:
	.ascii	"Derrick\0"
	.rept	32	# 填充到 40 字节
	.byte	0
	.endr

	.ascii	"McIntire\0"
	.rept	31	# 填充到 40 字节
	.byte	0
	.endr

	.ascii	"500 W Oakland\nSan Diego, CA 54321\0"
	.rept	206	# 填充到 240 字节
	.byte	0
	.endr

	.long	36

# 这是我们要写入文件的文件名
file_name:
	.ascii	"test.dat\0"

.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\0'

.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\0"
output_file_name:
	.ascii "testout.dat\0"

.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… 指令的地址,因为我们不知道地址是什么所以用指令代表。

在这里插入图片描述

参考
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值