本文共 5808 字,大约阅读时间需要 19 分钟。
首先何为系统调用?根据维基百科的解释:a system call is how a program requests a service from an operating system's kernel. 简意就是用户程序对操作系统内核服务的请求。我们知道用户程序所能做的事情很有限,比如我们不能自己写代码直接操作硬盘,不能自己写代码控制控制台的输入输出等等,那怎么办呢?办法就是请求操作系统帮我们完成,毕竟操作系统掌控着所有的计算资源,因此对它来说,控制硬盘,显示器啦肯定都是不在话下的。
其实我们平时写的很多用户程序都用到系统调用,只不过由于一般编程语言都会把系统调用封装在标准库里面,所以我们没有发觉。因此系统调用很多时候对我们来说就是简单的调用函数。比如c语言中,我们最常用的pritf格式化输出函数就需要用到系统调用,毕竟将一些东西显示到显示器上还不是那么容易的~
下面以JOS操作系统的源码来了解一下系统调用,以及他是怎么封装在函数当中的。
首先假如我们编写了以下用户程序
// hello, world#include首先需要调用打印函数,看一下打印函数的实现voidmain(int argc, char **argv){ cprintf("hello, world!\n");}
intcprintf(const char *fmt, ...){ va_list ap; int cnt; va_start(ap, fmt); cnt = vcprintf(fmt, ap); va_end(ap); return cnt;}打印函数需要调用 vcprintf 函数,接着看vcprintf 函数的实现
intvcprintf(const char *fmt, va_list ap){ struct printbuf b; b.idx = 0; b.cnt = 0; vprintfmt((void*)putch, &b, fmt, ap); sys_cputs(b.buf, b.idx); return b.cnt;}
可以看到vcprintf需要调用两个函数:vprintfmat 和 sys_cputs
以下是vprintfmat 函数的代码
voidvprintfmt(void (*putch)(int, void*), void *putdat, const char *fmt, va_list ap)//caller : vprintfmt((void*)putch, &cnt, fmt, ap);{ register const char *p; register int ch, err; unsigned long long num; int base, lflag, width, precision, altflag; char padc; while (1) { while ((ch = *(unsigned char *) fmt++) != '%') { if (ch == '\0') return; putch(ch, putdat); } // Process a %-escape sequence padc = ' '; width = -1; precision = -1; lflag = 0; altflag = 0; reswitch: switch (ch = *(unsigned char *) fmt++) { // flag to pad on the right case '-': padc = '-'; goto reswitch; // flag to pad with 0's instead of spaces case '0': padc = '0'; goto reswitch; // width field case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': for (precision = 0; ; ++fmt) { precision = precision * 10 + ch - '0'; ch = *fmt; if (ch < '0' || ch > '9') break; } goto process_precision; case '*': precision = va_arg(ap, int); goto process_precision; case '.': if (width < 0) width = 0; goto reswitch; case '#': altflag = 1; goto reswitch; process_precision: if (width < 0) width = precision, precision = -1; goto reswitch; // long flag (doubled for long long) case 'l': lflag++; goto reswitch; // character case 'c': putch(va_arg(ap, int), putdat); break; // error message case 'e': err = va_arg(ap, int); if (err < 0) err = -err; if (err >= MAXERROR || (p = error_string[err]) == NULL) printfmt(putch, putdat, "error %d", err); else printfmt(putch, putdat, "%s", p); break; // string case 's': if ((p = va_arg(ap, char *)) == NULL) p = "(null)"; if (width > 0 && padc != '-') for (width -= strnlen(p, precision); width > 0; width--) putch(padc, putdat); for (; (ch = *p++) != '\0' && (precision < 0 || --precision >= 0); width--) if (altflag && (ch < ' ' || ch > '~')) putch('?', putdat); else putch(ch, putdat); for (; width > 0; width--) putch(' ', putdat); break; // (signed) decimal case 'd': num = getint(&ap, lflag); if ((long long) num < 0) { putch('-', putdat); num = -(long long) num; } base = 10; goto number; // unsigned decimal case 'u': num = getuint(&ap, lflag); base = 10; goto number; // (unsigned) octal case 'o': num = getuint(&ap,lflag); base = 8; goto number; break; // pointer case 'p': putch('0', putdat); putch('x', putdat); num = (unsigned long long) (uintptr_t) va_arg(ap, void *); base = 16; goto number; // (unsigned) hexadecimal case 'x': num = getuint(&ap, lflag); base = 16; number: printnum(putch, putdat, num, base, width, padc); break; // escaped '%' character case '%': putch(ch, putdat); break; // unrecognized escape sequence - just print it literally default: putch('%', putdat); for (fmt--; fmt[-1] != '%'; fmt--) /* do nothing */; break; } }}
从vprintfmat函数的代码可以看出,他主要调用的是putch,所以下面再让我们看一下putch的代码
static voidputch(int ch, struct printbuf *b){ b->buf[b->idx++] = ch; if (b->idx == 256-1) { sys_cputs(b->buf, b->idx); b->idx = 0; } b->cnt++;}
可以看到putch中主要掉用的是sys_cputs 函数,是不是似曾相识?回头看一下vcprintf函数,就会看到sys_cputs 函数!因此抛开细节,sys_cputs函数是关键!
好了让我们看看sys_cputs函数的真面目
voidsys_cputs(const char *s, size_t len){ syscall(SYS_cputs, 0, (uint32_t)s, len, 0, 0, 0);}
大吃一惊,我还以为他会是几百行呢。原来它也是调用了另一个函数 : syscall
不过看这个函数名称,好像离我们的系统调用很近了!
好,我们接着看下面的syscall函数源码
static inline int32_tsyscall(int num, int check, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t a4, uint32_t a5)// interface{ int32_t ret; asm volatile("int %1\n" : "=a" (ret) : "i" (T_SYSCALL), "a" (num), "d" (a1), "c" (a2), "b" (a3), "D" (a4), "S" (a5) : "cc", "memory"); if(check && ret > 0) panic("syscall %d returned %d (> 0)", num, ret); return ret;}非常遗憾是看不懂的一段内联汇编,没事我们可以看看汇编后的汇编代码
800a18: 55 push %ebp 800a19: 89 e5 mov %esp,%ebp 800a1b: 57 push %edi 800a1c: 56 push %esi 800a1d: 53 push %ebx asm volatile("int %1\n" 800a1e: b8 00 00 00 00 mov $0x0,%eax 800a23: 8b 4d 0c mov 0xc(%ebp),%ecx 800a26: 8b 55 08 mov 0x8(%ebp),%edx 800a29: 89 c3 mov %eax,%ebx 800a2b: 89 c7 mov %eax,%edi 800a2d: 89 c6 mov %eax,%esi 800a2f: cd 30 int $0x30简单看一下这段代码,前面部分是建立栈帧。asm volatile部分的代码从 0x800ale地址开始。前面都是寄存器操作,看后面,有一个int 30,软中断。通过JOS源码可以知道,JOS的IDT对应的系统调用号是0x30。 这样我们就可以理解int 30 了!
其实我之前一直不太理解的问题是,int 30之后 系统怎么知道程序请求的具体是哪一个系统调用呢?我也知道是通过eax寄存器中的参数确定,但我不知道用户进程是怎么设置eax中的参数的,如今读了JOS源码才算知道。原来,对于用户进程调用的库函数,所有的库函数都有一个公共的接口(这里就是syscall函数),于是就可以通过这个接口,设置eax寄存器中的参数,使得各个库函数分别对应到各自的系统调用中。而这一切对用户程序来说都是透明的,他根本不知道什么是eax。
PS:以上代码源自MIT JOS系统源码
转载地址:http://yiqxi.baihongyu.com/