主要分析应用程序(C/C++)调用printf函数,操作系统是如何调用底层函数write,最终把字符输出到屏幕上?主要分析过程是通过哈工大操作系统学习总结。
背景介绍:
操作系统作为软件应用层和底层硬件之间的部分,向下提供服务,向上提供接口。系统调用便是操作系统向上层应用提供的操作系统底层硬件和核心服务的接口。API实现的方法是提供函数接口,只需要调用函数就可以实现相应的功能,底层的原理是程序地址的调转。因为操作系统和用户程序同时存在内存中,我们当然是不希望操作系统的数据被随意的篡改和访问,有可能造成十分严重的后果,所以操作系统对内存做了区分,核心态(0),服务态(1,2),用户态(3),数值越小,级别越高,底级别进程无权访问高级别的内存区域,因此隔离了系统程序和用户程序,提高了操作系统的安全性。
因为CS:IP表示当前指令,所以用CS最低两位(CPL)来表示当前程序属于用户态还是核心态,访问的数据段DS最低两位(DPL)表示目标代码属于用户态还是核心态,在地址跳转时检查DPL和CPL,只有在CPL<=DPL时才允许跳转。但是对于系统调用来说,就需要找到一种方法穿透用户态和内核态的屏障,在x86处理器中,当用户代码想要调用内核代码时,硬件通过终端指令int将CPL改为0,从而穿透屏障实现调用。
调用过程:
调用printf函数,实际调用宏:
#define __LIBRARY__
#include
_syscall3(int,write,int,fd,const char *,buf,off_t,count)
_syscall3是一个宏,定义在include/unistd.h中,可以发现其中很多个相似的_syscallx宏,x表示可传入的参数个数
#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 80的中断。其中__NR_##name被传入eax,因为所有调用都是通过80号中断,和eax中保存的函数调用号,调用相应的中断函数。调用后eax也存放返回值,ebx,ecx,edx存放3个参数。
unistd.h定义了函数调用号。
#ifdef __LIBRARY__
...
#define __NR_write 4
...
#endif
所以可以看出这是对系统调用的索引,而真正起作用的函数定义在fs/read_write.c中
int sys_write(unsigned int fd,char * buf,int count)
{
struct file * file;
struct m_inode * inode;
if (fd>=NR_OPEN || count <0 || !(file=current->filp[fd]))
return -EINVAL;
if (!count)
return 0;
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);
printk("(Write)inode->i_mode=%06o\n\r",inode->i_mode);
return -EINVAL;
}
在init/main.c中调用了sched_init(),sched_init()中有一句,设置中断号为80时,调用system_call函数
set_system_gate(0x80,&system_call);
出现了80号中断,这是一句宏,在include/asm/system.h中,实质就是设置中断向量表,展开得
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \ //将偏移地址低字与选择符组合成低4字节(eax)
"movw %0,%%dx\n\t" \ //将类型标识字与偏移高字组合成描述符高4字节
"movl %%eax,%1\n\t" \ //分别设置门描述符的低4字节和高4字节
"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)
...
idt是中断向量表基址,这里指定第80号中断,DPL被指定为3,同时在system_call为"处理函数入口点偏移",之后进入kernel/system_call.s的system_call
...
nr_system_calls = 72
...
.globl system_call
...
system_call:
cmpl $nr_system_calls-1,%eax
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
movl $0x10,%edx # set up ds,es to kernel space
mov %dx,%ds
mov %dx,%es
movl $0x17,%edx # fs points to local data space
mov %dx,%fs
call sys_call_table(,%eax,4)
pushl %eax
movl current,%eax
cmpl $0,state(%eax) # state
jne reschedule
cmpl $0,counter(%eax) # counter
je reschedule
nr_system_calls表示总系统调用数,所以最先比较eax中的调用号是否小于总数,之后将各个参数推入堆栈,之后调用地址为sys_call_table + %eax*4处的函数。sys_call_table的include/linux/sys.h中里面有包括sys_write在内的72个系统调用函数的函数地址列表。
...
extern int sys_write();
...
fn_ptr sys_call_table[] = { ...
sys_write, ...};
而sys_write()就是索引为4的函数。由此系统调用的过程结束,总结为:
转载:https://blog.csdn.net/u011452747/article/details/79771452
转载:https://hoverwinter.gitbooks.io/hit-oslab-manual/content/sy2_syscall.html