Linux操作系统分析实验(二):举例跟踪分析Linux内核5.0系统调用处理过程
学号后三位:482
原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/
实验要求
- 编译内核5.0
- 选择系统调用号后两位与您的学号后两位相同的系统调用进行跟踪分析
实验环境
- ubuntu 18.04 虚拟机
- VMware workstation 15
- 使用模拟器QEMU,运行内核代码
实验步骤
1 编译内核
1.1 下载linux5.0内核并解压
下载地址:https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.0.1.tar.xz
1.2 在桌面文件夹下新建LinuxKernel5.0文件夹
[email protected]:~$ cd Desktop/
[email protected]:~/Desktop$ mkdir LinuxKernel5.0
1.3 下载linux5.0源码并解压到LinuxKernel5.0文件夹下
[email protected]:~/Desktop/LinuxKernel5.0$ xz -d linux-5.0.tar.xz
[email protected]:~/Desktop/LinuxKernel5.0$ tar -xvf linux-5.0.tar
1.4 配置编译选项并编译内核
首先我们进入到解压的linux-5.0目录下,敲入以下命令对其编译信息进行配置。
1 # make defconfig
2 make i386_defconfig //这步很关键,我使用的是32位的qemu,因此kernel需要同为32位的
中间出现的问题:
(1) 缺少gcc:sudo apt install gcc
(2) 缺少flex:sudo apt-get install flex(make defconfig时出现)
(3) 缺少bison:sudo apt-get install bison(make i386_defconfig时出现)
用make i386_defconfig后:
输入以下命令,开启文本菜单选项,并找到kernel hacking -> Compile-time checks and compiler options,勾选 [*]compile the kernel with debug info
1 make menuconfig
出现以下问题,解决:sudo apt-get install libncurses-dev
在生成配置文件后,我们开始进行编译。
1 make 或 make -j* //*为cpu核⼼心数
编译成功后会看到如下图所示的信息:
出现的问题:
缺少libssl-dev:sudo apt-get install libssl-dev
2 制作根文件系统并启动qemu
2.1 构造MenuOS根文件系统
下载menu文件,并进入该目录下:
1 mkdir rootfs
2 git clone https://github.com/mengning/menu.git
3 cd menu
由于我的实验环境是64位的,编译32的文件需要安装以下软件包:
1 sudo apt-get install libc6-dev-i386
进行menu文件编译:
1 gcc -pthread -o init linktable.c menu.c test.c -m32 -static
制作rootfs.img:
1 cd ../rootfs
2 cp ../menu/init ./
3 find . | cpio -o -Hnewc |gzip -9 > ../rootfs.img
2.2 启动MenuOS
通过以下命令启动qemu:
1 qemu-system-i386 -kernel Desktop/LinuxKernel5.0/linux-5.0/arch/x86/boot/bzImage -initrd rootfs.img
在qemu正常启动后会另外弹出一个窗口,如下图所示:
2.3 跟踪调试内核启动
1 qemu-system-i386 -kernel Desktop/LinuxKernel5.0/linux-5.0/arch/x86/boot/bzImage -initrd rootfs.img -S -s -append nokaslr
注意:-append nokaslr选项的说明见知乎。
运行qemu虚拟机后,在当前目录打开gdb一个终端窗口,运行下列命令:
1 cd Desktop/LinuxKernel5.0/linux-5.0
2 gdb
3 gdb>file vmlinux
//在gdb界面中targe remote之前加载符号表
4 gdb>target remote:1234
//建立gdb和gdbserver之间的连接,按c 让qemu上的Linux继续运行
5 gdb>break start_kernel
//断点的设置可以在target remote之前,也可以在之后
2.4 分析
首先,几乎所有的内核模块均会在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时间片,并且从不返回。
3 跟踪系统调用
3.1 查询系统调用,选择学号后两位为82的系统调用
1 cd Desktop/LinuxKernel5.0/linux-5.0
2 cat /usr/include/asm/unistd_32.h
3.2 在MenuOS的test.c中插入 select系统调用的代码(test.c在Home/menu/test.c)
/* 通过select系统调用,监听文件描述符上的可读、可写和异常等事件 */
int Select(int argc, char *argv[]){
fd_set rfds;
struct timeval tv;
int retval;
/* Watch stdin (fd 0) to see when it has input. */
FD_ZERO(&rfds);
FD_SET(0, &rfds);
/* Wait up to five seconds. */
tv.tv_sec = 5;
tv.tv_usec = 0;
retval = select(1, &rfds, NULL, NULL, &tv);
/* Don’t rely on the value of tv now! */
if (retval == -1)
perror("select()");
else if (retval)
printf("Data is available now.\n");
/* FD_ISSET(0, &rfds) will be true. */
else
printf("No data within five seconds.\n");
return 0;
}
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("select","select xxx",Select);
ExecuteMenu();
}
3.3 重新编译MenuOS,执行Select函数
1 cd menu
2 gcc -pthread -o init linktable.c menu.c test.c -m32 -static
3 cd ../rootfs
4 cp ../menu/init ./
5 find . | cpio -o -Hnewc |gzip -9 > ../rootfs.img
6 //直接在命令终端最开始的路径执行cd ~
qemu-system-i386 -kernel Desktop/LinuxKernel5.0/linux-5.0/arch/x86/boot/bzImage -initrd rootfs.img
3.4 在gbd下跟踪系统调用
打开QEMU后,进入linux-5.0目录下,打开gdb调试。
1 cd Desktop/LinuxKernel5.0/linux-5.0
2 gdb
3 gdb>file vmlinux
4 gdb>target remote:1234
5 //打断点
gdb>b do_select
6 //继续运行
c
3.5 输入ni,disass,info r三条命令,逐步跟踪系统调用
出栈操作,来恢复现场
3.6 系统调用分析
内核实现了很多的系统调用函数,函数会有自己的名称以及编号。用户要调用系统调用,首先需要使用 int 0x80 触发软中断。这个指令会在0x80代表十进制的128,所以这个指令会找终端向量表的128项,找到以后, 跳转到相应的函数(system_call)。
ENTRY(system_call)
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)
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
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
# 效果等同与iret, 返回到用户态程序继续执行
irq_return:
INTERRUPT_RETURN
如果进入syscall_exit_work,能会进行信号处理以及进程调度
syscall_exit_work:
testl $_TIF_WORK_SYSCALL_EXIT, %ecx
jz work_pending
TRACE_IRQS_ON
ENABLE_INTERRUPTS(CLBR_ANY) # could let syscall_trace_leave() call
# schedule() instead
movl %esp, %eax
call syscall_trace_leave
jmp resume_userspace
END(syscall_exit_work)
syscall_exit_work以后, 有语句work_pending, 可能会跳转进入work_pending
work_pending:
testb $_TIF_NEED_RESCHED, %cl
jz work_notifysig
work_resched:
call schedule
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
andl $_TIF_WORK_MASK, %ecx # is there any work to be done other
# than syscall tracing?
# .....
END(work_pending)
实验总结
(1) 用户态、内核态和中断
内核态:在高的执行级别下,代码可以执行特权指令,访问任意的物理地址,这时的CPU就对应内核态
用户态:在低级别的指令状态下,代码 只能在级别允许的特定范围内活动。在日常操作下,执行系统调用的方式是通过库函数,库函数封装系统调用,为用户提供接口以便直接使用。
在Linux下0级表示内核态,3级表示用户态
中断处理是从用户态进入内核态的主要方式,系统调用是一种特殊的中断。中断/int指令会在堆栈上保存用户态的寄存器上下文,其中包括用户态栈顶地址、当时的状态字、cs:eip的值,以及内核态的栈顶地址、当时的状态字、中断处理程序入口。中断处理结束前的最后一件事就是恢复现场,退出中断程序,恢复保存寄存器的数据。
(2) 系统调用的意义
操作系统为用户态进程与硬件设备进行交互提供的一组接口——系统调用
把用户从底层的硬件编程中解放出来
极大的提高了系统的安全性
使用户程序具有可移植性
(3) API(应用程序编程接口)与系统调用的关系
API是一个系统调用封装成的一个函数定义
系统调用通过软中断向内核发出一个明确的请求
Libc库定义的一些API引用了封装例程,目的是发布系统调用,让程序员写代码的时候可以通过函数调用而非汇编指令触发一个系统调用
一般每个系统调用对应一个封装例程,库再用这些封装例程定义出给用户的API
参考资料:https://blog.****.net/weixin_43956968/article/details/88628319