Linux内核分析
一:内核的五大结构

二:中断工作流程
1、ARM回忆
1.做CPU工作模式的转化
2.进行寄存器的拷贝与压栈
3.设置中断向量表
4.保存正常运行的函数返回值
5.跳转到对应的中断服务函数上运行
6.进行模式的复原及寄存器的复原
7.跳转回正常工作的函数地址继续运行
2、linux中中断的工作流程
1.将所有寄存器值入栈
2.将异常吗入栈(中断号)
3.将当前函数的返回地址入栈
4.调用中断函数
5.返回地址出栈
6.寄存器值出栈
3.中断源码
中断前后的处理 中断的执行
硬件中断的处理过程 asm.s trap.c
软件及系统调用的处理过程 system_call.s fork.c/signal.c/exit.c/sys.c
1、asm.s代码及trap.c分析 (OPENING)
2、system_call.s代码及fork.c/signal.c/exit.c/sys.c分析
1、fork.c
在system_call.s内有存在fork的系统调用,先call _find_empty_process,然后call _copy_process
1 | .align 2 |
1 |
|
1、在task链表中找一个进程空位存放
2、创建一个task_struct
3、设置task_struct
2、signal.c
这里只是进行一个简单的分析,详细分析请见第五章
1 |
|
1 | // Line 12 |
3、exit.c
1 |
|
do_exit()
- 释放进程的代码段和数据段占用的内存
- 关闭进程打开的所有文件,对当前目录和i节点进行同步(文件操作)
- 如果当前要销毁的进程有子进程,就让1号进程作为新的父进程
- 如果当前进程是一个会话头进程,则会终止会话中的所有进程
- 改变当前进程的运行状态,变成TASK_ZOMBIE(僵死)状态,并且向其父进程发送SIGCHLD信号,说明自己要死了
sys_waitpid()
1.父进程在运行子进程时一般都会运行wait waitpid这两个函数,用来父进程等待子进程终止
2.当父进程收到SIGCHLD信号时,父进程会终止僵死状态的子进程
3.父进程会把子进程的运行时间累加到自己的运行时间上
4.把对应子进程的进程描述结构体进行释放,置空数组空槽
三:进程
1.内核进程初始化与创建
每创建一个进程就对应着一个task_struct结构体
1 | struct task_struct { |
1、0号和1号进程的创建
Linux在初始化的过程中会进行0号进程的创建
注:分析0.11的main函数
1 | void main(void) /* This really IS void, no error here. */ |
内核要先切换到用户态之后再fork生成0号进程
1 |
iret是从中断返回的指令,在iret之前,之前5个push压入的数据会出栈,分别赋给ss,esp,eflags,cs,eip,
fork生成0进程之后,会进行初始化,进一步分析如下
1 | static char * argv_rc[] = { "/bin/sh", NULL }; |
1、0号进程打开标准输入输出错误句柄
2、创建1号进程,首先打开”/dev/rc”文件,执行shell
3、如果1号进程创建失败,会换一种方式再次创建
4、之后就是进行pause()暂停状态,系统等待运行下一步
2.普通进程的创建(WORKING)
众所周知每创建一个进程都会创建一个相对应的task_struct结构体,task结构体里就有代表该进程唯一的PID;
3.进程的调度与切换
这是Sched.c函数,
1 |
|
4.进程的销毁
就是exit.c函数的操作
5.进程间的通信(WORKING)
1、进程,线程
创建一个进程之后,就会对应一个task_struct结构体,fork之后,会进行写实复制(Copy-On-Write),也就是说子进程和父进程的内容大部分是一致的。
问:一个进程多个线程的调度方式和一个进程一个线程时的调度方式有什么区别?
答:没有区别,内核中线程和进程都需要do_fork来实现,所以没有区别。
四:操作系统的引导与启动
1、BIOS/Bootloader:
由PC机的BIOS(0xFFFF0是BIOS存储的总线地址)把bootsect从某个固定的地址拿到了内存中的某个固定地址(0x90000),并且进行了一系列的硬件初始化和参数设置
2、bootsect.s(WORKING)
磁盘引导块程序,在磁盘的第一个扇区中的程序(0磁道,0磁头,1扇区)
作用:首先将后续的setup.s代码从磁盘中加载到紧接着bootsect.s的地方,在显示屏上显示loading system ,再将操作系统加载到0x10000,最后转到setup.s运行
1 | ! SYS_SIZE is the number of clicks (16 bytes) to be loaded. |
3、setup.s(WORKING)
解析BIOS/Bootloader传进来的参数,设置系统内核运行的LDT(局部描述符),IDT(中断描述符) GDT(全局描述符),设置中断控制芯片,进入保护模式运行;跳转到head.s运行
1 | setup.s (C) 1991 Linus Torvalds |
注:GDT,LDT,IDT表是什么?
GDT(global descriptor table),全局段描述符表,这些64kb数据整齐的排列在内存中某一位置。而该位置的内存地址以及有效的个数就存放在GDTR中,GDTR是特殊的寄存器。GDT在系统内只存在一个。
LDT(local descripotr table),局部段描述符表,LDT在系统内可存在多个,每个任务最多只能拥有一个LDT,另外,每一个LDT自身作为一个段存在,它们的段描述符被放在GDT中。
IDT(interrupt descriptor table),中断描述符表,IDT记录了0~255的中断号码和中断服务函数的关系。当发生中断的时候,通过中断号码去执行中断服务函数。
GDT可以被放在内存的任何位置,那么当程序员通过段寄存器来引用一个段描述符时,CPU必须知道GDT的入口,也就是基地址放在哪里,所以Intel的设计者门提供了一个寄存器GDTR用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过LGDT指令将GDT的入口地址装入此寄存器,从此以后,CPU就根据此寄存器中的内容作为GDT的入口来访问GDT了。
IA-32为LDT的入口地址也提供了一个寄存器LDTR,因为在任何时刻只能有一个任务在运行,所以LDT寄存器全局也只需要有一个。如果一个任务拥有自身的LDT,那么当它需要引用自身的LDT时,它需要通过LLDT指令将其LDT的段描述符装入此寄存器。LLDT指令与LGDT指令不同的时,LGDT指令的操作数是一个32-bit的内存地址,这个内存地址处存放的是一个32-bit GDT的入口地址,以及16-bit的GDT Limit。而LLDT指令的操作数是一个16-bit的选择子,这个选择子主要内容是:被装入的LDT的段描述符在GDT中的索引值。
4、head.s(WORKING)
加载内核运行时的各数据段寄存器,重新设置中断描述符表,开启内核正常运行时的协处理器等资源;设置内存管理的分页机制,跳转到main.c运行
1 | * head.s contains the 32-bit startup code. |
5、main.c(WORKING)
1 | void main(void) /* This really IS void, no error here. */ |
五:信号概述
内核的信号量是很重要的,关于信号的定义在/include/signal.h文件内,比如运行一个elf文件可能会出现段错误(SIGSEGV),玩pwn的同学应该很熟悉。在system_call.s中存在call do_signal,那么do_signal在/kernel/signal.c内定义。
硬件来源:信号由硬件驱动产生
软件来源:系统提供了些API,例如kill命令
当进程收到信号时,会有三种场景;
忽略:忽略信号
执行:执行每个信号所对应的操作
执行自定操作:用户自定义的操作
1.在系统中什么是信号,都有什么信号?
2.在系统接收到信号后,是如何进行处理的?
3.信号作用
1.signal.h
1 |
|
2.signal.c
1 |
|
3.sa_restorer
1 | /* 如果没有屏蔽码,使用该函数作为恢复函数 */ |
六:文件系统
顾名思义就是文件所组成的一个系统,linux下所谓“一切皆文件”,所以文件系统在内核中占了很大比重。
Linux启动过程:
1.PCB上电后先由uboot初始化板子,然后将linux内核迁移到内存中运行;
2.由linux内核进行初始化操作,挂载第一个应用程序即根文件系统(linuxrc);
3.根文件系统提供磁盘管理服务(glibc,设备节点,配置文件,应用程序 shell命令)。

1.根文件系统
当我们在Linux下输入Is的时候,见到的目录结构以及这些目录下的内容都大同小异,这是因为所有的Linux发行版在对根文件系统布局上都按照该标准规定了根目录下各个子目录的名称及其存放的内容:
目录名 存放的内容
/bin 必备的用户命令,例如Is、cp等
/sbin 必备的系统管理员命令,例如ifconfig、reboot等
/dev 设备文件,例如mtdblocko、ttv1等
/etc 系统配置文件,包括启动文件,例如inittab等
/lib 必要的链接库,例如C链接库、内核模块
/home 普通用户主目录
/root root用户主目录
/usr/bin 非必备的用户程序,例如find du等
/usr/sbin 非必备的管理员程序,例如chroot、inetd等
/usr/lib 库文件
/var 守护程序和工具程序所存放的可变,例如目志文件
/proc 用来提供内核与进程信息的虛拟文件系统,,由内核自动生成目录下的内容
/sys 用来提供内核与设备信息的虛拟文件系统,由内核自动生成目录下的内容
/mnt 文件系统挂接点,用于临时安装文件系统
/tmp 临时性的文件,重启后将自动清除
root 启动Linux时使用的一些核心文件。如操作系统内核、引导程序Grub等
home 存储普通用户的个人文件,ftp,httpd,samba,user1,user2等
bin 系统启动时需要的执行文件
sbin 可执行程序的目录,但大多存放涉及系统管理的命令,只有root权限才能执行
proc 存在linux内核镜像,保存所有内核参数以及系统配置信息
usr 用户目录,存放用户级的文件
boot 引导加载器所需文件,系统所需图片保存于此
lib 根文件系统目录下程序和核心模块的库文件
dev 设备文件目录
etc 配置文件
var
mnt 临时用于挂载文件系统的地方,一般情况下该目录是空的
2.文件系统概述
文件系统主要包括四个部分:高速缓冲区管理,文件底层操作,文件数据访问,文件高层访问控制。
1.文件系统底层函数
1.bitmap.c
程序包括对i节点位图和逻辑块位图进行释放和占用处理函数。操作i节点位图的函数是free_inode()和new_inode(),操作逻辑块位图的函数是free_block()和new_block()
2.truncate.c
程序包括对数据文件长度截断为0的函数**truncate()**,他将i节点指定的设备上文件长度截为0,并释放文件数据占用的设备逻辑块。
3.inode.c
程序包括分配i节点函数**iget()和放回对内存i节点存取函数iput()以及根据i节点信息取文件数据块在设备上对应的逻辑块号函数bmap()**。
4.namei.c
程序主要包括函数**namei(),该函数使用iget(),iput(),bmap()**将给定的文件路径名映射到其i节点。
5.super.c
程序专门用于处理文件系统超级块,包括函数**get_super(),put_super()和free_super()和free_super()等,还包括几个文件系统加载/卸载处理函数和系统调用,如sys_mount()**等
2.文件中数据的访问操作
1.block_dev.c
程序中的函数**block_read()和block_write()**是用于读写块设备特殊文件的数据,所使用的参数指定要访问的设备号,起始地址和长度
2.file_dev.c
程序中的**file_read()和file_write()**函数是用于访问一般的文件,所使用的参数指定文件对应的i节点和文件结构。
3.pipe.c
文件中实现了管道读写函数**read_pipe()和write_pipe(),另外还实现了创建无名管道的系统调用pipe()**,
4.char_dev.c
系统调用使用read()和write()会调用char_dev.c中的**rw_char()**函数来操作。字符设备包括控制台终端,串口终端和内存字符设备。
3.文件和目录管理系统调用
1.open.c
文件用于实现与文件操作相关的系统调用,主要有文件的创建,打开和关闭,文件宿主和属性修改,文件访问权限和操作时间的修改等。
2.exec.c
程序实现对二进制可执行文件和shell脚本文件的加载与执行,其中主要是的do_execve(),他是系统中断调用(int 0x80)的功能号__NR_execve()调用的C处理函数,更是exec()函数簇的主要实现函数。
3.fcntl.c
实现了文件控制操作的系统调用fcntl()和两个文件句柄(描述符)复制系统调用dup()和dup2(),dup2()指定了新句柄的数值,dup()则返回当前最小值的未用句柄。句柄复制操作主要用在文件的标准输入/输出重定向和管道操作方面。
4.ioctl.c
文件实现了输入/输出控制系统调用ioctl(),主要调用tty_ioctl()函数,对终端的I/O进行控制。
5.stat.c
文件用于实现取得文件状态信息的系统调用,stat()和fstat()。stat()是利用文件名取信息,而fstat()是利用文件句柄取信息。
2.高速缓冲区管理(buffer.c)
高速缓冲区位于内核代码与主内存区之间,在块设备与内核其他程序之间起着一个桥梁作用,除了块设备驱动程序以外,内核程序如果需要访问块设备中的数据,就需要通过高速缓冲区来进行操作。