免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 2815 | 回复: 6
打印 上一主题 下一主题

[内核入门] 内核态的“来”和“去” [复制链接]

论坛徽章:
13
15-16赛季CBA联赛之八一
日期:2016-07-08 21:00:1415-16赛季CBA联赛之同曦
日期:2017-02-15 14:26:1515-16赛季CBA联赛之佛山
日期:2017-02-20 14:19:2615-16赛季CBA联赛之青岛
日期:2017-05-07 16:49:1115-16赛季CBA联赛之广夏
日期:2017-07-30 09:13:1215-16赛季CBA联赛之广东
日期:2018-07-05 22:34:3615-16赛季CBA联赛之江苏
日期:2018-09-03 12:10:2115-16赛季CBA联赛之上海
日期:2018-09-25 03:49:2215-16赛季CBA联赛之广东
日期:2018-09-25 04:09:12
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2017-03-08 16:21 |只看该作者 |倒序浏览
本帖最后由 _nosay 于 2017-03-08 16:21 编辑

    计算机执行的过程,实质上就是硬件状态不断变化的过程,不管是简单的指令跳转,还是比较难理解的用户态与内核态切换。

  • 用户态、内核态
    如果把CPU想象成一座城市,不同进程(各个用户进程、各个内核进程)相当于发生在这个城市的不同的事情,用户态、内核态相当于做同一件事情发展到不同阶段时这个城市的状态。内核是在电脑刚启动,就加载到内存的一段指令和数据(先不较真.ko模块的动态加载),CPU起初按照实模式规则执行指令,这些指令逐步为一些管理机制做好准备,时机成熟后,打开EFLAGS相应标志位后切换到保护模式运行(软件可以向硬件描述做什么事,而为软件本身的执行建立规则也是一件事,只要硬件为表达这件事提供了充分的“单词”),多进程、用户态内核态切换各属于一种规则。
    ① 一个CPU中只有一个CS、EIP,本来不可能同时做多件事,无法表达现实世界,所谓多进程,只是让计算机在多件事件之间快速切换执行,表现出多件事“同时”进行的假象(图1),达到模拟现实世界的目的。
       硬件的设计决定了CS、EIP始终自动指向下一条指令,那么让它沿虚线箭头移动的“动力”是什么?
       先感性认识如下:
       ⑴ 可以在实线箭头的末端安排一个“沿虚线箭头移动”的指令;
       ⑵ 硬件上可能出现另一个“动力”,强行让它沿虚线箭头移动。
    ② 为了专注于用户态、内核态的理解,可以先撇开多进程的干扰,只理解CPU做一件事情的过程(图2),不妨以单进程就可以完成的“解析配置”为例:
       ⑴ 执行应用程序代码,分配缓存,调用系统调用切换为内核态;
       ⑵ 执行内核代码,读磁盘,将数据复制或映射到用户态可以访问的内存,切换为用户态;
       ⑶ 执行应用程序代码,解析磁盘数据。
       这个过程中,并没有切换进程,始终是为了完成解析配置这件事,只是由于不同阶段的需求,将硬件改变为不同的状态,比如阶段⑵的硬件状态要保证可以访问内核空间的指令、数据(段寄存器RPL值为0)等。
  
    ③ “内核指令”与“用户指令”区别:
        指令只关心当前的硬件状态,不关心是被内核程序员还是应用程序员使用。
        首先,编译器只是把高级语言翻译成CPU支持的机器指令,所以应用程序里使用特权指令,编译不会报错;另外,应用程序、内核在同一个CPU上执行,甚至如果能利用漏洞,从内核手中夺下大权,使得执行到应用程序中的特权指令时,满足指令对硬件状态的要求,执行也是没问题。所以,根本没有“用户指令”和“内核指令”的概念,软件利用特权指令设计了用户态、内核态的区分,而不是反过来用这两种状态区分“用户指令”、“内核指令”。

    硬件提供的特权指令有限,内核如何利用有限有权力达到全面限制应用程序乱来的目的的?
    就如同皇帝有杀头的权力,就有收税的权力,不交税就杀头,就有让老百姓乖乖听话的权力,不听话就杀头。内核为应用程序的执行设置一个框架,这个框架中有一些必经的门槛,而门槛依赖特权指令。只不过从管理角度考虑,这种限制是对所有人有利的,皇帝没有杀头的权力,大家就全都有杀头的权力了,皇帝不建立管理制度,世界就会很乱。

     总结:
     简单的指令跳转,也是一种状态的切换,至少CS、EIP寄存器的值要改变,用户态内核态的切换,只不过是很复杂的状态切换,要修改更多的硬件状态(比如SS、ESP指向内核栈,段寄存器rpl值修改为0等),仅此而已,不要把内核态想复杂了,越觉得它复杂可能就意味着你误会的越深。
     内核的代码,除了在程序用指令跳转,还可能被硬件强行“调用”,对于软件带有不可预测、透明性。
     进程从用户态切换到内核态,权限提高了,但与此同时CS、EIP也指向内核程序员写的代码了,从攻击的角度考虑,应用程序企图利用内核态的权限搞事,只能靠找到内核漏洞,让内核代码把用户态传的参数当指令执行。

  • 中断、陷阱、异常
    中断、陷阱、异常广义上都是中断(由于狭义中也有“中断”一词,所以用蓝色表示广义)。
    ① 中断与函数调用相比,除了在指令序列中表现了一次“跳转”动作,即CS、EIP寄存器改变,还有可能有更多的硬件状态发生改变,完成用户态与内核态切换;
    ② 中断对于应用软件有可能是不可预测的、透明的;
    ③ 从用户态进入内核态依赖中断,但中断并不一定导致用户态切换到内核态(比如发生中断时,就已经在内核态)。

    理解后面介绍的“门”以后,才能真正理解中断、陷阱、异常的区别,暂时大概了解即可:
   


    1. 门的样子
   
    ① 段描述符表
        段描述符表中通常为普通段描述符(s=1,GDTR/LDTR + 段寄存器/门中段选择码高13位);
        段描述符表中可能有TSS段描述符(s=0,GDTR/LDTR + TR寄存器/任务门中TSS段选择码高13位);
        段描述符表中可能有门描述符(s=0,除了int指令+IDT可以找到门,jmp/call指令+GDT/LDT也可以找到门)。
        intel手册对jmp指令的描述:
      
    ② 中断描述符表
        中断描述符表中可能为任务门(用于找到一个TSS结构内容);
        中断描述符表中可能为中断门、陷阱门、调用门(结构基本一致,用于找到一个函数)。
        助记:
        101:来任务-消灭任务-又来任务→任务门
        110:一直为1,最后断掉变成0了→中断门
        111:一排刺刀放在陷阱里→陷阱门
        100:每次考试100分,很diǎo→调用门
    ③ 任务门、调用门
        用于任务切换,但Linux内核几乎不使用这两种门,以下是直接通过任务门,完成任务切换的流程,可以发现它的2个缺点:
        ⑴ TSS结构包含了用于任务切换所有可能需要的信息,但不同条件下的任务切换,通常只需要交换其中一部分信息,而不用全部交换;
        ⑵ 每次任务切换,TR寄存器的值都会被修改,导致高级缓存中整个TSS结构内容都要从内存重新读一次。
      
        所以硬件的这种设计,让软件本可以用“一条指令”穿过任务门,就可以达到任务切换的目的,但由于以上缺点导致的效率问题,Linux内核并不使用任务门,也不使用调用门,而是在软件层实现进程切换(用task_struct结构记录进程信息),并且内核启动时设置好TR寄存器后,TSS段的位置就固定了,而只进程自己的TSS内容,修改TSS结构中需要修改的部分内容。

    2. 门的进入
    听过一个笑话:有个人迷路了,还好看见一个老太太,但老太太非常古怪,非要问路的人提个问题,老太太能答对,才为他指路。可他问什么,可耻的老太太都说“太深了”,假装不会。后来他问“有个人从井里打水,但为什么几天几夜都没打上来?”,老太太回答“太深了”,对了

    CPU中的CS、EIP就像笑话中的老太太,始终朝着下一条指令,要打破这个“疆局”,只能智取或者暴力的方法:
    ① 在CS、EIP经过的路上安排一个陷阱,比如放一条int指令或可能产生异常的指令,让CPU“不自觉”的走进门里(系统调用/异常);
    ② 外设向CPU中断引脚加电,将它强行拉到门里(外设中断)。
    每种进入门的场景,都对应着实际的需求,也正是广义中断为什么分中断、陷阱、异常的原因。由于指令执行、检查硬件状态、感应中断引脚的变化,都只有硬件本身才能做到,所以进入门的逻辑实现是在硬件上,而进入门干什么却是留给软件设置,什么时候进入门也给软件留了一部分选择。
   

  • come
    上述已经介绍过,Linux内核只使用陷阱门(系统调用、异常)和中断门(外设中断),以下详细分析系统调用、异常、外设中断穿过门以及穿过门之后的过程有什么区别。

    1. 权限检查、IF标志位影响
   
    ① 穿过门的过程中,总共会遇到4个跟权限相关的地方:段寄存器rpl(cpl)、门描述符dpl、门中的段选择子rpl、门目标段dpl,中断门与陷阱门的权限检查规则不同,内核初始化时对用于异常和系统调用的陷阱门描述符dpl设置也不同;
    ② 穿过中断门后,自动关中断,穿过陷阱门则保持IF标志位不变;
    ③ 陷阱门保留给内核自己用(0x20以下中断号+0x80,每个中断号的函数固定,操作系统的设计和实现必须遵守),中断门一般给外设驱动用(0x20以上中断号)。

    既然cpl代表是否有资格进入、穿过门,进程在用户态时有办法修改它而随意进入内核态吗?
    以下代码希望把当前CS寄存器的rpl改成0,并执行2:处的代码,去掉注释就会coredump,显然真正走到电路层修改段寄存器时,是有判断的,如果进程想通过直接修改段寄存器提升权限,硬件会产生异常,然后进入机器启动时安排的异常处理处执行(内核代码),将进程kill掉。
  1. #include <stdio.h>

  2. int main()
  3. {
  4. __asm__ __volatile__(
  5.     //"movw %%ax,%%cs\n\t"   // Illegal instruction (core dumped)

  6.     "call 1f\n\t" // push CS,IP
  7.     "1:popq %%rax\n\t"            // get IP
  8.     "addq $7,%%rax\n\t"          // make IP to 2: ($7?? "objdump -d" plz)
  9.     //"popw %%bx\n\t"            // get CS
  10.     //"andw $0xfffc,%%bx\n\t"    // make CS.rpl=0
  11.     //"pushw %%bx\n\t"          // modify CS
  12.     "pushq %%rax\n\t"            // modify IP
  13.     "ret\n\t"
  14.     "2:\n\t"
  15.     ::
  16.     );

  17.     printf("yhea~\n");
  18.     return 0;
  19. }
复制代码

    以下是从intel手册找到的,但好像没有找到修改CS段寄存器的说明,不过通过修改其它段寄存器的说明,也能看出来有些情况硬件是会产生异常的。
  

    2. Linux内核初始化阶段的相关准备
    ① 中断向量表中,0x20以下以及0x80号门都是陷阱门,并且每个门的意义是固定的,为内核本身保留,内核的设计,必须遵守硬件的约定,比如在14号门必须安排页面异常处理函数。
        通过trap_init()函数,了解每个门对应的函数分别是什么即可,以及理解_set_gate()函数中的汇编代码。
        

    ② 中断向量表中,0x20以上除0x80号门,都用于中断门(224个)。
        分析代码可知,内核安排0x00..、0xdf号门分别对应IRQ0x00_interrupt()、..、IRQ0xdf_interrupt()中断处理函数,这些函数分别将0x00、..、0xdf压入栈中,并跳转到common_interrupt标号处执行。
        

    ③ common_interrupt
    大部分中断门,是留给驱动使用的,内核无法预测将来会有哪些具体操作安排到这些门里,IRQ0xXX_interrupt()→common_interrupt→do_IRQ()其实是内核为驱动做的基本准备,do_IRQ()的进入,其中不同操作之间的互不干扰,都由内核做好基本准备,do_IRQ()中具体做什么,什么时候做,由驱动使用内核提供的接口决定。
   
    注意:
    ⑴ IRQ0xXX_interrupt()→common_interrupt,common_interrupt→do_IRQ(),都是用的jmp指令
        由于“jmp do_IRQ”前执行了“pushl $ret_from_intr”,所以do_IRQ()函数中执行ret指令时,就会“返回”到ret_from_intr处执行,也就是说,IRQ0x00_interrupt()、..、IRQ0xdf_interrupt()这些函数,看似间接调用了do_IRQ()函数,但do_IRQ()执行完却不返回到这些函数,而是都“返回”到同一处ret_from_intr。
        如果在每个IRQ0xXX_interrupt()函数后面加一条“jmp ret_from_intr”,common_interrupt、do_IRQ就可以用call指令跳转了,但显然不比jmp的方法直接了当,为什么要返回到IRQ0xXX_interrupt()才能执行ret_from_intr呢,是吧。对于执行频繁的底层操作,往往连一个时钟周期的节约都不放过的。
    ⑵ do_IRQ()的参数类型是struct pt_regs,而不是struct pt_regs*,因为do_IRQ()栈帧上方就是硬件和IRQ0xXX_interrupt()压入栈的pt_regs结构内容,并不是一个指向这块内容起始地址的指针。

    ④ 时钟中断、页面异常、系统调用比较
   
    栈中有个位置称“original eax”(先忽略为什么叫这个名字):
    ⑴ “origin eax”上方内容由硬件自动压入
       上述说明过,用户态不能通过修改段寄存器rpl值提升权限,用户态切换到内核态之前,自然就没办法通过指令将当前的状态存到内核栈(即使用户态有权访问内核栈,内核栈的位置也还得到TSS段找呢,而访问TR寄存器也是特权指令);
       从用户代码区切换到内核代码区,也面临同样的问题,用户态直接修改CS段寄存器rpl值提升权限,会被硬件阻拦,并且也不知道该切换到哪执行;
       所以,将用户态CS、EIP、SS、ESP、EFLAGS保存到内核栈(由TR指向的TSS结构内容获知),并且将用户栈切换到内核栈、用户指令区切换到内核指令区,这个过渡阶段由硬件主动帮忙完成(其中SS、ESP只有在穿过门前后权限级别变化时才切换),在这之后,就是内核代码在执行了,后面将ds、es段寄存器设置为__KERNEL_DS,就可以通过软件自己完成了;
    ⑵ 不同的情况,“origin eax”处填入的内容不同:
        外设中断,由IRQ0xXX_interrupt()填写中断号的负数形式;
        有些异常,硬件会在“origin eax”处填一个异常码(表示异常具体原因),硬件不填异常码的异常,由软件填补一个,保持后续处理的一致性;
        系统调用,“original eax”处填系统调用号。
    ⑶ TSS中为什么只有SS0、ESP0、SS1、ESP1、SS2、ESP2、,而没有SS3、ESP3?
        门的权限检查机制,保证了穿过门不可能从0、1、2级别向3级别切换,向3级别切换顶多是3级别本身,而当前SS、ESP就已经是SS3、ESP3,不用去TSS找。
    ⑷ “origin eax”下方内容由软件填写
        虽然此时在内核栈,执行内核的代码,但这些寄存器的值,从用户态切换前直到此时,都没有被改变过,比如SS、ES段段寄存器rpl都还是3呢,换句话说,从内核态切回用户态时,这些信息可以完全恢复切换到内核态之前的样子。
    ⑸ 页面异常过程,由于要向栈中压入异常处理函数地址,所以与中断、系统调用稍微有些不一样。
    ⑹ 异常处理函数又遇到同样的异常,造成循环的进入异常处理程序怎么办?(以后有空再总结)


    • 基于软中断的bottom-half
    1. “关中断”解释
    关中断、加锁,都可以防止函数重入。它们的区别是,从硬件角度看,标志寄存器的IF位始终不动,设计CPU电路时就知道它在硬件中的位置,而软件将来在内存什么位置申请一把锁,设计CPU时是无法预测的。所以关中断防止的是硬件(中断)导致的重入,加锁防止的是软件(多核程序、多进程程序、多线程程序)导致的重入。

    2. bottom-half由来
    先体会一下中断处理函数执行过程的两种情况:
    ① 如果整个过程开中断:时钟中断处理函数需要考虑被新来的时钟中断打断,执行到一半又要重头执行该函数(即重入),而有些操作是必须不允许重入的,比如累加一个全局变量;
    ② 如果整个过程关中断:由于需要很长时间执行完func(),才能在中断函数结束前开中断,所以可能丢失很多中断(中断的原因已经发生了,但CPU由于关中断没有响应)。

    可以看出全程开中断、全程关中断,在特定的情况下,都有严重的缺陷。
    所以诞生了bottom-half,举一个定时器的例子:时钟中断每发生100次时,执行一个比较耗时并且可重入的func()函数。
    可以按照bottom-half的思路,关中断时,执行累加操作,达到100次时,做个标记,并清零计数;开中断时,根据标记执行func()。这样,func()执行结束前如果又发生99次时钟中断,每次都不会导致func()重头执行;即使又达到100次,func()函数也被要求写成可重入的了。所以,该执行的还是执行了,但关中断的时间却大大缩小了。

    那么,如果func()函数特别耗时,不可能在100次时钟中断之内执行完,那么func()不是始终没机会完整执行吗?
    这个问题就如同你要求自己每天看一本书,又自责每本书都看不懂一样,我们无法叫醒一个装睡的人,内核又怎么纠正程序员故意犯的错误呢?

    3. 软中断
    bottom-half是一种特殊软中断,bh函数是否执行,直接的依据是软件设置的标志,而不是硬中断。上述说明过,软件也会导致重入,而且是通过加锁保证安全,而bottom-half在加锁方面做的过于保守了,后来新版本的Linux内核,设计了软中断框架,这个框架兼容老的bh函数,并提供更多的选择(毕竟过度的保护对于有些bh函数是多余的,去掉一些保护可以提高系统性能)。

    以下是软中断框架的初始化、填充和执行过程,可以对照源码,理解各个函数设置或使用了哪些变量,和它们执行的过程和时机,以及它是如何做到兼容bottom-half的:
   

  • go
    1. 进程调度
    上述说过,Linux的进程切换,不使用任务门和调用门,切换的时机,放在内核态返回用户态时。

    2. 软中断
    上述已经介绍过bottom-half机制,还有tasklet机制等,它们对具体操作的同步程度要求不一样。

    3. 信号
    理解了软中断,信号也就不难理解了,区别是信号处理函数由应用程序设置,而软中断的具体执行函数都在内核空间,信号设计中有意思的是,从内核态切换到用户态,执行完信号处理函数,不需要用户态的信号处理函数做什么,又能自动回到内核态。

  • 总结
    本帖猪头蛇尾,头特别大,尾特别轻,“go”部分详细总结的话,本应该有很多很多内容,但已经不是那么难理解了。


评分

参与人数 1信誉积分 +10 收起 理由
nswcfd + 10 细节是魔鬼,赞一个。

查看全部评分

论坛徽章:
20
程序设计版块每日发帖之星
日期:2015-08-17 06:20:00程序设计版块每日发帖之星
日期:2016-07-16 06:20:00程序设计版块每日发帖之星
日期:2016-07-18 06:20:00每日论坛发贴之星
日期:2016-07-18 06:20:00黑曼巴
日期:2016-12-26 16:00:3215-16赛季CBA联赛之江苏
日期:2017-06-26 11:05:5615-16赛季CBA联赛之上海
日期:2017-07-21 18:12:5015-16赛季CBA联赛之青岛
日期:2017-09-04 17:32:0515-16赛季CBA联赛之吉林
日期:2018-03-26 10:02:16程序设计版块每日发帖之星
日期:2016-07-15 06:20:0015-16赛季CBA联赛之江苏
日期:2016-07-07 18:37:512015亚冠之萨济拖拉机
日期:2015-08-17 12:21:08
2 [报告]
发表于 2017-03-16 15:16 |只看该作者
中断助记符是自己总结的么? 太牛了

论坛徽章:
13
15-16赛季CBA联赛之八一
日期:2016-07-08 21:00:1415-16赛季CBA联赛之同曦
日期:2017-02-15 14:26:1515-16赛季CBA联赛之佛山
日期:2017-02-20 14:19:2615-16赛季CBA联赛之青岛
日期:2017-05-07 16:49:1115-16赛季CBA联赛之广夏
日期:2017-07-30 09:13:1215-16赛季CBA联赛之广东
日期:2018-07-05 22:34:3615-16赛季CBA联赛之江苏
日期:2018-09-03 12:10:2115-16赛季CBA联赛之上海
日期:2018-09-25 03:49:2215-16赛季CBA联赛之广东
日期:2018-09-25 04:09:12
3 [报告]
发表于 2017-03-16 21:20 |只看该作者
回复 2# nswcfd

是的

论坛徽章:
12
寅虎
日期:2013-12-04 20:37:4915-16赛季CBA联赛之广东
日期:2017-08-22 19:23:1215-16赛季CBA联赛之上海
日期:2016-06-18 23:05:05操作系统版块每日发帖之星
日期:2016-06-06 06:20:00操作系统版块每日发帖之星
日期:2016-06-05 06:20:00操作系统版块每日发帖之星
日期:2016-06-03 06:20:002015年辞旧岁徽章
日期:2015-03-03 16:54:152015年亚洲杯之巴勒斯坦
日期:2015-02-10 21:38:08卯兔
日期:2014-10-31 20:42:23申猴
日期:2014-06-11 17:15:10处女座
日期:2014-05-22 09:00:1815-16赛季CBA联赛之广夏
日期:2017-09-25 23:37:46
4 [报告]
发表于 2017-03-17 18:29 |只看该作者

你再这样下去,找一份月薪30k的工作我看没问题。

论坛徽章:
13
15-16赛季CBA联赛之八一
日期:2016-07-08 21:00:1415-16赛季CBA联赛之同曦
日期:2017-02-15 14:26:1515-16赛季CBA联赛之佛山
日期:2017-02-20 14:19:2615-16赛季CBA联赛之青岛
日期:2017-05-07 16:49:1115-16赛季CBA联赛之广夏
日期:2017-07-30 09:13:1215-16赛季CBA联赛之广东
日期:2018-07-05 22:34:3615-16赛季CBA联赛之江苏
日期:2018-09-03 12:10:2115-16赛季CBA联赛之上海
日期:2018-09-25 03:49:2215-16赛季CBA联赛之广东
日期:2018-09-25 04:09:12
5 [报告]
发表于 2017-03-17 23:42 |只看该作者
回复 4# wait_rabbit

好的,我会坚持的

论坛徽章:
4
2015年亚洲杯之卡塔尔
日期:2015-04-27 11:18:032015亚冠之阿尔沙巴布
日期:2015-05-14 10:34:182015亚冠之阿尔艾因
日期:2015-05-18 18:12:2615-16赛季CBA联赛之八一
日期:2017-03-14 14:22:11
6 [报告]
发表于 2017-03-18 09:52 |只看该作者
感谢分享所得

论坛徽章:
3
处女座
日期:2015-03-18 14:35:45羊年新春福章
日期:2015-03-18 14:48:23午马
日期:2015-03-18 14:51:09
7 [报告]
发表于 2017-03-27 15:27 |只看该作者
   LDT呢?
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP