丁春阳+原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/
一、实验步骤
编译内核5.0
使用mkdir LinuxKernel命令创建LinuxKernel目录,下载Linux
5.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!");
}
这里选择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。