在汇编中使用文件有两种方式,一种是使用标准的C库函数,例如:fopen(),read()以及write()函数等,这些C库函数本质上是通过Linux内核提供的系统调用来实现的,因此,汇编里操作文件的第二种方式就是直接使用底层提供的系统调用。本章节主要介绍的是第二种方式,所以,下面就对与文件操作相关的系统调用进行介绍...

    本文由zengl.com站长对汇编教程英文版相应章节进行翻译得来。

    汇编教程英文版的下载地址:点此进入原百度盘   , 点此进入Dropbox网盘   , 点此进入Google Drive  (Dropbox与Google Drive里是Assembly_Language.pdf文档)

    另外再附加一个英特尔英文手册的共享链接地址:
    点此进入原百度盘点此进入Dropbox网盘点此进入Google Drive  (在某些例子中会用到,Dropbox与Google Drive里是intel_manual.pdf文档)

    本篇翻译对应汇编教程英文原著的第484页到第491页,对应原著第16章 (注意这里的页数不是页脚的页数,而是pdf电子档顶部,分页输入框中的页数,也就是包含了目录,前言部分的总页数,pdf电子档总页数是577,当前已翻译完的页数为491 / 577)。

    在汇编中使用文件有两种方式,一种是使用标准的C库函数,例如:fopen(),read()以及write()函数等,这些C库函数本质上是通过Linux内核提供的系统调用来实现的,因此,汇编里操作文件的第二种方式就是直接使用底层提供的系统调用。本章节主要介绍的是第二种方式,所以,下面就对与文件操作相关的系统调用进行介绍。

The File-Handling Sequence 文件操作的基本流程:

    下图显示了文件操作的基本流程:


图1

    从上图可以看出来,要对某文件进行读写操作,首先要通过Open系统调用打开指定的文件,然后才可以使用Read或Write系统调用对目标文件进行读或写操作,在操作完后,还需要使用Close系统调用关闭掉目标文件,以释放掉Open打开文件时所分配的系统资源。

    在之前的"汇编里使用Linux系统调用 (二)"的文章里,我们提到过,每个系统调用都有自己的系统调用号,要使用某个系统调用,只需将其系统调用号设置到EAX寄存器,并在EBX,ECX等通用寄存器里设置好相关的参数,最后就可以通过0x80软件中断来访问到该系统调用所提供的功能了,因此,如果我们要使用Open,Read,Write及Close这些和文件操作相关的系统调用的话,就必须首先知道它们各自的系统调用号,它们的系统调用号如下表所示:

System Call Value Description
Open 5 Open a file for access and create a file handle pointing to the file.
打开指定的文件,同时返回操作该文件用的文件句柄
Read 3 Read from an open file using the file handle.
通过文件句柄来读取指定文件的内容
Write 4 Write to the file using the file handle.
通过文件句柄对目标文件进行数据写入操作
Close 6 Close the file and remove the file handle.
关闭掉之前Open系统调用打开的文件,同时移除对应的文件句柄

    每个系统调用都有各自的用法,它们的输入参数也都不同,下面就分别对这些系统调用进行详细的介绍,同时通过例子来说明如何在汇编中使用它们。

Opening and Closing Files 打开和关闭文件:

    我们可以通过man 2 open命令来查看open系统调用的格式:

$ man 2 open
....................................
int open(const char *pathname, int flags, mode_t mode);
....................................
$


    从上面的输出可以看到,open系统调用一共有三个输入参数,第一个参数pathname是以null字符(即0)结尾的字符串,用以表示需要打开的文件的完整路径(包括目录名在内),如果只给出了文件名,则系统调用会从当前运行程序所在的目录内查找该文件。

    第二个参数flags用于指出文件的访问模式,例如:以只读方式、以只写方式,或是以读写方式来打开和访问文件等。

    第三个参数mode用于表示当创建文件时,文件的Linux读写执行权限。

    要在汇编里使用open系统调用,就必须先在以下几个寄存器里设置好系统调用号和所需的输入参数:
  • EAX: Contains the system call value 5
    EAX: 用于包含open的系统调用号即数值5
  • EBX: Contains the memory address of the start of the null-terminated filename string
    EBX: 用于包含指向文件名字符串的起始内存地址
  • ECX: Contains an integer value representing the flags requesting the type of access to the file
    ECX: 用于包含表示flags访问模式的整数值
  • EDX: Contains an integer value representing the UNIX permissions used if a new file is created
    EDX: 用于包含表示文件创建时的Linux读写执行权限的整数值
    在open系统调用的三个输入参数里,flags和mode参数对文件的访问都是至关重要的,下面就对这两个参数分别进行介绍。

Access types -- flags参数可用的访问模式:

    在C程序里使用open()函数时,可以直接使用标准头文件里定义的常量,如:O_RDONLY,O_WRONLY等。但是,在汇编开发时,就没有这些预定义好的常量可供使用了,只有要么直接使用这些常量的数值,要么在汇编的开头自定义这些常量。flags参数可用的访问模式如下表所示(常量的数值部分使用的是八进制格式):

C Constant
C中的常量名
Numeric Value
常量对应的数值
(八进制格式)
Description
常量描述
O_RDONLY 00 Open the file for read-only access.
以只读方式打开文件
O_WRONLY 01 Open the file for write-only access.
以只写方式打开文件
O_RDWR 02 Open the file for both read and write access.
以既可读又可写的方式打开文件
O_CREAT 0100 Create the file if it does not exist.
如果文件不存在则创建该文件
O_EXCL 0200 When used with O_CREAT, if the file exists, do not open it.
当与O_CREAT一起使用时,如果目标文件已经存在,则open系统调用会失败
O_TRUNC 01000 If the file exists and is open in write mode, truncate it to a length of zero.
若文件存在,且是以可写的方式打开的文件,则将文件的长度截为0,从而将文件的内容全部清空。
O_APPEND 02000 Append data to the end of the file.
追加数据到文件的末尾
O_NONBLOCK 04000 Open the file in nonblocking mode.
以非阻塞模式打开文件,这样在对文件进行读写操作时,就会立即返回,一般在没有设置非阻塞的情况下,读写操作时,内核会切换到其他的进程,从而将当前的进程阻塞,直到读写操作完成时,内核才会唤醒被阻塞的进程。
O_SYNC 010000 Open the file in synchronous mode (allow only one write at a time).
以同步模式打开文件(一次只允许一个写入操作)
O_ASYNC 020000 Open the file in asynchronous mode (allow multiple writes at a time).
以异步模式打开文件(一次允许多个写入操作)

    这些访问模式可以组合在一起使用,例如,如果你想创建某个文件,同时又以读写的方式来访问该文件的话,那么你可以使用下面的指令:

movl $0102, %ecx

    上面的八进制数0102是由0100与02组成,其中0100对应O_CREAT创建文件模式,而02对应O_RDWR读写模式,此外,还需要注意的是,这些都是八进制值,所以开头的0不可以少,如果少了开头的0,就会变为十进制数了。

    如果你想将数据追加到某个已存在的文件的末尾,则可以使用如下指令:

movl $02002, %ecx

    上面的02002是02000(即O_APPEND添加模式)与02(即O_RDWR读写模式)的组合。

UNIX permissions -- mode参数所使用的Linux读写执行权限:

    Linux中,文件的读写执行权限主要是针对三类用户:
  • The owner of the file 文件的拥有者
  • The default group for the file 文件的组用户
  • Everyone else on the system 系统中的其他用户
    每类用户都可以被赋予以下三个权限位:
  • The read bit 读权限位
  • The write bit 写权限位
  • The execute bit 可执行权限位
    这三个权限位在实际使用时,是以二进制位的形式出现的,如下图所示:


图2

    上图显示,三类用户,每类用户的文件读写执行权限都由三个二进制位构成,这三个二进制位在实际开发时,也可以用如下表所示的八进制值来表示:

Permission Bits
二进制格式的权限位
Value
对应的八进制值
Access
访问权限
001 1 Execute privileges 执行权限
010 2 Write privileges 写权限
011 3 Execute and write privileges 执行,写权限
100 4 Read privileges 读权限
101 5 Execute and read privileges 执行,读权限
110 6 Execute and write privileges 读,写权限
111 7 Execute, write, and read privileges 读,写,执行权限
   
    每类用户的权限都可以用一个上表所示的八进制值来表示,将这三类用户的八进制值组合在一起,就可以构成完整的文件访问权限,例如下面的指令:

movl $0644, %edx

    上面的指令用于将八进制值0644赋值给EDX寄存器,八进制中的第一个6表示owner(文件的拥有者)对文件具有读和写的权限,中间的4表示Group(组用户)对文件只有读权限,最后一个4表示Everyone(系统中的其他用户)对文件也只有读的权限。

    在设置文件的访问权限的时候,还有一个需要特别注意的地方就是:Linux系统为每个用户都分配了一个umask屏蔽掩码,这个umask值会将文件创建时所设置的一些访问权限给屏蔽掉,因此,通过open系统调用所创建的文件的最终权限会是:

file privs = privs & ~umask

    privs是使用open系统调用时,所提供的mode参数,umask通过反转二进制位,再与open系统调用的privs权限进行and(按位与运算),才能得到文件最终的权限值。

    在Linux命令行下,我们可以通过umask命令来查看当前用户的umask值:

$ umask
0022
$

    上面显示当前用户的umask值为八进制值022 。此时,如果某个open系统调用的第三个mode参数的值为0666(即将每类用户的文件访问权限都设置为读和写的权限),那么它所创建的文件的最终权限就会是:

final privileges = privs & ~umask
                    = 666 & ~022
                    = 666 & 755
                    = 644

    可以看到,通过反转和按位与运算,umask掩码将Group(组用户)与Everyone(系统其他用户)的写权限给屏蔽掉了,从安全角度来看,umask可以防止无意中授予某用户写入权限。

    如果你确实想将某文件的访问权限设为0666,那么,要么使用umask命令来修改掉当前用户的umask值:

$ umask 000
$ umask

0000
$

    要么通过chmod命令来手动更改掉所创建文件的权限:

$ chmod 666 test
$ ls -l
................................................
-rw-rw-rw-  1 root root    0  6月 13 00:04 test
................................................
$ 


Open file code  --  打开文件相关的代码:

    上面介绍了open系统调用的flags与mode参数,它的第一个参数pathname用于指定需要打开的文件的完整路径,如果没提供完整的路径,只提供了文件名的话,open系统调用就会从当前运行程式所在的目录中搜索文件。

    pathname参数指向的字符串必须是以null字符(即0)结尾的,在汇编里可以使用.asciz伪指令来声明:

.section .data
filename:
    .asciz "output.txt"
.......................
.......................
.section .text
.......................
.......................
movl $filename, %ebx


    EBX寄存器用于存储路径字符串的内存地址,因此movl指令中filename前的美元符不能省略,movl $filename, %ebx 指令表示将filename标签所在的内存地址传值给EBX寄存器。

    上面的filename是定义在data数据段里的全局变量,当然,你也可以将路径字符串的内存地址以函数参数的形式传递给EBX寄存器:

movl %esp, %ebp
......................................
......................................
movl 8(%ebp), %ebx


    上面的代码片段,假设open系统调用是用于某个函数里,例如,fopen()之类的C函数,这些函数会从存储在栈中的参数里,获取到路径字符串的内存地址,然后将内存地址赋值给EBX寄存器,以作为open系统调用的第一个输入参数,在之前的"汇编函数的定义和使用 (二)"的文章中,我们介绍过,8(%ebp)可以用于表示函数在栈里的第一个参数,因此,上面的movl 8(%ebp), %ebx指令的含义就是将当前函数的第一个栈参数对应的路径字符串的内存地址,赋值给EBX寄存器。

    以上介绍了设置open系统调用的pathname参数的两种方式,下面的代码片段就实现了一个较为完整的open系统调用:

movl $5, %eax
movl $filename, %ebx
movl $0102, %ecx
movl $0644, %edx
int $0x80
test %eax, %eax
js badfile


    上面的代码片段中,先将open的系统调用号5设置到EAX寄存器,接着将filename路径字符串的内存地址赋值给EBX,作为open的第一个输入参数,将0102的访问模式(之前提到过,0102表示创建filename,同时以读写方式来访问该文件)设置到ECX,作为open的第二个输入参数,然后将0644的文件访问权限设置到EDX,作为open的最后一个输入参数,最后通过int $0x80的软件中断来完成open系统调用。

    open系统调用的返回值会存储在EAX里,通过检测EAX的值是否为负值,可以判断open操作是否成功,通过test指令来设置符号位,如果test指令执行后,符号位被设置,则说明EAX是个负值,则js badfile就会发生跳转以表示open操作失败,否则就是非负值,js指令就不会跳转,表示open操作成功。

    在执行open系统调用后,EAX里的返回值如果是大于等于0的值,则说明该返回值是一个有效的文件句柄(或者说是有效的文件描述符,文件描述符可以为0,为0时表示标准输入设备),可以使用该文件句柄对目标文件进行各种读写操作,因此,有必要将文件句柄保存起来,以便其他的系统调用使用,你可以将文件句柄保存在以下几个位置处:
  • A memory location defined in the .data section
    保存在.data数据段里的某个内存位置中,以作为全局变量使用
  • A memory location defined in the .bss section
    保存在.bss段的某个内存位置里
  • The stack, in the local variables section
    保存在栈里,以作为局部变量使用
  • A register
    还可以直接保存在某个寄存器里
    提示:open系统调用所创建的文件句柄会一直保持有效,直到使用close系统调用将其释放为止。

Open error return codes -- open系统调用出错时,会返回的错误代码:

    如果open调用出错,则返回值里会包含错误代码,以表示出错的原因,在Linux系统下,可以在errno.h头文件里找到错误代码的相关定义(下面是我的Slackware系统里的errno.h头文件中的内容):

$ cat /usr/include/asm/errno.h 
#include <asm-generic/errno.h>
$ cat /usr/include/asm-generic/errno.h 
#ifndef _ASM_GENERIC_ERRNO_H
#define _ASM_GENERIC_ERRNO_H

#include <asm-generic/errno-base.h>
.......................................
.......................................
$ cat /usr/include/asm-generic/errno-base.h 
#ifndef _ASM_GENERIC_ERRNO_BASE_H
#define _ASM_GENERIC_ERRNO_BASE_H

#define	EPERM		 1	/* Operation not permitted */
#define	ENOENT		 2	/* No such file or directory */
#define	ESRCH		 3	/* No such process */
#define	EINTR		 4	/* Interrupted system call */
#define	EIO		 5	/* I/O error */
#define	ENXIO		 6	/* No such device or address */
#define	E2BIG		 7	/* Argument list too long */
#define	ENOEXEC		 8	/* Exec format error */
#define	EBADF		 9	/* Bad file number */
#define	ECHILD		10	/* No child processes */
#define	EAGAIN		11	/* Try again */
#define	ENOMEM		12	/* Out of memory */
#define	EACCES		13	/* Permission denied */
#define	EFAULT		14	/* Bad address */
#define	ENOTBLK		15	/* Block device required */
#define	EBUSY		16	/* Device or resource busy */
#define	EEXIST		17	/* File exists */
#define	EXDEV		18	/* Cross-device link */
#define	ENODEV		19	/* No such device */
#define	ENOTDIR		20	/* Not a directory */
#define	EISDIR		21	/* Is a directory */
#define	EINVAL		22	/* Invalid argument */
#define	ENFILE		23	/* File table overflow */
#define	EMFILE		24	/* Too many open files */
#define	ENOTTY		25	/* Not a typewriter */
#define	ETXTBSY		26	/* Text file busy */
#define	EFBIG		27	/* File too large */
#define	ENOSPC		28	/* No space left on device */
#define	ESPIPE		29	/* Illegal seek */
#define	EROFS		30	/* Read-only file system */
#define	EMLINK		31	/* Too many links */
#define	EPIPE		32	/* Broken pipe */
#define	EDOM		33	/* Math argument out of domain of func */
#define	ERANGE		34	/* Math result not representable */

#endif
$ 


    从上面的输出可以看到,/usr/include/asm/errno.h头文件会去包含/usr/include/asm-generic/errno.h头文件,asm-generic/errno.h最后会再去包含/usr/include/asm-generic/errno-base.h头文件,在errno-base.h头文件里就定义了一些基本的错误代码,每个错误代码的右侧都有一个英文注释来描述具体的错误详情。

    不过,需要注意的是,由于open之类的系统调用,返回的错误码是负值,所以需要将其返回的负值变为正值,然后再和上面的错误代码进行对照。

    下面是一个open系统调用的简单例子,它会返回一个负数形式的错误代码:

# open.s -- open a file that not exist
.section .data
filename:
    .asciz "output.txt"
.section .text
.global _start
_start:
	movl $5, %eax
	movl $filename, %ebx
	movl $0002, %ecx
	movl $0644, %edx
	int $0x80
	test %eax, %eax
	js badfile
	movl $0,%ebx
	movl $1,%eax
	int $0x80
badfile:
	movl $-1,%ebx
	movl $1,%eax
	int $0x80


    上面的open.s程式试图通过open系统调用来打开一个在当前目录中并不存在的output.txt文件,由于该文件不存在,所以会返回一个负数形式的错误代码,我们可以通过gdb调试器来查看open系统调用的返回值:

$ as -o open.o open.s -gstabs
$ ld -o open open.o
$ gdb -q open
Reading symbols from /root/asm_example/opfile/open...done.
(gdb) b _start
Breakpoint 1 at 0x8048074: file open.s, line 8.
(gdb) r
Starting program: /root/asm_example/opfile/open 

Breakpoint 1, _start () at open.s:8
8		movl $5, %eax
(gdb) n
9		movl $filename, %ebx
(gdb) n
11		movl $0002, %ecx
(gdb) n
12		movl $0644, %edx
(gdb) n
13		int $0x80
(gdb) n
14		test %eax, %eax
(gdb) p $eax
$1 = -2
(gdb) 


    从gdb调试器的p $eax命令的输出,可以看到open系统调用后,EAX返回值为-2 ,将-2前面的负号去掉,可以知道其错误代码为2,在errno-base.h头文件里,2号错误码的定义为:

#define    ENOENT         2    /* No such file or directory */

    说明错误原因是No such file or directory即没有该文件。

Closing files 关闭打开的文件:

    当我们对某个打开的文件进行完相关的操作后,最后需要做的事就是通过close系统调用来关闭掉打开的文件句柄,否则,有可能损坏该文件。

    close系统调用的格式如下:

$ man 2 close
..............................
#include 

int close(int fd);
..............................
..............................
$


    可以看到,它只需接受一个fd(文件句柄或者叫做文件描述符)作为输入参数即可,在使用时,只需将需要关闭的文件句柄设置到EBX寄存器,作为close系统调用的输入参数即可:

movl filehandle, %ebx
movl $6, %eax
int $0x80

    上面EBX里存储需要关闭的文件句柄,EAX里存储close的系统调用号6 ,最后调用int $0x80即可。

    如果close系统调用成功,则EAX返回值里将包含0值,如果关闭失败,则类似open系统调用那样,会返回一个负数形式的错误代码,错误代码的含义可以参考errno.h头文件(在我的slackware里最终是参考errno-base.h头文件)。

    限于篇幅,本章就到这里,下一篇继续介绍其他的和文件操作相关的系统调用。

    OK,休息,休息一下 o(∩_∩)o~~
上下篇

下一篇: 汇编中使用文件 (二)

上一篇: 优化汇编指令 (三)

相关文章

Moving Data 汇编数据移动 (一)

IA-32平台(二)

什么是汇编语言(一) 汇编底层原理,指令字节码

使用内联汇编 (二)

使用内联汇编 (一)

使用内联汇编 (三) 内联汇编结束篇