举例跟踪分析Linux内核5.0系统调用处理过程

丁春阳+原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/

一、实验步骤

编译内核5.0

使用mkdir LinuxKernel命令创建LinuxKernel目录,下载Linux5.0内核源代码,并解压到LinuxKernel目录下,cd linux-5.0.1调整当前目录。

make menuconfig

执行make menuconfig过程中可能会报错,若出错则执行如下指令:

sudo apt-get install bison
sudo apt-get install flex

执行make

制作根文件系统

cd ~/LinuxKernel/
mkdir rootfs
git clone https://github.com/mengning/menu.git
cd menu
gcc -pthread -o init linktable.c menu.c test.c -m32 -static
cd ../rootfs
cp ../menu/init ./
find . | cpio -o -Hnewc |gzip -9 > ../rootfs.img

 启动MenuOS

qemu-system-i386 -kernel linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img

 gdb跟踪调试内核启动

qemu-system-i386 -kernel linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img -S -s -append nokaslr(注:若不加 -append nokaslr,则会停不下来)

#-S freeze CPU at startup (use ’c’ to start execution)
# -s shorthand for -gdb tcp::1234 若不想使用1234端口,则可以使用-gdb tcp:xxxx来取代-s选项

打开另外一个shell窗口,将当前目录设为Linuxkernel,进行gdb调试:
(gdb)file linux-5.0/vmlinux # 在gdb界面中targe remote之前加载符号表
(gdb)target remote:1234 # 建立gdb和gdbserver之间的连接
(gdb)break start_kernel # 在start_kernel处设置断点。

查看系统调用列表,可得59号系统调用为oldolduname,系统调用列表如下图所示

ustname.h部分代码如下所示:

<sys/utsname.h>:

struct utsname {
    char sysname[];    /* Operating system name (e.g., "Linux") */
    char nodename[];   /* Name within "some implementation-defined
                          network" */
    char release[];    /* Operating system release (e.g., "2.6.28") */
    char version[];    /* Operating system version */
    char machine[];    /* Hardware identifier */
#ifdef _GNU_SOURCE
    char domainname[]; /* NIS or YP domain name */
#endif
};

    随着时间的推移,utsname结构大小的增加导致了uname()的三个连续版本:sys_olduname()(slot __NR_oldolduname)sys_uname() (slot __NR_olduname), and sys_newuname() (slot __NR_uname) 第一个使用长度为9的字段; 第二次使用长度为65的字段; 第三个也是使用长度为65的字段,但添加了域名字段。glibc中uname函数从应用程序中隐藏这些细节,调用内核提供的最新版本的系统调用。uname是用户代码调用的函数,它调用内核函数sys_newuname,sys_uname或sys_olduname之一,这具体取决于Linux内核的版本。

使用库函数api触发一个系统调用

void getUname()
  {
     int ret;
     struct utsname name;
     uname(&name);
     if(ret == 0)
     {
         printf("successfully!\n");
         printf("%s\n", name.sysname);
         printf("%s\n", name.release);
         printf("%s\n", name.version);
         printf("%s\n", name.machine);
      }
     else
         printf("failed!");
     
   
 }

使用嵌入式汇编代码触发一个系统调用

void getUnameASM()
{
    struct utsname name;
    int ret;
    asm volatile(
        "mov %1, %%ebx\n\t"
        "mov $0x3B, %%eax\n\t"
        "int $0x80\n\t"
        "mov %%eax,%0\n\t "
        :"=m"(ret)
        :"b"(&name)
    );

    if(ret == 0)
     {
         printf("successfully!\n");
         printf("%s\n", name.sysname);
         printf("%s\n", name.release);
         printf("%s\n", name.version);
         printf("%s\n", name.machine);
      }
     else
         printf("failed!");
     
  
}

 

修改menuOS使用系统调用

这里选择59号系统调用,59号系统调用glibc中接口为int uname(struct utsname *buf),作用是获得当前系统的信息。

在menu/test.c中添加函数getUname(),getUnameASM()     并在main函数中注册

MenuConfig("getoldolduname","Get information of system",getUname);

MenuConfig("getoldoldunameasm","Get information of system",getUnameASM);

重新编译rootfs.img之后执行。

测试效果

 

系统调用的传参方式

因为内核实现了许多不同系统调用,用户进程必须指明哪个系统调用号,即用eax传递系统调用号的参数。除了eax传递系统调用号之外,参数按照ebx,ecx,edx,esi,edi,ebp顺序进行赋值,参数个数不能超过六个,若超过六个则用某一寄存器作为指针指向内存,通过内存传递更多参数。

 

二、分析

用户态和内核态

为了减少对有限资源的访问以及使用的冲突,操作系统必须对用户程序进行权限划分。内核态下可以执行特权指令访问任意物理内存,用户态则有所限制,这是保证系统稳定的一种机制。内核态和用户态一种显著的区分方法是CS:EIP指向的范围,32位的x86中4GB的进程地址空间都可访问,用户态下,只能访问0x00000000-0xbfffffff的地址空间。

系统调用的三层机制

即系统调用三层机制即oldolduname( ),system_call( ),sys_olduname( )

中断向量0x80和system_call终端服务入口的关系

start_kernel函数里调用trap_init函数,trap_init函数中调用set_system_trap_gate函数,通过set_system_trap_gate函数绑定了中断向量和system_call中断程序入口即set_system_trap_gate(SYSCALL_VRCTOR,&system_call),一旦执行int 0X80,cpu直接跳转到system_call这个位置。

system_call汇编代码中的系统调用内核处理函数

# system call handler stub
ENTRY(system_call)            #系统调用处理入口(内核态)
    RING0_INT_FRAME         
    ASM_CLAC
    pushl_cfi %eax          #保存系统调用号
    SAVE_ALL              # 保存现场,将用到的所有cpu寄存器保存到栈中
    GET_THREAD_INFO(%ebp) # ebp用于存放当前进程的thread_info结构的地址,
    testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)   # 检测是否由系统跟踪 
    jnz syscall_trace_entry      
cmpl $(NR_syscalls), %eax   # 检查系统调用号,系统调用号应该小于NR_syscalls
    jae syscall_badsys          #不合法则跳转到syscall_badsys,小于则跳转到相应系统调用号所对应的服务例程当中。
syscall_call:
    call *sys_call_table(,%eax,4) # 在系统调用表中的调用相应的系统调用内核函数
syscall_after_call:               
    movl %eax,PT_EAX(%esp)      # store the return value # 保存返回值到栈中

syscall_exit:
    testl $_TIF_ALLWORK_MASK, %ecx    	# 检查是否有任务需要处理
    jne   syscall_exit_work  #需要则进入syscall_exit_work,常见的进程调度时期

restore_all:
    TRACE_IRQS_IRET #恢复现场
irq_return:
    INTERRUPT_RETURN #iret
system_call这一段代码就是系统调用处理过程,它被称之为软中断,因此也具有保护现场和恢复现场,即  SAVE_ALL、restore_all等,sys_call_table是一个系统调用的表,eax传递系统调用号,在调用它时要根据eax中的值来调用相应的系统调用内核处理函数,调用完之后,需要先保存它的返回值,退出之前会有sys_exit_work的判断,如果没有则恢复现场并且iret返回到用户态。
work_pending:
594	testb $_TIF_NEED_RESCHED, %cl
595	jz work_notifysig
596work_resched:
597	call schedule
598	LOCKDEP_SYS_EXIT
599	DISABLE_INTERRUPTS(CLBR_ANY)	# make sure we don't miss an interrupt
600					# setting need_resched or sigpending
601					# between sampling and the iret
602	TRACE_IRQS_OFF
603	movl TI_flags(%ebp), %ecx
604	andl $_TIF_WORK_MASK, %ecx	# is there any work to be done other
605					# than syscall tracing?
606	jz restore_all
607	testb $_TIF_NEED_RESCHED, %cl
608	jnz work_resched
609
610work_notifysig:				# deal with pending signals and
611					# notify-resume requests
612#ifdef CONFIG_VM86
613	testl $X86_EFLAGS_VM, PT_EFLAGS(%esp)
614	movl %esp, %eax
615	jne work_notifysig_v86		# returning to kernel-space or
616					# vm86-space
6171:
618#else
619	movl %esp, %eax
620#endif
621	TRACE_IRQS_ON
622	ENABLE_INTERRUPTS(CLBR_NONE)
623	movb PT_CS(%esp), %bl
624	andb $SEGMENT_RPL_MASK, %bl
625	cmpb $USER_RPL, %bl
626	jb resume_kernel
627	xorl %edx, %edx
628	call do_notify_resume
629	jmp resume_userspace
630
631#ifdef CONFIG_VM86
632	ALIGN
633work_notifysig_v86:
634	pushl_cfi %ecx			# save ti_flags for do_notify_resume
635	call save_v86_state		# %eax contains pt_regs pointer
636	popl_cfi %ecx
637	movl %eax, %esp
638	jmp 1b
639#endif
640END(work_pending)
641
642	# perform syscall exit tracing
643	ALIGN
644syscall_trace_entry:
645	movl $-ENOSYS,PT_EAX(%esp)
646	movl %esp, %eax
647	call syscall_trace_enter
648	/* What it returned is what we'll actually use.  */
649	cmpl $(NR_syscalls), %eax
650	jnae syscall_call
651	jmp syscall_exit
652END(syscall_trace_entry)
653
654	# perform syscall exit tracing
655	ALIGN
656syscall_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
665END(syscall_exit_work)

syscall_exit_work需要跳转到work_pending,里面有work_notifysig处理信号,work_reshed是需要重新调度的,调度的时间点为 call schedule,跳转到restore_all,恢复现场返回系统调用到用户态。

流程图如下:

三、总结

从系统调用处理过程的入口开始,SAVE_ALL保存现场,然后找到sys_call_table,call  *sys_call_table(,%eax,4)即调用了系统调用的内核处理函数,然后restore_all以及INTERRUPT_RETURN(iret)用于恢复现场并且返回系统调用到用户态。还有可能执行syscall_exit_work,其中有work_pending,其中的work_notifysig是处理信号量的,work_pending中还有可能调用进程切换的代码即call schedule。                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值