TSS中保存的栈信息,是在特权级转移的时候被用到,具体就是在从低特权级转移到高特权级的时候会用。注意 从低到高

问题:为什么要保存 段寄存器值和通用寄存器的值?
既然是为了实现多任务,那么任务之间显然要进行切换,切换任务的时候需要保存当前任务上下文,那么什么是任务上下文? 具体就是任务执行时的关键寄存器的值,包括段寄存器和通用寄存器的值。把这些寄存器值保存之后,等任务切换回来的时候,直接把这些寄存器值恢复 任务的执行状态就恢复了。

TSS这个数据结构中很大一部分就是保存了寄存器的值的。

在X86处理器中,如果说特权级进行了转移,那么所使用的栈也会发生变化,每一个特权级使用自己一个独立的栈,不同特权级的栈是相互独立的。所以TSS中也保存了 任务所需要的不同特权级的栈信息,这些不同特权级的栈信息是怎么表示的呢? 其实就是保存 ss寄存器的值 和 esp寄存器的值。众所周知 栈需要两个寄存器来表示,第一个是ss寄存器 保存栈基地址,第二个是esp寄存器 保存栈顶地址。

这样保存之后的好处是什么?
很明显,假如我们现在要从 3特权级 跳转到 0特权级执行,发生特权级的转移了,要切换栈,此时所需要的 0特权级的栈信息 就可以直接到 TSS结构体中找就可以了。

注意 TSS结构体中 只保存了3个栈的信息 分别是特权级 0 1 2 的栈信息



调用门可以做 特权级的转移,从低特权级转移到高特权级,转移的时候栈的变化是这样的:

1 首先从 TSS中获取 高特权级栈的信息(包括栈基地址 ss寄存器值,栈顶指针位置 esp寄存器值)

2 获取之后,将 低特权级的栈信息(包括栈基地址 ss寄存器值,栈顶指针位置 esp寄存器值) 压入到 高特权级栈中,此操作的意义就是为了返回,函数调用完需要返回,返回的时候就从 高特权级转移到低特权级了,也就发生了特权级变化,那么栈就会发生变化。

回忆上一节知识,高特权级跳转到低特权级,我们在跳转的时候,手工的将低特权级栈的信息压入了栈中,并且将低特权级代码段入口压入栈中,然后使用 retf指令 。

如果是 调用门的话 在调用的瞬间,低特权级的栈信息以及低特权级的代码段信息 都会被压入栈中,这样 当遇到 retf指令的时候,就会将低特权级的栈信息 从高特权级的栈中取出来 恢复到 ss寄存器和 esp寄存器。


TSS中保存的栈信息,是在特权级转移的时候被用到,具体就是在从低特权级转移到高特权级的时候会用。注意 从低到高,而一共只有0 1 2 3,四个特权级,没有比3特权级更低的特权级,所以不可能有特权级从比3更低的特权级 转移到 3特权级。


注意:
1, 32位核心代码段和数据段 特权级为0 模拟内核态

2, 32位任务代码段和数据段 特权级为3 模拟用户态

3, 在系统启动后会首先执行内核态核心代码 之后就会跳转到 任务代码去执行,此时就是 高特权级 到 低特权级的转移(retf 远返回)。这里模拟的就是操作系统启动后去执行某个应用程序。模拟操作系统内核加载执行 应用程序

4, 在用户态的应用程序中 调用 内核高特权级代码,即系统函数。这个时候就涉及到了特权级转移,必然要陷入内核态,对应实验当中 就是使用调用门来做特权级的转移,完成某个任务,然后返回。


1 当系统开始执行了之后,显然是在实模式的,我们需要转换到保护模式执行,转换到保护模式之后,特权级为0,对应的就是核心代码段的执行。

2 核心代码段做好工作后,通过远返回执行指令 从高特权级0的内核态 跳转到 低特权级为3的用户态执行任务,模拟操作系统内核加载执行 应用程序

3 低特权级为3的用户态 执行任务中 需要调用一个系统函数,那么就要陷入内核态,这个过程的本质就是特权级转移了,通过调用门从3特权级的用户态转移到特权级为0的内核态,然后执行系统函数,执行完之后 又通过远返回执行 做特权级的转移,从特权级为0的内核态系统函数代码段 转移到 特权级为3的用户态任务代码段。


特权级转移时会发生栈变换,栈信息到TSS结构体中查找,TSS结构体存在于内存当中,既然TSS 要存在于内存当中,那么他就应该是保护模式下的一个段,所以必然要有相应的段描述符和选择子,但凡内存中的一个段 就会有相应的段描述符 和 选择子,TSS结构体也不例外。

在TSS结构体定义好之后,如何使用呢?
通过 ltr指令加载使用。

实验 :

实验说明:

32位保护模式下的
CODE32_DESC   代码段
DATA32_DESC  数据段
STACK32_DESC 栈段
特权级都是0,用来模拟 内核态,CODE32_DESC 模拟内核 核心代码段32位保护模式下的 FUNCTION_DESC 特权级为0 用来模拟 内核态系统函数32位保护模式下的
TASK_A_CODE32_DESC
TASK_A_DATA32_DESC
TASK_A_STACK32_DESC
特权级都是3 用来模拟用户态 任务代码段

makefile

; Segment Attribute
DA_32    equ    0x4000
DA_DR    equ    0x90
DA_DRW   equ    0x92
DA_DRWA  equ    0x93
DA_C     equ    0x98
DA_CR    equ    0x9A
DA_CCO   equ    0x9C
DA_CCOR  equ    0x9E; Segment Privilege
DA_DPL0     equ   0x00    ; DPL = 0
DA_DPL1     equ   0x20    ; DPL = 1
DA_DPL2     equ   0x40    ; DPL = 2
DA_DPL3     equ   0x60    ; DPL = 3; Special Attribute
DA_LDT       equ    0x82
DA_TaskGate  equ    0x85    ; 任务门类型值
DA_386TSS    equ    0x89    ; 可用 386 任务状态段类型值
DA_386CGate  equ    0x8C    ; 386 调用门类型值
DA_386IGate  equ    0x8E    ; 386 中断门类型值
DA_386TGate  equ    0x8F    ; 386 陷阱门类型值; Selector Attribute
SA_RPL0    equ    0
SA_RPL1    equ    1
SA_RPL2    equ    2
SA_RPL3    equ    3SA_TIG    equ    0
SA_TIL    equ    4; 描述符
; usage: Descriptor Base, Limit, Attr
;        Base:  dd
;        Limit: dd (low 20 bits available)
;        Attr:  dw (lower 4 bits of higher byte are always 0)
%macro Descriptor 3                           ; 段基址, 段界限, 段属性dw    %2 & 0xFFFF                         ; 段界限1dw    %1 & 0xFFFF                         ; 段基址1db    (%1 >> 16) & 0xFF                   ; 段基址2dw    ((%2 >> 8) & 0xF00) | (%3 & 0xF0FF) ; 属性1 + 段界限2 + 属性2db    (%1 >> 24) & 0xFF                   ; 段基址3
%endmacro                                     ; 共 8 字节; 门
; usage: Gate Selector, Offset, DCount, Attr
;        Selector:  dw
;        Offset:    dd
;        DCount:    db
;        Attr:      db
%macro Gate 4dw    (%2 & 0xFFFF)                      ; 偏移地址1dw    %1                                 ; 选择子dw    (%3 & 0x1F) | ((%4 << 8) & 0xFF00) ; 属性dw    ((%2 >> 16) & 0xFFFF)              ; 偏移地址2
%endmacro

loader.asm

%include "inc.asm"org 0x9000jmp ENTRY_SEGMENT[section .gdt]
; GDT definition
;                                 段基址,       段界限,       段属性GDT_ENTRY       :     Descriptor    0,            0,           0
CODE32_DESC     :     Descriptor    0,    Code32SegLen - 1,    DA_C + DA_32 + DA_DPL0
;注意显存段特权级为3
VIDEO_DESC      :     Descriptor 0xB8000,     0x07FFF,         DA_DRWA + DA_32 + DA_DPL3
DATA32_DESC     :     Descriptor    0,    Data32SegLen - 1,    DA_DR + DA_32 + DA_DPL0
STACK32_DESC    :     Descriptor    0,     TopOfStack32,       DA_DRW + DA_32 + DA_DPL0
;32位保护模式下的代码段,特权级0,模拟系统函数
FUNCTION_DESC   :     Descriptor    0,   FunctionSegLen - 1,   DA_C + DA_32 + DA_DPL0
;局部段描述附表,该段中定义 特权级为3的用户态 任务代码,模拟务代码段
TASK_A_LDT_DESC :     Descriptor    0,     TaskALdtLen - 1,    DA_LDT + DA_DPL0
;TSS任务状态段 段描述符
TSS_DESC        :     Descriptor    0,       TSSLen - 1,       DA_386TSS + DA_DPL0; Gate Descriptor 调用门描述符
;定义调用门描述符,对应 FUNCTION_DESC 代码段中两个函数的入口地址
;该调用门描述符中保存  FUNCTION_DESC段(函数段)的选择子,以及段内偏移地址(PrintString)(指的是该代码段中某个函数的入口地址)
;参数个数为0 表明我们没有使用栈来传递参数,而是使用寄存器来传递参数
;属性 是 调用门类型值 DA_386CGate  equ   0x8C    ; 386 调用门类型值,而特权级只能定义为3,因为他是给应用程序调用的,太高了应用程序就没权限使用它了
;                                           选择子,            偏移,          参数个数,         属性
FUNC_PRINTSTRING_DESC    :    Gate      FunctionSelector,   PrintString,       0,         DA_386CGate + DA_DPL3; GDT endGdtLen    equ   $ - GDT_ENTRYGdtPtr:dw   GdtLen - 1dd   0; GDT Selector
Code32Selector     equ (0x0001 << 3) + SA_TIG + SA_RPL0
VideoSelector      equ (0x0002 << 3) + SA_TIG + SA_RPL3
Data32Selector     equ (0x0003 << 3) + SA_TIG + SA_RPL0
Stack32Selector    equ (0x0004 << 3) + SA_TIG + SA_RPL0
FunctionSelector   equ (0x0005 << 3) + SA_TIG + SA_RPL0
TaskALdtSelector   equ (0x0006 << 3) + SA_TIG + SA_RPL0
;TSS任务状态段 段描述符选择子
TSSSelector        equ (0x0007 << 3) + SA_TIG + SA_RPL0
; Gate Selector
FuncPrintStringSelector    equ   (0x0008 << 3) + SA_TIG + SA_RPL3
; end of [section .gdt];任务状态段  本质上也是一块内存,所以也需要遵循32位保护模式的编程规则,需要定义对应的段描述符+选择子
[section .tss]
[bits 32]
TSS_SEGMENT:dd    0dd    TopOfStack32            ; 0 特权级对应的栈信息 栈顶指针dd    Stack32Selector         ; 0 特权级对应的栈信息 栈段基指针dd    0                       ; 1 特权级对应的栈信息 暂时为空dd    0                       ;dd    0                       ; 2 特权级对应的栈信息 暂时为空dd    0                       ;times 4 * 18 dd 0           ; 往下全部定义为0,因为次实验不涉及多任务切换,所以不需要使用那些保存寄存器值的字段dw    0dw    $ - TSS_SEGMENT + 2db    0xFF                    ; 结束符TSSLen    equ    $ - TSS_SEGMENTTopOfStack16    equ 0x7c00[section .s16]
[bits 16]
ENTRY_SEGMENT:mov ax, csmov ds, axmov es, axmov ss, axmov sp, TopOfStack16; initialize GDT for 32 bits code segmentmov esi, CODE32_SEGMENTmov edi, CODE32_DESCcall InitDescItemmov esi, DATA32_SEGMENTmov edi, DATA32_DESCcall InitDescItemmov esi, STACK32_SEGMENTmov edi, STACK32_DESCcall InitDescItemmov esi, FUNCTION_SEGMENTmov edi, FUNCTION_DESCcall InitDescItem;初始化 任务A的局部段描述符表 的段描述符的段基址信息mov esi, TASK_A_LDT_ENTRYmov edi, TASK_A_LDT_DESCcall InitDescItem;初始化 局部段描述符表中的 数据段 的 段描述符的段基址信息mov esi, TASK_A_DATA32_SEGMENTmov edi, TASK_A_DATA32_DESCcall InitDescItem;初始化 局部段描述符表中的  可执行代码段  的 段描述符的段基址信息mov esi, TASK_A_CODE32_SEGMENTmov edi, TASK_A_CODE32_DESCcall InitDescItem;初始化 局部段描述符表中的  栈段 的 段描述符的段基址信息mov esi, TASK_A_STACK32_SEGMENTmov edi, TASK_A_STACK32_DESCcall InitDescItem;初始化 TSS 段描述符段基址信息mov esi, TSS_SEGMENTmov edi, TSS_DESCcall InitDescItem; initialize GDT pointer structmov eax, 0mov ax, dsshl eax, 4add eax, GDT_ENTRYmov dword [GdtPtr + 2], eax; 1. load GDTlgdt [GdtPtr]; 2. close interruptcli ; 3. open A20in al, 0x92or al, 00000010bout 0x92, al; 4. enter protect modemov eax, cr0or eax, 0x01mov cr0, eax; 5. jump to 32 bits codejmp dword Code32Selector : 0; esi    --> code segment label
; edi    --> descriptor labelInitDescItem:push eaxmov eax, 0mov ax, csshl eax, 4add eax, esimov word [edi + 2], axshr eax, 16mov byte [edi + 4], almov byte [edi + 7], ahpop eaxret[section .dat]
[bits 32]
DATA32_SEGMENT:DTOS               db  "D.T.OS!", 0DTOS_OFFSET        equ DTOS - $$Data32SegLen equ $ - DATA32_SEGMENT[section .s32]
[bits 32]
CODE32_SEGMENT:mov ax, VideoSelectormov gs, axmov ax, Data32Selectormov ds, axmov ax, Stack32Selectormov ss, axmov eax, TopOfStack32mov esp, eax;打印内核数据段当中的字符串,用于说明已经执行核心代码段mov ebp, DTOS_OFFSETmov bx, 0x0cmov dh, 12mov dl, 33;通过 段选择子:段内偏移地址 的方式 调用打印函数;不发生特权级转移,因为本段内存段特权级 与 FunctionSelector段特权级 都是0call FunctionSelector : PrintString;加载 TSS 结构体mov ax, TSSSelectorltr ax;加载局部段描述符表mov ax, TaskALdtSelectorlldt ax;从高特权级内核态核心代码段 跳转到 低特权级用户态任务代码段
;模拟操作系统内核加载执行 应用程序;将关键寄存器压入栈;将低特权级用户态任务代码段的栈信息 压入到 当前内核态核心代码段的高特权级栈中push TaskAStack32Selector ;栈选择子push TaskATopOfStack32     ;栈顶地址;将低特权级用户态任务代码段的选择子,即入口地址 压入到 当前内核态核心代码段的高特权级栈中push TaskACode32Selector;将低特权级用户态任务代码段中目标代码的 偏移地址 0 压入 当前内核态核心代码段的高特权级栈中push 0;跳转到 低特权级用户态任务代码段,此时就会发生特权级转移retfCode32SegLen    equ    $ - CODE32_SEGMENT[section .gs]
[bits 32]
STACK32_SEGMENT:times 1024 * 4 db 0Stack32SegLen equ $ - STACK32_SEGMENT
TopOfStack32  equ Stack32SegLen - 1; ==========================================
;
;          Global Function Segment
; 模拟内核系统函数,从操作系统的角度看 这就是内核中的函数
; ==========================================[section .func]
[bits 32]
FUNCTION_SEGMENT:; ds:ebp    --> string address
; bx        --> attribute
; dx        --> dh : row, dl : col
PrintStringFunc:push ebppush eaxpush edipush cxpush dxprint:mov cl, [ds:ebp]cmp cl, 0je endmov eax, 80mul dhadd al, dlshl eax, 1mov edi, eaxmov ah, blmov al, clmov [gs:edi], axinc ebpinc dljmp printend:pop dxpop cxpop edipop eaxpop ebpretfPrintString    equ   PrintStringFunc - $$FunctionSegLen    equ   $ - FUNCTION_SEGMENT; ==========================================
;
;            Task A Code Segment
;
; ==========================================[section .task-a-ldt]; Task A LDT definition
;                                             段基址,                段界限,                段属性TASK_A_LDT_ENTRY:
TASK_A_CODE32_DESC    :    Descriptor          0,           TaskACode32SegLen - 1,        DA_C + DA_32 + DA_DPL3
TASK_A_DATA32_DESC    :    Descriptor          0,           TaskAData32SegLen - 1,        DA_DR + DA_32 + DA_DPL3
TASK_A_STACK32_DESC   :    Descriptor          0,           TaskAStack32SegLen - 1,       DA_DRW + DA_32 + DA_DPL3
TaskALdtLen  equ   $ - TASK_A_LDT_ENTRY
; Task A LDT SelectorTaskACode32Selector  equ   (0x0000 << 3) + SA_TIL + SA_RPL3
TaskAData32Selector  equ   (0x0001 << 3) + SA_TIL + SA_RPL3
TaskAStack32Selector equ   (0x0002 << 3) + SA_TIL + SA_RPL3[section .task-a-dat]
[bits 32]
TASK_A_DATA32_SEGMENT:TASK_A_STRING        db   "This is Task A!", 0TASK_A_STRING_OFFSET equ  TASK_A_STRING - $$TaskAData32SegLen  equ  $ - TASK_A_DATA32_SEGMENT;用户态低特权级任务代码段的 栈 大小1024Byte
[section .task-a-gs]
[bits 32]
TASK_A_STACK32_SEGMENT:times 1024 db 0TaskAStack32SegLen  equ  $ - TASK_A_STACK32_SEGMENT
;栈顶
TaskATopOfStack32   equ  TaskAStack32SegLen - 1;模拟用户态低特权级 任务代码段
[section .task-a-s32]
[bits 32]
TASK_A_CODE32_SEGMENT:  ;设置 用户态任务数据段寄存器mov ax, TaskAData32Selectormov ds, ax;打印函数参数,通过寄存器传参,没有用栈传参mov ebp, TASK_A_STRING_OFFSETmov bx, 0x0cmov dh, 14mov dl, 29;通过调用门描述符的选择子 调用高特权级的目标函数;说明 这里的0是语法需要 表示是段间跳转 0本身无意义,但是不能删除;如果删除后 就变成了 call FuncPrintStringSelector,成了段内跳转,意义本身发生变化,所以必须留着;通过调用门描述符的选择子 调用高特权级的目标函数 那么此时就会发生特权级转移,而特权级转移的时候栈;也会变化,所以我们就要找到高特权级的栈信息,到TSS中去找高特权级的栈信息call FuncPrintStringSelector : 0jmp $
TaskACode32SegLen   equ  $ - TASK_A_CODE32_SEGMENT

关于显存段特权级设置为3的合理性分析:
将显存段特权级设置为3,即便应用程序向显存段中写入了乱七八糟的数据,后果最多就是屏幕上显示乱七八糟的数据,不会有更严重的后果,可以接受

将机器码 反编译成 32位的汇编代码
ndisasm -b 32 -o 0x9000 loader > loader.txt

可以定位到 特权级切换前后,查看关键寄存器值 CS寄存器值,CS寄存器最低2位 为CPL 即特权级。可证明跳转成功。


如果说 当前所执行的代码段的 CPL 为 0,那是不是就意味着 他可以访问任何资源了呢?

答案是否定的,因为还要看它的请求特权级 RPL,如果太低 也是不能够访问对应资源的。


当前代码想要访问某个放在数据段中的资源,于是就要去做请求,通过选择子进行请求,处理器通过 CPL RPL DPL 共同确定该请求是否合法,如果合法就放行,可以访问数据段中的资源。

特权级转移之 高,低特权级互相转移,模拟操作系统内核态与用户态切换以及应用程序调用系统资源相关推荐

  1. 高特权级代码段转向低特权级代码段(利用 ret(retf) 指令实现 jmp from ring0 to ring3)

    [0]写在前面 0.1)本代码旨在演示 从 ring0 转移到 ring3(即,从高特权级 转移到 低特权级) 0.2)本文 只对 与 门相关的 代码进行简要注释,言简意赅: 0.3)文末的个人总结是 ...

  2. 操作系统之进程管理(上),研究再多高并发,都不如啃一下操作系统进程!!!...

    目录: 进程管理 程序运行过程 进程实体的组成 进程的组织 进程的状态与转换 进程控制 为什么需要原语? 原语的实现? 中断机制 进程通信 共享内存 管道通信 消息传递 小结 线程 三种线程模型 多对 ...

  3. 《Orange’s 一个操作系统的实现》3.保护模式7-特权级转移(通过调用门转移目标段-无特权级转换)...

    在上次的代码基础上,添加一个代码段作为通过调用门转移的目标段.了解一下调用的工作方法,代码分析如下: <<红色标识部分为新增代码>> ; =================== ...

  4. 代码段间转移控制时的特权级检查(JMP/CALL)——《x86汇编语言:从实模式到保护模式》读书笔记28

    代码段间转移控制时的特权级检查(JMP或者CALL指令) 在保护模式下,JMP或CALL指令可以用以下四种方法之一来引用另外一个代码段: 1. 目标操作数含有目标代码段的段选择子和偏移 2. 目标操作 ...

  5. 真香!百度网盘超级会员等级制度,等级越高,特权越多!容量,解压,转存上限,回收站保存时间,全都有!

    百度网盘超级会员等级制度,等级越高,特权越多!容量,解压,转存上限,回收站保存时间,全都有! (手机端升级最新版本就可以查看哦) 百度网盘免费用户容量是2T,超级会员可以扩容至5T,会员过期即收回空间 ...

  6. 四管前级怎么去掉高低音音调_TDG Audio达芬奇:什么是前级,后极?

    前置放大器:接在音源和功率放大器之间.别名:前级.功率放大器:接在音箱之前,能驱动音箱.别名:后级.功放.纯功放等.它们的功能:前级主要是后级功放提供合适的音频电平信号,调节音质的,如高低音效果,左右 ...

  7. 信噪比:高端科研级相机的核心参数

    信噪比:高端科研级相机的核心参数 如果从成像结果来考察一款科研相机,最重要的一般有3个特征: 黑白还是彩色.彩色相机能带来颜色信息,但灵敏度和分辨率都不及同参数的黑白相机. 帧速.无论是高速移动的样品 ...

  8. 四管前级怎么去掉高低音音调_烧友自荐:2SK304四管前级制作难点浅析

    用2SK304制作的liushuliang四管前级完全可以堪称是一款名机电路,但在日常实际制作中遭遇失败,屡见不鲜.分析原因,少数则是管子选择不当,大多则是电路自激.可以发生在某一级,也可以多级同时发 ...

  9. 百度可观测系列 | 如何构建亿级指标的高可用 TSDB 存储集群?

    [百度云原生导读]在前一篇<采集亿级别指标,Prometheus 集群方案这样设计中>,我们为大家介绍了针对针对亿级指标场景,百度云原生团队基于Prometheus 技术方案的研究,包括资 ...

最新文章

  1. 原本要与Hinton当同事,最后被迫Bengio门下读博?| 独立研究员的坎坷之路
  2. 1.3 单一数字评估指标-深度学习第三课《结构化机器学习项目》-Stanford吴恩达教授
  3. Python脚本实现图片加水印
  4. 阿里格林深瞳计算机视觉岗实习面经
  5. CVE-2022-0847-DirtyPipe-Exploit
  6. 接口规范 11. 串流相关接口
  7. Xcode升级后插件失效
  8. [渝粤教育] 西南科技大学 经济法学 在线考试复习资料(1)
  9. 绘制AutoCad中的曲线(Curve)
  10. 软路由ros(MIKROTIK)安装教程:[11]端口映射
  11. C++ “system“的详解
  12. 电驴服务器软性文件,电驴服务器.doc
  13. C#查找Excel()重复项
  14. mencoder MPlayer 参数详细
  15. git常用命令+代码上传冲突+vscode拉取代码报would clobber existing tag错误
  16. item_sku-获取淘宝商品sku详细信息接口接入获取方案
  17. 关于深度学习目标检测的一些改进方法
  18. 学生网课网页设计成品 在线视频学习类网页制作 三层结构网页模板 静态HTML注册登录网页模板 学生毕业设计网页制作作品 网校类网页代制做
  19. Neo4J 统计根节点、叶节点个数
  20. java 堆栈 pop_为什么Joshua Bloch在有效的java中减少pop方法中堆栈的“大小”值?...

热门文章

  1. 为什么很多善良的人一生痛苦、磨难很多?
  2. 考研党的寒假学习之spring+hibernate(1)
  3. 会计计算机敲打大赛,老会计的办公史 从打算盘到敲键盘
  4. 新手入门前端代码单文件在线编辑器:codepen
  5. 机器学习如何评估模型结果的好坏
  6. 轻量级Rpc框架设计--motan源码解析六:client端服务发现
  7. 使用 JS 阻止网页的内容被选中
  8. java 窗口大小改变事件_onresize
  9. 理解面向消息的中间件和JMS
  10. 【虚拟网络编辑器】vmnet8设置中出现错误,子网IP和子网掩码不一致