Linux 内核解读入门
喻锋荣 [本文由《软件工程师》杂志提供]
针对好多Linux 爱好者对内核很有兴趣却无从下口,本文旨在介绍一种解读linux内核源码的入门方法,而不是解说linux复杂的内核机制;
一.核心源程序的文件组织:
1.Linux核心源程序通常都安装在/usr/src/linux下,而且它有一个非常简单的编号约定:任何偶数的核心(例如2.0.30)都是一个稳定地发行的核心,而任何奇数的核心(例如2.1.42)都是一个开发中的核心。
本文基于稳定的2.2.5源代码,第二部分的实现平台为 Redhat Linux 6.0。
2.核心源程序的文件按树形结构进行组织,在源程序树的最上层你会看到这样一些目录:
●Arch :arch子目录包括了所有和体系结构相关的核心代码。它的每一个子目录都代表一种支持的体系
结构,例如i386就是关于intel cpu及与之相兼容体系结构的子目录。PC机一般都基于此目录;
●Include: include子目录包括编译核心所需要的大部分头文件。与平台无关的头文件在 include/linux
子目录下,与 intel cpu相关的头文件在include/asm-i386子目录下,而include/scsi目录则是有关
scsi设备的头文件目录;
●Init: 这个目录包含核心的初始化代码(注:不是系统的引导代码),包含两个文件main.c和Version.c,
这是研究核心如何工作的一个非常好的起点。
●Mm :这个目录包括所有独立于 cpu 体系结构的内存管理代码,如页式存储管理内存的分配和释放等;而和体系结构相关的内存管理代码则位于arch/*/mm/,例如arch/i386/mm/Fault.c
●Kernel:主要的核心代码,此目录下的文件实现了大多数linux系统的内核函数,其中最重要的文件当属sched.c;同样,和体系结构相关的代码在arch/*/kernel中;
●Drivers: 放置系统所有的设备驱动程序;每种驱动程序又各占用一个子目录:如,/block 下为块设备驱动程序,比如ide(ide.c)。如果你希望查看所有可能包含文件系统的设备是如何初始化的,你可以看 drivers/block/genhd.c中的device_setup()。它不仅初始化硬盘,也初始化网络,因为安装nfs文件系统的时候需要网络其他: 如, Lib放置核心的库代码; Net,核心与网络相关的代码; Ipc,这个目录包含核心的进程间通讯的代码; Fs ,所有的文件系统代码和各种类型的文件操作代码,它的每一个子目录支持一个文件系统,例如fat和ext2; Scripts, 此目录包含用于配置核心的脚本文件等。
一般,在每个目录下,都有一个 .depend 文件和一个 Makefile 文件,这两个文件都是编译时使用的辅助文件,仔细阅读这两个文件对弄清各个文件这间的联系和依托关系很有帮助;而且,在有的目录下还有Readme 文件,它是对该目录下的文件的一些说明,同样有利于我们对内核源码的理解;
二.解读实战:为你的内核增加一个系统调用
虽然,Linux 的内核源码用树形结构组织得非常合理、科学,把功能相关联的文件都放在同一个子目录下,这样使得程序更具可读性。然而,Linux 的内核源码实在是太大而且非常复杂,即便采用了很合理的文件组织方法,在不同目录下的文件之间还是有很多的关联,分析核心的一部分代码通常会要查看其它的几个相关的文件,而且可能这些文件还不在同一个子目录下。
体系的庞大复杂和文件之间关联的错综复杂,可能就是很多人对其望而生畏的主要原因。当然,这种令人生畏的劳动所带来的回报也是非常令人着迷的:你不仅可以从中学到很多的计算机的底层的知识(如下面将讲到的系统的引导),体会到整个操作系统体系结构的精妙和在解决某个具体细节问题时,算法的巧妙;而且更重要的是:在源码的分析过程中,你就会被一点一点地、潜移默化地专业化;甚至,只要分析十分之一的代码后,你就会深刻地体会到,什么样的代码才是一个专业的程序员写的,什么样的代码是一个业余爱好者写的。
为了使读者能更好的体会到这一特点,下面举了一个具体的内核分析实例,希望能通过这个实例,使读者对 Linux 的内核的组织有些具体的认识,从中读者也可以学到一些对内核的分析方法。
以下即为分析实例:
【一】操作平台:
硬件:cpu intel Pentium II ;
软件:Redhat Linux 6.0; 内核版本2.2.5
【二】相关内核源代码分析:
1.系统的引导和初始化:Linux 系统的引导有好几种方式:常见的有 Lilo, Loadin引导和Linux的自举引导(bootsect-loader),而后者所对应源程序为arch/i386/boot/bootsect.S,它为实模式的汇编程序,限于篇幅在此不做分析;无论是哪种引导方式,最后都要跳转到 arch/i386/Kernel/setup.S, setup.S主要是进行时模式下的初始化,为系统进入保护模式做准备;此后,系统执行 arch/i386/kernel/head.S (对经压缩后存放的内核要先执行 arch/i386/boot/compressed/head.S); head.S 中定义的一段汇编程序setup_idt ,它负责建立一张256项的 idt 表(Interrupt Descriptor Table),此表保存着所有自陷和中断的入口地址;其中包括系统调用总控程序 system_call 的入口地址;当然,除此之外,head.S还要做一些其他的初始化工作;
2.系统初始化后运行的第一个内核程序asmlinkage void __init start_kernel(void) 定义在/usr/src/linux/init/main.c中,它通过调用usr/src/linux/arch/i386/kernel/traps.c 中的一个函数void __init trap_init(void) 把各自陷和中断服务程序的入口地址设置到 idt 表中,其中系统调用总控程序system_cal就是中断服务程序之一;void __init trap_init(void) 函数则通过调用一个宏 set_system_gate(SYSCALL_VECTOR,&system_call); 把系统调用总控程序的入口挂在中断0x80上;其中SYSCALL_VECTOR是定义在 /usr/src/linux/arch/i386/kernel/irq.h中的一个常量0x80; 而 system_call 即为中断总控程序的入口地址;中断总控程序用汇编语言定义在/usr/src/linux/arch/i386/kernel/entry.S中;
3.中断总控程序主要负责保存处理机执行系统调用前的状态,检验当前调用是否合法, 并根据系统调用向量,使处理机跳转到保存在 sys_call_table 表中的相应系统服务例程的入口; 从系统服务例程返回后恢复处理机状态退回用户程序;而系统调用向量则定义在/usr/src/linux/include/asm-386/unistd.h 中;sys_call_table 表定义在/usr/src/linux/arch/i386/kernel/entry.S 中; 同时在 /usr/src/linux/include/asm-386/unistd.h 中也定义了系统调用的用户编程接口;
4.由此可见 ,linux 的系统调用也象 dos 系统的 int 21h 中断服务, 它把0x80 中断作为总的入口, 然后转到保存在 sys_call_table 表中的各种中断服务例程的入口地址 , 形成各种不同的中断服务;
由以上源代码分析可知,要增加一个系统调用就必须在 sys_call_table 表中增加一项 ,并在其中保存好自己的系统服务例程的入口地址,然后重新编译内核,当然,系统服务例程是必不可少的。
由此可知在此版linux内核源程序<2.2.5>中,与系统调用相关的源程序文件就包括以下这些:
1.arch/i386/boot/bootsect.S
2.arch/i386/Kernel/setup.S
3.arch/i386/boot/compressed/head.S
4.arch/i386/kernel/head.S
5.init/main.c
6.arch/i386/kernel/traps.c
7.arch/i386/kernel/entry.S
8.arch/i386/kernel/irq.h
9.include/asm-386/unistd.h
当然,这只是涉及到的几个主要文件。而事实上,增加系统调用真正要修改文件只有include/asm-386/unistd.h和arch/i386/kernel/entry.S两个;
【三】 对内核源码的修改:
1.在kernel/sys.c中增加系统服务例程如下:
asmlinkage int sys_addtotal(int numdata)
{
int i=0,enddata=0;
while(i<=numdata)
enddata+=i++;
return enddata;
}
该函数有一个 int 型入口参数 numdata , 并返回从 0 到 numdata 的累加值; 当然也可以把系统服务例程放在一个自己定义的文件或其他文件中,只是要在相应文件中作必要的说明;
2.把 asmlinkage int sys_addtotal( int) 的入口地址加到sys_call_table表中:
arch/i386/kernel/entry.S 中的最后几行源代码修改前为:
... ...
.long SYMBOL_NAME(sys_sendfile)
.long SYMBOL_NAME(sys_ni_syscall) /* streams1 */
.long SYMBOL_NAME(sys_ni_syscall) /* streams2 */
.long SYMBOL_NAME(sys_vfork) /* 190 */
.rept NR_syscalls-190
.long SYMBOL_NAME(sys_ni_syscall)
.endr
修改后为: ... ...
.long SYMBOL_NAME(sys_sendfile)
.long SYMBOL_NAME(sys_ni_syscall) /* streams1 */
.long SYMBOL_NAME(sys_ni_syscall) /* streams2 */
.long SYMBOL_NAME(sys_vfork) /* 190 */
/* add by I */
.long SYMBOL_NAME(sys_addtotal)
.rept NR_syscalls-191
.long SYMBOL_NAME(sys_ni_syscall)
.endr
3. 把增加的 sys_call_table 表项所对应的向量,在include/asm-386/unistd.h 中进行必要申明,以供用户进程和其他系统进程查询或调用:
增加后的部分 /usr/src/linux/include/asm-386/unistd.h 文件如下:
... ...
#define __NR_sendfile 187
#define __NR_getpmsg 188
#define __NR_putpmsg 189
#define __NR_vfork 190
/* add by I */
#define __NR_addtotal 191
4.测试程序(test.c)如下:
#include
#include
_syscall1(int,addtotal,int, num)
main()
{
int i,j;
do
printf("Please input a number\n");
while(scanf("%d",&i)==EOF);
if((j=addtotal(i))==-1)
printf("Error occurred in syscall-addtotal();\n");
printf("Total from 0 to %d is %d \n",i,j);
}
对修改后的新的内核进行编译,并引导它作为新的操作系统,运行几个程序后可以发现一切正常;在新的系统下对测试程序进行编译(*注:由于原内核并未提供此系统调用,所以只有在编译后的新内核下,此测试程序才能可能被编译通过),运行情况如下:
$gcc -o test test.c
$./test
Please input a number
36
Total from 0 to 36 is 666
可见,修改成功;
而且,对相关源码的进一步分析可知,在此版本的内核中,从/usr/src/linux/arch/i386/kernel/entry.S 文件中对 sys_call_table 表的设置可以看出,有好几个系统调用的服务例程都是定义在 /usr/src/linux/kernel/sys.c 中的同一个函数:
asmlinkage int sys_ni_syscall(void)
{
return -ENOSYS;
}
例如第188项和第189项就是如此:
... ...
.long SYMBOL_NAME(sys_sendfile)
.long SYMBOL_NAME(sys_ni_syscall) /* streams1 */
.long SYMBOL_NAME(sys_ni_syscall) /* streams2 */
.long SYMBOL_NAME(sys_vfork) /* 190 */
... ...
而这两项在文件 /usr/src/linux/include/asm-386/unistd.h 中却申明如下:
... ...
#define __NR_sendfile 187
#define __NR_getpmsg 188 /* some people actually want streams */
#define __NR_putpmsg 189 /* some people actually want streams */
#define __NR_vfork 190
由此可见,在此版本的内核源代码中,由于asmlinkage int sys_ni_syscall(void) 函数并不进行任何操作,所以包括 getpmsg, putpmsg 在内的好几个系统调用都是不进行任何操作的,即有待扩充的空调用; 但它们却仍然占用着sys_call_table表项,估计这是设计者们为了方便扩充系统调用而安排的; 所以只需增加相应服务例程(如增加服务例程getmsg或putpmsg),就可以达到增加系统调用的作用。
结语:当然对于庞大复杂的 linux 内核而言,一篇文章远远不够,而且与系统调用相关的代码也只是内核中极其微小的一部分;但重要的是方法、掌握好的分析方法;所以上的分析只是起个引导的作用,而正真的分析还有待于读者自己的努力。
Linux 核心二
reset 于 00-7-11 19:20:22 加贴在 UNIX系统安全:
这是一次对Linux介绍后的整理。
对象是一些刚对Linux核心感兴趣,并且准备进一步研究和改造的同志。
四部分内容:
一、Linux核心源码结构介绍
二、编译和配置的过程
三、系统启动顺序的相关文件
四、核心改造的一些经验
一、
当我们安装好一个Linux系统,通常核心源码存放在/usr/src/linux/目录。
下面先看看这目录下的各个子目录及文件。
[/]#cd /usr/src/linux
[linux]#ls -aF
./ MAINTAINERS drivers/ kernel/ scripts/
../ Makefile fs/ lib/
COPYING README include/ mm/
CREDITS Rules.make init/ modules/
Documentation/ arch/ ipc/ net/
下面我们逐一描述:
COPYING
## GPL版权申明,看后你至少应该知道,你对具有GPL版权的源代码改动而形成的程序,或使用GPL工具产生的程序,具有使用GPL发表的义务。其中之一就是公开源代码。
CREDITS
## 光荣榜,你应当感谢的一些人的信息,其中的每一个人都对Linux做出过很大贡献。
Documentation/
## 文档目录,可有选择地看一下你感兴趣的部分
MAINTAINERS
## 维护人员列表,对当前版本的内核各部分都有谁负责,如果你研究的
够深入,可以与他们讨论Makefile
## 如果你在UNIX编译过程序,可以看明白README
## Linus 所写,核心及其编译配置方法简单介绍
Rules.make
## make时使用的一些共同规则
arch/
## architecture(体系结构)我关心的i386启动过程在其中,
## 包括Linux在多种平台下的实现。如果要移植系统到一个新的
##CPU环境中,这就是你要关心的目录
drivers/
## 驱动程序目录,包含大量设备驱动的实现,按类别分子目录
fs/
## 文件系统,实现了当前流行的几乎所有文件系统。Cool
include/
## 嵌入文件目录
init/
## 初始化文件,包含main.c和version.c两个文件。Initialize
ipc/
## ipc的实现,与SYS V兼容
kernel/
## 最核心代码,调度,中断,信号等的处理
lib/
## 一些工具。
mm/
## 内存管理,Memory Manager,虚拟页、缓冲的实现。
modules/
## 模块文件目录,用于存放编译时产生的模块目标文件(参考编译过程)
net/
## 网络实现,包括TCP/IP在内的大量网络协议的实现。
scripts/
## 描述文件,脚本,用于对核心的配置。
二、
构造内核
常用命令包括:
make config, dep, clean, mrproper, zImage, bzImage, modules, modules_install
(1) make config
核心配置,调用./scripts/Configure 按照arch/i386/config.in 来进行配置。命令执行完后产生文件.config,其中保存着配置信息。
下一次再做make config将产生新的.config文件,原.config被改名为.config.old
(2)make dep
寻找依存关系。
产生两个文件.depend和.hdepend其中.hdepend表示每个.h文件都包含其它哪些嵌入文件。而.depend 文件有多个,在每个会产生目标文件(.o)文件的目录下均有,它表示每个目标文件都依赖哪些嵌入文件(.h)。
(3)make clean
清出以前构核所产生的所有目标文件、模块文件、核心以及一些临时文件等,
不产生任何文件
(4)make rmproper
删除所有因构核过程中产生的所有文件,及除了做make clean外,还要删除.config,.depend等文件,把核心源码恢复到最原始的状态。下次构核时就必须重新配置了。
(5)make, make zImage, make bzImage
make:
构核。通过各目录的Makefile文件进行。会在各个目录下产生一大堆目标文件,若核心代码没有错误,将产生文件vmlinux,这就是所构的核心。并产生映射文件System.map通过各目录的Makefile文件进行。
.version 文件中的数加1,表示版本号(又产生一个新的版本了),让你
明白,你已经对核心改动过多少次了。
make zImage:
在make的基础上产生压缩的核心映象文件./arch/$(ARCH)/boot/zImage以及在./arch/$(ARCH)/boot/compresed/目录下产生一些临时文件。
make bzImage:
在make 的基础上产生压缩比例更大的核心映象文件 ./arch/$(ARCH)/boot/bzImage以及在./arch/$(ARCH)/boot/compresed/目录下产生一些临时文件。在核心太大时进行。
(6)make modules
编译模块文件,你在make config时所配置的所有模块将在这时编译,形成模块目标文件,并把这些目标文件存放在modules目录中。使用如下命令看一看。
ls modules
(7)make modules_install
把上面编译好的模块目标文件目录/lib/modules/$KERNEL_VERSION/ 中。比如我的版本是2.0.36,做完这个操作后可使用下面的命令看看:
ls /lib/modules/2.0.36/
相关的命令还有很多,有兴趣可看相关资料和Makefile文件。另外注意,这儿我们产生了一些隐含文件
.config
.oldconfig
.depend
.hdepend
.version
它们的意义应该很清楚了。
三、
系统的启动顺序及相关文件
仍在核心源码目录下,看以下几个文件
./arch/$ARCH/boot/bootsect.s
./arch/$ARCH/boot/setup.s
./init/main.c
bootsect.S 及 setup.S
这个程序是linux kernel的第一个程序,包括了linux自己的bootstrap程序,但是在说明这个程序前,必须先说明一般IBM PC开机时的动作(此处的开机是指"打开PC的电源"):
一般PC在电源一开时,是由内存中地址FFFF:0000开始执行(这个地址一定
在ROM BIOS中,ROM BIOS一般是在FEOOOh到FFFFFh中),而此处的内容则是一个jump指令,jump到另一个位於ROM BIOS中的位置,开始执行一系列的动作,包括了检查RAM,keyboard,显示器,软硬磁盘等等,这些动作是由系统测试代码(system test code)来执行的,随着制作BIOS厂商的不同而会有些许差异,但都是大同小异,读者可自行观察自家机器开机时,萤幕上所显示的检查讯息。
紧接着系统测试码之后,控制权会转移给ROM中的启动程序(ROM bootstrap routine),这个程序会将磁盘上的第零轨第零扇区读入内存中(这就是一般所谓的boot sector,如果你曾接触过电脑病毒,就大概听过它的大名),至於被读到内存的哪里呢? --绝对位置07C0:0000(即07C00h处),这是IBM系列PC的特性。而位在linux开机磁盘的boot sector上的正是linux的bootsect程序,也就是说,bootsect是第一个被读入内存中并执行的程序。现在,我们可以开始来看看到底bootsect做了什么。
第一步
首先,bootsect将它"自己"从被ROM BIOS载入的绝对地址0x7C00处搬到
0x90000处,然后利用一个jmpi(jump indirectly)的指令,跳到新位置的
jmpi的下一行去执行,
第二步
接着,将其他segment registers包括DS,ES,SS都指向0x9000这个位置,
与CS看齐。另外将SP及DX指向一任意位移地址( offset ),这个地址等一下
会用来存放磁盘参数表(disk para- meter table )
第三步
接着利用BIOS中断服务int 13h的第0号功能,重置磁盘控制器,使得刚才
的设定发挥功能。
第四步
完成重置磁盘控制器之后,bootsect就从磁盘上读入紧邻着bootsect的setup
程序,也就是setup.S,此读入动作是利用BIOS中断服务int 13h的第2号功能。
setup的image将会读入至程序所指定的内存绝对地址0x90200处,也就是在内存中紧邻着bootsect 所在的位置。待setup的image读入内存后,利用BIOS中断服务int 13h的第8号功能读取目前磁盘的参数。
第五步
再来,就要读入真正linux的kernel了,也就是你可以在linux的根目录下看到的"vmlinuz" 。在读入前,将会先呼叫BIOS中断服务int 10h 的第3号功能,读取游标位置,之后再呼叫BIOS 中断服务int 10h的第13h号功能,在萤幕上输出字串"Loading",这个字串在boot linux时都会首先被看到,相信大家应该觉得很眼熟吧。
第六步
接下来做的事是检查root device,之后就仿照一开始的方法,利用indirect
jump 跳至刚刚已读入的setup部份比较把大家所熟知的MS DOS 与linux的开机部份做个粗浅的比较,MS DOS 由位於磁盘上boot sector的boot程序负责把IO.SYS载入内存中,而IO.SYS则负有把DOS的kernel --MSDOS.SYS载入内存的重责大任。而linux则是由位於boot sector 的bootsect程序负责把setup及linux的kernel载入内存中,再将控制权交给setup。
##这几步内容主要参照一个台湾同胞写的文档,setup.s的内容希望有人补充。
start_kernel()
当核心被载入后,首先进入的函数就是start_kernel。
./init/main.c 中函数start_kernel包含核心的启动过程及顺序。通过它来看核心整个初始化过程。
首先进行一系列初始化,包括:
trap_init(); ##./arch/i386/kernel/traps.c 陷入
init_IRQ(); ##./arch/i386/kernel/irq.c setup IRQ
sched_init(); ##./kernel/sched.c 调度初始化,并初始化bottom_half
time_init(); ##./arch/i386/kernel/time.c
init_modules(); ##模块初始化
mem_init(memory_start,memory_end);
buffer_init(); ## ./fs/buffer.c 缓冲区
sock_init(); ## ./net/socket.c socket初始化,并初始化各协议(TCP等)
ipc_init();
sysctl_init();
然后通过调用kernelthread()产生init进程,全权交由init进程处理。调用cpu_idle(NULL)休息。
感兴趣又有时间的同志可以写一个startkernel()函数的详细分析报告。
下面看一看init进程的工作:
首先创建进程
bdflush ##./fs/buffer.c 缓冲区管理
和kswapd ##./mm/vmscan.c 虚拟内存管理
这两个进程非常重要
系统初始化(系统调用setup)
系统初始化包含设备初始化及各文件系统初始化。
sys_setup (./fs/filesystems.c)
|
|-device_setup
| |
| -- chr_dev_init(); ##字符设备
| blk_dev_init(); ##块设备
| scsi_dev_init(); ##SCSI
| net_dev_init(); ##网络设备
| console_map_init(); ##控制台
|-binfmt_setup();
|-init_nls() ##各文件系统初始化
|-init_ext_fs()
|-init_ext2_fs()
. .
. .
. .
|-init_autofs_fs()
--mount_root() ##mount root fs
##从这儿看看设备及文件的初始化顺序,加入我们的设备时就有了大局观。
执行/etc/rc ( rc.sysinit, rc.local, rc.# ) 和
执行/bin/sh
把握主线
袁哥 于 00-7-12 12:20:55 加贴在 UNIX系统安全:
我说说我自己高效快速看LINUX系统代码的方法吧,其实要快速深入看一个操作系统差不多都可以这么入手。思想原理大致就是把握主线,找准入口。
不了解的前提下,首先当然是从启动开始,因为这时只有了解启动这个入口。看启动文件,ARCH里面的启动文件调入内核,做些简单的初始化,进入保护模式的工作,再转到INIT目录的MAIN.C文件的start_kernel(void)
asmlinkage void __init start_kernel(void)
{
char * command_line;
#ifdef __SMP__
static int boot_cpu = 1;
/* "current" has been set up, we need to load it now */
if (!boot_cpu)
initialize_secondary();
boot_cpu = 0;
#endif
/*
* Interrupts are still disabled. Do necessary setups, then
* enable them
*/
printk(linux_banner);
setup_arch(&command_line, &memory_start, &memory_end);
memory_start = paging_init(memory_start,memory_end);
trap_init();
/* 设置中断入口等 YRG 2000。7。7 */
memory_start = init_IRQ( memory_start );
sched_init();
time_init();
parse_options(command_line);
/*
* HACK ALERT! This is early. We're enabling the console before
* we've done PCI setups etc, and console_init() must be aware of
* this. But we do want output early, in case something goes wrong.
*/
memory_start = console_init(memory_start,memory_end);
#ifdef CONFIG_MODULES
init_modules();
#endif
if (prof_shift) {
prof_buffer = (unsigned int *) memory_start;
/* only text is profiled */
prof_len = (unsigned long) &_etext - (unsigned long) &_stext;
prof_len >>= prof_shift;
memory_start += prof_len * sizeof(unsigned int);
memset(prof_buffer, 0, prof_len * sizeof(unsigned int));
}
memory_start = kmem_cache_init(memory_start, memory_end);
sti();
calibrate_delay();
#ifdef CONFIG_BLK_DEV_INITRD
if (initrd_start && !initrd_below_start_ok && initrd_start < memory_start) {
printk(KERN_CRIT "initrd overwritten (0x%08lx < 0x%08lx) - "
"disabling it.\n",initrd_start,memory_start);
initrd_start = 0;
}
#endif
mem_init(memory_start,memory_end);
kmem_cache_sizes_init();
#ifdef CONFIG_3215_CONSOLE
con3215_activate();
#endif
#ifdef CONFIG_PROC_FS
proc_root_init();
#endif
uidcache_init();
filescache_init();
dcache_init();
vma_init();
buffer_init(memory_end-memory_start);
page_cache_init(memory_end-memory_start);
signals_init();
inode_init();
file_table_init();
#if defined(CONFIG_SYSVIPC)
ipc_init();
#endif
#if defined(CONFIG_QUOTA)
dquot_init_hash();
#endif
check_bugs();
printk("POSIX conformance testing by UNIFIX\n");
/*
* We count on the initial thread going ok
* Like idlers init is an unlocked kernel thread, which will
* make syscalls (and thus be locked).
*/
smp_init();
kernel_thread(init, NULL, CLONE_FS | CLONE_FILES | CLONE_SIGHAND);
current->need_resched = 1;
cpu_idle(NULL);
}
一些文件系统、网络、硬件设备驱动等初始化都在这,要看什么就可以相应看那初始化代码。要再深入就得找准入口,这得看ARCH目录文件traps.c设置中断等的调用 trap_init(void)。 这里面就有一些重要的该看的一些入口,显然要抓住重点看系统调用 set_system_gate(SYSCALL_VECTOR,&system_call);还有两个保护模式的重要内容异常中断和缺页中断 set_trap_gate(13,&general_protection);
set_trap_gate(14,&page_fault); 这关系到一些重要处理过程。
/* 设置中断入口,系统服务入口等 YRG 2000。7。7 */
void __init trap_init(void)
{
if (readl(0x0FFFD9) == 'E' + ('I'<<8) + ('S'<<16) + ('A'<<24))
EISA_bus = 1;
set_call_gate(&default_ldt,lcall7);
set_trap_gate(0,÷_error);
set_trap_gate(1,&debug);
set_trap_gate(2,&nmi);
set_system_gate(3,&int3); /* int3-5 can be called from all */
set_system_gate(4,&overflow);
set_system_gate(5,&bounds);
set_trap_gate(6,&invalid_op);
set_trap_gate(7,&device_not_available);
set_trap_gate(8,&double_fault);
set_trap_gate(9,&coprocessor_segment_overrun);
set_trap_gate(10,&invalid_TSS);
set_trap_gate(11,&segment_not_present);
set_trap_gate(12,&stack_segment);
set_trap_gate(13,&general_protection);
set_trap_gate(14,&page_fault);
set_trap_gate(15,&spurious_interrupt_bug);
set_trap_gate(16,&coprocessor_error);
set_trap_gate(17,&alignment_check);
set_system_gate(SYSCALL_VECTOR,&system_call);
/* set up GDT task & ldt entries */
set_tss_desc(0, &init_task.tss);
set_ldt_desc(0, &default_ldt, 1);
/* Clear NT, so that we won't have troubles with that later on */
__asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl");
load_TR(0);
load_ldt(0);
#ifdef CONFIG_X86_VISWS_APIC
superio_init();
lithium_init();
cobalt_init();
#endif
}
重要入口就在目录ARCH里面的文件entry.s,异常中断、缺页中断,再就是系统调用入口。
ENTRY(system_call)
pushl %eax # save orig_eax
SAVE_ALL
GET_CURRENT(%ebx)
cmpl $(NR_syscalls),%eax
jae badsys
testb $0x20,flags(%ebx) # PF_TRACESYS
jne tracesys
call *SYMBOL_NAME(sys_call_table)(,%eax,4)
movl %eax,EAX(%esp) # save the return value
ALIGN
.globl ret_from_sys_call
.globl ret_from_intr
ret_from_sys_call:
movl SYMBOL_NAME(bh_mask),%eax
andl SYMBOL_NAME(bh_active),%eax
jne handle_bottom_half
ret_with_reschedule:
cmpl $0,need_resched(%ebx)
jne reschedule
cmpl $0,sigpending(%ebx)
jne signal_return
restore_all:
RESTORE_ALL
ALIGN
signal_return:
sti # we can get here from an interrupt handler
testl $(VM_MASK),EFLAGS(%esp)
movl %esp,%eax
jne v86_signal_return
xorl %edx,%edx
call SYMBOL_NAME(do_signal)
jmp restore_all
ALIGN
v86_signal_return:
call SYMBOL_NAME(save_v86_state)
movl %eax,%esp
xorl %edx,%edx
call SYMBOL_NAME(do_signal)
jmp restore_all
ALIGN
tracesys:
movl $-ENOSYS,EAX(%esp)
call SYMBOL_NAME(syscall_trace)
movl ORIG_EAX(%esp),%eax
cmpl $(NR_syscalls),%eax
jae 1f
call *SYMBOL_NAME(sys_call_table)(,%eax,4)
movl %eax,EAX(%esp) # save the return value
1: call SYMBOL_NAME(syscall_trace)
jmp ret_from_sys_call
badsys:
movl $-ENOSYS,EAX(%esp)
jmp ret_from_sys_call
ALIGN
ret_from_exception:
movl SYMBOL_NAME(bh_mask),%eax
andl SYMBOL_NAME(bh_active),%eax
jne handle_bottom_half
ALIGN
ret_from_intr:
GET_CURRENT(%ebx)
movl EFLAGS(%esp),%eax # mix EFLAGS and CS
movb CS(%esp),%al
testl $(VM_MASK | 3),%eax # return to VM86 mode or non-supervisor?
jne ret_with_reschedule
jmp restore_all
ALIGN
handle_bottom_half:
call SYMBOL_NAME(do_bottom_half)
jmp ret_from_intr
ALIGN
reschedule:
call SYMBOL_NAME(schedule) # test
jmp ret_from_sys_call
ENTRY(divide_error)
pushl $0 # no error code
pushl $ SYMBOL_NAME(do_divide_error)
ALIGN
error_code:
pushl %ds
pushl %eax
xorl %eax,%eax
pushl %ebp
pushl %edi
pushl %esi
pushl %edx
decl %eax # eax = -1
pushl %ecx
pushl %ebx
cld
movl %es,%cx
xchgl %eax, ORIG_EAX(%esp) # orig_eax (get the error code. )
movl %esp,%edx
xchgl %ecx, ES(%esp) # get the address and save es.
pushl %eax # push the error code
pushl %edx
movl $(__KERNEL_DS),%edx
movl %dx,%ds
movl %dx,%es
GET_CURRENT(%ebx)
call *%ecx
addl $8,%esp
jmp ret_from_exception
ENTRY(coprocessor_error)
pushl $0
pushl $ SYMBOL_NAME(do_coprocessor_error)
jmp error_code
ENTRY(device_not_available)
pushl $-1 # mark this as an int
SAVE_ALL
GET_CURRENT(%ebx)
pushl $ret_from_exception
movl %cr0,%eax
testl $0x4,%eax # EM (math emulation bit)
je SYMBOL_NAME(math_state_restore)
pushl $0 # temporary storage for ORIG_EIP
call SYMBOL_NAME(math_emulate)
addl $4,%esp
ret
ENTRY(debug)
pushl $0
pushl $ SYMBOL_NAME(do_debug)
jmp error_code
ENTRY(nmi)
pushl $0
pushl $ SYMBOL_NAME(do_nmi)
jmp error_code
ENTRY(int3)
pushl $0
pushl $ SYMBOL_NAME(do_int3)
jmp error_code
ENTRY(overflow)
pushl $0
pushl $ SYMBOL_NAME(do_overflow)
jmp error_code
ENTRY(bounds)
pushl $0
pushl $ SYMBOL_NAME(do_bounds)
jmp error_code
ENTRY(invalid_op)
pushl $0
pushl $ SYMBOL_NAME(do_invalid_op)
jmp error_code
ENTRY(coprocessor_segment_overrun)
pushl $0
pushl $ SYMBOL_NAME(do_coprocessor_segment_overrun)
jmp error_code
ENTRY(reserved)
pushl $0
pushl $ SYMBOL_NAME(do_reserved)
jmp error_code
ENTRY(double_fault)
pushl $ SYMBOL_NAME(do_double_fault)
jmp error_code
ENTRY(invalid_TSS)
pushl $ SYMBOL_NAME(do_invalid_TSS)
jmp error_code
ENTRY(segment_not_present)
pushl $ SYMBOL_NAME(do_segment_not_present)
jmp error_code
ENTRY(stack_segment)
pushl $ SYMBOL_NAME(do_stack_segment)
jmp error_code
ENTRY(general_protection)
pushl $ SYMBOL_NAME(do_general_protection)
jmp error_code
ENTRY(alignment_check)
pushl $ SYMBOL_NAME(do_alignment_check)
jmp error_code
ENTRY(page_fault)
pushl $ SYMBOL_NAME(do_page_fault)
jmp error_code
ENTRY(spurious_interrupt_bug)
pushl $0
pushl $ SYMBOL_NAME(do_spurious_interrupt_bug)
jmp error_code
显然系统服务是根据功能号在系统服务函数表里面找到函数入口,再调用相应过程。这要了解什么就可以看相关的系统服务,比如要了解文件系统就可以看sys_open,sys_read,sys_write看下去就可以搞清文件系统怎么个结构怎么过处理。LINUX的文件系统还是比较成熟的了,但别的不怎么样,层次结构不好。还有一个应该要仔细看的就是 sys_execve,sys_fork,就是加载程序、创建进程,看下去可以了解LINUX给予一个程序、进程的环境。同时也可以注意系统服务里面是怎么对付一些想突破保护模式保护的恶意尝试。
.data
ENTRY(sys_call_table)
.long SYMBOL_NAME(sys_ni_syscall) /* 0 - old "setup()" system call*/
.long SYMBOL_NAME(sys_exit)
.long SYMBOL_NAME(sys_fork)
......
<br>生如夏花之绚丽,死如秋叶之静美。<br>
上文作者希望有人能补充Setup.S的内容,佳佳这里补上。。。
jjgirl 于 00-7-12 10:46:31 加贴在 UNIX系统安全:
首先,Setup.S对已经调入内存的操作系统代码进行检查,如果没有错误的话,他会通过BIOS中断获取内存容量信息,设置键盘的响应速度,显示器的基本模式,获取硬盘信息,是否有PS/2鼠标等,这些都是在i386的实模式下进行的。
这时,系统就准备进行让CPU进入保护模式了。当然首先要屏蔽信号,然后再次设置32位启动代码的位置。
完成上面的工作后,操作系统指令lidt和lgdt被调用了,中断向量表(idt)和全局描述符表(gdt)就出现了。
这是我在分析内核时总结的,但是我还是有一些疑问。
希望大侠们赐教:
1.流程简图:
Hardware device driver
-------------------------------------
|
|(call netif_rx() )
|
---------------------------------------------------------
| |(net_bh() will dispacth packet according pack_type)
+----------------+
| ip_packet_type |
| handler |
+----------------+
|
|(call netif_rx()
| to pass to other
| device device)
+--------------------------------------------------------+
| |
| in ip_rcv(),it will dispatch packet to inet_protocol |
| handler |
| |
+--------------------------------------------------------+
2. In include/linux/if_packet.h,I find some packet types,but i don't
know what they exactly mean,and how kernel handle packet with these
types.(such PACKET_HOST,PACKET_BROADCAST,I don't find any piece of
code to handle them!!!)
3. I am still confused with struct dst_entry and struct dst_op,can somebody
tell how kernel make use of them?
//thx