学号085 原创作品转载请注明出处
本实验来源 https://github.com/mengning/linuxkernel/
实验简介
本实验要求举例跟踪分析Linux内核5.0系统调用处理过程,编译内核5.0,选择系统调用号后两位与您的学号后两位相同的系统调用进行跟踪分析,给出相关关键源代码及实验截图,仔细分析系统调用、保护现场与恢复现场、系统调用号及参数传递过程,阐明自己对系统调用工作机制的理解。
实验环境
- Unbuntu 16
- gcc 4.8
实验初探
1、下载Linux内核5.0内核代码,并配置编译Linux内核
mkdir LinuxKernel
下载内核源码:Linux内核5.0 source code并解压到LinuxKernel目录下
cd linux-5.0.2
make menuconfig,找到kernel hacking,->Compile-time checks and compiler options,选择 [*]compile the kernel with debug info
make
make时报错,错误和解决方案如下图(此处参考自 https://blog.csdn.net/zxhio/article/details/80312316)
2. 制作根文件系统
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
实验中遇到的错误及解决方案如下图
运行结果
3. 启动MenuOS
qemu-system-i386 -kernel linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img
跟踪调试内核启动
qemu-system-i386 -kernel linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img -S -s -append nokaslr
加-append nokaslr选项,是为了防止start_kernel断点断不住 详情见知乎 https://www.zhihu.com/question/270476360
cd LinuxKernel/linux-5.0.1
gdb vmlinux
(gdb) target remote:1234
输入help查询指令
系统调用过程
实验分析
首先,几乎所有的内核模块均会在start_kernel进行初始化。在start_kernel中,会对各项硬件设备进行初始化,包括一些page_address、tick等等,直到最后需要执行的rest_init中,会开始让系统跑起来。那rest_init这个过程中,会调用kernel_thread()来创建内核线程kernel_init,它创建用户的init进程,初始化内核,并设置成1号进程,这个进程会继续做相关的系统初始化。然后,start_kernel会调用kernel_thread并创建kthreadd,负责管理内核中得所有线程,然后进程ID会被设置为2。最后,会创建idle进程(0号进程),不能被调度,并利用循环来不断调号空闲的CPU时间片,并且从不返回。
本文参考自https://blog.csdn.net/pianogirl123/article/details/50866778
增加系统调用
根据学号后两位85,在/usr/include/asm/unistd_32.h中可查得#define __NR_readlink 85
在test.c中增加函数ReadLink()
int ReadLink(int argc, char *argv[])
{
#define PATH_MAX 32
char buf[PATH_MAX];
int ret;
ret = readlink("/proc/self/exe", buf, PATH_MAX);
if (ret == -1)
perror("readlink");
else
printf("%.*s\n", ret, buf);
}
int main()
{
PrintMenuOS();
SetPrompt("MenuOS>>");
MenuConfig("version", "MenuOS V1.0(Based on Linux 3.18.6)", NULL);
MenuConfig("quit", "Quit from MenuOS", Quit);
MenuConfig("time", "Show System Time", Time);
MenuConfig("time-asm", "Show System Time(asm)", TimeAsm);
MenuConfig("ReadLink", "read link of /proc/self/exe", ReadLink);
ExecuteMenu();
}
实验总结
- 系统调用的触发及参数传递
当调用一个系统调用时,CPU从用户态切换到内核态并开始执行一个system_call和系统调用内核函数。在Linux中通过执行int 0x80来触发系统调用,内核为每个系统调用分配一个系统调用号,用户态进程必须明确指明系统调用号,需要使用EAX寄存器来传递。
系统调用可能需要参数,但是不能通过像用户态进程函数中将参数压栈的方式传递,因为用户态和内核态有不同的堆栈,必须通过寄存器的方式传递参数。
概括:EAX用来传递系统调用号,EBX、ECX、EDX、ESI、EDI、EBP用来传递参数,若参数较多,则把指向内存的指针存入寄存器。 - 系统调用流程
系统调用机制初始化:
\init\main.c start_kernel
trap_init();
\arch\x86\kernel\traps.c
#ifdef CONFIG_X86_32
//系统调用的中断向量和system_call汇编代码的入口
//一旦执行int 0x80,CPU就跳转到system_call这个位置来执行
set_system_trap_gate(SYSCALL_VECTOR, &system_call);
set_bit(SYSCALL_VECTOR, used_vectors);
#endif
系统调用的工作机制,一旦start_kernel初始化好之后,在代码中一旦出现int 0x80的指令,就会立即跳转到system_call这个位置。
entry_32.S部分代码:
//这段代码就是系统调用处理的过程,其它的中断过程也是与此类似
//系统调用就是一个特殊的中断,也存在保护现场和回复现场
ENTRY(system_call)//这是0x80之后的下一条指令
RING0_INT_FRAME # can't unwind into user space anyway
ASM_CLAC
pushl_cfi %eax # save orig_eax
SAVE_ALL//保护现场
GET_THREAD_INFO(%ebp)
# system call tracing in operation / emulation
testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
jnz syscall_trace_entry
cmpl $(NR_syscalls), %eax
jae syscall_badsys
syscall_call:
// 调用了系统调用处理函数,实际的系统调用服务程序
call *sys_call_table(,%eax,4)//定义的系统调用的表,eax传递过来的就是系统调用号
syscall_after_call:
movl %eax,PT_EAX(%esp) # store the return value
syscall_exit:
LOCKDEP_SYS_EXIT
DISABLE_INTERRUPTS(CLBR_ANY) # make sure we don't miss an interrupt
# setting need_resched or sigpending
# between sampling and the iret
TRACE_IRQS_OFF
movl TI_flags(%ebp), %ecx
testl $_TIF_ALLWORK_MASK, %ecx # current->work
jne syscall_exit_work//退出之前,syscall_exit_work
//进入到syscall_exit_work里边有一个进程调度时机
restore_all: //恢复现场
TRACE_IRQS_IRET
restore_all_notrace://返回到用户态
#ifdef CONFIG_X86_ESPFIX32
movl PT_EFLAGS(%esp), %eax # mix EFLAGS, SS and CS
# Warning: PT_OLDSS(%esp) contains the wrong/random values if we
# are returning to the kernel.
# See comments in process.c:copy_thread() for details.
movb PT_OLDSS(%esp), %ah
movb PT_CS(%esp), %al
andl $(X86_EFLAGS_VM | (SEGMENT_TI_MASK << 8) | SEGMENT_RPL_MASK), %eax
cmpl $((SEGMENT_LDT << 8) | USER_RPL), %eax
CFI_REMEMBER_STATE
je ldt_ss # returning to user-space with LDT SS
#endif
restore_nocheck:
RESTORE_REGS 4 # skip orig_eax/error_code
irq_return:
INTERRUPT_RETURN//iret(宏),系统调用过程到这里结束