235 + 原创作品转载请注明出处 + 中科大孟宁老师的linux操作系统分析:
https://github.com/mengning/linuxkernel/
实验要求:
- 编译内核5.0
- qemu -kernel linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img
- 选择系统调用号后两位与您的学号后两位相同的系统调用进行跟踪分析
- https://github.com/mengning/menu
- 给出相关关键源代码及实验截图,撰写一篇博客,博客内容的具体要求如下:
- 题目自拟,内容围绕系统调用进行;
- 博客中需要使用实验截图
- 博客内容中需要仔细分析系统调用、保护现场与恢复现场、系统调用号及参数传递过程
- 总结部分需要阐明自己对系统调用工作机制的理解。
实验内容:
一、实验过程及截图
实验环境:VMware 虚拟机+Ubuntu 16.04.10(Linux4.15)
1、下载linux5.0.1内核源码
wget https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.0.1.tar.xz
2、解压并编译内核5.0.1
3、将git上的一个根文件系统下载下来打包成img文件,并启动Menu os
mkdir rootfs
git clone https://github.com/mengning/menu.git
cd menu
gcc -o init linktable.c menu.c test.c -m32 -static –lpthread
cd ../rootfs
cp ../menu/init ./
find . | cpio -o -Hnewc |gzip -9 > ../rootfs.img
cd ..
qemu -kernel linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img
4、利用gdb进行跟踪调试
qemu-system-i386 -kernel linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img -s -S
新建一个终端,进入Linux_test2
gdb
(gdb)file linux-5.0.1/vmlinux # 在gdb界面中targe remote之前加载符号表
(gdb)target remote:1234 # 建立gdb和gdbserver之间的连接,按c 让qemu上的Linux继续运行
(gdb)break start_kernel # 断点的设置可以在target remote之前,也可以在之后
(gdb)c
linux5.0.1内核编译成功。
5、选择系统调用号后两位与您的学号后两位相同的系统调用进行跟踪分析
我的学号后两位是35 ——#define __NR_ftime 35
ftime()函数为取得目前的时间和日期。
相关函数:time, ctime, gettimeofday
表头文件:#include <sys/timeb.h>
函数定义:int ftime(struct timeb *tp);
函数说明:ftime()将目前日期由tp所指的结构返回。tp结构定义:
struct timeb{
time_t time; /* 为1970-01-01至今的秒数*/
unsigned short millitm; /* 千分之一秒即毫秒 */
short timezonel; /* 为目前时区和Greenwich相差的时间,单位为分钟 */
short dstflag; /* 为日光节约时间的修正状态,如果为非0代表启用日光节约时间修正 */
};
返回值 :无论成功或失败都返回0
范例:
#include <sys/timeb.h>
main()
{
struct timeb tp;
ftime(&tp);
printf("%d/n", tp.time);
}
在test.c 中加入ftimetest函数:
进行重新编译:
输出为1970-01-01: 1552979003 //从1970年到现在的秒数
调用成功
以调试方式启动qemu:
qemu -kernel linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img -s -S
6、分析system_call()函数:
先写一个.c文件 235.c
#include<stdio.h>
#include <sys/timeb.h>
int main()
{
struct timeb t1,t2;
long t;
double x,sum=1,sum1;
int i,j,n;//n指x的n次方
printf("请输入x n:");
scanf("%lf%d",&x,&n);
ftime(&t1); // 求得当前时间
for(i=1;i<=n;i++)
{
sum1=1;
for(j=1;j<=i;j++)
sum1=sum1*(-1.0/x);
sum+=sum1;
}
ftime(&t2); // 求得当前时间
t=(t2.time-t1.time)*1000+(t2.millitm-t1.millitm); // 计算时间差
printf("sum=%lf 用时%ld毫秒\n",sum,t);
}
将235.c文件编译运行并查看输出结果
使用gdb断点,分析系统调用
break ftime //设置断点
r //运行到断点
ni //单步调试
disass //显示步骤详情
info r //显示运行时寄存器详情
如上图所示,运行到call 步骤之后,eax的值发生了变化,显示ftime函数的返回值0,打印结果后,最后回到main函数;
二、实验总结与分析
系统调用整个过程如下:
首先指令流执行到系统调用函数时,系统调用函数通过int 0x80指令进入系统调用入口程序,并且把系统调用号放入%eax中,如果需要传递参数,则把参数放入%ebx,%ecx和%edx中。进入系统调用入口程序(System_call)后,它首先把相关的寄存器压入内核堆栈(以备将来恢复),这个过程称为保护现场。保护现场的工作完成后,开始检查系统调用号是不是一个有效值,如果不是则退出。接下来根据系统调用号开始调用系统调用处理程序(这是一个正式执行系统调用功能的函数),从系统调用处理程序返回后,就会去检查当前进程是否处于就绪态、进程时间片是否用完,如果不在就绪态或者时间片已用完,那么就会去调用进程调度程序schedule(),转去执行其他进程。如果不执行进程调度程序,那么接下来就会开始执行ret_from_sys_call,顾名思义,这这个程序主要执行一些系统调用的后处理工作。比如它会去检查当前进程是否有需要处理的信号,如果有则去调用do_signal(),然后进行一些恢复现场的工作,返回到原先的进程指令流中。至此整个系统调用的过程就结束了。
系统调用陷入内核后作的参数传递过程 系统调用参数传递过程
当进程执行系统调用时,先调用系统调用库中定义某个函数,该函数通常被展开成前面提到的_syscallN的形式通过INT 0x80来陷入核心,其参数也将被通过寄存器传往核心。
在这一部分,我们将介绍INT 0x80的处理函数system_call。
思考一下就会发现,在调用前和调用后执行态完全不相同:前者是在用户栈上执行用户态程序,后者在核心栈上执行核心态代码。那么,为了保证在核心内部执行完系统调用后能够返回调用点继续执行用户代码,必须在进入核心态时保存时往核心中压入一个上下文层;在从核心返回时会弹出一个上下文层,这样用户进程就可以继续运行。
那么,这些上下文信息是怎样被保存的,被保存的又是那些上下文信息呢?这里仍以x86为例说明。
在执行INT指令时,实际完成了以下几条操作:
(1) 由于INT指令发生了不同优先级之间的控制转移,所以首先从TSS(任务状态段)中获取高优先级的核心堆栈信息(SS和ESP);
(2) 把低优先级堆栈信息(SS和ESP)保留到高优先级堆栈(即核心栈)中;
(3) 把EFLAGS,外层CS,EIP推入高优先级堆栈(核心栈)中。
(4) 通过IDT加载CS,EIP(控制转移至中断处理函数)
然后就进入了中断0x80的处理函数system_call了,在该函数中首先使用了一个宏SAVE_ALL,该宏的定义如下所示:
#define SAVE_ALL /
cld; /
pushl %es; /
pushl %ds; /
pushl %eax; /
pushl %ebp; /
pushl %edi; /
pushl %esi; /
pushl %edx; /
pushl %ecx; /
pushl %ebx; /
movl $(__KERNEL_DS),%edx; /
movl %edx,%ds; /
movl %edx,%es;
该宏的功能一方面是将寄存器上下文压入到核心栈中,对于系统调用,同时也是系统调用参数的传入过程,因为在不同特权级之间控制转换时,INT指令不同于CALL指令,它不会将外层堆栈的参数自动拷贝到内层堆栈中。所以在调用系统调用时,必须先象前面的例子里提到的那样,把参数指定到各个寄存器中,然后在陷入核心之后使用SAVE_ALL把这些保存在寄存器中的参数依次压入核心栈,这样核心才能使用用户传入的参数。
下面给出system_call的源代码:
ENTRY(system_call)
pushl %eax # save orig_eax
SAVE_ALL
GET_CURRENT(%ebx)
cmpl $(NR_syscalls),%eax
jae badsys
testb $0x20,flags(%ebx) # PF_TRACESYS
jne tracesys
call *SYMBOL_NAME(sys_call_table)(,%eax,4)
. . . . . .
在这里所做的所有工作是:
Ⅰ.保存EAX寄存器,因为在SAVE_ALL中保存的EAX寄存器会被调用的返回值所覆盖;
Ⅱ.调用SAVE_ALL保存寄存器上下文;
Ⅲ.判断当前调用是否是合法系统调用(EAX是系统调用号,它应该小于NR_syscalls);
Ⅳ.如果设置了PF_TRACESYS标志,则跳转到syscall_trace,在那里将会把当前程挂起并向其父进程发送SIGTRAP,这主要是为了设置调试断点而设计的;
Ⅴ.如果没有设置PF_TRACESYS标志,则跳转到该系统调用的处理函数入口。这里是以EAX(即前面提到的系统调用号)作为偏移,在系统调用表sys_call_table中查找处理函数入口地址,并跳转到该入口地址。