linux内核相关的两个问题

来源:互联网 发布:nba2k17亚洲人捏脸数据 编辑:程序博客网 时间:2024/04/30 02:59

如果在命令行执行kill -9 1,那么结果是没有反应,连个提示都没有,实际上init进程是杀不死的,到底为何呢?kill指令实际上是发信号,如果一个进程对一个信号没有反应那么 原因可能有以下三点:1.该进程屏蔽了此信号;2.该进程是内核线程,手动屏蔽了此信号;3.内核忽略了此信号.我们看看init进程,它不是内核线程 (实际上在rest_init之初的init是内核线程,只是它马上exec到用户空间了),而且SIGKILL(9)是用户线程所不能忽略和屏蔽的,因 此只有第三种可能,内核忽略了此信号,找找代码,看看下面的函数就得到了确切的答案。

int get_signal_to_deliver(siginfo_t *info, struct k_sigaction *return_ka, struct pt_regs *regs, void *cookie)

{

...

         spin_lock_irq(¤t->sighand->siglock);

         for (;;) {

...

                 if (current->pid == 1)//由pid判断是否传送信号,执行到此,信号只可能是SIGKILL或STOP之类的巨猛信号了,如果是init进程那么忽略,不向它传送

                         continue;

...

                 do_group_exit(signr);            

         }

         spin_unlock_irq(¤t->sighand->siglock);

         return signr;

}

因此我们知道内核只是通过进程的pid来识别init进程的,如果我们找到init,然后修改它的pid,使之不再为1,是否就可以杀死init进程了呢?理论是这样,难道真的是这么简单吗?于是我写下以下的模块:

#include

#include

static __init int test_init(void)

{

    task_t *task=find_task_by_pid(1);  //找到1号进程的task_struct结构指针

        task->pid = 3314;  //将init的pid改为一个没有使用的pid

        force_sig(SIGKILL,task);  //然后杀死它,就是杀死init进程

        return 0;

}

static __exit void test_exit(void)

{

    return ;

}

module_init(test_init);

module_exit(test_exit);

MODULE_LICENSE("Dual BSD/GPL");

MODULE_AUTHOR("Zhaoya");

MODULE_DESCRIPTION("kill init");

MODULE_VERSION("Ver 0.1");

结果如何呢?当然是系统崩溃,linux临死前coredump,调用堆栈中有choose_new_parent函数,找到choose_new_parent函数看了一下:

static inline void choose_new_parent(task_t *p, task_t *reaper, task_t *child_reaper)

{

         BUG_ON(p == reaper || reaper->state >= TASK_ZOMBIE);

         p->real_parent = reaper;

         if (p->parent == p->real_parent)  //这里出错,p是init的孩子,p->parent当然是init,而reaper也是init,所以if为真,BUG乎!

                 BUG();

}

那里为何出错呢?实际上SIGKILL信号真的发给了init,然后init就do_exit了,当中调用了以下函数:参数的father就是init本身,该函数过继了init的孩子们给一个新的父亲。

static inline void forget_original_parent(struct task_struct * father, struct list_head *to_release)

{

         struct task_struct *p, *reaper = father;

         struct list_head *_p, *_n;

         do {

                 reaper = next_thread(reaper);

                 if (reaper == father) {

                         reaper = child_reaper;//child_reaper在初始化时设置为init进程,于是到此为止father和reaper是一样的。

                         break;

                 }

         } while (reaper->state >= TASK_ZOMBIE);

         list_for_each_safe(_p, _n, &father->children) {

                 int ptrace;

                 p = list_entry(_p,struct task_struct,sibling);

                 ptrace = p->ptrace;

                 BUG_ON(father != p->real_parent && !ptrace);

                 if (father == p->real_parent) {

                         choose_new_parent(p, reaper, child_reaper);//到了此点。

                         reparent_thread(p, father, 0);

                 } else {

...

}

因 此init进程不可杀并不是用户空间的策略,而是内核的机制,是操作系统的一部分,操作系统用这个机制实现了很多事情,比如僵尸进程管理回收问题,多用户安全问题等等,不要指望杀死init了,即使通过rootkit做到了,试问有意义吗?就是个儿戏罢了,没有实用性的。 
下面谈谈内核野指针问题,这其实是一个争论的话题,争论主题就是要将不用的指针清零还是采用懒惰策略,待下次使用的时候再清零。前者更安全,后者更高效,两全不得其美,必选其一。如果说发生了内核错误,我的期望是马上出错马上崩溃,不然等到以后出错的时间就是不确定的了,那会很不安全,同样给调试带来困 难,安全性永远比性能重要。 
我采用了一个很极端的例子,在一个进程执行的时候释放掉其task_struct,为了使事情简单,我的进程如下:

int main()

{

    while(1){}

}

编译后运行,ps后它的pid是2732,然后写如下模块:

#include

#include

static __init int test_init(void)

{

    task_t *task=find_task_by_pid(2732); 

    free_task(task);

    return 0;

}

static __exit void test_exit(void)

{

         return ;

}

module_init(test_init);

module_exit(test_exit);

MODULE_LICENSE("Dual BSD/GPL");

MODULE_AUTHOR("Zhaoya");

MODULE_DESCRIPTION("free task");

MODULE_VERSION("Ver 0.1");

加 载模块后,只要不要动键盘,一点问题没有,程序依旧完好地运行,内核没有崩溃,但是一旦敲入ps,内核立马崩溃,让我们来看看这是为什么。简单起见,我用 2.6.9内核分析。free_task实际调用kmem_cache_free(task_struct_cachep, (task)),而后者就是:

static inline void __cache_free (kmem_cache_t *cachep, void* objp)

{

         struct array_cache *ac = ac_data(cachep);

         check_irq_off();

         objp = cache_free_debugcheck(cachep, objp, __builtin_return_address(0));

         if (likely(ac->avail < ac->limit)) {

                 STATS_INC_FREEHIT(cachep);

                 ac_entry(ac)[ac->avail++] = objp;

                 return;

         } else {

                 STATS_INC_FREEMISS(cachep);

                 cache_flusharray(cachep, ac);

                 ac_entry(ac)[ac->avail++] = objp;

         }

}

仅 此而已,操作task_struct的字段了吗?没有,实际上task_struct还是那个task_struct,一点没变,内核要想取该 task_struct的某个字段,照取不误,还是原来的地址(内核的前896m与物理内存一一对应),只是操作了slab的指针。因此进程依旧完好运 行,那为何一旦ps就崩溃了呢?或者ls也崩溃。我们知道,你执行ps或ls以及任何一个命令的时候,shell都要fork出一个新进程,而fork要分配一个task_struct,该task_struct当然在slab分配,我们看看分配代码:

static inline void * __cache_alloc (kmem_cache_t *cachep, int flags)

{

         unsigned long save_flags;

         void* objp;

         struct array_cache *ac;

         cache_alloc_debugcheck_before(cachep, flags);

         local_irq_save(save_flags);

         ac = ac_data(cachep);

         if (likely(ac->avail)) {

                 STATS_INC_ALLOCHIT(cachep);

                 ac->touched = 1;

                 objp = ac_entry(ac)[--ac->avail];

         } else {

                 STATS_INC_ALLOCMISS(cachep);

                 objp = cache_alloc_refill(cachep, flags);

         }

         local_irq_restore(save_flags);

         objp = cache_alloc_debugcheck_after(cachep, flags, objp, __builtin_return_address(0));

         return objp;

}

看 看这个--ac->avail,再对比一下前面释放时的ac->avail++,这里最新分配的task_struct就是最新被释放的 task_struct,而这个task_struct就是那个我的模块中被变态释放的task_struct了,于是新进程一产生,原来的被释放的进程的task_struct的所有字段几乎都要被重置,如果我敲入了ps,那么那个while循环进程的task_struct将和ps的 task_struct一样,因此乱套崩溃在情理之中,为何乱套?最简单的例子,内核要调度while,切出while循环,但是他们的 task_struct一样,如果你是内核你该咋办,绝对崩溃,要你放下一个桔子拿起另一个桔子,而这两个桔子是一个桔子,你难道不崩溃吗?呵呵。 
因此,我认为因该在slab对象初始化时只初始化公共部分,只要释放一个slab对象到slab中,那么就把非公共部分清零,这样才安全,把一切清零,不要什么工公共部分更安全,当然这样slab的ctor构造函数也就没有必要了,不过这样的效率会小低一些,仅仅小低一些。还是为了简单,我写下如下模块,释放后随即将整个结构清零,不保留任何公共部分:

#include

#include

static __init int test_init(void)

{

    task_t *task=find_task_by_pid(1824); 

    free_task(task);

    memset(task,0,sizeof(task_t));  //将task_struct清零,实际上就是将其内部的字段清零,包括指针变量,由此消除了野指针

    return 0;

}

static __exit void test_exit(void)

{

         return ;

}

module_init(test_init);

module_exit(test_exit);

MODULE_LICENSE("Dual BSD/GPL");

MODULE_AUTHOR("Zhaoya");

MODULE_DESCRIPTION("zero task");

MODULE_VERSION("Ver 0.1");

还 是以上面的while循环作试验,加载模块后,内核立即崩溃,达到了目的。但是内核是怎么崩溃的呢?很简单,典型的有两个时机,一个是时钟中断,一个是调度,当然还有别的很多,只是这两个更具有典型性,在这两个时机场景的处理时候会用到current,而它就是被释放那的task_struct,字段全是 0,这样很容易就崩溃了,可能访问了0指针也就是空指针,也可能是别的。在应用程序设计时,我们大谈特谈野指针的危害,内核中也要关注它,不过不必像用户 空间那么过分关注它,毕竟内核中做的事情应该形成一种约定,这样就不用浪费资源去验证这验证那了,内核做的事情要少而有效,最重要的是保证安全。 
task_struct来自slab,而slab有ctor构造函数和dtor析构函数,dtor我们想想实际上是没有什么用的,于是在最新的内核中就去掉了,不再为slab对象提供析构函数了(参考slub)。 
附:2.6内核模块的编译 
1.写好模块c文件,文件名为:XX.c; 
2.写Makefile文件,内容:obj-m += XX.o 
3.make -C /lib/modules/`uanem -r`/build/ SUBDIRS=$PWD modules

0 0
原创粉丝点击