Linux:早期操作系统的系统调用
相关阅读
Linuxhttps://blog.csdn.net/weixin_45791458/category_12234591.html?spm=1001.2014.3001.5482
简介
本文将以Linux1.0为例说明早期操作系统的系统调用过程。
Linux1.0总共提供了135个系统调用(其中一些是保留或未实现),可以在源码路径linux-1.0/include/linux/sys.h下找到系统调用函数声明,在源码路径linux-1.0/include/linux/unistd.h下找到系统调用号定义。
下面列举出了一些系统调用的相关信息。
系统调用号 | 系统调用函数名 | 系统调用函数原型 | 含义 | 定义位置 |
0 | sys_setup | asmlinkage int sys_setup(void * BIOS) | 完成系统设备初始化(磁盘)、加载 RAM 盘、挂载根文件系统。 | linux-1.0/drivers/block/genhd.c |
1 | sys_exit | asmlinkage int sys_exit(int error_code) | 终止当前进程的执行,并清理它所拥有的所有系统资源。 | linux-1.0/kernel/exit.c |
2 | sys_fork | asmlinkage int sys_fork(struct pt_regs regs) | 创建当前进程的一个子进程(即“复制进程”),子进程几乎完全复制父进程的执行上下文。 | linux-1.0/kernel/fork.c |
3 | sys_read | asmlinkage int sys_read(unsigned int fd,char * buf,unsigned int count) | 从文件描述符所表示的文件中读取数据,并将其存入用户空间缓冲区中。 | linux-1.0/fs/read_write.c |
4 | sys_write | asmlinkage int sys_write(unsigned int fd,char * buf,unsigned int count) | 将用户空间缓冲区 中的数据写入由文件描述符所表示的文件中。 | linux-1.0/fs/read_write.c |
5 | sys_open | asmlinkage int sys_open(const char * filename,int flags,int mode) | 根据指定的路径 打开一个文件,按指定的方式访问,并在需要创建文件时指定权限。最终返回一个文件描述符。 | linux-1.0/fs/open.c |
6 | sys_close | asmlinkage int sys_close(unsigned int fd) | 关闭一个打开的文件描述符,释放它所占用的内核资源,并使该描述符可被重新使用。 | linux-1.0/fs/open.c |
7 | sys_waitpid | asmlinkage int sys_waitpid(pid_t pid,unsigned long * stat_addr, int options) | 使父进程等待其一个子进程状态发生变化(如退出或被信号停止),并获取该子进程的退出状态。 | linux-1.0/kernel/signal.c |
8 | sys_creat | asmlinkage int sys_creat(const char * pathname, int mode) | 创建一个新文件(如果文件不存在),或清空已有文件的内容(如果文件已存在),并返回一个用于后续读写操作的文件描述符。 | linux-1.0/fs/open.c |
..... |
使用标准C库
以glibc1.09.1为例,对于系统调用sys_read(),glibc提供了一个系统调用包装器函数read(),其声明位于文件glibc-1.09.1\posix\unistd.h中,函数原型如下所示。
extern ssize_t read __P ((int __fd, __ptr_t __buf, size_t __nbytes));
其中__P是一个宏,其目的是在头文件中写一次函数声明,而能同时支持老旧编译器和新标准的编译器。
接下来就是寻找read()函数的定义文件,可以在文件glibc-1.09.1\io\read.c中找到以下内容。
function_alias(read, __read, __ssize_t, (fd, buf, n),DEFUN(read, (fd, buf, n),int fd AND PTR buf AND size_t n))
其中function_alias是一个宏,用于创建函数别名,DEFUN宏也是为了支持老旧编译器和新标准的编译器,功能上类似于以下写法。
__ssize_t read(int fd, void *buf, size_t n) {return __read(fd, buf, n);
}
接着可以在文件glibc-1.09.1\sysdeps\stub\__read.c中找到以下内容。
ssize_t
DEFUN(__read, (fd, buf, nbytes),int fd AND PTR buf AND size_t nbytes)
{if (nbytes == 0)return 0;if (fd < 0){errno = EBADF;return -1;}if (buf == NULL){errno = EINVAL;return -1;}errno = ENOSYS;return -1;
}
但遗憾的是,该函数看起来并没有任何与系统调用有关的内容,因为它只是一个stub(桩函数),也叫“占位函数”。
继续寻找,可以在文件glibc-1.09.1\sysdeps\unix\__read.S中找到找到以下内容。
SYSCALL__ (read, 3)ret
SYSCALL__是一个宏,定义如下所示。
SYSCALL__(name, args) PSEUDO (__##name, name, args)
PSEUDO是一个与平台相关的宏,对于i386架构,定义如下所示。
#define PSEUDO(name, syscall_name, args) \.text; \.globl syscall_error; \ENTRY (name) \XCHG_##argsmovl $SYS_##syscall_name, %eax; \int $0x80; \test %eax, %eax; \jl syscall_error; \XCHG_##args
这段汇编代码设置eax寄存器为系统调用号($SYS_##syscall_name将被宏展开为$SYS_read,而后者又在glibc-1.09.1\sysdeps\unix\sysv\linux\syscall.h文件中被宏定义为3),最后通过int $0x80软中断进入内核模式,执行系统调用。
直接使用系统调用宏
以Linux1.0为例,Linux提供了六个宏用于定义系统调用包装器函数,位于文件linux-1.0/include/linux/unistd.h中,定义如下所示。
#define _syscall0(type,name) \
type name(void) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \: "=a" (__res) \: "0" (__NR_##name)); \
if (__res >= 0) \return (type) __res; \
errno = -__res; \
return -1; \
}#define _syscall1(type,name,atype,a) \
type name(atype a) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \: "=a" (__res) \: "0" (__NR_##name),"b" ((long)(a))); \
if (__res >= 0) \return (type) __res; \
errno = -__res; \
return -1; \
}#define _syscall2(type,name,atype,a,btype,b) \
type name(atype a,btype b) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \: "=a" (__res) \: "0" (__NR_##name),"b" ((long)(a)),"c" ((long)(b))); \
if (__res >= 0) \return (type) __res; \
errno = -__res; \
return -1; \
}#define _syscall3(type,name,atype,a,btype,b,ctype,c) \
type name(atype a,btype b,ctype c) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \: "=a" (__res) \: "0" (__NR_##name),"b" ((long)(a)),"c" ((long)(b)),"d" ((long)(c))); \
if (__res>=0) \return (type) __res; \
errno=-__res; \
return -1; \
}#define _syscall4(type,name,atype,a,btype,b,ctype,c,dtype,d) \
type name (atype a, btype b, ctype c, dtype d) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \: "=a" (__res) \: "0" (__NR_##name),"b" ((long)(a)),"c" ((long)(b)), \"d" ((long)(c)),"S" ((long)(d))); \
if (__res>=0) \return (type) __res; \
errno=-__res; \
return -1; \
}#define _syscall5(type,name,atype,a,btype,b,ctype,c,dtype,d,etype,e) \
type name (atype a,btype b,ctype c,dtype d,etype e) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \: "=a" (__res) \: "0" (__NR_##name),"b" ((long)(a)),"c" ((long)(b)), \"d" ((long)(c)),"S" ((long)(d)),"D" ((long)(e))); \
if (__res>=0) \return (type) __res; \
errno=-__res; \
return -1; \
}
以_syscall1为例,它用于定义一个有一个参数的系统调用包装器函数,它的第一个参数是系统调用的返回值类型,第二个参数是系统调用名(去除前面的sys_前缀)。
以系统调用sys_close为例,如果需要使用它,首先需要使用_syscall1宏进行声明,如下所示。
_syscall1(int, close, unsigned int, fd)
它将宏展开为以下包装器函数定义。
int close(unsigned int fd)
{
long __res;
__asm__ volatile ("int $0x80" : "=a" (__res) : "0" (__NR_close),"b" ((long)(fd)));
if (__res >= 0) return (int) __res;
errno = -__res;
return -1;
}
这段汇编代码设置eax寄存器为系统调用号(__NR_##name将被宏展开为__NR_close,而后者又在linux-1.0/include/linux/unistd.h文件中被宏定义为6),设置eax寄存器为fd参数,最后通过int $0x80软中断进入内核模式,执行系统调用。
写在最后
在Linux2.6.20后,_syscall0类的宏被废除了,转而使用glibc提供的函数syscall(),函数原型如下所示。
extern int syscall __P ((int __sysno, ...));
syscall()函数可以根据系统调用号直接执行系统调用,就像是文中第一节中使用标准C库的方式进行系统调用那样。
在Linux2.5版本时,为了更加高效地执行系统调用,对于Pentium II处理器支持了新的指令sysenter和sysexit,到目前的x86-64架构,更加高效的syscall和sysret指令被引入了。