接上文:
系统调用的子功能要用eax寄存器来指定,所以咱们要看看有哪些系统调用啦,在linux系统中,系统调用是定义在/usr/include/asm/unistd.h文件中,该文件只是个统一的入口,指向了32位和64位两种版本。在asm目录下提供了这两个版本,文件名分别是unistd_32.h 和unistd_64.h,这里给大家摘录了部分32位x86平台下的unistd_32.h文件,见图
在/usr/include/asm/unistd_32.h文件中共定义了348个系统调用,哦,给大家说一下,我用的linux版本是CentOS release 6.3 (Final),不知道新版本内核中是否增加了新的系统调用功能。
我们要用的系统调用是第4号调用,即__NR_write。不要被它前面的两个下滑线吓到,就是个命名而已,它代表就是我们所说的write系统调用。
如果不知道某个系统调用的用法,可以用man命令来查看,方法是man 2 系统调用名。咱们执行man 2 write看看,见图
man后面的数字2是表示查看System Calls方面的帮助,对于man自己的帮助信息,man命令也可以man自己,可以用man man来查看。上图只是部分帮助信息,咱们了解这些就够用了。write的功能是把buf指向的缓冲区中的count个字节写入fd指向的文件描述符,执行成功后返回写入的字节数,失败则返回-1。
如果在c语言中调用write的话,直接代入实参就行了,这是最简单的方式,如代码c_syscall.c:
#include <unistd.h>
int main(){write(1,"hello,world\n",4);return 0;
}
为了使用c标准库中的write函数,文件开头包含了标准头文件unistd.h,通过该函数可以使用系统的write系统调用,该文件在磁盘上的路径是/usr/include/unistd.h。不过在本机上测试发现不包含unistd.h,其编译、运行都没问题,也许这和隐式声明有关,这里不再深究。
调用“系统调用”有两种方式:
- 将系统调用指令封装为c库函数,通过库函数进行系统调用,操作简单。
- 不依赖任何库函数,直接通过汇编指令int与操作系统通信。
以上的c代码就是用的第一种方式,不知道您是否对write函数的内部实现感兴趣,其实我也没研究过,不过万变不离其宗,核心思想是必须与进行内核沟通才能获得内核提供的功能。所以,write内部封装的一定是系统调用指令,按照这种设想,下次咱们会模拟一下它的实现。
调用“系统调用”有两种方式:
- 将系统调用指令封装为c库函数,通过库函数进行系统调用,操作简单。
- 不依赖任何库函数,直接通过汇编指令int与操作系统通信。
以上的c代码就是用的第一种方式,不知道您是否对write函数的内部实现感兴趣,其实我也没研究过,不过万变不离其宗,核心思想是必须与进行内核沟通才能获得内核提供的功能。所以,write内部封装的一定是系统调用指令,按照这种设想,一会咱们会模拟一下它的实现。
我们这里要介绍下第二种:跨过库函数直接与系统内核通信,这样最终的程序是不需要与任何库文件链接,这是获得系统功能效率最高的方式。
我相信,如果曾经学过汇编语言,老师都给咱们演示过第二种方式,但大多数同学还是觉得云里雾里,即使照葫芦画瓢完成了打印字符串的工作,也有部分同学不清楚自己在做什么,所以我在这里尽量多说一点。
前面我们已经知道了write系统调用函数的c语言使用方式,我们要用汇编代码直接与内核通信该怎么做?我们要看看系统调用输入参数的传递方式。
当输入的参数小于等于5个时,linux是用寄存器传递参数。当参数个数大于5个时,把参数按照顺序放入连续的内存区域,并将该区域的首地址放到ebx寄存器。这里我们只演示参数小于等于5个的情况。
eax寄存器用来存储子功能号(寄存器eip、ebp、esp是不能使用的)。5个参数是存放在以下寄存器中,传送参数的顺序是:
- ebx存储第1个参数
- ecx存储第2个参数
- edx存储第3个参数
- esi存储第4个参数
- edi存储第5个参数
好啦,理论知识够用啦,现在赶紧实践一把,见以下代码syscall_write.S
1 section .data2 str_c_lib: db "c library says: hello world!", 0xa ;0xa为LF ascii码3 str_c_lib_len equ $-str_c_lib45 str_syscall: db "syscall says: hello world!", 0xa6 str_syscall_len equ $-str_syscall78 section .text9 global _start10 _start:11 ;;;;;;;;;;;;; 方式1: 模拟c语言中系统调用库函数write ;;;;;;;;;;;;;12 push str_c_lib_len ;按照c调用约定压入参数13 push str_c_lib14 push 11516 call simu_write ;调用下面定义的simu_write17 add esp,12 ;回收栈空间1819 ;;;;;;;;;;;;; 方式2: 跨过库函数,直接进行系统调用 ;;;;;;;;;;;;;20 mov eax,4 ;第4号子功能是write系统调用(不是c库函数write)21 mov ebx, 122 mov ecx, str_syscall23 mov edx, str_syscall_len24 int 0x80 ;发起中断,通知linux完成请求的功能。2526 ;;;;;;;;;;;;; 退出程序 ;;;;;;;;;;;27 mov eax,1 ;第1号子功能是exit28 int 0x80 ;发起中断,通知linux完成请求的功能。2930 ;;;;;;;下面自定义的simu_write用来模拟c库中系统调用函数write,;;;;;;这里模拟它的实现原理31 simu_write:32 push ebp ;备份ebp33 mov ebp,esp34 mov eax,4 ;第4号子功能是write系统调用(不是c库函数write) 35 mov ebx, [ebp+8] ;第1个参数36 mov ecx, [ebp+12] ;第2个参数37 mov edx, [ebp+16] ;第3个参数38 int 0x80 ;发起中断,通知linux完成请求的功能39 pop ebp ;恢复ebp40 ret
代码syscall_write.S中,我们演示了系统调用的两种方式。程序开头定义了两种方式下打印的字符串,其中0xa为LF(LineFeed)ascii码,这样就会输出一个换行符。
第11~17行是在演示方式1,模拟调用c库函数write的方式。因为write是c库函数,按一般的做法是,汇编程序需要与c代码生成的目标文件链接才能调用c的代码。在这个例子中我们并没有这样帮做,因为我想让大家了解write函数的本质,所以,在这里为大家定义了simu_write来代替c库函数write,用它来简单解释write的原理,它定义在31~40行。这里是按照c调用约定将参数从右到左依次入栈,随后调用simu_write实现字符串打印功能。
第19~24行是在演示第2种系统调用的方式,这是最简单直接可依赖的方式。20~24行是在eax中赋予子功能号、将参数按照顺序依次写入对应的寄存器。
第31~40行是simu_write的实现,它内部在本质上是和第2种方式一样,都是在内部调用int指令直接和系统通信实现系统调用。此函数只是为了试图揭开c库函数的实现原理,良苦用心您懂的。
好啦,编译链接过程如下:
nasm -f elf -o syscall_write.o syscall_write.S
其中-f参数是用来指定编译输出的文件格式,这里需要指定为elf,目的是将来要和gcc编译的elf格式的目标文件链接,所以格式必须相同。nasm输出为目标文件,已经用-o指定文件名为syscall_write.o。
最后用ld程序将syscall_write.o链接成elf格式的二进制可执行文件。
ld -o syscall_write.bin syscall_write.o
程序执行后的效果如图
顺便说一句,syscall_write.bin如果因为权限不足而无法执行时,可以用以下指令增加执行权限:
chmod u+x syscall_write.bin
本文摘自《操作系统真象还原》,请大家支持正版,多谢。