linux内核分析(网课期末&地面课期中)

堆栈变化过程:html

Linux内核分析——计算机是如何工做的linux

计算机是如何工做的?(总结)——三个法宝git

 

 

 

  • 存储程序计算机工做模型,计算机系统最最基础性的逻辑结构;程序员

  • 函数调用堆栈,高级语言得以运行的基础,只有机器语言和汇编语言的时候堆栈机制对于计算机来讲并不那么重要,但有了高级语言及函数,堆栈成为了计算机的基础功能;github

    • enter  算法

      • pushl %ebpshell

      •  movl %esp,%ebp编程

    • leave  数据结构

      • movl %ebp,%esp架构

      • popl %ebp

    • 函数参数传递机制和局部变量存储

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

 

反汇编

 

 

 

  gcc -g  生成可执行文件

 

  

 

  objdump  -S  得到反汇编文件

 

  C语言内嵌汇编语言(模板):

 

      asm volatile(

 

        输出:

 

        输入:

 

        破坏描述部分

 

        );

 

1、Linux内核源代码介绍

 

一、根目录

 

arch/x86目录下的代码是咱们重点关注的,arch中包括支持不一样CPU的源代码。

 

init目录下包含内核启动相关的代码,如main.c(start_kernel函数至关于普通C程序的main函数,是Linux内核初始化的起点)。

 

ipc:进程间通讯

 

kernel:Linux内核的核心代码

 

关注readme文件

 

 

 

2、构造一个简单的Linux系统MenuOS

 

一、在实验楼环境下:

 

cd LinuxKernel/

 

qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img

 

便可启动内核,完成后进入menu程序,支持三个命令help、version和quit。

 

二、使用本身的Linux系统环境搭建MenuOS的过程

 

# 下载内核源代码编译内核

 

cd ~/LinuxKernel/

 

wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.18.6.tar.xz

 

xz -d linux-3.18.6.tar.xz

 

tar -xvf linux-3.18.6.tar(解压)

 

cd linux-3.18.6

 

make i386_defconfig

 

make # 通常要编译很长时间,少则20分钟多则数小时

 

 

 

# 制做根文件系统

 

cd ~/LinuxKernel/

 

mkdir rootfs

 

git clone https://github.com/mengning/menu.git  # 若是被墙,可使用附件menu.zip

 

cd menu

 

gcc -o init linktable.c menu.c test.c -m32 -static –lpthread(init是第一个用户态进程,是1号进程,采用的是静态编译的方式)

 

cd ../rootfs

 

cp ../menu/init ./

 

find . | cpio -o -Hnewc |gzip -9 > ../rootfs.img(img镜像文件)

 

 

 

# 启动MenuOS系统

 

cd ~/LinuxKernel/

 

qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img

 

三、从新配置编译Linux使之携带调试信息

 

(1)在原来配置的基础上,make menuconfig选中以下选项从新配置Linux,使之携带调试信息

 

kernel hacking—>

 

[*] compile the kernel with debug info

 

(2)make从新编译(时间较长)

 

 

 

四、使用gdb跟踪调试内核

 

qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S # 关于-s和-S选项的说明:

 

# -S freeze CPU at startup (use ’c’ to start execution)

 

# -s shorthand for -gdb tcp::1234 若不想使用1234端口,则可使用-gdb tcp:xxxx来取代-s选项

 

另开一个shell窗口

 

gdb

 

(gdb)file linux-3.18.6/vmlinux # 在gdb界面中target remote以前加载符号表    file home/shiyanlou/LinuxKernel/vmlinux

 

(gdb)target remote:1234 # 创建gdb和gdbserver之间的链接,按c 让qemu上的Linux继续运行

 

(gdb)break start_kernel # 断点的设置能够在target remote以前,也能够在以后

Linux内核分析实验二:mykernel实验指导(操做系统是如何工做的)

 Linux内核分析实验三----跟踪分析Linux内核的启动过程

 

1、用户态、内核态

  权限分级——为了系统自己更稳定,使系统不宜崩溃。(并非全部程序员缩写的代码都很健壮!!)

  x86 CPU四种不一样的执行级别:0(内核态)—3(用户态)

  区分方法:CS:EIP(CPU的每条指令都是经过这里执行)(代码段选择寄存器:偏移量寄存器)

       CS寄存器的最低两位代表了代码级别——逻辑地址(不是物理地址)

       内核态:能够访问4G地址空间的任意地址    用户态:0x00000000—0xbfffffff空间内的地址

2、中断——从用户态进入内核态

  系统调用是一种特殊的中断(用户态进程与硬件设备的接口)

  切换时就会涉及到上下文的问题?

       即中断int指令在堆栈上会保存一些值:1.用户态栈顶地址  2.当时状态字  3.当时CS:EIP的值

        save—cs:eip  ss:eip  eflag  

        load—cs:eip  ss:eip

  过程:1.SAVE_ALL——保存现场

      判断是否有进程调度?

     (无进程调度)last.RESTORE_ALL+iret(popl cs:eip/ss:eip/efalg)

     (有进程调度)last.暂时保存当前的值直到该进程再次被调度

3、API——应用程序编程接口(不一样于系统调用)

  • API是一个函数定义
  • 系统调用请求一个软中断
  • 触发系统调用经过传递系统调用号实现(压栈传递参数)

 

Linux内核分析实验四

 

1、给MenuOS增长time和time-asm命令

 

1. 克隆并自动编译MenuOS

 

rm menu -rf强制删除原menu文件

 

git clone http:

 

makecd menurootfs运行自动编译脚本,生成根文件系统,启动MenuOS

 

2. 给MenuOS增长time和time-asm命令

 

更新menu代码到最新版

 

test.c中main函数里,增长MenuConfig

 

增长对应的两个函数:Time和TimeAsm

 

make rootfs

2、调试内核

1. 使用gdb跟踪调试内核

-3.18.6/arch//boot/qemu -kernel linuxx86bzImage -initrd rootfs.img -s -S
gdb
-3.18.6/vmlinux   (gdb)file linux
remote:1234 (gdb)target

b sys_time:在系统调用time的位置设置断点

c:在MenuOs里使用time,停在断点处

s 单步调试—一步一步执行(PS:没法执行time_asm中的int 0X80部分)

3、系统调用在内核代码中的处理过程

1. 系统调用在内核代码中的工做机制和初始化

  • main.c中start_kernel函数:trap_init()
  • set_system_trap_gate(SYSCALL_VECTOR,&system_call)
    • SYSCALL_VECTOR:系统调用的中断向量
    • &system_call:汇编代码入口
  • 一执行int 0x80,系统直接跳转到system_call。

2. 系统调用——一个特殊的中断          

 

SAVE_ALL:保存现场

call *sys_call_table(,%eax,4)调用了系统调度处理函数,eax存的是系统调用号,是实际的系统调度程序。

sys_call_table:系统调用分派表

syscall_after_all:保存返回值

         如有sys_exit_work,则进入sys_exit_work:会有一个进程调度时机。

      work_pending -> work_notifysig,用来处理信号

可能call schedule:进程调度代码

可能跳转到restore_all,恢复现场。

若无sys_exit_work,就执行restore_all恢复,返回用户态。

INTERRUPT_RETURN <=> iret,结束。

- - - 在系统调用返回以前,可能发生进程调度,进程调度里就会出现进程上下文的切换。进程间通讯可能有信号须要处理。能够将内核视为一系列中断指令的集合。

Linux内核分析实验五

进程控制块PCB——task_struct(进程描述符)

 

为了管理进程,内核必须对每一个进程进行清晰的描述,进程描述符提供了内核所需了解的进程信息。

struct task_struct数据结构很庞大

  • Linux进程的状态与操做系统原理中的描述的进程状态彷佛有所不一样,好比就绪状态和运行状态都是TASK_RUNNING,为何呢?

    TASK_RUNN有没有在CPU上执行决定他是就绪仍是运行状态。

        和操做系统相似:就绪态,运行态,阻塞态

进程的标示pid

    SMP条件编译器

链表代码——双向链表

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Linux内核分析实验六

1、获得一个可执行程序

1. 预处理、编译、连接

gcc hello.c -o hello.exe
  • gcc编译源代码生成最终可执行的二进制程序,GCC后台隐含执行了四个阶段步骤。

    预处理 => 编译 => 汇编 => 连接
  • 预处理:编译器将C源代码中包含的头文件编译进来和执行宏替换等工做。

    gcc -E hello.c -o hello.i
  • 编译:gcc首先要检查代码的规范性、是否有语法错误等,以肯定代码的实际要作的工做,在检查无误后,gcc把代码翻译成汇编语言。

    gcc –S hello.i –o hello.s
    -S:该选项只进行编译而不进行汇编,生成汇编代码。
  • 汇编:把编译阶段生成的.s文件转成二进制目标代码.

    gcc –c hello.s –o hello.o
  • 连接:将编译输出.o文件连接成最终的可执行文件。

    gcc hello.o –o hello
  • 运行:若连接没有-o指明,则生成可执行文件默认为a.out

    ./hello

  

2. 目标文件格式

    a.out——COFF——ELF(Linux)或PE(Windows)

  重点介绍ELF——三种重要格式文件

  • 可重定位文件:保存着代码和适当的数据,用来和其余的object文件一块儿来建立一个可执行文件或者是一个共享文件。
  • 可执行文件:保存着一个用来执行的程序;该文件指出了exec(BA_OS)如何来建立程序进程映象。
  • 共享文件:保存着代码和合适的数据,用来被下面的两个连接器连接。
    • 第一个是链接编辑器[请参看ld(SD_CMD)],能够和其余的可重定位和共享object文件来建立其余的object。
    • 第二个是动态连接器,联合一个可执行文件和其余的共享object文件来建立一个进程映象。
  • object文件参与程序的连接(建立)和执行。

3. 静态连接的ELF可执行文件和进程的地址空间

 

  • 入口点:程序从0x804800开始。
  • 可执行文件加载到内存中开始执行的第一行代码。
  • 通常静态连接将会把全部代码放在同一个代码段。
  • 动态链接的进程会有多个代码段。

2、可执行程序的执行环境

1. 命令行参数和shell环境

  • 列出/usr/bin下的目录信息

    $ ls -l /usr/bin
  • Shell自己不限制命令行参数的个数,命令行参数的个数受限于命令自身

    int main(int argc, char *argv[], char *envp[])
  • Shell会调用execve将命令行参数和环境参数传递给可执行程序的main函数

    int execve(const char * filename,char * const argv[ ],char * const envp[ ]);
  • 库函数exec*都是execve的封装例程

2. 命令行参数和shell环境变量的保存与传递

shell程序 => execve => sys_execve
  • 命令行参数和环境串都放在用户态堆栈中
  • 初始化新程序堆栈时拷贝进去

3. 可执行程序动态连接

(1)动态连接

  • 关注:load_elf_binary

    load_elf_binary(...)
    {
        ...
        kernel_read();//其实就是文件解析
        ...
        //映射到进程空间 0x804 8000地址
        elf_map();//
        ...
        if(elf_interpreter) //依赖动态库的话
        {
            ...
            //装载ld的起点  #得到动态链接器的程序起点
            elf_entry=load_elf_interp(...);
            ...
        }
        else //静态连接
        {
            ...
            elf_entry = loc->elf_ex.e_entry;
            ...
        }
        ...
        //static exe: elf_entry: 0x804 8000
        //exe with dyanmic lib: elf_entry: ld.so addr
        start_thread(regs,elf_entry,bprm->p);
    }
  • 实际上,装载过程是一个广度遍历,遍历的对象是“依赖树”。
  • 主要过程是动态连接器完成、用户态完成。

(2)装载时动态连接

/*准备.so文件*/     
shlibexample.h (1.3 KB) - Interface of Shared Lib Example
shlibexample.c (1.2 KB) - Implement of Shared Lib Example

/*编译成libshlibexample.so文件*/
$ gcc -shared shlibexample.c -o libshlibexample.so -m32

/*使用库文件(由于已经包含了头文件因此能够直接调用函数)*/
SharedLibApi();

(3)运行时动态连接

dllibexample.h (1.3 KB) - Interface of Dynamical Loading Lib Example
dllibexample.c (1.3 KB) - Implement of Dynamical Loading Lib Example

/*编译成libdllibexample.so文件*/
$ gcc -shared dllibexample.c -o libdllibexample.so -m32

/*使用库文件*/
void * handle = dlopen("libdllibexample.so",RTLD_NOW);//先加载进来
int (*func)(void);//声明一个函数指针
func = dlsym(handle,"DynamicalLoadingLibApi");//根据名称找到函数指针
func(); //调用已声明函数

(4)运行

$ gcc main.c -o main -L/path/to/your/dir -lshlibexample -ldl -m32
$ export LD_LIBRARY_PATH=$PWD 
/*将当前目录加入默认路径,不然main找不到依赖的库文件,固然也能够将库文件copy到默认路径下。*/

3、可执行程序的装载

1. sys_execve内核处理过程

(1)新的可执行程序起点

  • 通常是地址空间为0x8048000或0x8048300

(2)execve与fork

execve和fork都是特殊一点的系统调用:通常的都是陷入到内核态再返回到用户态。
  • fork两次返回,第一次返回到父进程继续向下执行,第二次是子进程返回到ret_from_fork而后正常返回到用户态。

  • execve执行的时候陷入到内核态,用execve中加载的程序把当前正在执行的程序覆盖掉,当系统调用返回的时候也就返回到新的可执行程序起点。

execve
- 执行到可执行程序 -> 陷入内核
- 构造新的可执行文件 -> 覆盖掉原可执行程序
- 返回到新的可执行程序,做为起点(也就是main函数)
- 须要构造其执行环境;
  • Shell会调用execve将命令行参数和环境参数传递给可执行程序的main函数,先函数调用参数传递,再系统调用参数传递。

(3)静态连接的可执行程序和动态连接的可执行程序execve系统调用返回时不一样

  • 静态连接:elf_entry指向可执行文件的头部,通常是main函数,是新程序执行的起点。
  • 动态连接:elf_entry指向ld(动态连接器)的起点,加载load_elf_interp

2. 庄周梦蝶

庄周(调用execve的可执行程序)入睡(调用execve陷入内核),醒来(系统调用execve返回用户态)发现本身是蝴蝶(被execve加载的可执行程序)。

3. 动态连接的可执行程序的装载

(1)可执行文件开始执行的起点在哪里?如何才能让execve系统调用返回到用户态时执行新程序?

  • 修改int 0x80压入内核堆栈的EIP,经过修改内核堆栈中EIP的值做为新程序的起点。

(2)Linux内核是如何支持多种不一样的可执行文件格式

static struct linux_binfmt elf_format//声明一个全局变量 = {
.module     = THIS_MODULE,
.load_binary    = load_elf_binary,//观察者自动执行
.load_shlib = load_elf_library,
.core_dump  = elf_core_dump,
.min_coredump   = ELF_EXEC_PAGESIZE,
};

static int __iit init_elf_binfmt(void)
{n
    register_binfmt(&elf_format);//把变量注册进内核链表,在链表里查找文件的格式
    return 0;
}

(3)动态连接

  • 可执行程序须要依赖动态连接库,而这个动态连接库可能会依赖其余的库,这样造成了一个关系图——动态连接库会生成依赖树。
  • 依赖动态连接器进行加载库并进行解析(这就是一个图的遍历),装载全部须要的动态连接库;以后ld将CPU的控制权交给可执行程序
  • 动态连接的过程主要是动态连接器在起做用,而不是内核完成的。

LINUX内核分析第七周——可执行程序的装载

1、进程调度与进程调度的时机分析

一、不一样类型的进程有不一样的调度需求

Linux既支持普通的分时进程,也支持实时进程。

Linux中的调度是多种调度策略和调度算法的混合。

二、调度策略:是一组规则,它们决定何时以怎样的方式选择一个新进程运行。

Linux的调度基于分时和优先级。

三、内核中的调度算法相关代码使用了相似OOD的策略模式。

四、进程调度的时机

(1)中断处理过程当中,schedule函数实现调度:

    中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule()。

(2)用户态进程只能被动调度

    用户态进程没法实现主动调度,仅能经过陷入内核态后的某个时机点进行调度,即在中断处理过程当中进行调度。

(3)内核线程是只有内核态没有用户态的特殊进程

    内核线程能够直接调用schedule()进行进程切换,也能够在中断处理过程当中进行调度,也就是说内核线程做为一类的特殊的进程能够主动调度,也能够被动调度。

 

2、进程上下文切换相关代码分析

一、为了控制进程的执行,内核必须有能力挂起正在CPU上执行的进程,并恢复之前挂起的某个进程的执行,这叫作进程切换、任务切换、上下文切换。

二、挂起正在CPU上执行的进程,与中断时保存现场是不一样的,中断先后是在同一个进程上下文中,只是由用户态转向内核态执行;进程上下文的切换是两个进程的切换。

三、进程上下文包含了进程执行须要的全部信息

(1)用户地址空间:包括程序代码,数据,用户堆栈等。

(2)控制信息:进程描述符,内核堆栈等。

(3)硬件上下文(注意中断也要保存硬件上下文只是保存的方法不一样)。

四、schedule()函数选择一个新的进程来运行,并调用context_switch进行上下文的切换,这个宏调用switch_to来进行关键上下文切换。

next = pick_next_task(rq, prev);//进程调度算法、策略都封装这个函数内部

context_switch(rq, prev, next);//完成进程上下文切换

switch_to利用了prev和next两个参数:prev指向当前进程,next指向被调度的进程。

3、Linux系统的通常执行过程分析

一、最通常的状况:正在运行的用户态进程X切换到运行用户态进程Y的过程

(1)正在运行的用户态进程X

(2)发生中断——save cs:eip/esp/eflags(current) to kernel stack,then load cs:eip(entry of a specific ISR) and ss:esp(point to kernel stack).

(3)SAVE_ALL //保存现场

(4)中断处理过程当中或中断返回前调用了schedule(),其中的switch_to作了关键的进程上下文切换

(5)标号1以后开始运行用户态进程Y(这里Y曾经经过以上步骤被切换出去过所以能够从标号1继续执行)

(6)restore_all //恢复现场

(7)iret - pop cs:eip/ss:esp/eflags from kernel stack

(8)继续运行用户态进程Y

二、关键:中断上下文的切换和进程上下文的切换

 

4、Linux系统执行过程当中的几个特殊状况

一、几种特殊状况

(1)经过中断处理过程当中的调度时机,用户态进程与内核线程之间互相切换和内核线程之间互相切换,与最通常的状况很是相似,只是内核线程运行过程当中发生中断没有进程用户态和内核态的转换,CS段没有变化;

(2)内核线程主动调用schedule(),只有进程上下文的切换,没有发生中断上下文的切换,与最通常的状况略简略;

(3)建立子进程的系统调用在子进程中的执行起点next_ip = ret_from_fork及返回用户态,如fork;

(4)加载一个新的可执行程序后返回到用户态的状况,如execve。

二、X86_32位系统下,每一个进程的地址空间4G。0-3G用户态,3G-4G仅内核态。

全部的进程3G以上的部分是共享的。

内核是各类中断处理过程和内核线程的集合。

 

5、Linux操做系统架构概览

一、操做系统的基本概念

二、典型的Linux操做系统的结构

 

6、最简单也是最复杂的操做---执行ls命令

 

7、从CPU和内存的角度看Linux系统的执行

一、从CPU的角度

0xc0000000如下是3G的部分,用户态。

(1)c=gets();系统调用,陷入内核态,将eip/esp/cs/ds等信息压栈。

(2)进程管理:等待键盘敲入指令。

(3)中断处理:在键盘上敲击ls发生I/O中断。

进程x陷入内核态后没有内容执行变成阻塞态,发生I/O中断后变成就绪态。

(4)系统调用返回。

二、从内存的角度

Linux内核分析实验八------理解进程调度时机跟踪分析进程调度与

相关文章
相关标签/搜索