内核相关问题:kill init和内核结构的释放

嵌入式技术

1368人已加入

描述


如果在命令行执行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


    打开APP阅读更多精彩内容
    声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
    • 相关推荐

    全部0条评论

    快来发表一下你的评论吧 !

    ×
    20
    完善资料,
    赚取积分