您的位置:首页 > 其它

从JOS源码了解系统调用

2016-01-16 19:37 381 查看
首先何为系统调用?根据维基百科的解释:a system call is how a program requests a service from an operating system's kernel. 简意就是用户程序对操作系统内核服务的请求。我们知道用户程序所能做的事情很有限,比如我们不能自己写代码直接操作硬盘,不能自己写代码控制控制台的输入输出等等,那怎么办呢?办法就是请求操作系统帮我们完成,毕竟操作系统掌控着所有的计算资源,因此对它来说,控制硬盘,显示器啦肯定都是不在话下的。

其实我们平时写的很多用户程序都用到系统调用,只不过由于一般编程语言都会把系统调用封装在标准库里面,所以我们没有发觉。因此系统调用很多时候对我们来说就是简单的调用函数。比如c语言中,我们最常用的pritf格式化输出函数就需要用到系统调用,毕竟将一些东西显示到显示器上还不是那么容易的~

下面以JOS操作系统的源码来了解一下系统调用,以及他是怎么封装在函数当中的。

首先假如我们编写了以下用户程序

// hello, world
#include <inc/lib.h>

void
main(int argc, char **argv)
{
cprintf("hello, world!\n");
}


首先需要调用打印函数,看一下打印函数的实现

int
cprintf(const char *fmt, ...)
{
va_list ap;
int cnt;

va_start(ap, fmt);
cnt = vcprintf(fmt, ap);
va_end(ap);

return cnt;
}


打印函数需要调用 vcprintf 函数,接着看vcprintf 函数的实现

int
vcprintf(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 函数的代码

void
vprintfmt(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 void
putch(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函数的真面目

void
sys_cputs(const char *s, size_t len)
{
syscall(SYS_cputs, 0, (uint32_t)s, len, 0, 0, 0);
}


大吃一惊,我还以为他会是几百行呢。原来它也是调用了另一个函数 : syscall

不过看这个函数名称,好像离我们的系统调用很近了!

好,我们接着看下面的syscall函数源码

static inline int32_t
syscall(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系统源码
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: