IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 系统运维 -> 系统调用代码流程 -> 正文阅读

[系统运维]系统调用代码流程

由用户态进入内核态时,CPU会自动按照SS、ESP、EFLAGS、CS、EIP的顺序,将这几个寄存器的值压入到内核栈中
父进程内核栈的样子
执行int 0x80将SS、ESP、EFLAGS、CS、EIP入栈。
在system_call中将DS、ES、FS、EDX、ECX、EBX入栈。

system_call:
    cmpl $nr_system_calls-1,%eax  调用号如果超出范围的话就在 eax 中置-1 并退出
    ja bad_sys_call 
    push %ds    # 保存原段寄存器值
    push %es
    push %fs
    pushl %edx     # ebx,ecx,edx 中放着系统调用相应的 C 语言函数的调用参数
    pushl %ecx      # push %ebx,%ecx,%edx as parameters
    pushl %ebx      # to the system call
    movl $0x10,%edx        # set up ds,es to kernel space
    mov %dx,%ds
    mov %dx,%es          ds,es 指向内核数据段(全局描述符表中数据段描述符)。
    movl $0x17,%edx        # fs points to local data space
    mov %dx,%fs                  fs 指向局部数据段(局部描述符表中数据段描述符)。
    # 下面这句操作数的含义是:调用地址 = _sys_call_table + %eax * 4。参见列表后的说明。
# 对应的 C 程序中的 sys_call_table 在 include/linux/sys.h 中,其中定义了一个包括 72 个
# 系统调用 C 处理函数的地址数组表。
    call sys_call_table(,%eax,4)  
###
在一个 8 字节为一个记录的数组中寻址指定的字符。其中 eax 中是指定的记录号,ebx 中是指定字
符在记录中的偏移址:
AT&T: _array(%ebx,%eax,8) Intel: [ebx + eax*8 + _array]
####

    pushl %eax       # 把系统调用号入栈。
    movl current,%eax    # 取当前任务(进程)数据结构地址?eax。
#下面 4 行查看当前任务的运行状态。 如果不在就绪状态(state 不等于 0)就去执行调度程序。
# 如果该任务在就绪状态但 counter[??]值等于 0,则也去执行调度程序。
    cmpl $0,state(%eax)        # state
    jne reschedule
    cmpl $0,counter(%eax)      # counter
    je reschedule

在system_call中执行完相应的系统调用sys_call_xx后,又将函数的返回值eax压栈。若引起调度,则跳转执行reschedule。否则则执行ret_from_sys_call。

reschedule:
	pushl $ret_from_sys_call
	jmp schedule

在执行schedule前将ret_from_sys_call压栈,因为schedule是c函数,所以在c函数末尾的},相当于ret指令,将会弹出ret_from_sys_call作为返回地址,跳转 ret_from_sys_call执行。
总之,在系统调用结束后,将要中断返回前,内核栈的样子如下:
在这里插入图片描述

void schedule(void)
{
    int i,next,c;
    struct task_struct *pnext = &(init_task.task);
    struct task_struct ** p;    // 任务结构指针的指针。
    /* check alarm, wake up any interruptible tasks that have got a signal */
/* 检测 alarm,唤醒得到了一个信号的任何可以中断的任务 */
// 从任务数组中最后一个任务开始检测 alarm。
	for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
		if (*p) {
		// 如果任务的 alarm 时间已经过期(alarm<jiffies),在信号位图中置 SIGALRM 信号,然后清 alarm。
			if ((*p)->alarm && (*p)->alarm < jiffies) {
					(*p)->signal |= (1<<(SIGALRM-1));
					(*p)->alarm = 0;
				}
// 如果信号位图中除去被阻塞的信号外还有其它信号并且任务处于可中断状态,则置任务为就绪态。
// 其中'~(_BLOCKABLE & (*p)->blocked)'用于忽略被阻塞的信号,但 SIGKILL 和 SIGSTOP 不能被阻塞。
			if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
			(*p)->state==TASK_INTERRUPTIBLE)
				(*p)->state=TASK_RUNNING;  //置为就绪(可执行)状态。
		}
/* 这里是调度程序的主要部分 */
	while (1) {
		c = -1;
		next = 0;
		i = NR_TASKS;
		p = &task[NR_TASKS];
	// 这段代码比较每个就绪状态任务的 counter 值,哪一个大 next 就指向哪个的任务号。
		while (--i) {
			if (!*--p)
				continue;
			if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
				c = (*p)->counter, next = i;
		}
	// 如果比较得出有 counter 值大于 0 的结果,则退出循环,执行任务调度切换(switch_to(next);)。
		if (c) break;
	// 否则就根据每个任务的优先权值,更新每一个任务的 counter 值,然后回到 125 行重新比较。
// counter 值的计算方式为 counter = counter /2 + priority。
		for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
			if (*p)
				(*p)->counter = ((*p)->counter >> 1) +
						(*p)->priority;
	}
	switch_to(next);// 切换到任务号为 next 的任务,并运行之。
    
}

在schedule()函数中,当调用函数switch_to(pent, _LDT(next))时,会依次将返回地址}、参数2 _LDT(next)、参数1 pnext压栈。当执行switch_to的返回指令ret时,就回弹出schedule()函数的}执行schedule()函数的返回指令}。关于执行switch_to时内核栈的样子,在后面改写switch_to函数时十分重要。
此处将跳入到switch_to中执行时,内核栈的样子如下:
在这里插入图片描述

/*
 *	switch_to(n) should switch tasks to task nr n, first
 * checking that n isn't the current task, in which case it does nothing.
 * This also clears the TS-flag if the task we switched to has used
 * tha math co-processor latest.
 */
 * switch_to(n)将切换当前任务到任务 nr,即 n。首先检测任务 n 不是当前任务,
* 如果是则什么也不做退出。如果我们切换到的任务最近(上次运行)使用过数学
* 协处理器的话,则还需复位控制寄存器 cr0 中的 TS 标志。
// 输入:%0 - 新 TSS 的偏移地址(*&__tmp.a); %1 - 存放新 TSS 的选择符值(*&__tmp.b);
// dx - 新任务 n 的选择符;ecx - 新任务指针 task[n]。
// 其中临时数据结构__tmp 中,a 的值是 32 位偏移值,b 为新 TSS 的选择符。在任务切换时,a 值
// 没有用(忽略)。
#define switch_to(n) {\
struct {long a,b;} __tmp; \
__asm__("cmpl %%ecx,current\n\t" \ // 任务 n 是当前任务吗?(current ==task[n]?)
	"je 1f\n\t" \    // 是,则什么都不做,退出。
	"movw %%dx,%1\n\t" \   // 将新任务的选择符->*&__tmp.b。
	"xchgl %%ecx,current\n\t" \  // current = task[n];ecx = 被切换出的任务。
	"ljmp *%0\n\t" \     // 长跳转,造成任务切换。
	"cmpl %%ecx,last_task_used_math\n\t" \   // 新任务上次使用过协处理器吗?
	"jne 1f\n\t" \    // 没有则跳转,退出。
	"clts\n" \   // 新任务上次使用过协处理器,则清 cr0 的 TS 标志。
	"1:" \
	::"m" (*&__tmp.a),"m" (*&__tmp.b), \
	"d" (_TSS(n)),"c" ((long) task[n])); \
}

要实现基于内核栈的任务切换,主要完成如下三件工作

(1)重写 switch_to;
(2)将重写的 switch_to 和 schedule() 函数接在一起;
(3)修改现在的 fork()。

schedule 与 switch_to
目前 Linux 0.11 中工作的 schedule() 函数是首先找到下一个进程的数组位置 next,而这个 next 就是 GDT 中的 n,所以这个 next 是用来找到切换后目标 TSS 段的段描述符的,一旦获得了这个 next 值,直接调用上面剖析的那个宏展开 switch_to(next);就能完成如图 TSS 切换所示的切换了。

现在,我们不用 TSS 进行切换,而是采用切换内核栈的方式来完成进程切换,所以在新的 switch_to 中将用到当前进程的 PCB、目标进程的 PCB、当前进程的内核栈、目标进程的内核栈等信息。由于 Linux 0.11 进程的内核栈和该进程的 PCB 在同一页内存上(一块 4KB 大小的内存),其中 PCB 位于这页内存的低地址,栈位于这页内存的高地址;另外,由于当前进程的 PCB 是用一个全局变量 current 指向的,所以只要告诉新 switch_to()函数一个指向目标进程 PCB 的指针就可以了。同时还要将 next 也传递进去,虽然 TSS(next)不再需要了,但是 LDT(next)仍然是需要的,也就是说,现在每个进程不用有自己的 TSS 了,因为已经不采用 TSS 进程切换了,但是每个进程需要有自己的 LDT,地址分离地址还是必须要有的,而进程切换必然要涉及到 LDT 的切换。

综上所述,需要将目前的 schedule() 函数(在 kernal/sched.c 中)做稍许修改,即将下面的代码:

if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
    c = (*p)->counter, next = i;

//......

switch_to(next);
copy

修改为:

if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
    c = (*p)->counter, next = i, pnext = *p;

//.......

switch_to(pnext, LDT(next));

实现 switch_to
删除头文件sched.h中的长跳转指令:“ljmp *%0\n\t”
在system_call.s中添加系统调用函数switch_to():

.align 2
switch_to:
	pushl %ebp
	movl %esp,%ebp
	pushl %ecx
	pushl %ebc
	pushl %eax

	movl 8(%ebp),%ebx
	cmpl %ebx,current
	je 1f

 // PCB的切换
	movl %ebx,%eax
	xchgl %eax,current
	
	// TSS中内核栈指针的重写
	movl tss,%ecx
	addl $4096,%ebx
	movl %ebx,ESP0(%ecx)

	//切换内核栈
	movl %esp,KERNEL_STACK(%eax)
	movl 8(%ebp),%ebx
	movl KERNEL_STACK(%ebx),%esp

	//LDT的切换
	movl 12(%ebp),%ecx
	lldt %cx
	movl $0x17,%ecx
	mov %cx,%fs
	
	movl $0x17,%ecx
	mov %cx,%fs
	cmpl %eax,last_task_used_math
	jne 1f
	clts

1:	popl %eax
	popl %ebx
	popl %ecx
	popl %ebp
	ret

fs 是一个选择子,即 fs 是一个指向描述符表项的指针,这个描述符才是指向实际的用户态内存的指针,所以上一个进程和下一个进程的 fs 实际上都是 0x17,真正找到不同的用户态内存是因为两个进程查的 LDT 表不一样,所以这样重置一下 fs=0x17 有用吗,有什么用?要回答这个问题就需要对段寄存器有更深刻的认识,实际上段寄存器包含两个部分:显式部分和隐式部分,如下图给出实例所示,就是那个著名的 jmpi 0, 8,虽然我们的指令是让 cs=8,但在执行这条指令时,会在段表(GDT)中找到 8 对应的那个描述符表项,取出基地址和段限长,除了完成和 eip 的累加算出 PC 以外,还会将取出的基地址和段限长放在 cs 的隐藏部分,即图中的基地址 0 和段限长 7FF。为什么要这样做?下次执行 jmp 100 时,由于 cs 没有改过,仍然是 8,所以可以不再去查 GDT 表,而是直接用其隐藏部分中的基地址 0 和 100 累加直接得到 PC,增加了执行指令的效率。
更改fork.c

int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,
		long ebx,long ecx,long edx,
		long fs,long es,long ds,
		long eip,long cs,long eflags,long esp,long ss)
{
	struct task_struct *p;
	int i;
	struct file *f;


	p->tss.back_link = 0;
	p->tss.esp0 = PAGE_SIZE + (long) p;
	p->tss.ss0 = 0x10;


	*(--krnstack) = ss & 0xffff;
	*(--krnstack) = esp;
	*(--krnstack) = eflags;
	*(--krnstack) = cs & 0xffff;
	*(--krnstack) = eip;

	*(--krnstack) = (long) first_return_kernel;//处理switch_to返回的位置

	*(--krnstack) = ebp;
	*(--krnstack) = ecx;
	*(--krnstack) = ebx;
	*(--krnstack) = 0;

	//把switch_to中要的东西存进去
	p->kernelstack = krnstack;
	...

写first_return_kernel在system_call.s中:

首先需要将first_return_kernel设置在全局可见:
.globl switch_to,first_return_kernel

然后需要在fork.c中添加该函数的声明:
extern void first_return_from_kernel(void);

最后就是将具体的函数实现放在system_call.s头文件里面:
first_return_kernel:
 popl %edx
 popl %edi
 popl %esi
 pop %gs
 pop %fs
 pop %es
 pop %ds
 iret

  系统运维 最新文章
配置小型公司网络WLAN基本业务(AC通过三层
如何在交付运维过程中建立风险底线意识,提
快速传输大文件,怎么通过网络传大文件给对
从游戏服务端角度分析移动同步(状态同步)
MySQL使用MyCat实现分库分表
如何用DWDM射频光纤技术实现200公里外的站点
国内顺畅下载k8s.gcr.io的镜像
自动化测试appium
ctfshow ssrf
Linux操作系统学习之实用指令(Centos7/8均
上一篇文章      下一篇文章      查看所有文章
加:2022-02-26 12:11:44  更:2022-02-26 12:11:55 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/16 5:46:17-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码