破解Linux操作系统的工作奥秘
2013-06-27 16:01
274 查看
破解Linux操作系统的工作奥秘
张颜(SA***111)
程序运行的过程中其实就是顺序从存储器里读取指令,然后利用函数调用堆栈机制不断入栈出栈(详见这里)。当一个进程在执行的过程中,可能会遇到各种事件,使其被挂起,内核调度CPU资源给进程队列中其他的进程,使其执行。Linux操作系统在宏观上来看,基本上就是这样:进程执行——>中断——>进程切换——>进程执行,这样循环往复进行工作。
从微观上来看,相对复杂:进程队列中的某个进程A正在执行,由于某种事件(系统调用、时间片用完等)产生中断,从用户态转向内核态,在内核态中,首先进行SAVE_ALL,然后执行中断处理程序,在中断处理过程中或其结束后,由于某种原因(比如时间片用完等),内核可能会进行进程调度,将当前进程切换为进程队列中的另一个进程B,然后RESTORE_ALL、iret,再由内核态转向用户态。若发生了进程切换,则必然会用到switch_to宏,将涉及到内核堆栈状态的变化,详见附录。
1.切换页全局目录以安装一个新的地址空间;
2.切换内核态堆栈和硬件上下文,因为硬件上下文提供了内核执行新进程所需要的所有信息,包含CPU和寄存器。
每个进程可以拥有属于自己的地址空间,但所有进程必须共享CPU寄存器。因此,恢复一个进程的执行之前,内核必须确保每个寄存器装入了挂起进程时的值。
进程切换的第二步由switch_to宏执行,该宏有三个参数,它们是prev,next,和last,其中,prev指向被替换进程的描述符,而next指向被激活进程的描述符,last则用来记录该进程是由哪个进程切换过来的。
贴出switch_to宏的内核代码(linux-3.2.1/arch/x86/include/asm/system.h),以便更好的分析:
上面代码没看懂?木有关系,看下面的具体分析,你就会豁然开朗了O(∩_∩)O~
switch_to从A进程切换到B进程的步骤:
step1:复制两个变量到寄存器:
[prev] "a" (prev)
[next] "d" (next)
即:
eax <== prev_A 或 eax <==%p(%ebp_A)
edx <== next_A 或 edx <==%n(%ebp_A)
这里prev和next都是A进程的局部变量。
step2:保存进程A的ebp和eflags
pushfl
pushl %ebp
注意,因为现在esp还在A的堆栈中,所以这两个东西被保存到A进程的内核堆栈中。
step3:保存当前esp到A进程内核描述符中:
"movl %%esp,%[prev_sp]\n\t" /* save ESP */
它可以表示成: prev_A->thread.sp <== esp_A
在调用switch_to时,prev是指向A进程自己的进程描述符的。
step4:从next(进程B)的描述符中取出之前从B切换出去时保存的esp_B。
"movl %[next_sp],%%esp\n\t" /* restore ESP */
它可以表示成:esp_B <== next_A->thread.sp
注意,在A进程中的next是指向B的进程描述符的。
从这个时候开始,CPU当前执行的进程已经是B进程了,因为esp已经指向B的内核堆栈。但是,现在的ebp仍然指向A进程的内核堆栈,所以所有局部变量仍然是A中的局部变量,比如next实质上是%n(%ebp_A),也就是next_A,即指向B的进程描述符。
step5:把标号为1的指令地址保存到A进程描述符的ip域:
"movl $1f,%[prev_ip]\n\t" /* save EIP */
它可以表示成:prev_A->thread.ip <== %1f,当A进程下次被switch_to回来时,会从这条指令开始执行。具体方法看后面被切换回来的B的下一条指令。
step6:将返回地址保存到堆栈,然后调用__switch_to()函数,__switch_to()函数完成硬件上下文切换。
"pushl %[next_ip]\n\t" /* restore EIP */
"jmp __switch_to\n" /* regparm call */
这里,如果之前B也被switch_to出去过,那么[next_ip]里存的就是下面这个1f的标号,但如果进程B刚刚被创建,之前没有被switch_to出去过,那么[next_ip]里存的将是ret_ftom_fork(参看copy_thread()函数)。这就是这里为什么不用call __switch_to而用jmp,因为call会导致自动把下面这句话的地址(也就是1:)压栈,然后__switch_to()就必然只能ret到这里,而无法根据需要ret到ret_from_fork。
另外请注意,这里__switch_to()返回时,将返回值prev_A又写入了%eax,这就使得在switch_to宏里面eax寄存器始终保存的是prev_A的内容,或者,更准确的说,是指向A进程描述符的“指针”。这是有用的,下面step8中将会看到。
step7:从__switch_to()返回后继续从1:标号后面开始执行,修改ebp到B的内核堆栈,恢复B的eflags:
"popl %%ebp\n\t" /* restore EBP */
"popfl\n" /* restore flags */
如果从__switch_to()返回后从这里继续运行,那么说明在此之前B肯定被switch_to调出过,因此此前肯定备份了ebp_B和flags_B,这里执行恢复操作。
注意,这时候ebp已经指向了B的内核堆栈,所以上面的prev,next等局部变量已经不是A进程堆栈中的了,而是B进程堆栈中的(B上次被切换出去之前也有这两个变量,所以代表着B堆栈中prev、next的值了),因为prev == %p(%ebp_B),而在B上次被切换出去之前,该位置保存的是B进程的描述符地址。如果这个时候就结束switch_to的话,在后面的代码中(即 context_switch()函数中switch_to之后的代码)的prev变量是指向B进程的,因此,进程B就不知道是从哪个进程切换回来。而从context_switch()中switch_to之后的代码中,我们看到finish_task_switch(this_rq(),
prev)中需要知道之前是从哪个进程切换过来的,因此,我们必须想办法保存A进程的描述符到B的堆栈中,这就是last的作用。
step8:将eax写入last,以在B的堆栈中保存正确的prev信息。
"=a" (last) 即 last_B <== %eax
而从context_switch()中看到的调用switch_to的方法是:
switch_to(prev, next, prev);
所以,这里面的last实质上就是prev,因此在switch_to宏执行完之后,prev_B就是正确的A的进程描述符了。
这里,last的作用相当于把进程A堆栈中的A进程描述符地址复制到了进程B的堆栈中。
至此,switch_to已经执行完成,A停止运行,而开始了B。在以后,可能在某一次调度中,进程A得到调度,就会出现switch_to(C, A)这样的调用,这时,A再次得到调度,得到调度后,A进程从context_switch()中switch_to后面的代码开始执行,这时候,它看到的prev_A将指向C的进程描述符。
参考链接:
1.http://baike.baidu.com/view/2083958.htm
2.http://wenku.baidu.com/view/e3296fda50e2524de5187e3d.html
3.深入理解Linux内核
张颜(SA***111)
总结
Linux操作系统能够正常工作是建立在:存储程序计算机、函数调用堆栈机制和中断机制这三个基础之上的。而对操作系统的讨论可以归结到对进程运行情况的讨论,Linux操作系统中进程主要分为:内核线程和普通进程。其中内核线程只工作在内核态,主要负责操作系统的初始化和一些周期性管理任务,常见的有0号进程(idle进程)和1号进程(init进程 );而普通进程既可以运行在内核态,也可以运行在用户态。程序运行的过程中其实就是顺序从存储器里读取指令,然后利用函数调用堆栈机制不断入栈出栈(详见这里)。当一个进程在执行的过程中,可能会遇到各种事件,使其被挂起,内核调度CPU资源给进程队列中其他的进程,使其执行。Linux操作系统在宏观上来看,基本上就是这样:进程执行——>中断——>进程切换——>进程执行,这样循环往复进行工作。
从微观上来看,相对复杂:进程队列中的某个进程A正在执行,由于某种事件(系统调用、时间片用完等)产生中断,从用户态转向内核态,在内核态中,首先进行SAVE_ALL,然后执行中断处理程序,在中断处理过程中或其结束后,由于某种原因(比如时间片用完等),内核可能会进行进程调度,将当前进程切换为进程队列中的另一个进程B,然后RESTORE_ALL、iret,再由内核态转向用户态。若发生了进程切换,则必然会用到switch_to宏,将涉及到内核堆栈状态的变化,详见附录。
附录
存储程序计算机
存储程序和程序控制原理的要点是,程序输入到计算机中,存储在内存储器中(存储原理),在运行时,控制器按地址顺序取出存放在内存储器中的指令(按地址顺序访问指令),然后分析指令,执行指令的功能,遇到转移指令时,则转移到转移地址,再按地址顺序访问指令(程序控制)。函数调用堆栈机制
详见实验一:计算机是怎样工作的中断机制
操作系统之所以具备如此强大的功能,很重要的一个原因是其具备中断处理这个机制。中断提供了一种特殊的方式,使处理器转而去运行正常控制流之外的代码。当一个中断信号到达时,CPU必须停止它当前正在做的事情,并且切换到一个新的活动。为了做到这一点,就要在内核态堆栈保存程序计数器的当前值(即eip和cs寄存器的内容),并把与中断类型相关的一个地址放进程序计数器中。进程切换
为了控制进程的执行,内核必须有能力挂起正在CPU上执行的进程,并恢复以前挂起的某个进程的执行,这叫做进程切换。进程切换可能只发生在精心定义的点:schedule()函数;从本质上说,每个进程切换由两步组成:1.切换页全局目录以安装一个新的地址空间;
2.切换内核态堆栈和硬件上下文,因为硬件上下文提供了内核执行新进程所需要的所有信息,包含CPU和寄存器。
每个进程可以拥有属于自己的地址空间,但所有进程必须共享CPU寄存器。因此,恢复一个进程的执行之前,内核必须确保每个寄存器装入了挂起进程时的值。
进程切换的第二步由switch_to宏执行,该宏有三个参数,它们是prev,next,和last,其中,prev指向被替换进程的描述符,而next指向被激活进程的描述符,last则用来记录该进程是由哪个进程切换过来的。
贴出switch_to宏的内核代码(linux-3.2.1/arch/x86/include/asm/system.h),以便更好的分析:
/* * Saving eflags is important. It switches not only IOPL between tasks, * it also protects other tasks from NT leaking through sysenter etc. */ #define switch_to(prev, next, last) \ do { \ /* \ * Context-switching clobbers all registers, so we clobber \ * them explicitly, via unused output variables. \ * (EAX and EBP is not listed because EBP is saved/restored \ * explicitly for wchan access and EAX is the return value of \ * __switch_to()) \ */ \ unsigned long ebx, ecx, edx, esi, edi; \ \ asm volatile("pushfl\n\t" /* save flags */ \ "pushl %%ebp\n\t" /* save EBP */ \ "movl %%esp,%[prev_sp]\n\t" /* save ESP */ \ "movl %[next_sp],%%esp\n\t" /* restore ESP */ \ "movl $1f,%[prev_ip]\n\t" /* save EIP */ \ "pushl %[next_ip]\n\t" /* restore EIP */ \ __switch_canary \ "jmp __switch_to\n" /* regparm call */ \ "1:\t" \ "popl %%ebp\n\t" /* restore EBP */ \ "popfl\n" /* restore flags */ \ \ /* output parameters */ \ : [prev_sp] "=m" (prev->thread.sp), \ [prev_ip] "=m" (prev->thread.ip), \ "=a" (last), \ \ /* clobbered output registers: */ \ "=b" (ebx), "=c" (ecx), "=d" (edx), \ "=S" (esi), "=D" (edi) \ \ __switch_canary_oparam \ \ /* input parameters: */ \ : [next_sp] "m" (next->thread.sp), \ [next_ip] "m" (next->thread.ip), \ \ /* regparm parameters for __switch_to(): */ \ [prev] "a" (prev), \ [next] "d" (next) \ \ __switch_canary_iparam \ \ : /* reloaded segment registers */ \ "memory"); \ } while (0)
上面代码没看懂?木有关系,看下面的具体分析,你就会豁然开朗了O(∩_∩)O~
switch_to从A进程切换到B进程的步骤:
step1:复制两个变量到寄存器:
[prev] "a" (prev)
[next] "d" (next)
即:
eax <== prev_A 或 eax <==%p(%ebp_A)
edx <== next_A 或 edx <==%n(%ebp_A)
这里prev和next都是A进程的局部变量。
step2:保存进程A的ebp和eflags
pushfl
pushl %ebp
注意,因为现在esp还在A的堆栈中,所以这两个东西被保存到A进程的内核堆栈中。
step3:保存当前esp到A进程内核描述符中:
"movl %%esp,%[prev_sp]\n\t" /* save ESP */
它可以表示成: prev_A->thread.sp <== esp_A
在调用switch_to时,prev是指向A进程自己的进程描述符的。
step4:从next(进程B)的描述符中取出之前从B切换出去时保存的esp_B。
"movl %[next_sp],%%esp\n\t" /* restore ESP */
它可以表示成:esp_B <== next_A->thread.sp
注意,在A进程中的next是指向B的进程描述符的。
从这个时候开始,CPU当前执行的进程已经是B进程了,因为esp已经指向B的内核堆栈。但是,现在的ebp仍然指向A进程的内核堆栈,所以所有局部变量仍然是A中的局部变量,比如next实质上是%n(%ebp_A),也就是next_A,即指向B的进程描述符。
step5:把标号为1的指令地址保存到A进程描述符的ip域:
"movl $1f,%[prev_ip]\n\t" /* save EIP */
它可以表示成:prev_A->thread.ip <== %1f,当A进程下次被switch_to回来时,会从这条指令开始执行。具体方法看后面被切换回来的B的下一条指令。
step6:将返回地址保存到堆栈,然后调用__switch_to()函数,__switch_to()函数完成硬件上下文切换。
"pushl %[next_ip]\n\t" /* restore EIP */
"jmp __switch_to\n" /* regparm call */
这里,如果之前B也被switch_to出去过,那么[next_ip]里存的就是下面这个1f的标号,但如果进程B刚刚被创建,之前没有被switch_to出去过,那么[next_ip]里存的将是ret_ftom_fork(参看copy_thread()函数)。这就是这里为什么不用call __switch_to而用jmp,因为call会导致自动把下面这句话的地址(也就是1:)压栈,然后__switch_to()就必然只能ret到这里,而无法根据需要ret到ret_from_fork。
另外请注意,这里__switch_to()返回时,将返回值prev_A又写入了%eax,这就使得在switch_to宏里面eax寄存器始终保存的是prev_A的内容,或者,更准确的说,是指向A进程描述符的“指针”。这是有用的,下面step8中将会看到。
step7:从__switch_to()返回后继续从1:标号后面开始执行,修改ebp到B的内核堆栈,恢复B的eflags:
"popl %%ebp\n\t" /* restore EBP */
"popfl\n" /* restore flags */
如果从__switch_to()返回后从这里继续运行,那么说明在此之前B肯定被switch_to调出过,因此此前肯定备份了ebp_B和flags_B,这里执行恢复操作。
注意,这时候ebp已经指向了B的内核堆栈,所以上面的prev,next等局部变量已经不是A进程堆栈中的了,而是B进程堆栈中的(B上次被切换出去之前也有这两个变量,所以代表着B堆栈中prev、next的值了),因为prev == %p(%ebp_B),而在B上次被切换出去之前,该位置保存的是B进程的描述符地址。如果这个时候就结束switch_to的话,在后面的代码中(即 context_switch()函数中switch_to之后的代码)的prev变量是指向B进程的,因此,进程B就不知道是从哪个进程切换回来。而从context_switch()中switch_to之后的代码中,我们看到finish_task_switch(this_rq(),
prev)中需要知道之前是从哪个进程切换过来的,因此,我们必须想办法保存A进程的描述符到B的堆栈中,这就是last的作用。
step8:将eax写入last,以在B的堆栈中保存正确的prev信息。
"=a" (last) 即 last_B <== %eax
而从context_switch()中看到的调用switch_to的方法是:
switch_to(prev, next, prev);
所以,这里面的last实质上就是prev,因此在switch_to宏执行完之后,prev_B就是正确的A的进程描述符了。
这里,last的作用相当于把进程A堆栈中的A进程描述符地址复制到了进程B的堆栈中。
至此,switch_to已经执行完成,A停止运行,而开始了B。在以后,可能在某一次调度中,进程A得到调度,就会出现switch_to(C, A)这样的调用,这时,A再次得到调度,得到调度后,A进程从context_switch()中switch_to后面的代码开始执行,这时候,它看到的prev_A将指向C的进程描述符。
参考链接:
1.http://baike.baidu.com/view/2083958.htm
2.http://wenku.baidu.com/view/e3296fda50e2524de5187e3d.html
3.深入理解Linux内核
相关文章推荐
- sa12***161 Linux操作系统是如何工作的?破解操作系统的奥秘
- 【实验五】Linux操作系统是如何工作的?破解操作系统的奥秘
- 实验五:Linux操作系统是如何工作的?破解操作系统的奥秘
- 【实验五】Linux操作系统是如何工作的?破解操作系统的奥秘
- Linux操作系统分析-(3)Linux操作系统是如何工作的?破解操作系统的奥秘
- Linux操作系统是如何工作的?破解操作系统的奥秘、
- Linux操作系统是如何工作的?破解操作系统的奥秘
- Linux操作系统是如何工作的?破解操作系统的奥秘
- Linux操作系统是如何工作的?破解操作系统的奥秘
- 【Linux操作系统分析】破解操作系统的奥秘
- Linux操作系统是如何工作的?破解操作系统的奥秘
- Linux操作系统分析(8)- 破解Linux操作系统的奥秘
- Linux操作系统分析(8)- 破解Linux操作系统的奥秘
- Linux操作系统分析__破解操作系统的奥秘
- 浅析Linux操作系统工作的基础
- Linux内和分析(二)操作系统是如何工作的
- Linux操作系统之奥秘(第2版)
- Linux操作系统 内核工作队列的操作模式
- Linux操作系统学习_操作系统是如何工作的