一。堆栈的基本原理
1.在linux C程序执行过程中,整个堆栈段都在随着数据的压栈、出栈而增长、消减。 堆栈是C语言程序运行时必须的一个记录调用路径和参数
的空间。并且各种编译器构成的堆栈都不一致。
2.堆栈的寄存器以及图解
需要注意的是,堆栈是从高地址向低地址增长的。每次压栈,都会讲esp指针下移4个字节(32位机是4个字节)
其他跟堆栈操作相关的寄存器:
cs : eip:总是指向下一条的指令地址
? 顺序执行:总是指向地址连续的下一条指令
? 跳转/分支:执行这样的指令的时候, cs : eip的值会
根据程序需要被修改
? call:将当前cs : eip的值压入栈顶, cs : eip指向被
调用函数的入口地址
? ret:从栈顶弹出原来保存在这里的cs : eip的值,放
入cs : eip中
3.堆栈的构造
堆栈构造时,寄存器的运行流程如下:
-
pushl %ebp
-
movl %esp, %eb
堆栈结束时
-
movl %ebp,%esp
-
popl %ebp
-
ret
此时ret一般是从函数进行返回时有的操作。详见
linux内核学习 第一周
例子如下,我们把一段简单的代码进行编译,并且通过反汇编(objdump -S)来得到此段函数的汇编指令。
-
#include <stdio.h>
-
-
int g(int a)
-
{
-
// printf("a = %d\n", a);
-
return a + 3;
-
}
-
int main(int argc, char *argv[])
-
{
-
return g(4);
-
}
通过反汇编之后得到文件为:
-
080483b4 <g>:
-
80483b4: 55 push %ebp
-
80483b5: 89 e5 mov %esp,%ebp
-
80483b7: 8b 45 08 mov 0x8(%ebp),%eax
-
80483ba: 83 c0 03 add $0x3,%eax
-
80483bd: 5d pop %ebp
-
80483be: c3 ret
-
-
080483bf <main>:
-
80483bf: 55 push %ebp
-
80483c0: 89 e5 mov %esp,%ebp
-
80483c2: 83 ec 04 sub $0x4,%esp
-
80483c5: c7 04 24 04 00 00 00 movl $0x4,(%esp)
-
80483cc: e8 e3 ff ff ff call 80483b4 <g>
-
80483d1: c9 leave
-
80483d2: c3 ret
对比上下代码即可看到,在调用g函数(
call 80483b4 <g>)后,g函数首先进行的是 push 与mov,这两条指令就构成了g 的栈底,知道g函数执行完ret 指令以后,g函数的栈就被清空,重新进入到main函数的栈中。也就是说,
函数的调用,本质上是堆栈的建立与销毁的过程。
二。进程调度与堆栈的简易实现
1.LinuxC中内嵌汇编代码的实现:
-
#include <stdio.h>
-
-
int main(int argc, char *argv[])
-
{
-
int input, output, temp;
-
input = 4;
-
__asm__ __volatile__ (
-
"movl $9, %%eax;\n\t"
-
"movl %%eax, %1;\n\t"
-
"movl %2,%%eax;\n\t"
-
"movl %%eax, %0;\n\t"
-
:"=m"(output),"=m"(temp)
-
:"r"(input)
-
:"eax"
-
);
-
printf("%d, %d\n", temp, output);
-
return 0;
-
}
这是个简单的例子。第7行是内嵌汇编的格式。 8行:将立即数9赋值给eax寄存器。9行:将eax寄存器的值放入 参数1中,也就是下面第12行的temp, 10行:将参数2的值放入eax中,通过代码可以看到input的值被初始化成了4,第11行中,eax的值被付给了0号参数:output。
所以这个程序的输出结果为:9,4。
2.在内核中实现简易的进程调度程序,环境搭建可参考: 。
a.关于中断,中断指当出现需要时,CPU暂时停止当前程序的执行转而执行处理新情况的程序和执行过程。在系统层面上,你能够随意切换当前运行的程序,比如随时从QQ切换到浏览器,但是在这个切换的过程中,就会把指定的程序切换到当前进程中,如何切换呢,这个就得看中断以及进程调度的工作。中断是计算机的一大特点,否则的话,计算机运行程序时就得从头运行到尾,不能被打断。可见中断是一种可以人为参与(软件)或者硬件自动完成的,使CPU发生的一种程序跳转。在linux中,对于中断的处理分为:
第一步,保护现场就是进入中断程序保存需要用到的寄存器的数据。
第二部,恢复现场,就是退出中断程序恢复保存寄存器的数据。
b.根据mykernel的实验结果,内核中对于中断触发的处理,就可以通过上述的汇编指令来实现。
下面是代码解析,我们只对关键的一部分代码进行分析,其他的可以通过demo自己动手测试。
初始化部分:my_start_kernel函数
-
/* start process 0 by task[0] */
-
pid = 0;
-
my_current_task = &task[pid];
-
asm volatile(
-
"movl %1,%%esp\n\t" /* set task[pid].thread.sp to esp。将当前esp指向 新进程的堆栈空间 */
-
"pushl %1\n\t" /* push ebp,将 新进程的esp值进行压栈 */
-
"pushl %0\n\t" /* push task[pid].thread.ip , 将当前进程的ip进行压栈*/
-
"ret\n\t" /* pop task[pid].thread.ip to eip , 将当前进程的ip 赋给eip*/
-
"popl %%ebp\n\t" /*将当前进程的sp赋给ebp,此时构造出了一个新的栈空间,即 代码中的task[pid].thread.sp*/
-
:
-
: "c" (task[pid].thread.ip),"d" (task[pid].thread.sp) /* input c or d mean %ecx/%edx*/
-
);
参考注释可以知道,这段代码构造0号pid 的堆栈空间,这样第一次运行时将直接进入到0号进程。
在my_schedule函数中,切换进程的汇编执行的功能可以参考注释:
-
if(next->state == 0)/* -1 unrunnable, 0 runnable, >0 stopped */
-
{
-
/* switch to next process */
-
asm volatile(
-
"pushl %%ebp\n\t" /* save ebp ,将原进程ebp压栈,ebp位于栈顶位置*/
-
"movl %%esp,%0\n\t" /* save esp ,将esp赋值给prev的sp。这一句会将当前进程的栈顶指针保存在sp中*/
-
"movl %2,%%esp\n\t" /* restore esp ,将next的sp赋值给esp,也就是将esp指向了新的栈*/
-
"movl $1f,%1\n\t" /* save eip ,等同于将eip进行保存,设置prev的ip为 1f,这四句指令执行完毕后,esp会指向新进程的栈空间*/
-
"pushl %3\n\t" /*将next的ip进行压栈*/
-
"ret\n\t" /* restore eip ,将ip 赋给当前的eip寄存器,接下来将从ip的位置开始执行新的进程。原先的进程已经被压栈处理。*/
-
"1:\t" /* next process start here */
-
"popl %%ebp\n\t"
-
: "=m" (prev->thread.sp),"=m" (prev->thread.ip)
-
: "m" (next->thread.sp),"m" (next->thread.ip)
-
);
-
my_current_task = next;
-
printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid);
-
}
-
else
-
{
-
next->state = 0;
-
my_current_task = next;
-
printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid);
-
/* switch to new process */
-
asm volatile(
-
"pushl %%ebp\n\t" /* save ebp ,将原进程ebp压栈,ebp位于栈顶位置*/
-
"movl %%esp,%0\n\t" /* save esp ,将esp赋值给prev的sp,这一句会将当前进程的栈顶指针保存在sp中*/
-
"movl %2,%%esp\n\t" /* restore esp ,将next的sp赋值给esp,也就是将esp指向了新的栈*/
-
"movl %2,%%ebp\n\t" /* restore ebp ,初始化栈底指针。此时栈为空*/
-
"movl $1f,%1\n\t" /* save eip ,保存prev 的ip。*/
-
"pushl %3\n\t"
-
"ret\n\t" /* restore eip */
-
: "=m" (prev->thread.sp),"=m" (prev->thread.ip)
-
: "m" (next->thread.sp),"m" (next->thread.ip)
-
);
-
}
当state为0时,说明这个进程之前已经在运行了,此时可以继续执行,就切换到下一个进程。当下一个进程的state不为0时,那么也就是说下一个进程还从来都没有执行过,所以这一段内联汇编的作用是开始执行一个新进程。如此将会完成从一个process切换到另一个process的功能。
上述demo使用的时间片轮转的方式控制着进程的切换,这里主需要了解当进程进行切换时的堆栈的变化。
其中时间片轮转: my_timer_handler ,此函数注册在 arch/x86/kernel/time.c 中。
作者程大鹏, 转载请注明出处 http://blog.chinaunix.net/blog/post.html
Linux内核分析》MOOC课程 ”
阅读(1954) | 评论(0) | 转发(0) |