一、实验内容
1. 通过内核的方式使用系统调用
需要使用的命令
rm menu -rf //强制删除当前menu
git clone http://github.com/mengning/menu.git //重新克隆新版本的menu cd menu ls make rootfs //rootfs是事先写好的一个脚本,自动编译自动生成根文件系统,同时自动启动MenuOS
2. 将上周选择的系统调用添加到MenuOS中
打开menu中的 test.c文件,添加Gitpid和Gitpidasm代码
int Getpid(int argc , char * argv[]) { int pid; pid=getpid(); printf("pid=%d\n",pid); return 0; } int Getpidasm(int argc , char *argv[]) { int pid; asm volatile( "mov $0,%%ebx\n\t" "mov $0x14,%%eax\n\t" "int $0x80\n\t" "mov %%eax, %0\n\t" :"=m"(pid) ); printf("pid = %d\n",pid); return 0; }
在main函数中添加
MenuConfig(“getpid","Show pid",Getpid); MenuConfig("getpid-asm","Show pid(asm)",Getpidasm);
重新make 后。可以看到menuOS有了getpid的命令,功能为返回当前进程的标识。
3.使用gdb跟踪分析这该系统调用内核函数
系统中已经成功添加了该函数调用功能。然后对该程序进行调试分析,使用gdb跟踪分析这该系统调用内核函数。
需要使用命令为
qemu -kernel linux.3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S 调试。 file linux-3.18.6/vmlinux 加载调试内核符号表。 b 设置断点 n 单步执行
二、系统调用过程分析
1. 系统调用在内核代码中的工作机制和初始化
int 0x80——>system call:通过中断向量匹配
system call——>sys_xyz():通过系统调用号匹配
一旦执行int 0x80后立刻跳转到system_call执行
2. 系统调用system_call的处理过程
syscall_call 函数到系统调用服务例程通过系统调用号联系起来:在上面执行软中断 0x80 时,系统调用号会被放入eax寄存器(参数的传递),system_call 函数读取eax寄存器获取参数(当前系统调用的调用号),将其乘以4生成偏移地址。然后以中断向量表(sys_call_table)为基址,以系统调用号所确定的为偏移地址相加得到最后的物理地址:基址+偏移地址 => 系统调用服务例程的地址。其中 sys_call_table 基址在文件 arch/x86/kernel/syscall_table_32.S 中定义,同时表中每一项例程的地址占用4个字节,所以上面乘以4。
由于系统调用例程在定义时时用 asmlinkage 标记了的,所以编译器仅从堆栈中获取该函数的参数。在进入system_call函数前,用户应用会把参数存放到寄存器中,system_call 函数执行时会首先把这些寄存器压入堆栈。这样对系统调用服务例程可以直接从堆栈照片能够获取参数。
3.从system_call开始到iret结束之间的过程
从整体过程来看,系统通过 int 0x80 从用户态进入内核态。在这个过程中系统先保存了中断环境,然后执行系统调用函数。system_call() 函数通过系统调用号查找系统调用表 sys_cal_table 来查找到具体的系统调用服务进程。在执行完系统调用后在执行 iret 之前,内核做了一系列检查,用于检查是否有新的中断产生。如果没有新的中断,则通过已保存的系统中断环境返回用户态。这样就完成了一个系统调用过程。系统调用通过 INT 0x80 进入内核,跳转到 system_call() 函数,然后执行相应服务进程。因为代表了用户进程,所以这个过程并不属于中断上下文,而是属于进程上下文。
4.系统调用处理过程的汇编代码分析
.macro INTERRUPT_RETURN ; 中断返回 iret .endm .macro SAVE_ALL ; 保护现场 ... .macro RESTORE_INT_REGS ... .endm ENTRY(system_call) SAVE_ALL syscall_call: call *sys_call_table(,%eax,4) movl %eax, PT_EAX(%esp) ; store the return value syscall exit: testl $_TIF_ALLWORK_MASK, %ecx # current->work jne syscall_exit_work restore_all: RESTORE_INT_REGS irq_return: INTERRUPT_RETURN ENDPROC(system_call) syscall_exit_work: testl $_TIF_WORK_SYSCALL_EXIT, %ecx jz work_pending END(syscall_exit_work) work_pending: testb $_TIF_NEED_RESCHED, %cl jz work_notifysig work_resched: call schedule jz restore_all work_notifysig: ... ; deal with pending signals END(work_pending)
无论是中断返回(ret_from_intr) ,还是系统调用返回,都使用了 work_pending 和resume_userspace。对于宏SAVE_ALL来说,这条语句会把将寄存器的值压入堆栈当中,压入堆栈的顺序对应struct pt_regs ,出栈时,这些值传递到struct pt_regs的成员,实现从汇编代码向C程序传递参数。struct pt_regs可以在arch/x86/include/asm/ptrace.h中查看。用户态到内核态需要int 0x80进行中断,只有生成了中断向量后才可以切换状态。中断处理让CPU停止当前工作转为执行系统内核中预设的一些任务,因此必须要对当前CPU执行的任务进行执行现场的保护工作,并对一些其他工作进行检查,完成调用后,再进行检查,才能执行iret返回。系统内部调用涉及CPU架构等内容,不同的CPU对于系统调用的汇编具体代码是不一样的。
三、总结
(1)open 函数通过系统调用号与 sys_open 系统调用服务例程函数联系起来
(2)int 0x80 通过中断向量(0x80)与 system_call 联系起来的
(3)系统调用中断本质上是一个保存当前工作状态,然后处理,最后返回并且恢复进程的过程
刘帅
原创作品转载请注明出处
《Linux内核分析》MOOC课程