原创作品转载请注明出处 ,《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000 ”
一、为MenuOS添加命令
1、clone一下最新的menuos代码
1 |
git clone https://github.com/mengning/menu.git |
2、编辑test.c,增加自定义函数
1 2 |
cd menu vim test.c |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
int echo() { char str[10]; char *str2="OutPut:"; scanf("%s",str); printf("%s%s\n",str2,str); return 0; } int echo_asm() { char str[10]={0}; char *str2="OutPut:"; // scanf("%s",str); __asm__ __volatile__( "mov $3, %%eax\n\t" "mov $0, %%ebx\n\t" "lea %0, %%ecx\n\t" "mov $9, %%edx\n\t" "int $0x80\n\t" : : "m"(str) ); // printf("%s%s\n",str2,str); __asm__ __volatile__( "mov $4, %%eax\n\t" "mov $1, %%ebx\n\t" "mov %0, %%ecx\n\t" "mov $7, %%edx\n\t" "int $0x80\n\t" "mov $4, %%eax\n\t" "mov $1, %%ebx\n\t" "lea %1, %%ecx\n\t" "mov $10, %%edx\n\t" "int $0x80\n\t" : : "m"(str2), "m"(str) ); return 0; } |
3、编译生成新的rootfs
1 2 3 |
make rootfs #因为已经有了make脚本,所以在menu目录下面直接执行 make rootfs 就可以启动内核 |
图1. 虚拟机启动
二、GDB跟踪调试系统调用
1、启动内核,打开调试端口
1 |
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S |
2、打开一个新的终端,启动gdb
1 2 3 4 5 6 7 8 9 10 |
(gdb) file linux-3.18.6/vmlinux #加载内核符号表 (gdb) target remote:1234 #连接调试端口 (gdb) b start_kernel (gdb) b sys_read (gdb) b sys_write #设置了三个断点 |
3、调试过程
因为选择了read() 和 write()两个系统调用,所以调试过程简直要命。看图吧。
启动的时候就是一堆输出,然后进入MenuOS之后又是不停的read、write,所以几乎是一直不停地在敲回车。。。
和老师视频中一样,进入system_call之后是无法进行单步调试的。
三、系统调用过程分析
下面按照系统调用的过程逐步给出代码以及简单分析
1、系统调用的定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/* * file: linux-3.18.6/arch/x86/include/generated/uapi/asm/unistd_32.h */ 4 #define __NR_restart_syscall 0 5 #define __NR_exit 1 6 #define __NR_fork 2 7 #define __NR_read 3 8 #define __NR_write 4 9 #define __NR_open 5 10 #define __NR_close 6 11 #define __NR_waitpid 7 ...... ...... |
2、系统调用号对应服务例程的入口向量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
/* * file: linux-3.18.6/arch/x86/syscalls/syscall_32.tbl */ 1 # 2 # 32-bit system call numbers and entry vectors 3 # 4 # The format is: 5 # <number> <abi> <name> <entry point> <compat entry point> 6 # 7 # The abi is always "i386" for this file. 8 # 9 0 i386 restart_syscall sys_restart_syscall 10 1 i386 exit sys_exit 11 2 i386 fork sys_fork stub32_fork 12 3 i386 read sys_read 13 4 i386 write sys_write 14 5 i386 open sys_open compat_sys_open 15 6 i386 close sys_close 16 7 i386 waitpid sys_waitpid sys32_waitpid ...... ...... |
3、中断处理程序以及0x80定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/* * file: linux-3.18.6/arch/x86/kernel/traps.c */ 792 void __init trap_init(void) 793 { ...... ...... 838 #ifdef CONFIG_X86_32 839 set_system_trap_gate(SYSCALL_VECTOR, &system_call); 840 set_bit(SYSCALL_VECTOR, used_vectors); 841 #endif ...... ...... } |
下面是SYSCALL的值的定义:
1 2 3 4 5 6 7 8 9 10 11 12 |
/* * file: linux-3.18.6/arch/x86/include/asm/irq_vectors.h */ ...... ...... 49 #define IA32_SYSCALL_VECTOR 0x80 50 #ifdef CONFIG_X86_32 51 # define SYSCALL_VECTOR 0x80 52 #endif ...... ...... |
4、中断的入口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
/* * file: linux-3.18.6/arch/x86/kernel/entry_32.S */ 489 # system call handler stub 490 ENTRY(system_call) #系统调用的入口 491 RING0_INT_FRAME #展开如下: /* * file: linux-3.18.6/arch/x86/kernel/entry_32.S * 257 .macro RING0_INT_FRAME * 258 CFI_STARTPROC simple * 259 CFI_SIGNAL_FRAME * 260 CFI_DEF_CFA esp, 3*4 * 261 //CFI_OFFSET cs, -2*4; * 262 CFI_OFFSET eip, -3*4 * 263 .endm */ #这里对%esp和%eip进行了处理,使其指向内核栈 # can't unwind into user space anyway 492 ASM_CLAC 493 pushl_cfi %eax # save orig_eax,寄存器%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 501 syscall_call: 502 call *sys_call_table(,%eax,4) #调用函数,地址是sys_call_table里偏移量为%eax*4处所保存的值 #有文章里说sys_call_table存在于arch/x86/kernel/syscall_table_32.S中, #但是我硬是没找到。。。类似地,我找到了arch/x86/um/sys_call_table_32.c, #可能是需要编译成.S才看得到那些sys_call_table. 503 syscall_after_call: 504 movl %eax,PT_EAX(%esp) # store the return value 505 syscall_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 #判断在结束前是否需要其他工作 514 515 restore_all: 516 TRACE_IRQS_IRET #还原现场 517 restore_all_notrace: 518 #ifdef CONFIG_X86_ESPFIX32 519 movl PT_EFLAGS(%esp), %eax # mix EFLAGS, SS and CS 520 # Warning: PT_OLDSS(%esp) contains the wrong/random values if we 521 # are returning to the kernel. 522 # See comments in process.c:copy_thread() for details. 523 movb PT_OLDSS(%esp), %ah 524 movb PT_CS(%esp), %al 525 andl $(X86_EFLAGS_VM | (SEGMENT_TI_MASK << 8) | SEGMENT_RPL_MASK), %eax 526 cmpl $((SEGMENT_LDT << 8) | USER_RPL), %eax 527 CFI_REMEMBER_STATE 528 je ldt_ss # returning to user-space with LDT SS 529 #endif 530 restore_nocheck: 531 RESTORE_REGS 4 # skip orig_eax/error_code #恢复寄存器,还原现场 532 irq_return: 533 INTERRUPT_RETURN ...... ...... 656 syscall_exit_work: 657 testl $_TIF_WORK_SYSCALL_EXIT, %ecx 658 jz work_pending 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 #退回到用户空间 665 END(syscall_exit_work) |
5、sys_read()和sys_write()服务例程的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
/* * file: linux-3.18.6/fs/read_write.c */ 562 SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count) 563 { 564 struct fd f = fdget_pos(fd); 565 ssize_t ret = -EBADF; 566 567 if (f.file) { 568 loff_t pos = file_pos_read(f.file); 569 ret = vfs_read(f.file, buf, count, &pos); 570 if (ret >= 0) 571 file_pos_write(f.file, pos); 572 fdput_pos(f); 573 } 574 return ret; 575 } 576 577 SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, 578 size_t, count) 579 { 580 struct fd f = fdget_pos(fd); 581 ssize_t ret = -EBADF; 582 583 if (f.file) { 584 loff_t pos = file_pos_read(f.file); 585 ret = vfs_write(f.file, buf, count, &pos); 586 if (ret >= 0) 587 file_pos_write(f.file, pos); 588 fdput_pos(f); 589 } 590 591 return ret; 592 } /* 这里调用了vfs虚拟文件系统的函数进行read操作,和旧版本内核的代码区别还很大,似乎被修改了很多次。。。*/ |
从上面的代码可以看到,服务例程真正的实现使用的是SYSCALL_DEFINE[X]这样的宏定义,[X] 代表了要传递的参数个数。
附上粗糙的流程图一张。。。
图4. 流程图
四、小小的总结
在整个系统调用过程中,首先触发INT 0x80系统中断,进入内核空间。然后根据%eax里保存的调用号跳转至对应的中断服务例程。中断服务例程完工之后,检测是否需要进程调度以及收尾工作,然后就是恢复寄存器,恢复现场,退回到用户空间。
Linux内核毕竟是很复杂的,以上只是个人理解,如有错误欢迎指正。
参考文献:
http://www.linuxidc.com/Linux/2012-07/65758p2.htm
http://www.cnblogs.com/zhuyp1015/archive/2012/05/29/2524936.html