Linux 下用汇编语言处理文件
作者:高玉涵
时间:2021.10.12 15:56
博客: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
对待生命,你不妨大胆一点,因为我们始终要失去它。
——尼采
计算机编程都涉及处理文件的任务。毕竟,当我们重新启动计算机后,唯一能从之前的会话中保留下来的就是磁盘上的数据。存储在文件中的数据称为永久数据,因为即使程序没有运行,数据仍保留在磁盘上的文件里。
1.1 UNIX文件的概念
每个操作系统都有自己的文件处理方式。然而,Linux 中用的是 UNIX 的处理方式,也是最简单最普遍处理方式。无论 UNIX 文件是什么程序创建的,都可以作为连续的字节流进行访问(在 Linux 中几乎一切都是“文件”。键盘输入以及屏幕显示也被认为是文件)。当你访问一个文件时,通过文件名打开它,操作系统会给你一个编号,称为文件描述符,你用它来指代该文件,直到使用完毕。接下来,你就可以使用文件描述符对该文件进行读取和写入。完成读取和写入后,关闭文件,关闭后文件描述符即失效。
1.2 打开和关闭文件
告诉 Linux 要打开文件的文件名,并以特定模式打开(读取、写入、读写、如不存在则创建等)。这是通过 open 系统调用处理的,其所需参数为一个文件名、一个表示模式的数字以及一个权限集合。接着,Linux 将返回文件描述符到 %eax 。记住,你将在整个程序中用这个数字来指代这一文件。完成处理之后,使用系统调用 close 关闭文件。
系统调用 | 值 | 描述 |
---|---|---|
打开 | 5 | 打开要访问的文件并且创建指向文件的文件描述符 |
读取 | 3 | 使用文件描述符读取打开的文件 |
写入 | 4 | 使用文件描述符写入文件 |
关闭 | 6 | 关闭文件并且删除文件描述符 |
- EAX:包含系统调用值 5 。
- EBX:包含以空字符结尾的文件名字符串的开始位置的内在地址。
- ECX:包含表示需要文件的访问类型的标志的整数值。
- EDX:如果是创建新文件,则包含表示 UNIX 权限的整数值。
1.3 访问类型
对文件的所有 open 请求都必须声明用于打开文件的访问类开。如果你曾经使用 C 函数 open() 打开文件,可能熟悉使用预先定义的常量,比如 O_RDONLY 或者 O_RDWR 。不幸的是,在汇编语言程序中没有定义这些常量。必须使用它们代表的数字值或者自己定义常量。这些常量的数字值通常表示为八进制值。常量的数字值显示在下表中。
C 常量 | 数字值 | 描述 |
---|---|---|
O_RDONLY | 00 | 打开文件、用于只读访问 |
O_WRONLY | 01 | 打开文件,用于只写访问 |
O_RDWR | 02 | 打开文件,用于读写访问 |
O_CREAT | 0100 | 如果文件不存在,就创建文件 |
O_EXCL | 0200 | 和 O_CREAT 一起使用时,如果文件存在,就不打开它 |
O_TRUNC | 01000 | 如果文件存在并且按照写模式打开,则把文件长度截断为 0 |
O_APPEND | 02000 | 把数据追加到文件的结尾 |
O_NONBLOCK | 04000 | 按照非块模式打开文件 |
O_SYNC | 010000 | 按照同步模式打开文件(同时只允许一个写入操作) |
O_ASYNC | 020000 | 按照异步模式打开文件(同时允许多个写入操作) |
可以将文件访问类型组合起来以便启用多种访问特性。例如,如果希望以只写方式创建文件且发现同名文件覆盖此文件,可以使用下面的指令:
movl $03101
这把 O_CREATE 的值 0100、O_WRONLY 的值 01、O_TRUNC 的值 01000 和 O_APPEND 的值 02000 组合一起。常量值中高位部分的 0 很重要。这表示这个值是八进制的。如果使用常量 $3101,就会得到错误结果,因为编译器使用十进制的值 3101 。
1.4 UNIX 权限
设置 UNIX 权限经常会导致复杂情况。操作时必须谨慎,以便确保为文件访问设置适当的权限。标准 UNIX 权限针对 3 钟类别的用户进行设置:
- 文件的所有者
- 文件的默认组
- 系统上的其他所有用户
这 3 种类别的每一种都被分配了文件的特定权限。使用 3 位表示每个类别的访问:
- 读取位
- 写入位
- 执行位
对准这 3 位形成每个类别一个 3 位值,如上图所示。
可以使用一个八进制值表示 3 位,表时设置了哪些位。可以将值组合起来生成各种访问级别,如下表所示。
权限位 | 值 | 访问 |
---|---|---|
001 | 1 | 执行权限 |
010 | 2 | 写入权限 |
011 | 3 | 执行和写入权限 |
100 | 4 | 读取权限 |
101 | 5 | 执行和读取权限 |
110 | 6 | 执行和写入权限 |
111 | 7 | 执行、写入和读取权限 |
将文件模式属性组合起来,形成单一的 3 位八进制数字,表示所有者、组和所有用户的权限。这个 3 位的八进制数字在系统调用 open 中定义,还有高位部分的 0,使它成为八进制值。
指令:
movl $0666, %edx
把八进制值 666 赋值给 %edx 寄存器。 表示所有者、组和所有用户对文件都有读/写权限。
注意,对此有一个说明:Linux 系统为登录到系统上的每个用户分配一个 umask 值(权限掩码)。umask把默认的权限分配给这个用户创建的文件。创建的文件的最终权限如下:
file privs = privs & ~umask
umask 值被反转,并且和系统调用 open 中请求的权限进行 AND 操作。可以使用 umask 命令查看分配给用户账户的 umask 值:
umask
022
分配给这个用户的 umask 是八进制值 022 。如果执行的系统调用 open 请求使用 0666 权限(所有用户都可以读/写)来创建文件,分配给创建的文件的最终权限就如下:
final privileges = privs & ~umask
= 666 & ~022
= 666 & 755
= 644
没有修改为所有者请求的权限,但是通过 umask 值拒绝了为组和其它用户请求的写入权限。这是系统上经常使用的 umask 值。它防止无意中授予文件的写入权限。
如果确实希望授予所有用户对文件的写入权限,就必须改变 umask 值,或者使用 chmod 命令手动地发动文件权限。
1.5 打开错误返回代码
如果系统调用 open 返回错误代码,可以把它和系统的头文件 errno.h 中定义的 errno 值进行比较(汇编语言的 errno.h 文件通常位于系统上的 /usr/include/asm 目录中)。下表介绍可能返回的一些比较常见的错误代码。
错误名称 | 错误值 | 描述 |
---|---|---|
EPERM | 1 | 操作不被许可 |
ENOENT | 2 | 没有此文件 |
EBADF | 9 | 坏文件名柄数字 |
EACCES | 13 | 权限被拒绝 |
EFAULT | 14 | 坏文件地址 |
EBUSY | 16 | 设备或者资源忙 |
EEXIST | 17 | 文件存在 |
EISDIR | 21 | 是目录 |
EMFILE | 24 | 过多打开文件 |
EFBIG | 27 | 文件过大 |
EROFS | 30 | 只读文件系统 |
ENAMERTOOLONG | 36 | 文件名过长 |
记住错误代码被返回为负的数字,所以值将是 errno 表中显示值的负值。
1.6 缓冲区和 .bss
缓冲区是连续的字节块,用于批量数据传输。当你要求读取文件时,操作系统需要有一个地方来存储读取的数据,这个地方称为缓冲区。一般缓冲区仅用于暂时存储数据,然后数据被从缓冲区中读出并转换成便于程序处理的形式,但我们的程序不会这么复杂。例如,你想从一个文件中读取一行文件,但不知道该行文本的长度。那么你可以从缓冲区读取大量字节/字符,寻找行结束符,并将直到该行结束符为止的所有字符复制到另一个位置。如果没有找到行结束符,你就会分配另一个缓冲区,并继续读取。在这种情况下,很可能最终会获得缓冲区中剩余的字符,而当下次需要这一文件中的数据时就可将这些字符作为寻找的起点。
要创建缓冲区,你需要保留静态或动态存储。静态存储就是 long 或 .byte 指令声明的存储位置。动态存储留给将来有时间讨论。不过,用 .byte 声明缓冲区会存在问题。首先,输入工作让人倍感乏味。如果你想要用 500 字节,你不得不在 .byte 声明后键入 500 数字,而除了用于占用空间,这些数字没有任何用处。其次,这会占用可执行空间的位置(增加了可执行文件的体积)。就浪费了可执行程序的 500 字节空间。除此之外,还存在 .bss 段。.bss段类似于数据段,不同的是它不占用可执行程序空间。.bss 段可以保留存储位置,却不能对其进行初始化。在数据段中,你既可以保留存储位置,也能为其设置被始值;在 .bss 段中却不能设置初始值。对于缓冲区来说,这非常有用,因为我们并不需要初始化,只需要保留存储位置。为了做到这一点,我们发出以下指令:
.section .bss
.lcomm my_buffer, 500
.lcomm 指令将创建一个符号 my_buffer,指代我们用作缓冲区的 500 字节存储位置。接着执行以下指令,假定我们已经打开一个文件进行读取,并将文件描述符放置在 %ebx 中:
movl $my_buffer, %ecx
movl 500, %edx
movl 3, %eax
int $0x80
上述指令将最多 500 字节的数据读入缓冲区。在本例中,我在 my_buffer 之前放置一个美元符号。记住,这样做的原因是:若没有美元符号,my_buffer 将被视为以直接寻址方式访问的内在位置。美元符号则将寻址方式变为立即寻址方式,实际上就是将 my_buffer 表示的数字本身(即缓冲区的起始地址,也就是 my_buffer 的地址)加载到 %ecx 中。
1.7 标准文件和特殊文件
你可能会认为默认情况下程序启动时不打开任何文件,但事实上并非如此。Linux 程序开始时至少有三个打开文件的描述符,分别是:
- STDIN
这是标准输入,是一个只读文件,通常代表键盘,文件描述符 0 。
- STDOUT
这是标准输出,是一个只写文件,通常代表屏幕,文件描述符 1 。
- STDERR
这是标准错误,是一个只写文件,通常代表屏幕,处理结果通常输出到 STDOU,但在这个过程中出现的任何错误消息都输出到 STDERR 。这样一来,如果你希望的话,可以将两者分别存放在不同的地方。STDERR 文件描述符 2 。
这些”文件“都可以重定向,以一个真正的文件取代屏幕或键盘;限于篇幅这里不再讨论,但任何一本关于 UNIX 命令行的好书都会对此进行详述。程序本身甚至无需了解这种重定向的情况,因为程序可以像往常一样使用标准的文件描述符。
注意,你写的许多”文件“根本就不是文件。基于 UNIX 的操作系统把所有输入/输出系统都视作文件。网络连接被视为文件,串行端口被视为文件,甚至音频设备都被视为文件。进程之间的通信常是通过称为管道的特殊文件实现的。与普通文件相比,这些文件有一些不同的打开和创建方法(例如,不使用 open 系统调用),但可以使用标准的 read 和 write 系统调用进行读写。
1.8 在程序中使用文件
这里给出一个简单的程序来说明这些概念。这个程序将使用两个文件,从其中一个文件中读取数据,将所有小写字母转换为大写形式,然后写入另一个文件。在这样做之前,让我们想想需要做什么才能完成这一工作。
-
要有一个函数,它接受一个内存块,并将其内容转换为大写形式。这个函数将需要内存块地址及大小作为参数。
-
要有一个反复读取数据到缓冲区的代码,并对缓冲区调用转换函数,然后将缓冲区写回另一个文件。
-
打开所需的文件以启动程序。
注意,上面逆序说明了事情的完成顺序。首先决定主要目标这种技巧在编写复杂程序时很有用,在本例中就是将字符块内容转换为大写形式。接下来考虑要完成目标需要进行的所有设置和处理:在本例中,你必须打开文件,不断地读取并写入块到磁盘。编程的一个关键是不断将问题分解为更小问题,直到你可以轻松解决这个问题,接着再将这些小问题重新组合为能运行的程序。
你可能认为自己永远无法记住所有这些数字——系统调用号、中断号等。我们可以使用 .equ 这条指令会对你有所帮助。 .equ 允许你为数字分配名称。例如,如果在写下 LINUX_SYSCALL 之后的任何时候发出 .equ LINUX_CALL, 0x80 指令,汇编程序都将以 0x80 替换 LINUX_SYSCALL 。那么现在你可以写:
int $LINUX_SYSCALL
这样就容易阅读,更容易记住。编程确实很复杂,但我们可以做许多与此类似的事情,以使编程更容易。
# 目的: 本程序将输入文件的所有字母转换为大写字母,然后输出到输出文件
#
# 处理过程:
# (1)打开输入文件
# (2)打开输出文件
# (3)如果未达到输入文件尾部:
# (a)将部分文件读入内在缓冲区
# (b)读取内在缓冲区的每个字节,如果该字节为小写字母。
# 就将其转换为大写字母
# (c)将内存缓冲区写入输出文件
#
# 注意: 我们所用的标签多于实际跳转所用的标准,因为其中一部分只是为了使
# 程序保持清晰
#
.section .data
##### 常数 #####
# 系统调用号
.equ SYS_OPEN, 5
.equ SYS_WRITE, 4
.equ SYS_READ, 3
.equ SYS_CLOSE, 6
.equ SYS_EXIT, 1
# 文件打开选项(不同的值请参见 /usr/include/asm/fcntl.h)
# 你可以通过将选项值相加或者进行 OR 操作组合使用选项
.equ O_RDONLY, 0
.equ O_CREATE_WRONLY_TRUNC, 03101
# 标准文件描述符
.equ STDIN, 0
.equ STDOUT, 1
.equ STDERR, 2
# 系统调用中断
.equ LINUX_SYSCALL, 0x80
.equ END_OF_FILE, 0 # 这是读操作的返回值,表明到达文件结束处
.equ NUMBER_ARGUMENTS, 2 # 参数个数
.section .bss
# 缓冲区 - 从文件中将数据加载到这里,也要从这里将数据写入输出文件
# 由于种种原因,缓冲区大小不应超过 16 0000 字节
.equ BUFFER_SIZE, 500
.lcomm BUFFER_DATA, BUFFER_SIZE
.section .text
# 栈位置
# 当Linux程序开始时,所有指向命令行参数的指针都存储于栈中
# 参数数目存储于 8(%esp),程序名存储于 12(%esp),而参数存储于 16(%esp)及其后的存储位置
.equ ST_SIZE_RESERVE, 8 # 栈预留存储大小
.equ ST_FD_IN, -4
.equ ST_FD_OUT, -8
.equ ST_ARGC, 0 # 参数数目
.equ ST_ARGV_0, 4 # 程序名
.equ ST_ARGV_1, 8 # 输入文件名
.equ ST_ARGV_2, 12 # 输出文件名
.globl _start
_start:
##### 程序初始化 #####
# 保存栈指针
movl %esp, %ebp
# 在栈上为文件描述符分配空间
subl $ST_SIZE_RESERVE, %esp
open_files:
open_fd_in:
##### 打开输入文件 #####
# 打开系统调用
movl $SYS_OPEN, %eax
# 将输入文件名放入 ebx
movl ST_ARGV_1(%ebp), %ebx
# 只读标志
movl $O_RDONLY, %ecx
# 这实际上并不影响读操作
movl $0666, %edx
# 调用Linux, 此时 eax 值是 3,打开文件
int $LINUX_SYSCALL
store_fd_in:
# 保存给定的文件描述符
movl %eax, ST_FD_IN(%ebp)
open_fd_out:
##### 打开输出文件 #####
# 打开文件
movl $SYS_OPEN, %eax
# 将输出文件名放入 ebx
movl ST_ARGV_2(%ebp), %ebx
# 写入文件标志
movl $O_CREATE_WRONLY_TRUNC, %ecx
# 新文件模式(如果已创建)
movl $0666, %edx
# 调用Linux, 此时 eax 值是 3,打开文件
int $LINUX_SYSCALL
store_fd_out:
# 这里存储文件描述符
movl %eax, ST_FD_OUT(%ebp)
# 主循环开始
read_loop_begin:
# 从输入文件中读取一个数据块
movl $SYS_READ, %eax
# 获取输入文件描述符
movl ST_FD_IN(%ebp), %ebx
# 放置读取数据的存储位置
movl $BUFFER_DATA, %ecx
# 缓冲区大小
movl $BUFFER_SIZE, %edx
# 读取缓冲区大小返回到 eax 中
int $LINUX_SYSCALL
# 如到达文件结束处就退出
# 检查文件结束标记
cmpl $END_OF_FILE, %eax
# 如果发现文件结束符或出现错误,就跳转到程序结束处
jle end_loop
continue_read_loop:
# 将字符块内容转换成大写形式
pushl $BUFFER_DATA # 缓冲区位置
pushl %eax # 缓冲区大小
call convert_to_upper
popl %eax # 重新获取大小
addl $4, %esp
# 将字符串块写入输出文件
# 缓冲区大小
movl %eax, %edx
movl $SYS_WRITE, %eax
# 要使用的文件
movl ST_FD_OUT(%ebp), %ebx
# 缓冲区位置
movl $BUFFER_DATA, %ecx
int $LINUX_SYSCALL
# 循环继续
jmp read_loop_begin
end_loop:
# 关闭文件
# 注意 - 这里我们无需进行错误检测
# 因为错误情况不代表任何特殊含义
movl $SYS_CLOSE, %eax
movl ST_FD_OUT(%ebp), %ebx
int $LINUX_SYSCALL
movl $SYS_CLOSE, %eax
movl ST_FD_IN(%ebp), %ebx
int $LINUX_SYSCALL
# 退出
movl $SYS_EXIT, %eax
movl $0, %ebx
int $LINUX_SYSCALL
# 目的: 这个函数实际上将字符块内容转换为大写形式
#
# 输入: 第一参数是要转换的内在块的位置
# 第二个参数是缓冲区的长度
# 输出: 这个函数以大写字符块覆盖当前缓冲区
# 变量:
# %eax - 缓冲区起始地址
# %ebx - 缓冲区长度
# %edi - 当前缓冲区偏移量
# %cl - 当前正在检测的字节(ecx的第一部分)
#
# 常数
# 我们搜索的下边界
.equ LOWERCASE_A, 'a' # ascii字母是以数字表示的。
# 我们搜索的上边界
.equ LOWERCASE_Z, 'z'
# 大小写转换
.equ UPPER_CONVERSION, 'A' - 'a' # 用大写字母减去同一字母的小写字母
# 即可知道要使小写字母加上哪个数才
# 能转换为大写字母
# 栈相关信息
.equ ST_BUFFER_LEN, 8 # 缓冲区长度
.equ ST_BUFFER, 12 # 实际缓冲区
convert_to_upper:
pushl %ebp
movl %esp, %ebp
# 设置变量
movl ST_BUFFER(%ebp), %eax
movl ST_BUFFER_LEN(%ebp), %ebx
movl $0, %edi
# 如果给定的缓冲区长度为0即离开
cmpl $0, %ebx
je end_convert_loop
convert_loop:
# 获取当前字节
movb (%eax, %edi, 1), %cl
# 除非该字节在'a'和'z'之间,否则读取下一字节
cmpb $LOWERCASE_A, %cl
jl next_byte
cmpb $LOWERCASE_Z, %cl
jg next_byte
# 否则将字节转换为大写字母
addb $UPPER_CONVERSION, %cl
# 并存回原处
movb %cl, (%eax, %edi, 1)
next_byte:
incl %edi # 下一字节
cmpl %edi, %ebx # 继续执行程序
jne convert_loop
end_convert_loop:
# 无返回值,离开程序即可
movl %ebp, %esp
popl %ebp
ret
生成程序 toupper,可键入以下命令:
./toupper toupper.s toupper.up
你会发现文件 toupper.up 就是原文件的大写版。
让我们来看看程序的工作原理。
程序的第一部分被标记为 CONSTANTS,在编程中,常数是程序汇编或编译时分配的值,分配后就不再改变。我的习惯是将所有常数放在程序的开始处。实际上只需要在使用之前声明即可,但把所有常数放在开始处更方便其查找。而用大写字母表示使哪些值是常数在程序中更醒目,更容易找到。在汇编语言中,我们以之前提到过的 .equ 指令声明常数。这里,我们只是给迄今用到的数字赋值,例如系统调用号、系统中断号、文件打开选项等。
下一段被标记为 BUFFERS,我们在这个程序中只使用一个缓冲区,即 BUFFER_DATA 。我们也定义了一个常数 BUFFER_SIZE 来保存缓冲区大小。如果总是在需要用到缓冲区大小的时使用这个常数,而不是输入数字 500,若以后缓冲区大小有改变,我们只需要更改这个常数值,而无需通读整个程序并分别修改每个值。
在继续看程序的 _start 段之前,先来看看程序结束处的 convert_to_upper 函数。这是实际上进行大小写转换部分。
这一段以一列我们将用到的常数开始。将这些常数放在这里而不是整个程序开始处,是因为它们仅仅与这个函数有关。我们有如下定义:
.equ LOWERCASE_A, 'a'
.equ LOWERCASE_Z, 'z'
.equ UPPER_CONVERSION, 'A' - 'a'
前两条指令只是定义搜索边界。记住,在计算机中字母是以数字表示的。因此,我们在比较、加法、减法以及其他可使用数字的操作中使用 LOWERCASE_A 。同时,也请注意我们定义的常数 UPPER_CONVERSION 。由于字母以数字表示,我们可以将它们做相减操作。用大写字母减去同一字母的小写字母,即可知道要使小写字母加上哪个数才能转换为大写字母。如果这么说不便于你的理解,请看一看 ASCII 代码表。你会发现字符 A 对应数字 65,而字符 a 对应 97,那么转换因子就是 -32 。如果将小写字母与 -32 相加,即可得到相应的大写字母。
在这之后,就是一些标记为 STACK POSITIONS 的常数。记住要在函数调用前将函数参数入栈。这些常数(为了能表示清楚,前缀为 ST)定义了栈中哪个位置应有哪项数据。返回地址在位置 4 + %esp 处,缓冲区长度在位置 8 + %esp 处,缓冲区地址在位置 12 + %esp 处。用符号代替数字而不使用数字本身的话,要了解使用和移动了哪些数据对我们来说更直观。
接下来是标签 convert_to_upper,这是函数入口,开始两行是标准函数行,用于存储栈指针。接下来的两行是:
movl ST_BUFFER(%ebp), %eax
movl ST_BUFFER_LEN(%ebp), %ebx
它们将函数参数移至相应寄存器以使用。接着,我们将 0 加载到 %edi 。这里要做的是循环取缓冲区中的每一个字节,通过以下过程实现:从存储位置 %eax + %edi 加载,递增 %edi ,并重复这一过程直到 %edi 等于存储在 %ebx 中的缓冲区长度。来看以下两行代码:
cmpl $0, %ebx
je end_convert_loop
它们只是安全检查,以确保不会有大小为 0 的缓冲区。如果指定了大小为 0 的缓冲区,我们只需结束转换并离开。保护潜在用户,防止编程错误,这对于程序员来说是一项重要任务。你可以总说明自己的函数不应接受大小为 0 的缓冲区,但如果让函数对此进行检查并在发生这种情况时可靠地退出就更好了。
接下来,循环开始了。首先,将一字节移入 %cl,相应的代码是:
movb (%eax, %edi, 1), %cl
该指令使用间接变址寻址方式,表示从 %eax 开始,向前 %edi 个位置(每个位置为 1 字节大小),并将该位置的值放入 %cl 。在这条指令后,程序查看该值是否在小写字母 a 到 z 的范围内,要检测这点,只要查看字符是否比 a 小即可。如果比 a 小,那就不可能是小写字母。同样,如果比 z 大,也不可能是小写字母。因此,在上述两种情况下只需继续即可。如果在小写字母范围内,就与大写字母转换量相加,并将其存回缓冲区。
无论是哪种情况,接下来都通过递增 %cl 获取下一个值。然后,程序查看是否到达缓冲区结束处。如果未到达,就跳转到循环开始处( convert_loop 标签)。如果已经到达结束处,只要继续执行到函数结束即可。由于我们是直接更改缓冲区,因此无需向调用程序返回任何值——更改已经在缓冲区中。标签 end_convert_loop 不是必须的,但有了这个标签你就容易看到程序的大小写转换部分在哪里。
现在我们知道转换进程的原理了。现在需要弄清楚如何从文件中获得数据以及如何向文件写数据。
在读写文件前,我们先要打开文件。UNIX的 open 系统调用对此进行处理,需要以下参数。
- 与以往一样,%eax 含有系统调用号,在本例中为 5 。
- %ebx 中含有指向文件名(相应文件是要打开的那个)的字符串。该字符串必须以空字符结束。
- %ecx 中包含文件打开选项。这些选项告诉 Linux 如何打开文件,青蛙文件是打开用于读、写、读写、如果不存在则创建、如果已存在则删除等(参见 1.3)。
- %edx 包含用于打开文件的权限。当必须先创建文件时用到此项,以便让 Linux 了解创建文件时应设置什么权限(参见 1.4)。。
在进行系统调用后,新打开文件的文件描述符存储在 %eax 中。
那么,打开哪些文件呢?在本例中,我们将打开在命令行中指定的文件。幸运的是,命令行参数以经由 Linux 存放在一个易于访问的位置,而且以经以空字符结束。当 Linux 程序开始时,所有指向命令行参数的指针都存储于栈中。参数数目存储于 8(%esp),程序名存储于 12 (%esp),而参数存储于 16(%esp) 以及其后的存储位置。
在 C 编程语言中,这称为 argv 数组,因此我们也将在程序中用这个名字。
我们的程序首先在 %ebp 中保存当前栈位置,然后在栈中保留一些空间存储文件描述符。在此之后。这里将文件名置于 %ebx 中,只读模式对应的数字置于 %ecx 中,默认模式 $0666 置于 %edx 中,系统调用号置于 %eax 中。在系统调用后,文件打开了,文件描述符被存储到 %eax 中。接着,我们将文件描述符转移到栈中对应的位置。
对输出文件也做同样处理,唯一区别在于输出文件被创建为“只写、如不存在则创建”形式。其文件描述符也存储下来。
现在来到了主要部分——读/写循环。基本上讲,我们将从输入文件读取固定大小的数据块,调用转换函数对数据块进行转换,然后写到输出文件。尽管我们读取固定大小的数据块,但数据块的大小并不影响这个程序——我们只是对字符序列进行操作。我们能随心所欲地读取任意大小的数据块,且程序仍然能正常运行。
循环的第一部分是使用 read 系统调用读取数据。这个调用的参数是一个用于读取的文件描述符、一个用于写入的缓冲区,以及缓冲区大小(即可写入的最大字节数)。系统调用返回实际读取的字节数或文件结束符(数字 0)。
在读取一个数据块后,我们检测 %eax 是否含有文件结束标记。如果找到即退出循环,否则就继续执行。
在读取数据后,程序调用 convert_to_upper 函数,参数为刚才读入的缓冲区以及前一个系统调用中读取的字符数。在执行这个函数后,缓冲区已经变为大写字符,程序准备写文件了。接着我们将寄存器恢复为函数调用之前的值。
最后发出 write 系统调用。此调用将数据从缓冲区移出到文件,其他方面与 read 系统调用一样。现在,我们回到循环起始处。
在循环退出后(记住,当一次读取后探测到文件结束时即退出),程序简单地关闭其文件描述符并退出。关闭系统调用将获取 %ebx 中的文件描述符来关闭文件。
然后,程序结束了!
GDB toupper 程序调试部分内容表
序号 | ESP | 值 | 说明 | EIP | 指令 | EBP | EAX | EBX | ECX | EDX | EFLAGS | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
65 | 0xff9fe820 | 3 | 命令行参数数目 | 0x8049000 | <_start:>movl %esp,%ebp | 0xff9fe820 | IF | |||||
67 | 0xff9fe818 | %esp - 8 | 文件描述符预留空间 | 0x8049002 | subl $ST_SIZE_RESERVE,%esp | PF AF SF IF | ||||||
73 | 0x8049005 | <open_file:>movl $SYS_OPEN,%eax | 5 | |||||||||
75 | %ebp + 8 | 输入文件名字符串指针 | 0x804900a | movl ST_ARGV_1(%ebp),%ebx | 0xff9ff88c | |||||||
77 | 0x804900d | movl $O_RDONLY,%ecx | 0 | |||||||||
79 | 0x8049012 | movl $0666,%edx | 0x1b6 | |||||||||
81 | 0x8049017 | int $LINUX_SYSCALL | 3 | |||||||||
85 | 0xFF9FE81C | 输入文件描述符 | 0x8049019 | movl %eax,ST_FD_IN(%ebp) | ||||||||
90 | 0x804901c | movl $SYS_OPEN,%eax | 5 | |||||||||
92 | 0xFF9FE832 | 输出文件名字符串指针 | 0x8049021 | movl ST_ARGV_2(%ebp),%ebx | 0xff9ff896 | |||||||
94 | 0x8049024 | movl $O_CREATE_WRONLY_TRUNC,%ecx | 0x641 | |||||||||
96 | 0x8049029 | movl $0666,%edx | 0x1b6 | |||||||||
98 | 0x804902e | int $LINUX_SYSCALL | 4 | |||||||||
102 | 0xFF9FE818 | 输出文件描述符 | 0x8049030 | movl %eax, ST_FD_OUT(%ebp) | ||||||||
107 | 0x8049033 | movl $SYS_READ, %eax | 3 | |||||||||
109 | 0xFF9FE81C | 输入文件描述符 | 0x8049038 | movl ST_FD_IN(%ebp),%ebx | 3 | |||||||
111 | 0x804903b | movl $BUFFER_DATA,%ecx | 0x804a000 | |||||||||
113 | 0x8049040 | movl $BUFFER_SIZE,%edx | 500 | |||||||||
115 | 0x8049045 | int $LINUX_SYSCALL | 500 | |||||||||
119 | 0x8049047 | cmpl $END_OF_FILE,%eax | IF | |||||||||
121 | 0x804904a | jle end_loop | ||||||||||
125 | 0xff9fe814 | 0x804a000 | 参数1:读文件缓冲区位置 | 0x804904c | pushl $BUFFER_DATA | |||||||
126 | 0xff9fe810 | 500 | 参数2:读文件缓冲区大小 | 0x8049051 | pushl %eax | |||||||
127 | 0xff9fe80c | 0x57 | 返回地址 | 0x8049052 | call convert_to_upper | |||||||
188 | 0xff9fe808 | 0xff9fe820 | 原EBP值 | 0x804908e | pushl %ebp | |||||||
189 | 0x804908f | movl %esp, %ebp | 0xff9fe808 | |||||||||
192 | 0xff9fe814 | 缓冲区位置赋值给EAX | 0x8049091 | movl ST_BUFFER(%ebp), %eax | 0x804a000 | |||||||
193 | 0xff9fe810 | 缓冲区大小赋值给EBX | 0x8049094 | movl ST_BUFFER_LEN(%ebp),%ebx | 500 | |||||||
194 | 0x8049097 | movl $0,%edi | 0 | |||||||||
197 | 0x804909c | cmpl $0, %ebx | ||||||||||
198 | 0x804909f | je end_convert_loop | ||||||||||
202 | 0x23 | 十进制35,字符 # | 0x80490a1 | movb (%eax, %edi, 1),%cl | 0x804a023 | |||||||
205 | 0x80490a4 | cmpb $LOWERCASE_A,%cl | CF SF IF | |||||||||
206 | 0x80490a7 | jl next_byte | CF IF | |||||||||
216 | 0x80490b4 | incl %edi | 1 | |||||||||
217 | 0x80490b5 | cmpl %edi,%ebx | ||||||||||
219 | 0x80490b7 | jne convert_loop | PF IF | |||||||||
202 | 0x20 | 十进制32,(空格) | 0x80490a1 | movb (%eax, %edi, 1),%cl | 0x804a020 | |||||||
205 | 0x80490a4 | cmpb $LOWERCASE_A, %cl | CF AF SF IF | |||||||||
206 | 0x80490a7 | jl next_byte | ||||||||||
216 | 0x80490b4 | incl %edi | 2 | |||||||||
… | … | … | … | … | … | … | … | … | … | … | … | … |