3.10 文件操作
在用户态使用常规IO方式读写一个文件的一般步骤是:首先打开或创建一个文件,获取文件的句柄,然后通过这个句柄调用ReadFile/WriteFile函数去读写文件,所有操作完成后关闭句柄。
在内核态中,文件操作的流程与用户态如出一辙,但需要注意的是,内核API有更为严格的调用要求,如IRQL。下面将根据上面文件的操作步骤,介绍内核态下的文件操作。
3.10.1 文件的打开与关闭
下面的函数用于打开一个文件。
这个函数的参数异常复杂,下面逐个说明其参数。
FileHandl参数表示句柄的指针。如果这个函数调用返回成功(STATUS_SUCCESS),那么打开的文件句柄就返回在这个地址内。
DesiredAccess参数表示申请的权限。如果打开写文件内容,请使用FILE_WRITE_DATA;如果需要读文件内容,请使用FILE_READ_DATA;如果需要删除文件或者给文件改名,请使用DELETE;如果想设置写文件属性,请使用FILE_WRITE_ATTRIBUTES;反之,设置读文件属性则使用FILE_READ_ATTRIBUTES。这些条件可以用|(位或)来组合。有两个宏分别组合了常用的读权限和常用的写权限,分别为GENERIC_READ和GENERIC_WRITE。还有一个宏代表全部权限,即GENERIC_ALL。此外,如果想同步打开文件,请加上SYNCHRONIZE。同步打开文件详见后面对CreateOptions的说明。
Object_Attribute参数表示对象属性。这个参数在上面章节中已经介绍过。
IoStatusBlock也是一个结构。这个结构在内核开发中经常会用到,它往往用于表示一个操作的结果。这个结构在文档中是公开的,如下:
在实际编程中很少会用到Pointer。一般返回的结果在Status中,成功则为STATUS_SUCCESS;否则是一个错误码。进一步的信息在Information中,在不同的情况下返回的Information信息意义不同。针对ZwCreateFile调用的情况,Information的返回值有以下几种可能:
FILE_CREATED:文件被成功地新建了。
FILE_OPENED:文件被打开了。
FILE_OVERWRITTEN:文件被覆盖了。
FILE_SUPERSEDED:文件被替代了。
FILE_EXISTS:文件已存在。
FILE_DOES_NOT_EXIST:文件不存在(因而打开失败了)。
AllocationSize参数是一个指针,指向64位整数,该参数定义了文件初始分配的大小。该参数仅关系到创建或重写文件操作,如果忽略它,那么文件长度从0开始,并随着写入而增长。
FileAttributes参数控制新建立的文件属性,一般地,设置为0或者FILE_ATTRIBUTE_NORMAL即可。在实际编程中,笔者没有尝试过其他的值。
ShareAccess是一个非常容易被人误解的参数。实际上,这是在本代码打开这个文件时,允许别的代码同时打开这个文件所持有的权限,所以称为共享访问。一共有三种共享标志可以设置:FILE_SHARE_READ、FILE_SHARE_WRITE和FILE_SHARE_ DELETE。这三种标志可以用|(位或)来组合。举例如下:如果本次打开只使用了FILE_SHARE_READ,那么这个文件在本次打开之后、关闭之前,别的代码试图以读权限打开,则被允许,可以成功打开;否则一定失败,返回共享冲突。
同时,如果本次打开只使用了FILE_SHARE_READ,而之前这个文件已经被另一次用写权限打开着,那么本次打开一定失败,返回共享冲突。其中的逻辑关系貌似比较复杂,读者应耐心理解。
CreateDisposition参数说明了这次打开的意图。可能的选择如下(请注意这些选择不能组合)。
FILE_CREATE:新建文件。如果文件已经存在,则请求失败。
FILE_OPEN:打开文件。如果文件不存在,则请求失败。
FILE_OPEN_IF:打开或新建文件。如果文件存在,则打开;如果不存在,则新建文件。
FILE_OVERWRITE:覆盖。如果文件存在,则打开并覆盖其内容;如果文件不存在,则请求返回失败。
FILE_OVERWRITE_IF:新建或覆盖。如果要打开的文件已存在,则打开它,并覆盖其内容;如果不存在,则简单地新建文件。
FILE_SUPERSEDE:新建或取代。如果要打开的文件已存在,则生成一个新文件替代之;如果不存在,则简单地生成新文件。
请联系上面IoStatusBlock参数中Information的说明。
最后一个重要的参数是CreateOptions。在惯常的编程中,笔者使用FILE_NON_ DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT。此时文件被同步地打开,而且打开的是文件(而不是目录,创建目录请用FILE_DIRECTORY_ FILE)。所谓同步地打开的意义在于,以后每次操作文件时,比如写入文件,调用ZwWriteFile,在ZwWriteFile返回时,文件写操作已经完成了,而不会有返回STATUS_PENDING(未决)的情况。在非同步文件的情况下,返回未决是常见的,此时文件请求没有被完成,使用者需要等待事件来等待请求的完成。当然,好处是使用者可以先去做别的事情。
要同步打开,前面的DesiredAccess参数必须含有SYNCHRONIZE。
此外还有一些其他的情况,比如不通过缓冲操作文件,希望每次读/写文件都是直接往磁盘上操作的。此时CreateOptions中应该带有标志FILE_NO_INTERMEDIATE_ BUFFERING。带了这个标志后,请注意操作文件每次读/写都必须以磁盘扇区大小(最常见的是512字节)对齐,否则会返回错误。
这个函数是如此的烦琐,以至于再多的文档也不如一个可以利用的例子。早期笔者调用这个函数时往往因为参数设置不对而导致打开失败,非常渴望找到一个实际可以使用的参数的范例。现在举例如下:
值得注意的是路径的写法,并不是像应用层一样直接写“C:\\a.dat”,而是写成了“\\??\\C:\\a.dat”。这是因为ZwCreateFile使用的是对象路径,“C:”是一个符号链接对象,符号链接对象一般都在“\\??\\”路径下。
RTL_CONSTANT_STRING是一个宏,这个宏的作用是初始化一个UNICODE_STRING结构体。
关闭文件句柄使用ZwClose函数,由于打开文件时使用了OBJ_KERNEL_HANDLE标志,所以打开的文件句柄是内核句柄,内核句柄的关闭不需要和打开在同一进程上下文中。示例如下:
3.10.2 文件的读写
打开文件之后,最重要的操作是对文件的读/写。读与写的方法是对称的,只是参数输入与输出的方向不同。读取文件内容一般使用ZwReadFile,写文件一般使用ZwWriteFile。
FileHandle参数是前面ZwCreateFile成功后所得到的FileHandle。如果是内核句柄,ZwReadFile和ZwCreateFile并不需要在同一个进程中。句柄是各进程通用的。
Event参数是一个事件对象的句柄,用于异步完成读时。下面的举例始终用同步读,所以忽略这个参数,请始终填写NULL。
ApcRoutine参数是回调例程,用于异步完成读时。下面的举例始终用同步读,所以忽略这个参数,请始终填写NULL。
IoStatusBlock参数表示返回结果状态。同ZwCreateFile中的同名参数。
Buffer参数是缓冲区。如果读文件内容成功,则内容被读到这个缓冲区里。
Length参数描述缓冲区的长度。这个长度也就是试图读取文件的长度。
ByteOffset参数表示要读取的文件的偏移量,也就是要读取的内容在文件中的位置。一般不要设置为NULL。文件句柄不一定支持直接读取当前偏移量。
Key参数表示读取文件时使用的一种附加信息,一般不使用,设置为NULL。
ZwReadFile为函数返回值,成功的返回值是STATUS_SUCCESS。只要读取到任意多个字节(不管是否符合输入的Length的要求),返回值都是STATUS_SUCCESS,即使试图读取的长度范围超出了文件本来的大小。但是,如果仅读取文件长度之外的部分,则返回STATUS_END_OF_FILE。
ZwWriteFile的参数与ZwReadFile完全相同。当然,除了读/写文件,有的读者可能会问是否提供了一个ZwCopyFile用来拷贝文件。这个需求未能被满足。如果有这个需求,这个函数必须自己来编写。下面是一个例子,用来拷贝一个文件,使用到了ZwCreateFile、ZwReadFile和ZwWriteFile三个函数。不过本节的例子只列出ZwReadFile和ZwWriteFile的部分。
除了读/写,文件还有很多其他操作,比如删除、重新命名、枚举,在后面实例中用到时,再详细讲解这些操作。