一。堆栈的基本原理
1.在linux C程序执行过程中,整个堆栈段都在随着数据的压栈、出栈而增长、消减。 堆栈是C语言程序运行时必须的一个记录调用路径和参数 的空间。并且各种编译器构成的堆栈都不一致。
2.堆栈的寄存器以及图解
需要注意的是,堆栈是从高地址向低地址增长的。每次压栈,都会讲esp指针下移4个字节(32位机是4个字节)
其他跟堆栈操作相关的寄存器:
cs : eip:总是指向下一条的指令地址
? 顺序执行:总是指向地址连续的下一条指令
? 跳转/分支:执行这样的指令的时候, cs : eip的值会 根据程序需要被修改
? call:将当前cs : eip的值压入栈顶, cs : eip指向被 调用函数的入口地址
? ret:从栈顶弹出原来保存在这里的cs : eip的值,放 入cs : eip中
3.堆栈的构造
堆栈构造时,寄存器的运行流程如下:
堆栈结束时
此时ret一般是从函数进行返回时有的操作。详见
linux内核学习 第一周
例子如下,我们把一段简单的代码进行编译,并且通过反汇编(objdump -S)来得到此段函数的汇编指令。
通过反汇编之后得到文件为:
对比上下代码即可看到,在调用g函数(
call
80483b4
<
g
>)后,g函数首先进行的是 push 与mov,这两条指令就构成了g 的栈底,知道g函数执行完ret 指令以后,g函数的栈就被清空,重新进入到main函数的栈中。也就是说,
函数的调用,本质上是堆栈的建立与销毁的过程。
二。进程调度与堆栈的简易实现
1.LinuxC中内嵌汇编代码的实现:
这是个简单的例子。第7行是内嵌汇编的格式。 8行:将立即数9赋值给eax寄存器。9行:将eax寄存器的值放入 参数1中,也就是下面第12行的temp, 10行:将参数2的值放入eax中,通过代码可以看到input的值被初始化成了4,第11行中,eax的值被付给了0号参数:output。
所以这个程序的输出结果为:9,4。
2.在内核中实现简易的进程调度程序,环境搭建可参考:https://github.com/jiamianshiye/mykernel 。
a.关于中断,中断指当出现需要时,CPU暂时停止当前程序的执行转而执行处理新情况的程序和执行过程。在系统层面上,你能够随意切换当前运行的程序,比如随时从QQ切换到浏览器,但是在这个切换的过程中,就会把指定的程序切换到当前进程中,如何切换呢,这个就得看中断以及进程调度的工作。中断是计算机的一大特点,否则的话,计算机运行程序时就得从头运行到尾,不能被打断。可见中断是一种可以人为参与(软件)或者硬件自动完成的,使CPU发生的一种程序跳转。在linux中,对于中断的处理分为:
第一步, 保护现场就是进入中断程序保存需要用到的寄存器的数据。
第二部,恢复 现场,就是退出中断程序恢复保存寄存器的数据。
b.根据mykernel的实验结果,内核中对于中断触发的处理,就可以通过上述的汇编指令来实现。
下面是代码解析,我们只对关键的一部分代码进行分析,其他的可以通过demo自己动手测试。
初始化部分:my_start_kernel函数
参考注释可以知道,这段代码构造0号pid 的堆栈空间,这样第一次运行时将直接进入到0号进程。
在my_schedule函数中,切换进程的汇编执行的功能可以参考注释:
当state为0时,说明这个进程之前已经在运行了,此时可以继续执行,就切换到下一个进程。当下一个进程的state不为0时,那么也就是说下一个进程还从来都没有执行过,所以这一段内联汇编的作用是开始执行一个新进程。如此将会完成从一个process切换到另一个process的功能。
上述demo使用的时间片轮转的方式控制着进程的切换,这里主需要了解当进程进行切换时的堆栈的变化。
其中时间片轮转: my_timer_handler ,此函数注册在 arch/x86/kernel/time.c 中。
作者程大鹏, 转载请注明出处 http://blog.chinaunix.net/blog/post.html
Linux内核分析》MOOC课程 http://mooc.study.163.com/course/USTC-1000029000 ”
1.在linux C程序执行过程中,整个堆栈段都在随着数据的压栈、出栈而增长、消减。 堆栈是C语言程序运行时必须的一个记录调用路径和参数 的空间。并且各种编译器构成的堆栈都不一致。
2.堆栈的寄存器以及图解
![](/attachment/201603/6/29798130_1457265916pglQ.png)
需要注意的是,堆栈是从高地址向低地址增长的。每次压栈,都会讲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
例子如下,我们把一段简单的代码进行编译,并且通过反汇编(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
二。进程调度与堆栈的简易实现
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;
- }
所以这个程序的输出结果为:9,4。
2.在内核中实现简易的进程调度程序,环境搭建可参考:https://github.com/jiamianshiye/mykernel 。
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*/
);
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*/
);
在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<<,prev->pid,next->pid);
- }
- else
- {
- next->state = 0;
- my_current_task = next;
- printk(KERN_NOTICE ">>>switch %d to %d<<,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)
- );
- }
上述demo使用的时间片轮转的方式控制着进程的切换,这里主需要了解当进程进行切换时的堆栈的变化。
其中时间片轮转: my_timer_handler ,此函数注册在 arch/x86/kernel/time.c 中。
作者程大鹏, 转载请注明出处 http://blog.chinaunix.net/blog/post.html
Linux内核分析》MOOC课程 http://mooc.study.163.com/course/USTC-1000029000 ”