系统调用实现原理(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 # 设置ds、es为0x10,内核数据段。 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队列读取字符到显卡寄存器内容一样的了。
参考自:【哈工大】操作系统 李治军。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构