从内核看系统调用
本周学习了孟宁老师的《Linux内核分析》,按照课程要求,做实验如下:
首先使用gdb跟踪一个系统调用,我们选择上周实验所写的代码,修改这两段代码成两个系统调用,放入根文件系统中,作为本次实验将要观察的系统调用。
修改代码如下:
1) c实现的系统调用
int mkdir_c(int argc, char **argv) { if(argc != 2) printf("Illegal parameter!!\n"); if(mkdir(argv[1]) == -1) { printf("Mkdir error!!\n"); return -1; }
printf("Mkdir success!!\n"); return 0; }
2) 使用汇编语言实现的系统调用
int mkdir_asm(int argc, char **argv) { if(argc != 2) printf("Illagel parameters!!\n"); int ret = 0; asm volatile( "mov $0, %%eax\n\t" "mov $0x27, %%eax\n\t" "int $0x80\n\t" "mov %%eax, %0\n\t" :"=m"(ret) :"b"(argv[1]) ); if(ret == -1) { printf("Mkdir Error!!\n"); return -1; }
printf("Mkdir success!!\n"); return 0; }
打开实验楼环境,进入menu目录下
cd /home/shiyanlou/LinuxKernel/menu
打开test.c文件,假如上面两段代码,如图所示:
在main函数中添加两行代码注册这两个系统调用:
重新编译,制作成根文件系统镜像。使用qemu仿真环境加载内核和根文件系统,启动系统。为了方便,我们使用把这一系列命令都写入Makefile文件中,所以只需在终端执行如下命令:
make rootfs
就能完成全部动作:
我们可以执行我们所添加的命令了。
接下来我们使用gdb工具来对系统调用进行调试。
首先使用qemu进入调试模式,冻结住内核:
启动gdb,加载调试符号表,开始进行调试:
在system_call函数处设置了断点,但是无法在此处停止,因为system_call函数所在的entry_32.s文件是汇编代码,gdb对汇编代码的调试能力有限。
为了了解系统调用机制的处理过程,我们对system_call这段代码进行阅读和分析:
具体的代码参见:http://codelab.shiyanlou.com/xref/linux-3.18.6/arch/x86/kernel/entry_32.S
首先内核要初始化系统调用机制,我们知道start_kernel是内核的入口地址,在这个函数中系统完成一系列的初始化工作,所以系统调用机制的初始化工作也必定在这个函数中,其中的trap_init函数就是负责对系统调用机制进行初始化,
// in init/main.c start_kernel
553 /* 554 * These use large bootmem allocations and must precede 555 * kmem_cache_init() 556 */ 557 setup_log_buf(0); 558 pidhash_init(); 559 vfs_caches_init_early(); 560 sort_main_extable(); 561 trap_init(); 562 mm_init();
我们进入trap_init函数:
837 838#ifdef CONFIG_X86_32 839 set_system_trap_gate(SYSCALL_VECTOR, &system_call); 840 set_bit(SYSCALL_VECTOR, used_vectors); 841#endif 842
这一段代码把中断向量表中的0x80号中断指向system_call代码段处,我们进入SYSCALL_VECTOR可以看到这个宏代表的就是0x80:
50#ifdef CONFIG_X86_32 51# define SYSCALL_VECTOR 0x80 52#endif 53
接下来我们还分析system_call这段代码:
489 # system call handler stub 490 ENTRY(system_call) 491 RING0_INT_FRAME # can't unwind into user space anyway 492 ASM_CLAC 493 pushl_cfi %eax # save orig_eax 494 SAVE_ALL //保存当前进程上下文,因为系统调用本身就是一种中断,所以和中断机制一样需要保存当前进程的一些信息,以便执行完系统调用后恢复现场 495 GET_THREAD_INFO(%ebp) 496 # system call tracing in operation / emulation 497 testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp) 498 jnz syscall_trace_entry 499 cmpl $(NR_syscalls), %eax 500 jae syscall_badsys 501syscall_call: 502 call *sys_call_table(,%eax,4) //调用系统调用对应的处理函数 503syscall_after_call: 504 movl %eax,PT_EAX(%esp) # store the return value 505syscall_exit: 506 LOCKDEP_SYS_EXIT 507 DISABLE_INTERRUPTS(CLBR_ANY) # make sure we don't miss an interrupt 508 # setting need_resched or sigpending 509 # between sampling and the iret 510 TRACE_IRQS_OFF 511 movl TI_flags(%ebp), %ecx 512 testl $_TIF_ALLWORK_MASK, %ecx # current->work //检测当前的任务 513 jne syscall_exit_work //是否跳到syscall_exit_work来处理 514 515restore_all: 516 TRACE_IRQS_IRET
我们来看syscall_exit_work这段代码:
# perform syscall exit tracing 655 ALIGN 656 syscall_exit_work: 657 testl $_TIF_WORK_SYSCALL_EXIT, %ecx 658 jz work_pending //跳转至信号处理,work_reached进程调度处理等 659 TRACE_IRQS_ON 660 ENABLE_INTERRUPTS(CLBR_ANY) # could let syscall_trace_leave() call 661 # schedule() instead 662 movl %esp, %eax 663 call syscall_trace_leave 664 jmp resume_userspace 665END(syscall_exit_work)
最后我们绘制流程图来表述从system_call到iret这一段代码的执行流程:
以上便是我对本次试验的全部理解,如有错误,还望指正。
Allen 原创作品转载请注明出处 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000