学号146
原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/
一、实验要求
- 编译内核5.0
- qemu -kernel linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img
- 选择系统调用号后两位与您的学号后两位相同的系统调用进行跟踪分析,本人学号146,使用64位Ubuntu18.04操作系统,故选择跟踪分析146号系统调用函数。
- 测试代码参考孟宁老师的开源项目https://github.com/mengning/menu
- 给出相关关键源代码及实验截图,撰写一篇博客(署真实姓名或学号最后3位编号),并在博客文章中注明“原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/ ”,博客内容的具体要求如下:
- 题目自拟,内容围绕系统调用进行;
- 博客中需要使用实验截图
- 博客内容中需要仔细分析系统调用、保护现场与恢复现场、系统调用号及参数传递过程
- 总结部分需要阐明自己对系统调用工作机制的理解。
- 博客URL提交到https://github.com/mengning/linuxkernel/issues/10 截止日期3月19日24:00
二、实验环境
- Ubuntu 18.04 64位
- gcc 7.3.0
- Windows 14 VMware Workstation
三、编译linux内核5.0.1
1、下载并编译内核5.0
从Linux Core 5.0 Source Code中下载相应源码压缩包,提取到linux-kernel文件夹
2、配置编译内核
make menuconfig #开启⽂本菜单选项
出现的问题:缺少flex和bison
解决:安装所需的内核编译工具
sudo apt install build-essential flex bison libssl-dev libelf-dev libncurses-dev
#其他的也是缺少什么安装什么
依次配置内核编译工具,这里可以根据自身情况选择若干个命令加入系统".config"
文件,并调出调整窗口来设置允许断掉调试。这里使用的是64 位配置文件,输入make -j4
make defconfig #按照默认值⽣成.config
make -j4 #or make -j*,*为cpu核⼼数
进行编译,让make
最多允许4个编译命令同时执行。
编译完成
3、制作根目录
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
4、通过QEMU虚拟机加载内核
sudo apt install qemu #安装qemu
qemu-system-x86_64 -kernel linux-5.0.1/arch/x86/boot/bzImage #使用64位qemu加载内核
sudo apt-get install libc6-dev-i386 # 在64位环境下编译32位需安装
qemu-system-x86_64 -kernel linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img
#启动menuOS
启动成功。
5、跟踪调试内核启动
qemu-system-x86_64 -kernel linux-5.0.1/arch/x86/boot/bzImage -initrd linux-5.0.1/rootfs.img -S -s -append nokaslr
新建一个终端,进入linux-5.0.1
gdb
(gdb)file vmlinux # 在gdb界面中targe remote之前加载符号表
(gdb)target remote:1234 # 建立gdb和gdbserver之间的连接,按c 让qemu上的Linux继续运行
(gdb)break start_kernel # 断点的设置可以在target remote之前,也可以在之后
(gdb)c
四、跟踪分析系统调用过程
1、添加系统调用
本人学号后三位为146,选择后三位为146的系统调用函数sched_get_priority_max()
函数原型及解释。
原型 | int sched_get_priority_max(int policy) |
作用 | 获取某种调度算法的最高优先级 |
说明 | int policy:调度算法的名称 Linux内核有三种调度策略: 1.SCHED_OTHER 分时调度策略 2.SCHED_FIFO 实时调度策略,先到先服务。一旦占用cpu则一直运行。一直运行直到有更高优先级任务到达或自己放弃 3.SCHED_RR实 时调度策略,时间片轮转。当进程的时间片用完,系统将重新分配时间片,并置于就绪队列尾。放在队列尾保证了所有具有相同优先级的RR任务的调度公平 |
在test.c中加入int sched_get_priority_max(int policy)函数:
/*需加头文件 <pthread.h>*/
int TestPriority(int argc, char *argv[]){
printf("SCHED_FIFO\tmax:%d\n",sched_get_priority_max(SCHED_FIFO)); //FIFO的最高优先级值
printf("SCHED_RR\tmax:%d\n",sched_get_priority_max(SCHED_RR)); //RR的最高优先级值
printf("SCHED_OTHER\tmax:%d\n",sched_get_priority_max(SCHED_OTHER));
return 0;
}
main函数进行如下修改:
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);
//调用testpriority()
MenuConfig("testpriority","Show max priority of deffirent algorithm",TestPriority);
ExecuteMenu();
}
重新编译并运行
可以见,输出了三种调度算法最高优先级的值:
SCHED_OTHER 是不支持优先级使用的,而 SCHED_FIFO 和 SCHED_RR 支持优先级的使用,他们分别为1和99,数值越大优先级越高。
2、分析system_call()函数
写一个c程序146.c,使用三个变量存放三次调用的返回值
#include <pthread.h>
#include<stdio.h>
int main(){
int max1=sched_get_priority_max(SCHED_FIFO);
int max2=sched_get_priority_max(SCHED_FIFO);
int max3=sched_get_priority_max(SCHED_FIFO);
printf("SCHED_FIFO\tmax1:%d\n",max1);
printf("SCHED_RR\tmax2:%d\n",max2);
printf("SCHED_OTHER\tmax3:%d\n",max3);
return 0;
}
编译并运行146.c,结果如下:
添加断点进行单步运行
break ftime //设置断点
r //运行到断点
ni //单步调试
disass //显示步骤详情
info r //显示运行时寄存器详情
首先指令流执行到系统调用函数时,系统调用函数通过int 0x80指令进入系统调用入口程序,并且把系统调用号放入%eax中。
五、实验总结与分析
1、系统调用过程,参考夏天的篮球
初始化:
\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传递过来的就是系统调用号,在例子中就是调用的systime
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(宏),系统调用过程到这里结束
综上所述,当一个系统调用发生的时候,进入内核处理这个系统调用,由内核提供服务,当这个服务结束返回到用户态之前,即在系统调用返回之前,有可能发生进程调度(call schedule),进程调度的里边就会发生进程上下文的切换。
把内核可以抽象成很多种不同的中断处理过程的集合。
2、保护现场与恢复现场
保护现场与恢复现场主要出现在中断处理的过程中。系统调用是一种特殊的中断,中断处理是从用户态进入内核态的主要方式。中断发生后的第一件事就是保存现场。从用户态切换到内核态,中断指令会在堆栈上保存用户态的寄存器上下文,其中包括用户态栈顶地址、当时的状态字、cs:eip的值,以及内核态的栈顶地址、当时的状态字、中断处理程序入口。中断处理结束前的最后一件事就是恢复现场,退出中断程序,恢复保存寄存器的数据。
interrupt(ex:int 0X80)//发生系统调用
save cs:eip/ss:esp/eflags(current)to kernel stack
//保存cs:eip的值,保存当前堆栈段寄存器当前栈顶和标志位寄存器
load cs:eip(entry of a specific ISR)and ss:eip(point to kenerl stack)
//把当前的中断信号或系统调用相关中断服务例程入口加载到cs:eip中,把当前的堆栈段和esp加载到CPU
SAVE_ALL//保存现场
...//内核代码,完成中断服务,发生进程调度
RESTORE_ALL//恢复现场
iret - pop cs:eip/ss:esp/eflags from kernel stack//iret对应相反的中断指令
3、系统调用号及参数传递过程
内核实现了很多不同的系统调用,进程必须指明需要哪个系统调用,这需要传递一个名为系统调用号的参数,system_call是linux中所有系统调用的入口点,每个系统调用至少有一个参数,使用eax寄存器传递系统调用号。
寄存器传递参数的原则如下:
(1)每个参数的长度不能超过寄存器的长度,即32位。
(2)在系统调用号( eax)之外,参数的个数不能超过6个( ebx,ecx, edx, esi, edi, ebp)
(3)超过6个的情况下,使用某一个寄存器作为指针,进入内核态之后可以访问所有的地址空间,通过某一片区域传递参数。
4、总结
简单来说,系统调用就是用户程序和硬件设备之间的桥梁。用户程序在需要的时候,通过系统调用来使用硬件设备。用户程序,系统调用,内核,硬件设备的调用关系如下图:
系统调用的实现分为以下三步:(参考千里之行,始于足下)
(1)通知内核调用一个哪个系统调用
每个系统调用都有一个系统调用号,系统调用发生时,内核就是根据传入的系统调用号来知道是哪个系统调用的。
在x86架构中,用户空间将系统调用号是放在eax中的,系统调用处理程序通过eax取得系统调用号。
(2)用户程序把系统调用的参数传递给内核
系统调用的参数也是通过寄存器传给内核的,在x86系统上,系统调用的前5个参数放在ebx,ecx,edx,esi和edi中,如果参数多的话,还需要用个单独的寄存器存放指向所有参数在用户空间地址的指针。
一般的系统调用都是通过C库(最常用的是glibc库)来访问的,Linux内核提供一个从用户程序直接访问系统调用的方法。
(3) 用户程序获取内核返回的系统调用返回值
获取系统调用的返回值也是通过寄存器,在x86系统上,返回值放在eax中
系统调用在应用程序和内核之间扮演了使者的角色,应用程序发送各种请求,而内核负责满足这些请求(或者让应用程序暂时搁置),使得计算机系统可以顺利而安全地完成所有的业务。