本文由zengl.com站长对汇编教程英文版相应章节进行翻译得来。
汇编教程英文版的下载地址:
点此进入原百度盘 ,
点此 ,
点此进入Google Drive (Dropbox与Google Drive里是Assembly_Language.pdf文档)
另外再附加一个英特尔英文手册的共享链接地址:
点此进入原百度盘,
点此 ,
点此进入Google Drive (在某些例子中会用到,Dropbox与Google Drive里是intel_manual.pdf文档)
本篇翻译对应汇编教程英文原著的第491页到第501页,对应原著第16章 (注意这里的页数不是页脚的页数,而是pdf电子档顶部,分页输入框中的页数,也就是包含了目录,前言部分的总页数,pdf电子档总页数是577,当前已翻译完的页数为501 / 577)。
Writing to Files 向文件中写入数据:
前一章介绍了如何用open系统调用来打开文件,一旦打开了文件,你就可以使用write系统调用向该文件里写入数据。
在之前的
"汇编开发示例 (一)"的文章里,介绍过一个cpuid.s的示例程式,该程式里就用到了write系统调用来向显示器输出字符串信息,因为在UNIX和类UNIX系统中,所有的东东都是以文件的形式来进行操作的,当前会话的显示终端即标准输出设备(STDOUT),对应的文件描述符是1 ,通过向该文件描述符写入数据,就可以将信息显示到屏幕上,STDOUT的文件描述符1是由系统打开的,不需要我们手动去打开,还有一个标准错误输出设备(STDERR)的文件描述符默认是2,也是可以直接进行写入操作的。
至于其他的非系统默认打开的文件,就必须先通过open系统调用打开文件,并获取到文件描述符,然后才能用write系统调用向该文件里写入数据。
A simple write example -- write系统调用的例子:
下面的cpuidfile.s程式就演示了如何通过write系统调用向打开的文件中写入数据:
# cpuidfile.s - An example of writing data to a file
.section .data
filename:
.asciz "cpuid.txt"
output:
.asciz "The processor Vendor ID is 'xxxxxxxxxxxx'\n"
.section .bss
.lcomm filehandle, 4
.section .text
.globl _start
_start:
movl $0, %eax
cpuid
movl $output, %edi
movl %ebx, 28(%edi)
movl %edx, 32(%edi)
movl %ecx, 36(%edi)
movl $5, %eax
movl $filename, %ebx
movl $01101, %ecx
movl $0644, %edx
int $0x80
test %eax, %eax
js badfile
movl %eax, filehandle
movl $4, %eax
movl filehandle, %ebx
movl $output, %ecx
movl $42, %edx
int $0x80
test %eax, %eax
js badfile
movl $6, %eax
movl filehandle, %ebx
int $0x80
badfile:
movl %eax, %ebx
movl $1, %eax
int $0x80
|
上面的代码其实是修改自
"汇编开发示例 (一)"的文章里的cpuid.s程式,通过修改将cpuid指令的返回信息写入到cpuid.txt的文件中。
一开始,代码先通过cpuid指令将处理器的供应商ID字符串信息给写入到output标签对应的字符串里,有关cpuid指令的输出信息格式,请参考上面提到过的
"汇编开发示例 (一)"的文章。
得到了所需的供应商字符串信息后,只需再通过open,write和close系统调用将字符串写入到指定的文件中即可。
上面的程式里,和open系统调用相关的代码如下:
movl $5, %eax
movl $filename, %ebx
movl $01101, %ecx
movl $0644, %edx
int $0x80
test %eax, %eax
js badfile
movl %eax, filehandle
|
上面代码中,EAX里存储着open的系统调用号5,EBX里存储着文件名字符串的内存地址(这里filename标签指向的是"cpuid.txt"的文件名),ECX里存储的是flags(访问模式),从上一篇文章中我们可以知道ECX里的01101这个八进制数字表示的是,使用O_TRUNC,O_CREAT以及O_WRONLY这三个模式来打开文件,这三个访问模式结合在一起的含义是:以只写方式打开文件,如果文件不存在则创建该文件,如果文件存在,则将文件里的原有内容清空,从头开始写入数据。
EDX寄存器里存储的0644的八进制数字表示,如果创建文件,则该文件的用户权限为:第一个6表示owner(文件的拥有者)对文件具有读和写的权限,中间的4表示Group(组用户)对文件只有读权限,最后一个4表示Everyone(系统中的其他用户)对文件也只有读的权限。
在设置好参数后,就可以使用int $0x80中断来执行open系统调用,返回的文件描述符存储在EAX里,通过test %eax, %eax指令来检测返回的值是否是一个有效的文件描述符,如果不是有效的文件描述符,则通过js badfile指令跳转到badfile处来退出程序,如果是有效的文件描述符,则将该文件描述符通过movl %eax, filehandle指令保存到filehandle所在的内存里,接下来的write系统调用就可以根据该文件描述符来向文件中写入数据。
我们先通过 man 2 write 命令来了解下write系统调用的具体格式:
$ man 2 write
........................................
ssize_t write(int fd, const void *buf, size_t count);
........................................
RETURN VALUE
On success, the number of bytes written is returned (zero indicates
nothing was written). On error, -1 is returned, and errno is set
appropriately.
........................................
$
|
可以看到,write有三个输入参数,第一个fd用于表示已经打开的文件描述符,第二个buf表示需要写入的数据的指针值,第三个count表示需要向文件中写入多少个字节的数据。简单来说就是:从buf缓冲区域向fd对应的文件里写入count个字节的数据。
因此要在汇编中使用write系统调用的话,就必须先在寄存器里准备好这三个参数,第一个fd参数放置在EBX里,第二个buf参数放置在ECX里,第三个count参数则放置在EDX中,例如,上面的cpuidfile.s程式里和write系统调用相关的代码如下:
movl $4, %eax
movl filehandle, %ebx
movl $output, %ecx
movl $42, %edx
int $0x80
test %eax, %eax
js badfile
|
上面代码先将write的系统调用号4设置到EAX中,再将filehandle(之前通过open系统调用打开的文件描述符)设置到EBX,接着将output标签的内存地址设置到ECX,表示需要将output所指向的字符串信息给写入到filehandle对应的文件里(output前面必须使用美元符来表示该标签所在的内存地址值),然后将output字符串的有效长度42设置到EDX里,最后通过执行int $0x80中断就可以将output字符串给写入到cpuid.txt的文件里了。
从前面
man 2 write 命令的输出里可以看到,write系统调用的返回值表示成功写入文件的数据的字节数,如果返回0则表示没有写入任何数据,返回负数则表示写入失败,所以在int $0x80中断执行后,还需要通过test %eax, %eax指令来检测EAX里的返回值,如果是负数,则接下来的js badfile指令就会跳转到badfile位置处退出程序。
在对文件操作完毕后,需要通过close系统调用来关闭文件,示例中的相关代码如下:
movl $6, %eax
movl filehandle, %ebx
int $0x80
|
上面EAX里存储的6是close的系统调用号(上一章讲解过),close系统调用只需要接收一个文件描述符作为输入参数。
我们可以通过汇编链接来测试该程式:
$ as -o cpuidfile.o cpuidfile.s
$ ld -o cpuidfile cpuidfile.o
$ ./cpuidfile
$ ls -l cpuid.txt
-rw-r--r-- 1 root root 42 6月 28 17:17 cpuid.txt
$ cat cpuid.txt
The processor Vendor ID is 'GenuineIntel'
$
|
从上面的输出可以看到,在执行完cpuidfile程式后,当前目录内就多了一个cpuid.txt的文件,该文件的用户权限为 -rw-r--r-- 即0644,与我们设置的一致,cpuid.txt文件的内容为
The processor Vendor ID is 'GenuineIntel' ,和预期的一样。
在open系统调用打开文件时,设置了一个O_TRUNC的访问模式,即如果文件存在的话,就将原来文件里的内容给清空掉,我们可以先在之前生成的cpuid.txt文件里添加一些数据,然后再执行一次cpuidfile程式,然后查看cpuid.txt文件里的内容是否被覆盖掉了,如果被覆盖掉了,则表示O_TRUNC起作用了:
$ cat cpuid.txt
The processor Vendor ID is 'GenuineIntel' xdfsd
$ ./cpuidfile
$ cat cpuid.txt
The processor Vendor ID is 'GenuineIntel'
$
|
上面先在原来的cpuid.txt文件的末尾添加了
xdfsd 的字符串,当执行完cpuidfile程式后,cpuid.txt里的内容就又被覆盖掉了,说明O_TRUNC的访问模式确实起了作用。
Changing file access modes -- 修改文件的访问模式:
上面的例子中使用的O_TRUNC访问模式会清除文件里的原有数据,如果想向文件末尾追加数据的话,只需对open系统调用的访问模式做些改动即可:
movl $5, %eax
movl $filename, %ebx
movl $02101, %ecx
movl $0644, %edx
int $0x80
test %eax, %eax
js badfile
movl %eax, filehandle
|
上面的代码仅仅是将ECX里的值修改为02101而已,从上一篇文章中可以知道,02101这个八进制数是由02000(O_APPEND),0100(O_CREAT)以及01(O_WRONLY)这三个访问模式组合而成,表示以只写方式打开文件,如果文件不存在则创建文件,如果文件存在则向文件的末尾追加数据。
我们将cpuidfile.s的代码拷贝到cpuidfile2.s里,然后按照上面的代码修改ECX的访问模式值,编译链接后的运行结果如下:
$ as -o cpuidfile2.o cpuidfile2.s
$ ld -o cpuidfile2 cpuidfile2.o
$ ./cpuidfile2
$ cat cpuid.txt
The processor Vendor ID is 'GenuineIntel'
The processor Vendor ID is 'GenuineIntel'
$
|
上面的 cat cpuid.txt 命令的输出中,第一行是之前cpuidfile程式运行时写入的字符串,第二行就是当前的cpuidfile2程式所追加的数据。
Handling file errors 处理文件错误:
在Linux系统中,有很多的原因可以导致文件操作失败,例如,某个进程锁住了文件,或者访问的文件根本不存在等等,要知道确切的原因,最好的办法就是检测系统调用的返回值,在上一篇文章中,我们已经介绍过如何通过返回值与errno.h头文件来定位到具体的出错原因。
我们可以将cpuid.txt文件删除,并将cpuidfile.s里的open系统调用的ecx值设置为01001,即将O_CREAT去掉,编译链接后,再执行一次cpuidfile程式:
$ rm cpuid.txt
$ as -o cpuidfile.o cpuidfile.s -gstabs
$ ld -o cpuidfile cpuidfile.o
$ ./cpuidfile
$ echo $?
254
$
|
可以看到,返回值为254,本质上返回的是-2值(因为write和open之类的系统调用是以错误码的负数形式进行返回的),只不过,
echo $? 命令将-2值以8位无符号数的形式进行的显示,我们也可以用256减去254,得到的2就是错误码,得到错误码后,就可以在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 */
.......................................
|
上面显示错误码2的出错原因是
No such file or directory 即找不到所需的文件。
Reading Files -- 从文件中读取内容:
我们可以使用read系统调用来读取文件的内容,read系统调用的具体格式如下:
$ man 2 read
....................................
ssize_t read(int fd, void *buf, size_t count);
....................................
$
|
read系统调用与write系统调用类似,也使用三个输入参数:第一个fd表示已经打开的有效的文件描述符,第二个buf表示要将fd对应文件里的内容读取到buf缓冲区域,第三个count参数表示需要从文件中读取count个字节的数据。
该系统调用的返回值用于表示从文件中实际读取的数据的字节数,返回值是一个ssize_t类型,该类型与size_t类型相似,不过ssize_t是一个有符号的整数类型,因为,read系统调用除了可以返回正数以表示读取的字节数外,还可以在错误发生时返回负数。如果返回值为0,则表示已经读取到文件的结束位置了。
另外,当文件里实际可供读取的数据小于count时,那么返回的字节数就会小于count参数所设置的值。
同样的,在汇编里使用read系统调用之前,也必须先在寄存器里准备好所需的输入参数:
-
EAX: The read system call value (3)
在EAX中设置好read系统调用号,read的系统调用号为3
-
EBX: The file handle of the open file
将目标文件的文件描述符设置到EBX里
-
ECX: The memory location of a data buffer
在ECX中设置buf读缓冲的内存地址
-
EDX: An integer value of the number of bytes to read
在EDX里设置需要读取的字节数
这里需要注意的是为了能让读取操作被正确执行,我们需要为read系统调用设置正确的访问模式,例如:O_RDONLY(只读模式)或O_RDWR(读写模式)。
另外,类似于STDOUT,像键盘之类的标准输入设备(STDIN)的文件描述符(值为0)默认已经被打开了,所以read系统调用可以直接从标准输入设备里获取到所输入的数据。
A simple read example -- read系统调用的例子:
下面的readtest1.s程式就演示了如何通过read系统调用来读取指定文件的内容:
# readtest1.s - An example of reading data from a file
.section .bss
.lcomm buffer, 42
.lcomm filehandle, 4
.section .text
.globl _start
_start:
nop
movl %esp, %ebp
movl $5, %eax
movl 8(%ebp), %ebx
movl $00, %ecx
movl $0444, %edx
int $0x80
test %eax, %eax
js badfile
movl %eax, filehandle
movl $3, %eax
movl filehandle, %ebx
movl $buffer, %ecx
movl $42, %edx
int $0x80
test %eax, %eax
js badfile
movl $4, %eax
movl $1, %ebx
movl $buffer, %ecx
movl $42, %edx
int $0x80
test %eax, %eax
js badfile
movl $6, %eax
movl filehandle, %ebx
int $0x80
badfile:
movl %eax, %ebx
movl $1, %eax
int $0x80
|
上面的readtest1.s程式会使用命令行里的参数1作为需要读取的文件名,在之前的
"汇编函数的定义和使用 (三) 汇编函数结束篇"的文章里,介绍过程序启动时栈里的完整结构如下图所示:
图1
由于程序一开始已经将ESP的值,赋值给了EBP,因此,EBP也指向了Number of Parameters(命令行的参数个数)的位置,EBP + 4则指向Program Name(程序名称),EBP + 8则指向了Pointer to Command Line Parameter 1(命令行参数1)的位置,因此,在代码中,就使用了movl 8(%ebp), %ebx指令将命令行参数1的字符串指针值设置到EBX里,这样该程式就可以用open系统调用来打开用户在命令行中所指定的文件了。
在open系统调用打开文件后,我们就可以使用read系统调用从指定的文件里读取出所需的数据了,上面程式中和read系统调用相关的代码如下:
movl $3, %eax
movl filehandle, %ebx
movl $buffer, %ecx
movl $42, %edx
int $0x80
test %eax, %eax
js badfile
|
上面的代码里,先将read系统调用号3设置到EAX,然后将filehandle里存储的之前open系统调用返回的文件描述符设置到EBX里,接着将buffer缓冲区域的指针设置到ECX,再将需要读取的字节数42设置到EDX里,最后执行int $0x80中断就可以将指定文件里的42个字节的数据给读取到buffer缓冲区域了,读操作执行完后,可以通过test指令来测试EAX里的返回值,如果是负数,则说明读操作失败,就通过js badfile指令跳转到badfile去退出程序。
在将数据读取到buffer缓冲区后,还使用write系统调用将buffer里的数据写入到STDOUT(标准输出设备即显示器上,文件描述符为1),这样就知道read到底读出了哪些数据:
movl $4, %eax
movl $1, %ebx
movl $buffer, %ecx
movl $42, %edx
int $0x80
|
所有文件相关的操作都执行完后,需要通过close系统调用来关闭掉打开的文件:
movl $6, %eax
movl filehandle, %ebx
int $0x80
|
上面的6是close的系统调用号。
readtest1的编译运行结果如下:
$ as -o readtest1.o readtest1.s
$ ld -o readtest1 readtest1.o
$ ./readtest1 cpuid.txt
The processor Vendor ID is 'GenuineIntel'
$
|
上面通过提供cpuid.txt的命令行参数,让readtest1将cpuid.txt文件里的内容给读取和显示出来,如果不提供任何命令行参数的话,read系统调用就会执行失败,可以通过返回值来查看到具体的错误码:
$ ./readtest1
$ echo $?
242
$
|
从输出可以看到返回值是242,用256减去242,得到的14就是错误码,在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 EFAULT 14 /* Bad address */
.......................................
|
原因是Bad address即错误的地址,因为没有提供命令行参数,因此之前代码里的8(%ebp)就指向了错误的文件名地址。
当然,在实际开发时,最好是先检查一下命令行参数的个数,这样可以有效的防止系统调用直接执行出错。
A more complicated read example -- 一个更复杂点的读文件的例子:
上面的readtest1.s程式只能读取出指定文件的头42个字节的数据,如果要将一个超过42个字节的文件的数据全部读取出来,可以利用到read系统调用的一个特性:每个打开的文件都有一个文件指针,该指针指示了需要从哪个位置开始读取数据,最开始这个文件指针指向了文件的第一个字节,每执行一次read系统调用,该文件指针就会根据读取的字节数进行移动,例如读取了5个字节,那么文件指针就会移动到5个字节之后的位置,这样下一次再执行read读操作时,就可以从之前读取的数据之后进行读操作,当文件指针移动到文件结束位置时,再执行read系统调用就会返回0值,这样我们就可以通过循环执行read系统调用把文件里的所有数据都读取出来,直到read返回0为止。
下面的readtest2.s程式就演示了如何将文件里的所有内容给读取并显示出来:
# readtest2.s - A more complicated example of reading data from a file
.section .bss
.lcomm buffer, 10
.lcomm filehandle, 4
.section .text
.globl _start
_start:
nop
movl %esp, %ebp
movl $5, %eax
movl 8(%ebp), %ebx
movl $00, %ecx
movl $0444, %edx
int $0x80
test %eax, %eax
js badfile
movl %eax, filehandle
read_loop:
movl $3, %eax
movl filehandle, %ebx
movl $buffer, %ecx
movl $10, %edx
int $0x80
test %eax, %eax
jz done
js done
movl %eax, %edx
movl $4, %eax
movl $1, %ebx
movl $buffer, %ecx
int $0x80
test %eax, %eax
js badfile
jmp read_loop
done:
movl $6, %eax
movl filehandle, %ebx
int $0x80
badfile:
movl %eax, %ebx
movl $1, %eax
int $0x80
|
上面在read_loop的循环里,每次read系统调用都会读取10个字节的数据到buffer缓冲里,然后通过test %eax, %eax指令来检测EAX里的返回值,如果大于0则将buffer里存储的读取出来的数据显示到屏幕上,这样循环下去,直到read系统调用的返回值为0(读到了文件结束位置)或者为负数(读操作出错)才跳转到done位置关闭掉打开的文件并退出程序。这样readtest2.s程式就可以将任意长度的文件的内容给读取并显示出来了,下面是编译运行的结果:
$ as -o readtest2.o readtest2.s
$ ld -o readtest2 readtest2.o
$ ./readtest2 readtest2.s
# readtest2.s - A more complicated example of reading data from a file
.section .bss
.lcomm buffer, 10
.lcomm filehandle, 4
.section .text
.globl _start
_start:
.........................................
.........................................
done:
movl $6, %eax
movl filehandle, %ebx
int $0x80
badfile:
movl %eax, %ebx
movl $1, %eax
int $0x80
$
|
从上面的输出可以看到,readtest2程式将指定的readtest2.s文件的内容都显示了出来,有点类似于cat命令。
另外,你还可以将代码中read每次读取的字节数增加(上面的代码中每次只读取了10个字节),这样可以有效减少read系统调用的执行次数,从而提高程序的执行性能。
Reading, Processing, and Writing Data -- 从文件中读取数据,经过处理后,再将处理过的数据写入文件:
我们通过read系统调用从文件中读取出数据后,还可以对这些数据做一些进一步的处理,例如,将小写字母转为大写字母之类的,然后再将处理的结果写入到其他文件里,下面的readtest3.s程式就演示了这种做法:
# readtest3.s - An example of modifying data read from a file and outputting it
.section .bss
.lcomm buffer, 10
.lcomm infilehandle, 4
.lcomm outfilehandle, 4
.lcomm size, 4
.section .text
.globl _start
_start:
# open input file, specified by the first command line param
movl %esp, %ebp
movl $5, %eax
movl 8(%ebp), %ebx
movl $00, %ecx
movl $0444, %edx
int $0x80
test %eax, %eax
js badfile
movl %eax, infilehandle
# open an output file, specified by the second command line param
movl $5, %eax
movl 12(%ebp), %ebx
movl $01101, %ecx
movl $0644, %edx
int $0x80
test %eax, %eax
js badfile
movl %eax, outfilehandle
# read one buffer’s worth of data from input file
read_loop:
movl $3, %eax
movl infilehandle, %ebx
movl $buffer, %ecx
movl $10, %edx
int $0x80
test %eax, %eax
jz done
js badfile
movl %eax, size
# send the buffer data to the conversion function
pushl $buffer
pushl size
call convert
addl $8, %esp
# write the converted data buffer to the output file
movl $4, %eax
movl outfilehandle, %ebx
movl $buffer, %ecx
movl size, %edx
int $0x80
test %eax, %eax
js badfile
jmp read_loop
done:
# close the output file
movl $6, %eax
movl outfilehandle, %ebx
int $0x80
# close the input file
movl $6, %eax
movl infilehandle, %ebx
int $0x80
badfile:
movl %eax, %ebx
movl $1, %eax
int $0x80
# convert lower case letters to upper case
.type convert, @function
convert:
pushl %ebp
movl %esp, %ebp
movl 12(%ebp), %esi
movl %esi, %edi
movl 8(%ebp), %ecx
convert_loop:
lodsb
cmpb $0x61, %al
jl skip
cmpb $0x7a, %al
jg skip
subb $0x20, %al
skip:
stosb
loop convert_loop
movl %ebp, %esp
popl %ebp
ret
|
上面的代码中,一开始会依次执行open系统调用来打开命令行参数1与命令行参数2所指定的文件,并将命令行参数1对应的文件作为输入文件(文件描述符存储在infilehandle中),命令行参数2对应的文件作为输出文件(文件描述符存储在outfilehandle里),接着在read_loop循环里,每次read系统调用都会从输入文件中读取10个字节的数据到buffer缓冲区域,然后通过convert函数将buffer里所有的字符全部转为大写字母,最后通过write系统调用将处理后的buffer中的数据写入到输出文件里,这样循环下去,就可以把输入文件里所有的字符都转为大写字母,并存储到输出文件中。
在convert函数中用到了lodsb与stosb的指令,在之前的
"汇编字符串操作 (二) 字符串操作结束篇"的文章里,详细的介绍过这两个指令的用法,lodsb指令用于将ESI指向的内存里的一个字节的数据给加载到AL寄存器,这样就可以对AL里加载的数据进行处理了,例如,上面会先判断AL里的字符是否是小写字母,如果是小写字母,则通过subb $0x20, %al指令将AL里的值减去0x20,就可以得到对应的大写字母了,接着就可以通过stosb指令将AL里的大写字母写入到EDI指向的内存里了,这里,ESI与EDI都指向同一个内存位置(即convert函数的第二个参数)。
下面是readtest3程式编译运行的结果:
$ as -o readtest3.o readtest3.s
$ ld -o readtest3 readtest3.o
$ ./readtest3 cpuid.txt output.txt
$ cat cpuid.txt
The processor Vendor ID is 'GenuineIntel'
$ cat output.txt
THE PROCESSOR VENDOR ID IS 'GENUINEINTEL'
$
|
可以看到,readtest3程式将cpuid.txt里的所有的字符都转为了大写字母,并存储到了output.txt文件中,和readtest2类似,readtest3一样可以用于转换任意尺寸大小的文件。
限于篇幅,本章就到这里,下一篇介绍和内存映射文件相关的内容。
OK,休息,休息一下 o(∩_∩)o~~