系统调用实现原理(Printf函数为例)

系统调用实现(Printf函数为例)

Sys_call

调用程序时,会检查当前段的CPL(位于CS中),与目标段的DPL(位于gdt中),如果权限不够无法执行,所以我们无法以用户态直接访问某些指令并执行。而通过系统调用可以从用户态转变为内核态,执行相关程序。实现的方法为0x80中断,改变CS中的CPL为0。

以printf函数为例,其本身调用了write函数,同时格式化字符串被缓冲到用户态buf中:

// linux/init/main.c
static int printf(const char *fmt, ...)
{
va_list args;
int i;
va_start(args, fmt);
write(1,printbuf,i=vsprintf(printbuf, fmt, args));
va_end(args);
return i;
}

write函数的源代码为:

// linux/lib/write.c
#define __LIBRARY__
#include <unistd.h>
_syscall3(int,write,int,fd,const char *,buf,off_t,count)
// linux/include/unistd.h
#define __NR_write 4
#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; \
}

经过宏展开为:

int write(int fd, const char * buf, off_t count)
{
long __res;
__asm__ volatile (
"int $0x80"
: "=a" (__res)
: "0" (__NR_write),
"b" ((long)(fd)),
"c" ((long)(buf)),
"d" ((long)(count))
);
if (__res >= 0) { return (int)__res; }
errno = -__res;
return -1;
}

其中汇编代码的意思是:将__NR_write放入EAX,fd放入EBX,buf放入ECX,count放入EDX,执行int $0x80中断,然后将EAX放入__res

__NR_##name(这里是__NR_write)被称为系统调用号,在unistd.h中已经宏定义,因为都通过int 0x80进入中断,可用这个区分需要执行的内核对应物。

0x80中断执行的程序是加载OS核心文件时已经配置好了的。在执行system中的main时,执行了sched_init函数,设置了int 0x80执行system_call函数:

// linux/kernel/sched.c
void sched_init(void)
{
...
set_system_gate(0x80,&system_call);
}
// linux/include/asm/system.h
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \
"movw %0,%%dx\n\t" \
"movl %%eax,%1\n\t" \
"movl %%edx,%2" \
: \
: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
"o" (*((char *) (gate_addr))), \
"o" (*(4+(char *) (gate_addr))), \
"d" ((char *) (addr)),"a" (0x00080000))
#define set_system_gate(n,addr) \
_set_gate(&idt[n],15,3,addr)

经过宏展开为:

gate_addr是idt[80]的地址,addr是system_call中断程序的地址。

__asm__(
"movw %%dx,%%ax\n\t"
"movw %0,%%dx\n\t"
"movl %%eax,%1\n\t"
"movl %%edx,%2"
:
: "i" ((short) (0x8000+(3<<13)+(15<<8))),
"o" (*((char *) (gate_addr))),
"o" (*(4+(char *) (gate_addr))),
"d" ((char *) (addr)),
"a" (0x00080000))
);

其实就是设置了以下的idt表:

观察看,0x80中断的idt表查询的DPL被设置为3,而用户态的CPL为3,所以该中断程序我们是有权跳转执行的。执行时,段选择符CS被设置为0x0008(内核代码段),此时CPL被设置为0,通过gdt表找到了内核代码段,IP为system_call的地址。

system_call函数主要执行的内容(在调用write时,我们的系统调用号已经被放在EAX中):

# linux/kernel/system_call.s
nr_system_calls = 72 # Linux 0.11 版本内核中的系统共调用总数。
.globl system_call # 定义入口点
system_call:
cmpl $nr_system_calls-1,%eax # 调用号如果超出范围的话就在eax中置-1并退出
ja bad_sys_call
push %ds # 保存原段寄存器值
push %es
push %fs
pushl %edx
pushl %ecx # push %ebx,%ecx,%edx as parameters
pushl %ebx # to the system call
# 设置dses0x10,内核数据段。
movl $0x10,%edx
mov %dx,%ds
mov %dx,%es
movl $0x17,%edx # fs points to local data space
mov %dx,%fs
call sys_call_table(,%eax,4) # 间接调用指定功能C函数
pushl %eax
...

sys_call_table+4*%eax就是相应系统调用处理函数入口地址。这里通过查sys_call_table全局函数数组,call sys_call_table(,%eax,4) 就是call sys_write

// linux/include/linux/sched.h
typedef int (*fn_ptr)();
// linux/include/linux/sys.h
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, ...};

所以,整体调用printf函数的过程是:

Sys_write

sys_write完成向I/O的寄存器写相应内容,I/O设备的控制器再执行相关操作,从而完成字符在显示器上的打印。而往寄存器上写最终的指令表现为out xxx,al

为了使得外设工作的更简单,使用统一的文件视图。

设备驱动实现我们对I/O设备的使用,要有3个功能:根据相关驱动程序最终发出out指令、形成文件视图、I/O设备发出中断。

write函数通过最终系统调用,使用sys_write处理:

// linux/fs/read_write.c
int sys_write(unsigned int fd,char * buf,int count)
{
...
struct file * file;
file = current -> filp[fd];
inode = file -> f_inode;
...
}

file对应了输出设备的文件(已被打开),而inode则为该文件的信息。而current当前PCB中的信息都是通过fork从父进程拷贝来的:

// kernel/fork.c
int copy_process(...)
{
struct task_struct *p;
...
*p = *current;
...
for (i=0; i<NR_OPEN;i++)
if ((f=p->filp[i]))
f->f_count++;
...
}

shell进程启动了whoami命令,shell是其父进程。一开始在init函数中,open打开了该设备文件,并使用dup拷贝了两份,三份文件对应的句柄分别为0、1、2,对应stdin、stdout、stderr。而上述sys_write的fd为1,对应着dev/tty设备。

// linux/init/main.c
void main(void)
{
...
if (!fork()) { init(); }
...
}
void init(void)
{
...
(void) open("/dev/tty0",O_RDWR,0);
(void) dup(0);
(void) dup(0);
...
execve("/bin/sh",argv_rc,envp_rc);
...
}

而open的系统调用处理函数为sys_open:sys_open根据传递过来的filename使用open_namei函数找到对应的inode,然后将相关信息赋值给f结构,而f被保存在current这个PCB中。

// linux/fs/open.c
int sys_open(const char * filename,int flag,int mode)
{
struct m_inode * inode;
struct file * f;
int i,fd;
...
i=open_namei(filename,flag,mode,&inode);
current->filp[fd]=f;
...
f->f_mode = inode->i_mode;
f->f_flags = flag;
f->f_count = 1;
f->f_inode = inode;
f->f_pos = 0;
return (fd);
}

继续看sys_write处理函数:判断该文件是什么类型,这里以输出到/dev/tty0的char设备为例

inode中的i_zone[0]保存了该设备是字符设备中的第几个,即设备号(ls -l /dev可查看)。

// linux/fs/read_write.c
int sys_write(unsigned int fd,char * buf,int count)
{
...
inode=file->f_inode;
if (inode->i_pipe)
return (file->f_mode&2)?write_pipe(inode,buf,count):-EIO;
if (S_ISCHR(inode->i_mode))
return rw_char(WRITE,inode->i_zone[0],buf,count,&file->f_pos);
if (S_ISBLK(inode->i_mode))
return block_write(inode->i_zone[0],&file->f_pos,buf,count);
if (S_ISREG(inode->i_mode))
return file_write(inode,file,buf,count);
...
}

现在查看rw_char:call_addr是对应设备的处理函数,根据crw_table找到对应的处理函数并执行。

// linux/fs/char_dev.c
int rw_char(int rw,int dev, char * buf, int count, off_t * pos)
{
crw_ptr call_addr = crw_table[MAJOR(dev)];
...
call_addr(rw,MINOR(dev),buf,count,pos);
}

这里的设备号是4,从设备号为0(可根据ls -l \dev查看),在crw_table表中处理函数为rw_ttyx:

// linux/fs/char_dev.c
static crw_ptr crw_table[]={
NULL, /* nodev */
rw_memory, /* /dev/mem etc */
NULL, /* /dev/fd */
NULL, /* /dev/hd */
rw_ttyx, /* /dev/ttyx */
rw_tty, /* /dev/tty */
NULL, /* /dev/lp */
NULL}; /* unnamed pipes */

rw_ttyx函数中,这里是write,调用tty_write函数:

// linux/fs/char_dev.c
static int rw_ttyx(int rw,unsigned minor,char * buf,int count,off_t * pos)
{
return ((rw==READ)?tty_read(minor,buf,count):
tty_write(minor,buf,count));
}

在tty_write函数中:首先查看tty的write_q队列是否满,满了则sleep,直到消费者消费后发出中断后唤醒。然后从buf(这个buf工作在用户态内存)里面逐个取出字符放入write_q中,然后调用tty的write将write_q队列中的字符输出。

// linux/kernel/chr_drv/tty_io.c
int tty_write(unsigned channel, char * buf, int nr)
{
...
struct tty_struct * tty;
tty = channel + tty_table;
sleep_if_full(&tty->write_q);
...
char c, *b=buf;
while (nr>0 && !FULL(tty->write_q)) {
c=get_fs_byte(b);
if (c=='\r') { PUTCH(13,tty->write_q); continue; }
if (O_LCUC(tty)) c = toupper(c);
b++; nr--;
cr_flag = 0;
PUTCH(c,tty->write_q);
}
tty->write(tty);
...
}

tty的结构体中,write是一个函数指针,被初始化为con_write:

// linux/include/linux/tty.h
struct tty_struct {
struct termios termios;
int pgrp;
int stopped;
void (*write)(struct tty_struct * tty);
struct tty_queue read_q;
struct tty_queue write_q;
struct tty_queue secondary;
};
// linux/kernel/chr_drv/tty_io.c
struct tty_struct tty_table[] = {
{
...
con_write,
...
},
...,
...
};

最终执行真正的执行函数con_write:根据汇编代码,AH中放属性、AL中放字符。最终表现为mov ax,pos(若对外设使用独立编指这里使用out),pos就是控制显卡的那个寄存器

// linux/kernel/chr_drv/console.c
void con_write(struct tty_struct * tty)
{
...
GETCH(tty->write_q,c);
while(...)
{
if (c>31 && c<127)
{
__asm__(
"movb attr,%%ah\n\t"
"movw %%ax,%1\n\t"
:
: "a" (c),
"m" (*(short *)pos)
);
pos += 2;
}
}
...
}

上述函数其实就是设备驱动。而做设备驱动其实就是写一些函数然后注册上去,注册就是把处理函数放到对应表中,创建dev文件并将其与相应的处理函数对应上。

pos初始化在con_init中:

// // linux/kernel/chr_drv/console.c
#define ORIG_X (*(unsigned char *)0x90000)
#define ORIG_Y (*(unsigned char *)0x90001)
static inline void gotoxy(unsigned int new_x,unsigned int new_y)
{
...
pos=origin + y*video_size_row + (x<<1);
}
void con_init(void)
{
...
gotoxy(ORIG_X,ORIG_Y);
...
}

键盘输入的实现

键盘的中断号为21,处理函数被设置为keyboard_interrupt:

// linux/kernel/chr_drv/console.c
void con_init(void)
{
...
set_trap_gate(0x21,&keyboard_interrupt);
...
}

keyboard_interrupt把60端口中的内容(扫描码)放入AL,调用key_table根据扫描码决定传入对应字符的ASCII码,put_queue函数将ASCII码放入缓冲队列con.read_q中。

# linux/kernel/chr_drv/keyboard.S
keyboard_interrupt:
...
inb $0x60,%al
...
call key_table(,%eax,4)
...
pushl $0
call do_tty_interrupt
...
key_table:
.long none,do_self,do_self,do_self /* 00-03 s0 esc 1 2 */
...
.long func,num,scroll,cursor /* 44-47 f10 num scr home */
...
do_self:
lea alt_map,%ebx
testb $0x20,mode /* alt-gr */
jne 1f
lea shift_map,%ebx
testb $0x03,mode
jne 1f
lea key_map,%ebx
1: movb (%ebx,%eax),%al
...
call put_queue
none: ret
key_map:
.byte 0,27
.ascii "&{\"'(-}_/@)="
...
shift_map:
.byte 0,27
.ascii "1234567890]+"
...
put_queue:
...
movl table_list,%edx # read-queue for console
movl head(%edx),%ecx
1: movb %al,buf(%edx,%ecx)
// linux/kernel/chr_drv/tty_io.c
struct tty_queue * table_list[]={
&tty_table[0].read_q, &tty_table[0].write_q,
&tty_table[1].read_q, &tty_table[1].write_q,
&tty_table[2].read_q, &tty_table[2].write_q
};

接下来对字符回显,将read_q缓冲队列中的内容放到write_q中:

// linux/kernel/chr_drv/tty_io.c
void do_tty_interrupt(int tty)
{
copy_to_cooked(tty_table+tty);
}
void copy_to_cooked(struct tty_struct * tty)
{
...
GETCH(tty->read_q,c);
if (L_ECHO(tty)) {
PUTCH(c,tty->write_q);
tty->write(tty);
}
PUTCH(c,tty->secondary);
wake_up(&tty->secondary.proc_list);
...
}

然后后续回显的内容就和上部分的sys_write的从write_q队列读取字符到显卡寄存器内容一样的了。

参考自:【哈工大】操作系统 李治军。

posted @   MeYokYang  阅读(764)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
点击右上角即可分享
微信分享提示