《Linux内核分析》课程第二周学习总结

---恢复内容开始---

 

 姓名:何伟钦  

学号:20135223

( *原创作品转载请注明出处*)

              ( 学习课程:《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000 

 

学习内容:通过汇编一个简单的C程序,分析汇编代码理解计算机是如何工作的

 

第一部分:教学笔记总结

一、计算机是如何工作的(总结)——三个法宝

(一)存储程序计算机工作模型,计算机系统最最基础性的逻辑结构。

(二)中断机制,多道程序操作系统的基点,没有中断机制程序只能从头一直运行结束才有可能开始运行其他程序。

(三)函数调用堆栈,高级语言得以运行的基础,只有机器语言和汇编语言的时候堆栈机制对于计算机来说并不那么重要,但有了高级语言及函数,堆栈成为了计算机的基础功能;函数参数传递机制和局部变量存储。

《Linux内核分析》课程第二周学习总结_第1张图片

 

 

二、函数调用堆栈

(一)堆栈

1、堆栈的元素:

  • 函数调用框架
  • 传递参数
  • 保存返回地址
  • 提供局部变量空间

2、堆栈相关寄存器:

esp:堆栈指针(stack pointer),指向系统栈最上面一个栈帧的栈顶

ebp: 基址指针(base pointer),指向系统栈最上面一个栈帧的底部

cs:eip:指令寄存器(extended instruction pointer),指向下一条等待执行的指令地址

ebp在C语言中用作记录当前函数调用基址。

3. 堆栈操作

push:以字节为单位将数据(对于32位系统可以是4个字节)压入栈,从高到低按字节依次将数据存入ESP-1、ESP-2、ESP-3、ESP-4的地址单元。

pop: 过程与push相反。

call: 用来调用一个函数或过程,此时,下一条指令地址会被压入堆栈,以备返回时能恢复执行下条指令。

 (将下一条指令的地址A保存在栈顶, 设置eip指向被调用程序代码开始处)

leave:当调用函数调用时,一般都有这两条指令pushl %ebpmovl %esp,%ebp,leave是这两条指令的反操作。

ret: 从一个函数或过程返回,之前call保存的下条指令地址会从栈内弹出到EIP寄存器中,程序转到CALL之前下条指令处执行。

push %eax    相当于 subl $4 ,%esp;

                             movl %eax ,(%esp)

pop %eax     相当于 movl (%esp),%eax;

                             addl $4, %esp

call 0x12345 相当于 push %eip(*);

                             movl $0x12345,%eip(*)

ret               相当于  popl %eip(*)

enter            相当于 push %ebp

                            movl %esp,%ebp

leave            相当于 movl %ebp,%esp ;

                             pop %ebp

 《Linux内核分析》课程第二周学习总结_第2张图片

三、参数传递与框架

(一)函数的堆栈框架

(1)建立框架(相当于执行call function)

push %ebp
movl %esp,%ebp
cs:eip原来的值指向call下一条指令,该值被保存到栈顶
cs:eip的值指向function的入口地址

(2)进入function

pushl %ebp //意为保存调用者的栈帧地址
movl %esp, %ebp //初始化function的栈帧地址

(3)拆除框架(相当于退出function)

movl %ebp,%esp
pop %ebp
ret

传递参数在建立子函数的框架之前,局部变量的值保存在调用者堆栈框架中,所以在子函数框架建立之前可以采用变址寻址的方式将变量值入栈。函数的返回值通过eax寄存器传递

《Linux内核分析》课程第二周学习总结_第3张图片《Linux内核分析》课程第二周学习总结_第4张图片

 

(二)参数传递例子

   1.二级调用

《Linux内核分析》课程第二周学习总结_第5张图片

《Linux内核分析》课程第二周学习总结_第6张图片《Linux内核分析》课程第二周学习总结_第7张图片

 

 

2.三级调用

2《Linux内核分析》课程第二周学习总结_第8张图片《Linux内核分析》课程第二周学习总结_第9张图片

 

四、在C语言中嵌入汇编代码

 

常用限制字符
分类 限定符 描述
通用寄存器 “a” 将输入变量放入eax
“b” 将输入变量放入ebx
“c” 将输入变量放入ecx
“d” 将输入变量放入edx
“s” 将输入变量放入esi
“d” 将输入变量放入edi
“q” 将输入变量放入eax,ebx,ecx,edx中的一个
“r” 将输入变量放入通用寄存器,也就是eax,ebx,ecx,edx,esi,edi中的一个
“A” 把eax和edx合成一个64 位的寄存器(use long longs)
内存 “m” 内存变量
“o” 操作数为内存变量,但是其寻址方式是偏移量类型,也即是基址寻址,或者是基址加变址寻址
“V” 操作数为内存变量,但寻址方式不是偏移量类型
“ ” 操作数为内存变量,但寻址方式为自动增量
“p” 操作数是一个合法的内存地址(指针)
寄存器或内存 “g” 将输入变量放入eax,ebx,ecx,edx中的一个或者作为内存变量
“X” 操作数可以是任何类型
立即数 “I” 0-31之间的立即数(用于32位移位指令)
“J” 0-63之间的立即数(用于64位移位指令)
“N” 0-255之间的立即数(用于out指令)
“n” 立即数
“p” 立即数,有些系统不支持除字以外的立即数,这些系统应该使用“n”而不是“i”
匹配 & 该输出操作数不能使用过和输入操作数相同的寄存器
操作数类型 “=” 操作数在指令中是只写的(输出操作数)
“+” 操作数在指令中是读写类型的(输入输出操作数)
浮点数 “f” 浮点寄存器
“t” 第一个浮点寄存器
“u” 第二个浮点寄存器
“G” 标准的80387浮点常数
其它 % 该操作数可以和下一个操作数交换位置
# 部分注释,从该字符到其后的逗号之间所有字母被忽略
* 表示如果选用寄存器,则其后的字母被忽略

 

 

 例一:

《Linux内核分析》课程第二周学习总结_第10张图片

 

分析与说明:

 

第 9 行:将eax的值置为0。

 

第10行:将%1(val1)中的值加在eax中,eax=1。

 

第11行:将%2(val2)中的值加在eax中,eax=3。

 

第12行:将eax中的值3赋给%0(val3)。

 

第13行:输出部分:val3只写。(=操作数在指令中是只写的(输出操作数),m表示内存变量)

第14行:输入部分:val1放入%ecx,val2放入%edx。

运行结果: 第一个printf:val1=1,val2=2,val3=0

           第二个printf:val1=1,val2=2,val3=3

例二:

《Linux内核分析》课程第二周学习总结_第11张图片

 

分析与说明:

   (" r"表示将输入变量放入通用寄存器,也就是eax,ebx,ecx,edx,esi,edi中的一个)

   (=操作数在指令中是只写的(输出操作数),m表示内存变量)

第6行:将eax的值置为0。

第7行:把eax中的0赋给%1(temp)。

第8行:把%2(input)的值1赋给eax。

第9行:把eax赋给%0(output)。

最终结果:temp=0,output=1

 

《Linux内核分析》课程第二周学习总结_第12张图片

《Linux内核分析》课程第二周学习总结_第13张图片

《Linux内核分析》课程第二周学习总结_第14张图片

《Linux内核分析》课程第二周学习总结_第15张图片

 

第二部分:借助Linux内核部分源代码模拟存储程序计算机工作模型及时钟中断

 

 

 

  • 使用实验楼的虚拟机启动mykernel

    cd LinuxKernel/linux-3.9.4 qemu -kernel arch/x86/boot/bzImage
  • 在QEMU窗口,不停的输出字符串:

从QEMU窗口可知:每执行my_ start_ kernel函数一次或两次,my_ time_ hander函数就执行一次。

 

 

简单的操作系统模拟

mykernel系统启动后,调用my_start_kernel函数,周期性的调用my_timer_handler函数,完成了系统进程的初始化和进程的轮转调度,如下分析

(一)mymain.c系统中唯一的进程:代码完成的工作是每循环10000次,打印一句话。在mymain.c的my_start_kernel函数中有一个循环,不停的输出my_start_kernel here

《Linux内核分析》课程第二周学习总结_第16张图片

 

(二)myinterrupt.c——时间中断处理程序:每执行一次,都会执行一次时钟中断,每次时钟中断都调用printk并输出

   在myinterrupt.c中,可以看到一个会被时钟中断周期调用的函数my_timer_handler ,在这个函数里,输出                           >>>>>my_timer_handler here <<<<<

《Linux内核分析》课程第二周学习总结_第17张图片

 

第三部分:实验与作业

完成一个简单的时间片轮转多道程序内核代码

(一)mypcb.h代码

  • 本文件中定义了Thread结构体,用于存储当前进程中正在执行的线程的eip和esp。
  • PCB结构体中:
    • pid:进程号
    • state:进程状态,在模拟系统中,所有进程控制块信息都会被创建出来,其初始化值就是-1,如果被调度运行起来,其值就会变成0
    • stack:进程使用的堆栈
    • thread:当前正在执行的线程信息
    • task_entry:进程入口函数(就像一般我们用的进程定义的是main)
    • next:指向下一个PCB,模拟系统中所有的PCB是以链表的形式组织起来的。
  • 函数的声明my_schedule:调度器。它在my_interrupt.c中实现,在mymain.c中的各个进程函数会根据一个全局变量的状态来决定是否调用它,从而实现主动调度。
#define MAX_TASK_NUM        4
#define KERNEL_STACK_SIZE   1024*8

/* CPU-specific state of this task */
struct Thread {
    unsigned long       ip;//保存eip
    unsigned long       sp;//保存esp
};

typedef struct PCB{//用于表示一个进程,定义了进程管理相关的数据结构
    int pid;
    volatile long state;    /* 定义进程的状态:-1 不可运行, 0 可运行, >0 停止 */
    char stack[KERNEL_STACK_SIZE];
    //内核堆栈
    struct Thread thread;
    unsigned long   task_entry; //指定进程入口
    struct PCB *next;//进程链表
}tPCB;

void my_schedule(void);//调用了my_schedule,表示调度器

(二)mymain.c代码:对内核进行初始化

系统启动后先调用函数my_start_kernel,完成了0号进程的初始化和启动;

   my_process函数:在模拟系统里,每个进程的函数代码都是一样的。

  my_process 在执行时,打印出当前进程的id,能够看到当前哪个进程正在执行。

   每循环100000次检查全局标志变量my_need_sched判断是否需要调度,一旦发现其值为1,就调用my_schedule完成进程的调度。

#include <linux/types.h>
#include <linux/string.h>
#include <linux/ctype.h>
#include <linux/tty.h>
#include <linux/vmalloc.h>

#include "mypcb.h"

tPCB task[MAX_TASK_NUM];
tPCB * my_current_task = NULL;
volatile int my_need_sched = 0;//定义一个标志,用来判断是否需要调度

void my_process(void);


void __init my_start_kernel(void)
{
    int pid = 0;//初始化一个进程0
    int i;
    /* Initialize process 0(初始化0号进程)*/
    task[pid].pid = pid;
    task[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped */
    task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process;
    //定义进程0的入口为my_process
    task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1];
    task[pid].next = &task[pid];
    //因为一开始系统里只有进程0,所以这一行代码表示的是pid的next还是指向自己
    /*fork more process */
    //创建更多其他的进程,在初始化这些进程的时候可以直接拷贝0号进程的代码
    for(i=1;i<MAX_TASK_NUM;i++)
    {
        memcpy(&task[i],&task[0],sizeof(tPCB));
        task[i].pid = i;
        task[i].state = -1;
        task[i].thread.sp = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-1];
        //每个进程都有自己的堆栈,把创建好的新进程放到进程列表的尾部,这样就完成了创建
        task[i].next = task[i-1].next;
        task[i-1].next = &task[i];
    }
    /* start process 0 by task[0] */
    pid = 0;
    my_current_task = &task[pid];
    asm volatile(//%0表示参数thread.ip,%1表示参数thread.sp。
        "movl %1,%%esp\n\t"     /* set task[pid].thread.sp to esp 把参数thread.sp放到esp中*/
        "pushl %1\n\t"          /* push ebp 由于当前栈是空的,esp与ebp指向相同,所以等价于push ebp*/
        "pushl %0\n\t"          /* push task[pid].thread.ip */
        "ret\n\t"               /* pop task[pid].thread.ip to eip */
        "popl %%ebp\n\t"
        : 
        : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp)   
        /* input c or d mean %ecx/%edx*/
    );
}
    /*  %0表示参数thread.ip,%1表示参数thread.sp。
        movl %1,%%esp表示把参数thread.sp放到esp中;
        接下来push %1,又因为当前栈为空,esp=ebp,所以等价于push ebp;
        然后push thread.ip;ret等价于pop thread.ip;最后pop ebp  */ 

void my_process(void)//定义所有进程的工作,if语句表示循环1000万次才有机会判断是否需要调度。
{
    int i = 0;
    while(1)
    {
        i++;
        if(i%100000 == 0)
        {
            printk(KERN_NOTICE "this is process %d -\n",my_current_task->pid);
            if(my_need_sched == 1)
            {
                my_need_sched = 0;
                my_schedule();
            }
            printk(KERN_NOTICE "this is process %d +\n",my_current_task->pid);
        }     
    }
}

(三)myinterrupt.c代码(my_timer_handler当时钟中断发生1000次,并且my_need_sched不等于1时,把my_need_sched赋为1。当进程发现my_need_sched=1时,就会执行my_schedule。

#include <linux/types.h>
#include <linux/string.h>
#include <linux/ctype.h>
#include <linux/tty.h>
#include <linux/vmalloc.h>

#include "mypcb.h"

extern tPCB task[MAX_TASK_NUM];
extern tPCB * my_current_task;
extern volatile int my_need_sched;
volatile int time_count = 0;

/*
 * Called by timer interrupt.
 * it runs in the name of current running process,
 * so it use kernel stack of current running process
 */
void my_timer_handler(void)
{
#if 1
    if(time_count%1000 == 0 && my_need_sched != 1) //设置时间片的大小,时间片用完时设置一下调度标志。当时钟中断发生1000次,并且my_need_sched!=1时,把my_need_sched赋为1。当进程发现my_need_sched=1时,就会执行my_schedule。
    {
        printk(KERN_NOTICE ">>>my_timer_handler here<<<\n");
        my_need_sched = 1;
    } 
    time_count ++ ;  
#endif
    return;     
}

void my_schedule(void)
{
    tPCB * next; //下一个进程 
    tPCB * prev; //当前进程

    if(my_current_task == NULL //task为空,即发生错误时返回
        || my_current_task->next == NULL)
    {
        return;
    }
    printk(KERN_NOTICE ">>>my_schedule<<<\n");
    /* schedule */
    next = my_current_task->next; //将当前进程的下一个进程赋给next
    prev = my_current_task;//当前进程为prev
    if(next->state == 0)/* -1 unrunnable, 0 runnable, >0 stopped */
    {
        //在两个正在执行的进程之间做上下文切换
        asm volatile(   
            "pushl %%ebp\n\t"       /* 保存当前进程的ebp */
            "movl %%esp,%0\n\t"     /* 保存当前进程的esp */
            "movl %2,%%esp\n\t"     /* 重新记录要跳转进程的esp,将下一进程中的sp放入esp中 */
            "movl $1f,%1\n\t"       /* $1f指标号1:的代码在内存中存储的地址,即保存当前的eip */    
            "pushl %3\n\t"  //将下一进程的eip压入栈,%3为 next->thread.ip
            "ret\n\t"               /* 记录要跳转进程的eip */
            "1:\t"                  /* 下一个进程开始执行 */
            "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  /*  与上一段代码不同的是如果下一个进程为新进程时,就运用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"       /* 保存当前进程的ebp */
            "movl %%esp,%0\n\t"     /* 保存当前进程的esp */
            "movl %2,%%esp\n\t"     /* 重新记录要跳转进程的esp */
            "movl %2,%%ebp\n\t"     /* 重新记录要跳转进程的ebp */
            "movl $1f,%1\n\t"       /* 保存当前eip  */  
            "pushl %3\n\t" 
            "ret\n\t"               /* 重新记录要跳转进程的eip */
            : "=m" (prev->thread.sp),"=m" (prev->thread.ip)
            : "m" (next->thread.sp),"m" (next->thread.ip)
        );          
    }   
    return; 
}

 

视频中的运行结果:

《Linux内核分析》课程第二周学习总结_第18张图片

结论:操作系统内核程序在0、1、2、3号进程循环交替切换执行

第四部分:学习总结

(一)操作系统工作的基础:存储程序计算机、堆栈(函数调用堆栈)机制和中断机制;

1、存储程序计算机:将根据特定问题编写的程序放在计算机存储器中,然后按照存储器的存储程序首地址执行程序的第一条指令,以后就按照该程序的规定顺序执行其他指令,直至程序结束执行。

2、堆栈(函数调用堆栈)机制:用户程序是一个进程,一个进程在执行过程中需要调用其他的进程或者是内核线程来完成功能,在函数调用的过程中就需要用到堆栈,通过栈基址指针EBP,栈顶指针ESP,以及保存CPU下一条指令的EIP寄存器来完成操作。当调用函数function的时候,系统根据EIP寄存器里存储的地址,CPU可以明确得出下一步应该做什么;EBP中的栈底地址是由ESP在函数调用之前就传过来的,等到函数调用后,EBP会把这个地址回传给ESP,也就是ESP又一次指向了函数调用结束后的栈顶指针,函数调用返回。

3、中断机制:CPU由于内部或者外部事件或者是由程序预先设定的事件引起的CPU暂停当前工作而转到别的服务中的过程称为中断。上半部分主要的功能是:当一个中断发生的时候,它就会把设备驱动程序中中断例程的下半部挂到该设备的下半部执行队列中去,然后等待,相当于做的是一种“中断登记”的任务,由于它完全屏蔽其他中断,所以它需要运行速度很快,要求尽可能多的对设备产生的中断进行服务处理;下半部分主要的功能是:下半部分不同于上半部分,它是可以被中断的,也就是常说的中断嵌套,它一般负责的工作时查看设备获得产生中断的时间信息,并根据这些信息进行相应的处理。同一个中断处理程序是不能并行的,也就是说一个中断处理程序在运行的时候,相同设备的中断是不能被嵌套的。

(二)Linux是一个多进程的操作系统,所以,其他的进程必须等到正在运行的进程空闲CPU后才能运行。当正在运行的进程等待其他的系统资源时,Linux内核将取得CPU的控制权,并将CPU分配给其他正在等待的进程,这就是进程切换。内核中的调度算法决定将CPU分配给哪一个进程。

(三)进程是正在运行的程序的一种抽象,它的工作路径是:用户态-内核态-用户态 反复循环,虽然途中可能会发生进程的切换,但是它总的工作方式不会改变,下面我们来具体总结下这过程的5个阶段:

第一阶段:进程从用户态切换到内核态

第二阶段:中断处理        

第三阶段:进程切换        

第四阶段: 处理挂起信号、虚拟模式等任务          

第五阶段: 从内核态返回用户态

 

学习体会:

操作系统是如何工作的,从大范围来说是比较容易理解的,但这周学习结合一个简单的时间片轮转多道程序内核代码分析进程的启动和进程的切换机制对于我来说就比较吃力,理解代码需要很长一段时间,而且还有很多地方是不懂的。

你可能感兴趣的:(《Linux内核分析》课程第二周学习总结)