内核态 用户态 切换模式切换用户模式 数据在哪个栈

《重要》从用户模式切换到内核模式的完整过程分析
《重要》从用户模式切换到内核模式的完整过程分析
Windows编程
&&&《重要》从用户模式切换到内核模式的完整过程分析
Windows定义了两种访问模式:用户模式和内核模式。应用程序代码运行在用户模式下,操作系统代码运行在内核模式下。
内核模式对应处理器的最高权限级别。在内核模式下执行的代码可以访问所有资源并可以执行所有特权指令。用户模式具有较低的优先级,用户模式只能访问用户空间,且不能执行特权指令。
如果用户代码不慎访问了系统空间的数据或执行了特权指令将会导致保护性异常的发生。但是用户代码可以通过调用系统服务来间接的访问系统空间中的数据或间接执行系统空间中的代码。当调用系统服务时,调用线程会从用户模式切换到内核模式,调用结束后再返回用户代码。这就是所谓的模式切换,也被称为上下文切换。
一、使用 INT 2E 切换到内核模式
2e 号向量号专门被用来做系统调用。在 windbg 中可以输入: !idt 2e 来查看该向量号在 IDT 对应的异常处理函数。如:
可以看到 2e 号向量对应的异常处理函数为: nt!KiSystemService。该函数是内核中用以分发系统调用的。
下面我们以调用 ReadFile 为例来展示使用 int2e 指令进行系统调用的步骤:
因为 ReadFile 是从 Kernel32 导出的,所以我们看到调用首先转到了Kernel32 的 ReadFile 函数。在 ReadFile 中又调用了 ntdll!NtReadFile 函数 ntdll.dll是内核空间和用户控件的桥梁,用户空间的代码通过这个 dll 来调用内核空间的系统服务。它会被加载到所有用户进程的进程空间中,且位于同一位置。
& 下图为 ntdll !NtReadFile 函数反汇编代码:
通过反汇编代码可以看到 ntdll!NtReadFile 非常的短,首先将请求的读文件的系统调用的系统服务号 0xa1&放入 eax 寄存器,然后便通过 INT 2e 指令引发系统调用异常。 INT 2e 会导致陷阱异常 。异常发生时,发生异常代码处得
CS 和EIP 寄存器会被压入堆栈,用于在处理完异常后的返回。然后系统会在 IDT 表中查询 2e 号向量对应的异常处理函数,得到 KiSystemService 函数地址。
在调用内核态的 KiSystemService 函数值前, cpu 会做一些模式切换工作。包括权限检查和准备内核态使用的栈空间。 N t!KisystemService ,得到传入的系统调用号,并将传入的参数从用户态复制到内核态,调用系统读取文件的系统调用。执行完毕后, nt!KiSystemService 会将操作结果复制到用户态。弹出 CS 和EIP 将执行权交给 NtReadFile 用以执行 INT 2e 后面的指令。
二、快速系统调用
前面介绍了 Windows 使用 INT 2e 来实现系统调用。但是它是使用中断机制实现的,伴随着中断产生的还有权限检查和查询 IDT 表等操作,这会导致额外的开销。因此在最新版本的 Windows 中,微软采用了被称为快速系统调用的机制。这主要是得益于 Intel 从奔腾 2 开始在处理器新加的三个特殊的 MSR 寄存器以及sysenter 和 sysexit 指令。
现在我们通过Windows API OpenProcess的调用过程,来看看Windows 7中快速系统调用过程:
前面的部分还是一样的:因为OpenProcess是从Kernel32导出的,所以调用首先转到了Kernel32的OpenProcess函数。在OpenProcess中又调用了ntdll!NtOpenProcess函数。
我们再来看看ntdll!NtOpenProcess的反汇编代码。
eax中保存系统调用号,此处NtOpenProcess的为BEh;edx是SharedUserData!SystemCallStub的地址,里面保存着KiFastSystemCall的地址。SharedUserData总是存放在0x7ffe0000处,其偏移0×300处正是SystemCall。
我们来看看0x7ffe0300处到底是不是SystemCall。
好了,我们看到了sysenter!!!
Sysenter时eax中保存着系统调用号,edx中保存着用户空间线程栈栈顶地址即保存着NtOpenProcess中call KiFastSystemCall的返回地址。
与指令对INT/IRET不同,快速系统调用指令对SYSENTER/SYSEXIT不具有调用、返回关系,因为指令SYSENTER并不会为指令SYSEXIT保存任何返回信息,不指示SYSEXIT返回到何处继续执行,也就是说指令SYSEXIT并不一定返回到指令SYSENTER后的下一个条指令继续执行。这两条指令所需的相关信息由处理器内部的一组相关的寄存器(MSR)提供,这些寄存器的名称及用途如下。
MSR_IA32_SYSENTER_CS:保存了系统调用处理过程所使用的内核态代码段的段选择子,用于在通过指令SYSENTER进入内核态时,设置代码段段选择子寄存器cs。同时,紧随在该寄存器所指示段描述符后面的三个段描述符被依次认为是内核数据段、用户代码段、用户数据段的段描述符(这些段描述符保存在全局描述符表GDT中,有先后次序)。快速系统调用指令SYSENTER/SYSEXIT依赖这些次序来完成内核态、用户态执行环境的设置工作。
MSR_IA32_SYSENTER_EIP:保存了系统调用处理过程的入口地址,用于在通过指令SYSENTER进入内核态时,设置指令指针寄存器eip。
MSR_IA32_SYSENTER_ESP:保存了系统调用处理过程所使用内核态的栈指针信息,用于在通过指令SYSENTER进入内核态时,设置内核态栈的栈指针寄存器esp。
1.请求快速系统调用处理过程
在用户态的代码执行了SYSENTER指令之后,处理器中的控制单元将会完成以下操作,然后进入内核态进行系统调用的处理。
(1)将寄存器MSR_IA32_SYSENTER_CS所指示的段描述符装载到代码段段选择子寄存器cs中。
(2)将寄存器MSR_IA32_SYSENTER_EIP的值装载到指令指针寄存器eip中。
(3)将寄存器MSR_IA32_SYSENTER_CS的值加8作为一个段选择子,然后将该段选择子对应的段描述符装载到栈基址寄存器ss中。
(4)将寄存器MSR_IA32_SYSENTER_ESP的值装载到栈指针寄存器esp中。
2.快速系统调用返回处理过程
在内核态对系统调用服务完毕之后,执行指令SYSEXIT完成系统调用的返回过程。在该过程中处理器的控制单元完成以下处理,设置用户态的执行环境。
(1)将寄存器MSR_IA32_SYSENTER_CS的值加16作为一个段选择子,然后将该段选择子对应的段描述符装载到代码段段选择子寄存器cs中。
(2)将寄存器edx的值装载到指令指针寄存器eip中。
(3)将寄存器MSR_IA32_SYSENTER_CS的值加24作为一个段选择子,然后将该段选择子对应的段描述符装载到栈段段选择子寄存器ss中。
(4)将寄存器ecx的值装载到栈指针寄存器esp中。
由上面的分析可知,在使用指令SYSENTER请求快速系统调用之前,需要初始化好相关的型号相关寄存器;在使用指令SYSEXIT进行快速系统调用返回之前,还要保证寄存器ecx、 edx的正确性,以便能正确地返回到用户态继续运行。
在内核的初始化过程中,函数enable_sep_cpu()负责完成这组寄存器的初始化:
void&enable_sep_cpu(void)
& & & &//调用宏定义get_cpu()首先禁用内核态抢占,然后返回当前处理器在系统中的编号.
& & & &int&cpu&=&get_cpu();
& & & &//获得当前处理器对应的任务状态段的地址,并将该地址保存在局部变量tss中。
& & & &//宏定义per_cpu()用于获得每处理器变量的本地拷贝。
& & & &struct&tss_struct&*tss&=&&per_cpu(init_tss,&cpu);
& & & &//判断负责系统引导的处理(BootSstrap Processor)是否支持快速系统调用指令SYSENTER/SYSEXIT,如果不支持,
& & & &//则调用宏定义put()使能内核态抢占,然后返回。也就是说,只有在系统引导处理器支持快速系统调用的情况下,
& & & &//才为系统中的每个处理器设置快速系统调用所需要的参数;否则,系统中不支持快速系统调用。
& & & &if&(!boot_cpu_has(X86_FEATURE_SEP))&{
& & & & & & put_cpu();
& & & & & &&return;
& & & &//设置当前任务状态段中的成员变量ss1、esp1分别为快速系统调用所使用的代码段段选择子(内核代码段__KERNEL_CS)
& & & &//和快速系统调用使用的临时(紧急)内核态栈的栈底。
& & & &tss-&ss1&=&__KERNEL_CS;
& & & &tss-&esp1&=&sizeof(struct&tss_struct)&+&(unsigned&long)&tss;
& & & &//调用宏定义wrmsr()设置快速系统调用所需的3个MSR寄存器。
& & & &//分别设置MSR_IA32_SYSENTER_CS为内核代码段段择子__KERNEL_CS;
& & & &wrmsr(MSR_IA32_SYSENTER_CS,&__KERNEL_CS,&0);
& & & &&//设置MSR_IA32_SYSENTER_ESP为任务状态段中预留的临时(紧急)内核态栈的栈底地址;
& & & &wrmsr(MSR_IA32_SYSENTER_ESP,&tss-&esp1,&0);
& & & &//设置MSR_IA32_SYSENTER_EIP为快速系统调用处理函数的入口地址sysenter_entry。
& & & &wrmsr(MSR_IA32_SYSENTER_EIP,&(unsigned&long)&sysenter_entry,&0);
& & & &//宏定义put_cpu()与get_cpu()相对,用于使能内核态抢占。
& & & &put_cpu();&&
WRMSR可以设置上面说的MSR寄存器,对应关系如下:
SYSENTER_CS_MSR 174H
SYSENTER_ESP_MSR 175H
SYSENTER_EIP_MSR 176H
我们来看看MSR寄存器相应位置保存的什么:
sysenter切换堆栈时是从用户栈切换到DPC栈,这是因为MSR寄存器中的值是操作系统安排好的固定的值,它与具体的线程上下文无关,所以需要在DPC栈中再切换到线程的内核栈。
kd&&u KiFastCallEntry l&80
nt!KiFastCallEntry:
3000000 & & &mov & & ecx,23h &&;&KGDT_R3_DATA&0x23&=&0x20&+&011b(CPL
a30 & & & & & &push & &30h & &;&KGDT_R0_PCR &0x30&=&0x30&+&000b(CPL
Ring0),&DPC
fa1 & & & & & &pop & & fs & &&;&DPC Stack
ed9 & & & & & &mov & & ds,cx
ec1 & & & & & &mov & & es,cx
8b0d &mov & & ecx,dword ptr fs:[40h]&&;fs
Base:ffdff000 Limit:1fff
Data RW,&KPCR&0x40处为TSS
Ptr32 _KTSS
b6104 & & & & &mov & & esp,dword ptr&[ecx+4]&&&;&KTSS&0x4处为Esp0
Uint4B,&Ring0下的esp
a23 & & & & & &push & &23h & &&;&此时已在内核堆栈,&Ring3下的ss,&KTRAP_FRAME.HardwareSegSs
&52&& & & & & & &push & &edx & &&;&Ring3下的esp,&KTRAP_FRAME.HardwareEsp
c & & & & & & &pushfd & & & & &;&eflags &;&KTRAP_FRAME.EFlags
a02 & & & & & &push & &2
c208 & & & & &add & & edx,8&&&;&Ring3堆栈的参数
d & & & & & & &popfd & & & & &&;&初始eflags为2,&即各位清零
c240102 & & &or & & &byte ptr&[esp+1],2&&
&&;&4字节eflags第二个字节的IF中断允许位置1
a1b & & & & & &push & &1Bh & & & & & & & & & &;&KGDT_R3_CODE&0x1B&=&0x18&+&011b(CPL
Ring3),&KTRAP_FRAME.SegCs
ff350403dfff & &push & &dword ptr ds:[0FFDF0304h]&&&;&KTRAP_FRAME.EIP,&0x7ffe0000与0xffdf000映射到同一块物理内存,&0xffdf0304处存放的是KiFastSystemCallRet
a00 & & & & & &push & &0&& &;&KTRAP_FRAME.ErrCode
;&以下四个寄存器从Ring3到Ring0没修改过,&直接保存
&55&& & & & & & &push & &ebp
&53&& & & & & & &push & &ebx
&56&& & & & & & &push & &esi
&57&& & & & & & &push & &edi
b1d1c000000 &mov & & ebx,dword ptr fs:[1Ch]&&
&&;&指向自己的指针,&头部即是TIB
a3b & & & & & &push & &3Bh & & & &;&KTRAP_FRAME.SegFs,&fs
Base:&Limit:fff
Data RW,&Ring3
bb & &mov & & esi,dword ptr&[ebx+124h]&&
&&;&KPCR.SelfPcr-&PrcbData.CurrentThead&Ptr32
ff33 & & & & & &push & &dword ptr&[ebx]&&
& & &&;&KTRAP_FRAME.ExceptionList,&TIB的头部即是ExceptionList
3ffffffff & &mov & & dword ptr&[ebx],0FFFFFFFFh
b6e28 & & & & &mov & & ebp,dword ptr&[esi+28h]&&
&&;&KTHREAD.InitialStack&Ptr32
a01 & & & & & &push & &1&& & &&;&KTRAP_FRAME.PreviousPreviousMode
ec48 & & & & &sub & & esp,48h & &&;&预留KTRAP_FRAME中Eax到DbgEbp的空间
ed9c020000 & &sub & & ebp,29Ch
63a &mov & & byte ptr&[esi+13Ah],1&&
&;&KTHREAD.PreviousMode
bec & & & & & &cmp & & ebp,esp
&7597&& & & & & &jne & & nt!KiFastCallEntry2+0x49&(8288a2fb)
52c00 & & & &and & & dword ptr&[ebp+2Ch],0&&&;&KTRAP_FRAME第12个(0x2C/4+1)参数Dr7
603df & & & &test & &byte ptr&[esi+3],0DFh
& &;&KTHREAD.DebugActive
ae & &mov & & dword ptr&[esi+128h],ebp
& &;&KTHREAD.TrapFrame&Ptr32
_KTRAP_FRAME
f8538feffff & &jne & & nt!Dr_FastCallDrSave&()&&&;&if(KTHREAD.DebugActive&!=&0)&...
b5d60 & & & & &mov & & ebx,dword ptr&[ebp+60h]
b7d68 & & & & &mov & & edi,dword ptr&[ebp+68h]
550c & & & & &mov & & dword ptr&[ebp+0Ch],edx
& &&;&将Ring3参数地址赋给KTRAP_FRAME.DbgArgPointer
508000ddbba &mov & & dword ptr&[ebp+8],0BADB0D00h
&&;&KTRAP_FRAME.DbgArgMark&=&0xBADB0D00
d00 & & & & &mov & & dword ptr&[ebp],ebx
& & &;&KTRAP_FRAME.DbgEbp&=&KTRAP_FRAME.Ebp
7d04 & & & & &mov & & dword ptr&[ebp+4],edi
& &;&KTRAP_FRAME.DbgEip&=&KTRAP_FRAME.Eip
8288a38e fb & & & & & & &sti
;&当调用KeServiceDescriptorTableShadow中的系统服务时,&eax是系统调用号再加0x1000
bf8 & & & & & &mov & & edi,eax
ef08 & & & & &shr & & edi,8
&83e710&& & & & &and & & edi,10h
bcf & & & & & &mov & & ecx,edi
;&经过以上4条指令后,&若ecx为0则为调用KeServiceDescriptorTable中的系统服务,&否则ecx为0x10调用KeServiceDescriptorTableShadow中的系统服务
bebc000000 & &add & & edi,dword ptr&[esi+0BCh]&&
&&;&edi&=&KTHREAD.ServiceTable&+&0x00或0x10
bd8 & & & & & &mov & & ebx,eax
25ff0f0000 & & &and & & eax,0FFFh &&;&系统调用号
3b4708 & & & & &cmp & & eax,dword ptr&[edi+8]&&
&;&系统调用号必须小于系统服务数
0f8333fdffff & &jae & & nt!KiBBTUnexpectedRange&()
8288a3af 83f910 & & & & &cmp & & ecx,10h
751a & & & & & &jne & & nt!KiFastCallEntry+0xce&(8288a3ce)&&&;&KeServiceDescriptorTable
8b8e & &mov & & ecx,dword ptr&[esi+88h]
8288a3ba 33f6 & & & & & &xor & & esi,esi
8288a3bc 0bb & &or & & &esi,dword ptr&[ecx+0F70h]&&
&;&TEB.GdiBatchCount
740a & & & & & &je & & &nt!KiFastCallEntry+0xce&(8288a3ce)&&
&;&KeServiceDescriptorTable
&52&& & & & & & &push & &edx & &;&Ring3参数地址
&50&& & & & & & &push & &eax & &;&系统调用号
ff154cf99a82 & &call & &dword ptr&[nt!KeGdiFlushUserBatch&(829af94c)]
8288a3cc&58&& & & & & & &pop & & eax
8288a3cd 5a & & & & & & &pop & & edx
8288a3ce 64ff05b0060000 &inc & & dword ptr fs:[6B0h]&&
&;&KeServiceDescriptorTable直接跳转到这来,&++KPCR.KPRCB.KeSystemCalls
8bf2 & & & & & &mov & & esi,edx
33c9 & & & & & &xor & & ecx,ecx
8b570c & & & & &mov & & edx,dword ptr&[edi+0Ch]&&&;&SSDT参数表的地址
8288a3dc 8b3f & & & & & &mov & & edi,dword ptr&[edi]&&&;&系统服务表地址
8288a3de 8a0c10 & & & & &mov & & cl,byte ptr&[eax+edx]&&
&;&该系统调用的参数个数
8b1487 & & & & &mov & & edx,dword ptr&[edi+eax*4]&&&;&该系统服务地址
2be1 & & & & & &sub & & esp,ecx &&;&在内核栈中分配参数空间
c1e902 & & & & &shr & & ecx,2&&&;&参数以DWORD大小拷贝
8bfc & & & & & &mov & & edi,esp
8288a3eb 3b351cf79a82 & &cmp & & esi,dword ptr&[nt!MmUserProbeAddress&(829af71c)]
0f832e020000 & &jae & & nt!KiSystemCallExit2+0xa5&()&&
&;&若参数地址超过用户空间地址(0x7fff0000)
f3a5 & & & & & &rep movs dword ptr es:[edi],dword
f6456c01 & & & &test & &byte ptr&[ebp+6Ch],1
8288a3fd&7416&& & & & & &je & & &nt!KiFastCallEntry+0x115&()
8288a3ff 648b0d &mov & & ecx,dword ptr fs:[124h]
b3c24 & & & & &mov & & edi,dword ptr&[esp]
93c010000 & &mov & & dword ptr&[ecx+13Ch],ebx
b92c010000 & &mov & & dword ptr&[ecx+12Ch],edi
bda & & & & & &mov & & ebx,edx
588c8978240 &test & &byte ptr&[nt!PerfGlobalGroupMask+0x8&()],40h
f954512 & & & &setne & byte ptr&[ebp+12h]
f858c030000 & &jne & & nt!KiServiceExit2+0x17b&()
ffd3 & & & & & &call & &ebx & & &&;&调用!
8288a42a f6456c01 & & & &test & &byte ptr&[ebp+6Ch],1
8288a42e&7434&& & & & & &je & & &nt!KiFastCallEntry+0x164&()
bf0 & & & & & &mov & & esi,eax
ff & &call & &dword ptr&[nt!_imp__KeGetCurrentIrql&()]
ac0 & & & & & &or & & &al,al
f853b030000 & &jne & & nt!KiServiceExit2+0x142&(8288a77b)
bc6 & & & & & &mov & & eax,esi
b0d &mov & & ecx,dword ptr fs:[124h]
ff &test & &byte ptr&[ecx+134h],0FFh
f & &jne & & nt!KiServiceExit2+0x160&()
b & &mov & & edx,dword ptr&[ecx+84h]
bd2 & & & & & &or & & &edx,edx
f & &jne & & nt!KiServiceExit2+0x160&()
be5 & & & & & &mov & & esp,ebp
d1200 & & & &cmp & & byte ptr&[ebp+12h],0
f & &jne & & nt!KiServiceExit2+0x187&()
b0d &mov & & ecx,dword ptr fs:[124h]
b553c & & & & &mov & & edx,dword ptr&[ebp+3Ch]
8288a47a&&& &mov & & dword ptr&[ecx+128h],edx
nt!KiServiceExit:
fa & & & & & & &cli
57202 & & & &test & &byte ptr&[ebp+72h],2&&
& &&;&eflags是否是Virtual-8086&Mode
&7506&& & & & & &jne & & nt!KiServiceExit+0xd&(8288a48d)&&
&&;&非Virtual-8086&Mode则跳
56c01 & & & &test & &byte ptr&[ebp+6Ch],1
8288a48b&7467&& & & & & &je & & &nt!KiServiceExit+0x74&()&&
& &;&KTRAP_FRAME.SegCs,&CPL为Ring0则跳
8b1d &mov & & ebx,dword ptr fs:[124h]&&&;&非Virtual-8086&Mode,&以及下面的交付APC,&跳到这来
30202 & & & &test & &byte ptr&[ebx+2],2
&7408&& & & & & &je & & &nt!KiServiceExit+0x22&()
8288a49a&50&& & & & & & &push & &eax
8288a49b&53&& & & & & & &push & &ebx
8288a49c e8a4dc0900 & & &call & &nt!KiCopyCounters&()
&58&& & & & & & &pop & & eax
c6433a00 & & & &mov & & byte ptr&[ebx+3Ah],0&&
&&;&KPCR.KPRCB.CurrentThread-&Alerted&=&0
807b5600 & & & &cmp & & byte ptr&[ebx+56h],0
8288a4aa&7448&& & & & & &je & & &nt!KiServiceExit+0x74&()&&
&&;&if(KPCR.KPRCB.CurrentThread-&ApcState.UserApcPending&==&0)跳
8288a4ac 8bdd & & & & & &mov & & ebx,ebp
8288a4ae&894344&& & & & &mov & & dword ptr&[ebx+44h],eax
& &;&KTRAP_FRAME.Eax,&系统调用号
c000 &mov & & dword ptr&[ebx+50h],3Bh
& &;&KTRAP_FRAME.SegFs&=&0x3B
c0 &mov & & dword ptr&[ebx+38h],23h
& &;&KTRAP_FRAME.SegDs&=&KGDT_R3_DATA
8288a4bf c0 &mov & & dword ptr&[ebx+34h],23h
& &;&KTRAP_FRAME.SegEs&=&KGDT_R3_DATA
c0 &mov & & dword ptr&[ebx+30h],0&&
& &;&KTRAP_FRAME.SegGs&=&0
下图为快速系统调用的完整过程:
现在我相信大家对用户模式到内核模式的切换有了一个比较清晰的认识。
本文链接:
转载提示:除非注明,AloneMonkey的文章均为原创,转载请以链接形式注明作者和出处。谢谢合作!
我的热门文章
即使是一小步也想与你分享从用户模式到内核模式
首先了解一下用户模式和内核模式。
一般说来,进程既可在用户模式下运行,又可在内核模式下运行。内核模式的权限高于用户模式的权限。进程每次调用一个系统调用时,进程的运行方式都发生变化:从用户模式切换到内核模式,然后继续执行。
可见:一个进程在CPU上运行可以有两种运行模式(进程状态):用户模式和内核模式。如果当前运行的是用户程序(用户代码),那么对应进程就处于用户模式(用户态),如果出现系统调用或者发生中断,那么对应进程就处于内核模式(核心态)。
在UNIX系统上,内核对外的接口是系统调用,系统调用以C函数的形式出现,所以内核外的程序都必须经由系统调用才能获得操作系统的服务。那么系统调用在核心模式运行和用户代码在用户模式运行有什么区别呢?
由于系统调用能直接进入内核执行,所以其执行效率很高。系统调用包含一种特殊的程序段,这些程序段成为原语,其特点是必须作为整体执行,不允许被中断,也不允许并发执行。你说效率高不高?
&&相关文章推荐
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:69228次
排名:千里之外
转载:35篇
(1)(1)(11)(1)(3)(9)(10)(6)(2)内核模式栈
kernel-mode stack
内核模式栈(Kernel-mode stack) 在OS中当应用程序传递参数给一个内核模式函数时,也会使用内核模式栈。
基于9个网页-
它显示线程的内核模式栈跟踪。
It displays the kernel mode stack trace of the thread.
$firstVoiceSent
- 来自原声例句
请问您想要如何调整此模块?
感谢您的反馈,我们会尽快进行适当修改!
请问您想要如何调整此模块?
感谢您的反馈,我们会尽快进行适当修改!1767人阅读
操作系统(5)
进程是程序的一次执行过程。用剧本和演出来类比,程序相当于剧本,而进程则相当于剧本的一次演出,舞台、灯光则相当于进程的运行环境。
进程的堆栈
每个进程都有自己的堆栈,内核在创建一个新的进程时,在创建进程控制块task_struct的同时,也为进程创建自己堆栈。一个进程 有2个堆栈,用户堆栈和系统堆栈;用户堆栈的空间指向用户地址空间,内核堆栈的空间指向内核地址空间。当进程在用户态运行时,CPU堆栈指针寄存器指向的 用户堆栈地址,使用用户堆栈,当进程运行在内核态时,CPU堆栈指针寄存器指向的是内核栈空间地址,使用的是内核栈;
进程用户栈和内核栈之间的切换
当进程由于中断或系统调用从用户态转换到内核态时,进程所使用的栈也要从用户栈切换到内核栈。系统调用实质就是通过指令产生中断,称为软中断。进程因为中断(软中断或硬件产生中断),使得CPU切换到特权工作模式,此时进程陷入内核态,进程进入内核态后,首先把用户态的堆栈地址保存在内核堆栈中,然后设置堆栈指针寄存器的地址为内核栈地址,这样就完成了用户栈向内核栈的切换。
当进程从内核态切换到用户态时,最后把保存在内核栈中的用户栈地址恢复到CPU栈指针寄存器即可,这样就完成了内核栈向用户栈的切换。
这里要理解一下内核堆栈。前面我们讲到,进程从用户态进入内核态时,需要在内核栈中保存用户栈的地址。那么进入内核态时,从哪里获得内核栈的栈指针呢?
要解决这个问题,先要理解从用户态刚切换到内核态以后,进程的内核栈总是空的。这点很好理解,当进程在用户空间运行时,使用的是用户 栈;当进程在内核态运行时,内核栈中保存进程在内核态运行的相关信息,但是当进程完成了内核态的运行,重新回到用户态时,此时内核栈中保存的信息全部恢 复,也就是说,进程在内核态中的代码执行完成回到用户态时,内核栈是空的。
理解了从用户态刚切换到内核态以后,进程的内核栈总是空的,那刚才这个问题就很好理解了,因为内核栈是空的,那当进程从用户态切换到内核态后,把内核栈的栈顶地址设置给CPU的栈指针寄存器就可以了。
X86 Linux内核栈定义如下(可能现在的版本有所改变,但不妨碍我们对内核栈的理解):
在/include/linux/sched.h中定义了如下一个联合结构:
union task_union {
&&&&&& struct task_
&&&&&& unsigned long stack[2408];
从这个结构可以看出,内核栈占8kb的内存区。实际上,进程的task_struct结构所占的内存是由内核动态分配的,更确切地说,内核根本不给task_struct分配内存,而仅仅给内核栈分配8K的内存,并把其中的一部分给task_struct使用。
这样内核栈的起始地址就是union task_union变量的地址+8K 字节的长度。例如:我们动态分配一个union task_union类型的变量如下:
unsigned char *gtaskkernelstack
gtaskkernelstack = kmalloc(sizeof(union task_union));
那么该进程每次进入内核态时,内核栈的起始地址均为:(unsigned char *)gtaskkernelstack + 8096
进程上下文
进程切换现场称为进程上下文(context),包含了一个进程所具有的全部信息,一般包括:进程控制块(Process Control Block,PCB)、有关程序段和相应的数据集。
进程控制块PCB(任务控制块)
进程控制块是进程在内存中的静态存在方式,Linux内核中用task_struct表示一个进程(相当于进程的人事档案)。进程的静 态描述必须保证一个进程在获得CPU并重新进入运行态时,能够精确的接着上次运行的位置继续进行,相关的程序段,数据以及CPU现场信息必须保存。处理机 现场信息主要包括处理机内部寄存器和堆栈等基本数据。
进程控制块一般可以分为进程描述信息、进程控制信息,进程相关的资源信息和CPU现场保护机构。
进程的切换
当一个进程的时间片到时,进程需要让出CPU给其他进程运行,内核需要进行进程切换。
Linux 的进程切换是通过调用函数进程切换函数schedule来实现的。进程切换主要分为2个步骤:
1.&调用switch_mm()函数进行进程页表的切换;
2.&调用&switch_to() 函数进行 CPU寄存器切换;
__switch_to定义在/arch/arm/kernel目录下的entry-armv.S 文件中,源码如下:
-----------------------------------------------------------------------------
ENTRY(__switch_to)
&UNWIND(.fnstart&&& )
&UNWIND(.cantunwind&)
&&& add ip, r1, #TI_CPU_SAVE
&&&&ldr&r3, [r2, #TI_TP_VALUE]
&&&&stmia&& ip!, {r4 - sl, fp, sp, lr}&@ Store most regs on stack
#ifdef CONFIG_MMU
&&& ldr r6, [r2, #TI_CPU_DOMAIN]
#if __LINUX_ARM_ARCH__ &= 6
#ifdef CONFIG_CPU_32v6K
&&&&strex&& r5, r4, [ip]&&&&&&&&&&& @ Clear exclusive monitor
#if defined(CONFIG_HAS_TLS_REG)
&&& mcr p15, 0, r3, c13, c0, 3&&&&& @ set TLS register
#elif !defined(CONFIG_TLS_REG_EMUL)
&&&&mov&r4, #0xffff0fff
&&&&str&r3, [r4, #-15]&&&&&&&&& @ TLS val at 0xffff0ff0
#ifdef CONFIG_MMU
&&& mcr p15, 0, r6, c3, c0, 0&&&&&& @ Set domain register
&&& mov r5, r0
&&& add r4, r2, #TI_CPU_SAVE
&&&&ldr&r0, =thread_notify_head
&&& mov r1, #THREAD_NOTIFY_SWITCH
&&& bl&atomic_notifier_call_chain
&&& mov r0, r5
&&& ldmia&& r4, {r4 - sl, fp, sp, pc}&& @ Load all regs saved previously
&UNWIND(.fnend&&&&& )
ENDPROC(__switch_to)
----------------------------------------------------------
Switch_to的处理流程如下:
1.&保存本进程的CPU寄存器(PC、R0 ~ R13)到本进程的栈中;
2.&保存SP(本进程的栈基地址)到task-&thread.save 中;
3.&从新进程的task-&thread.save恢复SP为新进程的栈基地址;
4.&从新进程的栈中恢复新进程的CPU相关寄存器值,
5.&新进程开始运行,完成任务切换。
这里读者可能会问,在进行任务切换的时候,到底是在运行进程1还是运行进程2呢?进程切换的时候,已经进行页表切换,那页表切换之后,切换进程使用的是进程1还是进程2的页表呢?
要回答这个问题,首先我们要明白由谁来完成进程切换?
通过对操作系统的理解,毫无疑问,进程切换是由内核来完成的,也就是说,在进行进程切换时,CPU运行在内核模式,使用的是内核空间的内核代码,它既不属于进程1,也不属于进程2,当进程的时间片到时,内核提供服务来完成进程的切换。既不使用进程1的页表,也不使用进程2的页表,使用的内核映射页表。这样我们就很好理解上面的问题了。
&&相关文章推荐
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:175348次
积分:3522
积分:3522
排名:第8403名
原创:142篇
转载:24篇
评论:140条
文章:15篇
阅读:22530
文章:33篇
阅读:22102
学长学姐以及小伙伴的博客~ ~
潜心学习~ ~
(1)(1)(1)(3)(3)(10)(6)(13)(1)(3)(3)(2)(16)(9)(4)(5)(11)(25)(19)(5)(1)(1)(2)(10)(7)(7)(3)(1)

我要回帖

更多关于 linux 内核栈 用户栈 的文章

 

随机推荐