一步步编写操作系统 67 系统调用的实现1-2 68

接上文:

系统调用的子功能要用eax寄存器来指定,所以咱们要看看有哪些系统调用啦,在linux系统中,系统调用是定义在/usr/include/asm/unistd.h文件中,该文件只是个统一的入口,指向了32位和64位两种版本。在asm目录下提供了这两个版本,文件名分别是unistd_32.h 和unistd_64.h,这里给大家摘录了部分32位x86平台下的unistd_32.h文件,见图

一步步编写操作系统 67 系统调用的实现1-2 68

 

在/usr/include/asm/unistd_32.h文件*定义了348个系统调用,哦,给大家说一下,我用的linux版本是CentOS release 6.3 (Final),不知道新版本内核中是否增加了新的系统调用功能。

我们要用的系统调用是第4号调用,即__NR_write。不要被它前面的两个下滑线吓到,就是个命名而已,它代表就是我们所说的write系统调用。

如果不知道某个系统调用的用法,可以用man命令来查看,方法是man 2 系统调用名。咱们执行man 2 write看看,见图

一步步编写操作系统 67 系统调用的实现1-2 68

 

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,其编译、运行都没问题,也许这和隐式声明有关,这里不再深究。

调用“系统调用”有两种方式:

  1. 将系统调用指令封装为c库函数,通过库函数进行系统调用,操作简单。
  2. 不依赖任何库函数,直接通过汇编指令int与操作系统通信。

以上的c代码就是用的第一种方式,不知道您是否对write函数的内部实现感兴趣,其实我也没研究过,不过万变不离其宗,核心思想是必须与进行内核沟通才能获得内核提供的功能。所以,write内部封装的一定是系统调用指令,按照这种设想,下次咱们会模拟一下它的实现。

 

调用“系统调用”有两种方式:

  1. 将系统调用指令封装为c库函数,通过库函数进行系统调用,操作简单。
  2. 不依赖任何库函数,直接通过汇编指令int与操作系统通信。

以上的c代码就是用的第一种方式,不知道您是否对write函数的内部实现感兴趣,其实我也没研究过,不过万变不离其宗,核心思想是必须与进行内核沟通才能获得内核提供的功能。所以,write内部封装的一定是系统调用指令,按照这种设想,一会咱们会模拟一下它的实现。

我们这里要介绍下第二种:跨过库函数直接与系统内核通信,这样最终的程序是不需要与任何库文件链接,这是获得系统功能效率最高的方式。

我相信,如果曾经学过汇编语言,老师都给咱们演示过第二种方式,但大多数同学还是觉得云里雾里,即使照葫芦画瓢完成了打印字符串的工作,也有部分同学不清楚自己在做什么,所以我在这里尽量多说一点。

前面我们已经知道了write系统调用函数的c语言使用方式,我们要用汇编代码直接与内核通信该怎么做?我们要看看系统调用输入参数的传递方式。

当输入的参数小于等于5个时,linux是用寄存器传递参数。当参数个数大于5个时,把参数按照顺序放入连续的内存区域,并将该区域的首地址放到ebx寄存器。这里我们只演示参数小于等于5个的情况。

eax寄存器用来存储子功能号(寄存器eip、ebp、esp是不能使用的)。5个参数是存放在以下寄存器中,传送参数的顺序是:

  1. ebx存储第1个参数
  2. ecx存储第2个参数
  3. edx存储第3个参数
  4. esi存储第4个参数
  5. edi存储第5个参数

好啦,理论知识够用啦,现在赶紧实践一把,见以下代码syscall_write.S

 1 section .data
 2 str_c_lib: db "c library says: hello world!", 0xa ;0xa为LF ascii码
 3 str_c_lib_len equ $-str_c_lib
 4
 5 str_syscall: db "syscall says: hello world!", 0xa
 6 str_syscall_len equ $-str_syscall
 7
 8 section .text
 9 global _start
 10 _start:
 11 ;;;;;;;;;;;;; 方式1: 模拟c语言中系统调用库函数write ;;;;;;;;;;;;;
 12 push str_c_lib_len ;按照c调用约定压入参数
 13 push str_c_lib
 14 push 1
 15
 16 call simu_write ;调用下面定义的simu_write
 17 add esp,12 ;回收栈空间
 18
 19 ;;;;;;;;;;;;; 方式2: 跨过库函数,直接进行系统调用 ;;;;;;;;;;;;;
 20 mov eax,4 ;第4号子功能是write系统调用(不是c库函数write)
 21 mov ebx, 1
 22 mov ecx, str_syscall
 23 mov edx, str_syscall_len
 24 int 0x80 ;发起中断,通知linux完成请求的功能。
 25
 26 ;;;;;;;;;;;;; 退出程序 ;;;;;;;;;;;
 27 mov eax,1 ;第1号子功能是exit
 28 int 0x80 ;发起中断,通知linux完成请求的功能。
 29
 30 ;;;;;;;下面自定义的simu_write用来模拟c库中系统调用函数write,
 ;;;;;;这里模拟它的实现原理
 31 simu_write:
 32 push ebp ;备份ebp
 33 mov ebp,esp
 34 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 ;恢复ebp
 40 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

程序执行后的效果如图

一步步编写操作系统 67 系统调用的实现1-2 68

 

顺便说一句,syscall_write.bin如果因为权限不足而无法执行时,可以用以下指令增加执行权限:

chmod u+x syscall_write.bin

本文摘自《操作系统真象还原》,请大家支持正版,多谢。

上一篇:P3932 浮游大陆的68号岛


下一篇:CSS3之box-shadow